unjwt 0.9.0 → 0.9.1

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.
@@ -200,7 +200,7 @@ interface SessionRequestBase {
200
200
  readonly headers: Headers;
201
201
  readonly errHeaders?: Headers;
202
202
  };
203
- /** Per-request state shared with the framework. Sessions are stored in its `sessions` member. Without it, state is keyed on `req`, else on the event. */
203
+ /** Per-request state shared with the framework. Sessions are stored in its `unjwt` member. Without it, state is keyed on `req`, else on the event. */
204
204
  readonly context?: Record<string, unknown>;
205
205
  }
206
206
  /** A request event, by its `Request` or by its `Headers` (a `Request` itself, a WebSocket upgrade request). `req` is read first. */
@@ -261,6 +261,91 @@ interface SessionDriver<TEvent> {
261
261
  token: string | undefined;
262
262
  } | undefined;
263
263
  }
264
+ /** The `Set-Cookie` attributes of a session cookie (RFC 6265, RFC 6265bis, and CHIPS: Cookies Having Independent Partitioned State). */
265
+ interface CookieAttributes {
266
+ /** `Domain`. A `__Host-` name refuses it. */
267
+ domain?: string;
268
+ /** `Expires`. An issued token with an `exp` sets it to that time. */
269
+ expires?: Date;
270
+ /** `HttpOnly`: page scripts cannot read the cookie. */
271
+ httpOnly?: boolean;
272
+ /** `Max-Age` in seconds. An issued token with an `exp` replaces it with `expires`. */
273
+ maxAge?: number;
274
+ /** `Path`. A `__Host-` name requires `"/"`. */
275
+ path?: string;
276
+ /** `Priority`. */
277
+ priority?: "low" | "medium" | "high";
278
+ /** `SameSite`. `true` is `Strict`, and `"none"` requires `secure`. */
279
+ sameSite?: boolean | "lax" | "strict" | "none";
280
+ /** `Secure`. A `__Host-` or `__Secure-` name requires it. */
281
+ secure?: boolean;
282
+ /** `Partitioned` (CHIPS). Requires `secure`. */
283
+ partitioned?: boolean;
284
+ }
285
+ /** The attributes a session cookie gets unless its config sets them: `path` `"/"`, `secure`, `httpOnly` and `sameSite` `"lax"`. */
286
+ export declare const DEFAULT_SESSION_COOKIE: Readonly<CookieAttributes>;
287
+ /** The token sources of one session, and the attributes its cookie is written with. */
288
+ interface SessionTransportOptions {
289
+ /** Session name: the cookie name, and the base of the default session header. */
290
+ name: string;
291
+ /** Request header to read the token from, before the cookie: a name, or `true` for `x-<name>-session`. Unset or `false` reads no header. */
292
+ sessionHeader?: boolean | string;
293
+ /** The cookie's attributes and the chunk size its token is split at (`4000` when unset), or `false` for no cookie. */
294
+ cookie: false | {
295
+ attrs: CookieAttributes;
296
+ chunkMaxLength?: number;
297
+ };
298
+ }
299
+ /** How one session reads its token from the request and writes it to the response. */
300
+ interface SessionTransport {
301
+ /** Where the token is read from: the driver's token in hand, the session header, or the cookie. Only a cookie is written back. */
302
+ source: SessionSource;
303
+ /** The token in hand, else the session header's, else the cookie this response wrote, else the request's. Absent without a token source. */
304
+ read?: () => string | undefined;
305
+ /** The session header the request sent, by its lowercase name, when it holds neither a bare token nor `Bearer <token>`. The session decides how to report it. */
306
+ malformed?: {
307
+ header: string;
308
+ };
309
+ /**
310
+ * Writes the token to the cookie, split into chunks past `chunkMaxLength`, and returns `true`. With `expiresAt` (ms), the cookie gets that `expires` and no
311
+ * `maxAge`, which would take precedence (RFC 6265 §5.3). Once the response was sent, writes nothing and returns `false`.
312
+ * Present when the session reads its token from a cookie the event can write.
313
+ * @throws `ERR_SESSION_CONFIG_INVALID` if the token needs more than 16 cookie chunks.
314
+ */
315
+ write?: (token: string, options?: {
316
+ expiresAt?: number;
317
+ }) => boolean;
318
+ /**
319
+ * Expires the cookie and every chunk, and returns `true`. Once the response was sent, writes nothing and returns `false`.
320
+ * Present when the session reads its token from a cookie the event can write.
321
+ */
322
+ drop?: () => boolean;
323
+ /** The session reads its token from a cookie, but the event cannot write one (a request without a response). */
324
+ readonly: boolean;
325
+ /** Whether the response can still change. `false` once it was sent. */
326
+ writable: () => boolean;
327
+ }
328
+ /**
329
+ * Resolves the transport of one request, before any token is opened: a token the driver hands in, else the session header the request sends, else the cookie.
330
+ * A header that holds the very token of the request's cookie is the cookie. Any other header is read alone, the cookie left as it is.
331
+ * A token in hand or a header token is reissued onto the session only. Only a cookie session writes back.
332
+ */
333
+ export declare function resolveSessionTransport<TEvent>(driver: SessionDriver<TEvent>, event: TEvent, options: SessionTransportOptions): SessionTransport;
334
+ /** Throws `ERR_SESSION_CONFIG_INVALID` for a transport without a reader: the session has no token source to read or clear. */
335
+ export declare function assertSessionTokenSource(transport: SessionTransport, options: {
336
+ prefix: string;
337
+ }): asserts transport is SessionTransport & {
338
+ read: () => string | undefined;
339
+ };
340
+ /** Throws `ERR_SESSION_READONLY_EVENT` on an update when a cookie session cannot write its cookie, and on an update or clear once the response was sent. */
341
+ export declare function assertSessionWritable(transport: SessionTransport, options: {
342
+ operation: "update" | "clear";
343
+ prefix: string;
344
+ }): void;
345
+ /** Refuses a `sessionHeader` that is neither a boolean nor a header name (RFC 7230 §3.2). */
346
+ export declare function assertSessionHeader(header: unknown, options: {
347
+ prefix: string;
348
+ }): void;
264
349
  /**
265
350
  * JWS verify options a session accepts. `algorithms` names the `alg` values a key without its own `alg` (a `CryptoKey`, an `oct` or RSA JWK
266
351
  * without `alg`) verifies, and with `onKeyLookup` set, may also name those only the keys it returns serve. The session sets `typ`,
@@ -292,27 +377,6 @@ interface JWSSessionConfig<MaxAge extends ExpiresIn | undefined = ExpiresIn | un
292
377
  */
293
378
  jws?: JWSSessionOptions;
294
379
  }
295
- /** The `Set-Cookie` attributes of a session cookie (RFC 6265, RFC 6265bis, and CHIPS: Cookies Having Independent Partitioned State). */
296
- interface CookieAttributes {
297
- /** `Domain`. A `__Host-` name refuses it. */
298
- domain?: string;
299
- /** `Expires`. An issued token with an `exp` sets it to that time. */
300
- expires?: Date;
301
- /** `HttpOnly`: page scripts cannot read the cookie. */
302
- httpOnly?: boolean;
303
- /** `Max-Age` in seconds. An issued token with an `exp` replaces it with `expires`. */
304
- maxAge?: number;
305
- /** `Path`. A `__Host-` name requires `"/"`. */
306
- path?: string;
307
- /** `Priority`. */
308
- priority?: "low" | "medium" | "high";
309
- /** `SameSite`. `true` is `Strict`, and `"none"` requires `secure`. */
310
- sameSite?: boolean | "lax" | "strict" | "none";
311
- /** `Secure`. A `__Host-` or `__Secure-` name requires it. */
312
- secure?: boolean;
313
- /** `Partitioned` (CHIPS). Requires `secure`. */
314
- partitioned?: boolean;
315
- }
316
380
  /** Session state for the current request, from the claims of its JWS token. */
317
381
  interface SessionJWS<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined> extends SessionState<T, MaxAge> {}
318
382
  /** JWS session hooks. `onRead`, `onExpire` and `onError` are mutually exclusive per token. */
@@ -452,6 +516,75 @@ export declare function defineJWESession<T extends SessionRecord = SessionClaims
452
516
  export declare function defineJWESession<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TEvent = WebSessionEvent>(config: SessionConfigJWE<T, MaxAge, TEvent>, options: {
453
517
  driver: SessionDriver<TEvent>;
454
518
  }): SessionDefinition<T, MaxAge, TEvent>;
455
- /** Returns `driver` typed as a `SessionDriver`. Pass it to `defineJWSSession` or `defineJWESession` as `{ driver }`. */
519
+ /**
520
+ * Returns `driver` typed as a `SessionDriver`. Pass it to `defineJWSSession` or `defineJWESession` as `{ driver }`.
521
+ * The Web driver, `webSessionDriver()`, is the reference implementation of the contract.
522
+ */
456
523
  export declare function defineSessionDriver<TEvent>(driver: SessionDriver<TEvent>): SessionDriver<TEvent>;
457
- export { type CompositeKey, type ContentEncryptionAlgorithm, type CookieAttributes, type CookieJar, type DeriveJWKOptions, type DeriveJWKReturn, type DeriveKeyOptions, type DeriveKeyReturn, type Duration, type ECDHEphemeralKeyInput, type ExpiresIn, type GenerateJWKOptions, type GenerateJWKReturn, type GenerateKeyAlgorithm, type GenerateKeyOptions, type GenerateKeyReturn, type GeneratedJWKMap, type GeneratedKeyMap, type ImportedKey, type ImportedKeyMap, type ImportedPEMJWK, type JOSEConsumerOptions, type JOSELimits, type JOSEPayload, type JOSEPayloadMode, type JOSEPayloadTypeMap, type JWEAsymmetricPrivateJWK, type JWEAsymmetricPublicJWK, type JWEDecryptJWK, type JWEDecryptOptions, type JWEDecryptResult, type JWEEncryptJWK, type JWEEncryptOptions, type JWEFlattenedSerialization, type JWEGeneralRecipient, type JWEGeneralSerialization, type JWEHeaderParameters, type JWEKeyLookup, type JWEKeyManagementHeaderParameters, type JWELibraryHeaderName, type JWEMultiDecryptKey, type JWEMultiDecryptOptions, type JWEMultiDecryptResult, type JWEMultiEncryptOptions, type JWEMultiRecipient, type JWEProtectedHeader, type JWESessionKey, type JWESessionKeyLookup, type JWESessionOctJWK, type JWK, type JWKAlgorithm, type JWKCacheAdapter, type JWKLookupFunction, type JWKLookupFunctionHeader, type JWKMetadata, type JWKPEMAlgorithm, type JWKParameters, type JWKSet, type JWK_AES_Algorithm, type JWK_AES_CBC_HMAC, type JWK_AES_GCM, type JWK_AES_GCM_KW, type JWK_AES_KW, type JWK_Asymmetric, type JWK_Asymmetric_Algorithm, type JWK_EC, type JWK_ECDH_ES, type JWK_ECDSA, type JWK_EC_Private, type JWK_EC_Public, type JWK_HMAC, type JWK_OKP, type JWK_OKP_Private, type JWK_OKP_Public, type JWK_OKP_SIGN, type JWK_PBES2, type JWK_Pair, type JWK_Private, type JWK_Public, type JWK_RSA, type JWK_RSA_ENC, type JWK_RSA_PSS, type JWK_RSA_Private, type JWK_RSA_Public, type JWK_RSA_SIGN, type JWK_Symmetric, type JWK_Symmetric_Algorithm, type JWK_oct, type JWSAlgorithm, type JWSAsymmetricPrivateJWK, type JWSAsymmetricPublicJWK, type JWSFlattenedSerialization, type JWSGeneralSerialization, type JWSGeneralSignature, type JWSHashedAlgorithm, type JWSHeaderParameters, type JWSKeyLookup, type JWSMultiSignOptions, type JWSMultiSigner, type JWSMultiVerifyAllOptions, type JWSMultiVerifyOptions, type JWSMultiVerifyOutcome, type JWSMultiVerifyResult, type JWSProtectedHeader, type JWSSessionKey, type JWSSessionKeyLookup, type JWSSignJWK, type JWSSignOptions, type JWSVerifyJWK, type JWSVerifyOptions, type JWSVerifyResult, type JWT, type JWTClaimValidationOptions, type JWTClaims, type JWTConfirmation, type JWTTimeClaimOptions, type JoseHeaderParameters, type KeyManagementAlgorithm, type KeyWrapAlgorithmOf, type KeyWrappingAlgorithm, type MaxTokenAge, type NotBeforeIn, type OpenedSessionState, type PEMType, type PayloadOf, type SessionClaims, type SessionConfigJWE, type SessionConfigJWS, type SessionData, type SessionDecryptOptions, type SessionDefinition, type SessionDriver, type SessionEncryptOptions, type SessionHandedToken, type SessionHooksJWE, type SessionHooksJWS, type SessionIssueOptions, type SessionJWE, type SessionJWS, type SessionManager, type SessionRequestEvent, type SessionSignOptions, type SessionSource, type SessionUpdate, type SessionUpdateTrigger, type SessionVerifyOptions, type ToPublicJWKReturn, type UnwrapKeyOptions, type UnwrapKeyResult, type UnwrappingKeyFor, type WebSessionEvent, type WrapKeyOptions, type WrapKeyResult, type WrappingKeyFor, deriveJWKFromPassword, exportPEM, generateJWK, importPEM, thumbprint, toPublicJWK };
524
+ /**
525
+ * The Web session driver: a request event by its `Request` or its `Headers`, or a token in hand.
526
+ * `prefix` (default `"[unjwt/session]"`) starts the message of every error a session over it throws, the driver's own included.
527
+ * Every driver it returns shares the state of a request with the others.
528
+ */
529
+ export declare function webSessionDriver(options?: {
530
+ prefix?: string;
531
+ }): SessionDriver<WebSessionEvent>;
532
+ /**
533
+ * A cookie jar over a request's `Headers` and, when given, its response's: `read` parses the request's `Cookie` header,
534
+ * `write` sets a `Set-Cookie` line on `response.headers` through `serializeCookie`, replacing the line of the same cookie
535
+ * (name, domain, path, partition) instead of adding a second one, and `written` reads back what the response sets.
536
+ * Each write is copied to `errHeaders` too: h3 and Nitro send only these for a thrown handler.
537
+ * Without `response`, the jar only reads.
538
+ */
539
+ export declare function webCookieJar(request: Headers, response?: {
540
+ headers: Headers;
541
+ errHeaders?: Headers;
542
+ }): CookieJar<CookieAttributes>;
543
+ /** The cookies of a `Cookie` header value, by name, values percent-decoded. The first of two cookies with one name wins. The record has no prototype. */
544
+ export declare function parseCookie(header: string): Record<string, string | undefined>;
545
+ /** One `Set-Cookie` line, the value percent-encoded and `path` `"/"` unless `attrs` sets one. Throws a `TypeError` for a name or attribute RFC 6265bis refuses. */
546
+ export declare function serializeCookie(name: string, value: string, attrs?: CookieAttributes): string;
547
+ /** The members of a session config that {@link assertSessionConfig} checks. */
548
+ interface SessionConfigChecks {
549
+ /** Request header to read the token from: a name, or `true` for `x-<name>-session`. */
550
+ sessionHeader?: boolean | string;
551
+ /** Cookie attributes over the defaults, or `false` for no cookie. */
552
+ cookie?: false | (CookieAttributes & {
553
+ chunkMaxLength?: number;
554
+ });
555
+ /** Session lifetime. */
556
+ maxAge?: ExpiresIn;
557
+ /** Reissues a session read from its cookie: on every read with `true`, else once the token is that old. Requires `maxAge`, and is shorter than it. */
558
+ rolling?: true | ExpiresIn;
559
+ }
560
+ /**
561
+ * Checks what the config alone decides: its name, session header, cookie prefix over `defaultCookie`, and `rolling`.
562
+ * `defaultCookie` is {@link DEFAULT_SESSION_COOKIE} when unset.
563
+ * @throws `ERR_SESSION_CONFIG_INVALID` if `name` is not a cookie name or ends in a dot and digits, `sessionHeader` is not a header name,
564
+ * a `__Host-` or `__Secure-` cookie's attributes break its prefix rules, or `rolling` has no `maxAge` or is not shorter than it. The message starts with `prefix`.
565
+ * @throws `ERR_INVALID_OPTION` if `rolling` is neither `true` nor a duration.
566
+ */
567
+ export declare function assertSessionConfig(config: SessionConfigChecks, options: {
568
+ name: string;
569
+ prefix: string;
570
+ defaultCookie?: CookieAttributes;
571
+ }): void;
572
+ /**
573
+ * The transport options of a session config: its session header, and its cookie attributes over `defaultCookie` with `chunkMaxLength` set apart.
574
+ * `defaultCookie` is {@link DEFAULT_SESSION_COOKIE} when unset. `cookie: false` is no cookie.
575
+ */
576
+ export declare function sessionTransportOptions(config: Pick<SessionConfigChecks, "sessionHeader" | "cookie">, options: {
577
+ name: string;
578
+ defaultCookie?: CookieAttributes;
579
+ }): SessionTransportOptions;
580
+ /**
581
+ * Claims a session name on one request: one name is one cookie, so the first family to claim a name on a request keeps it.
582
+ * `state` is `driver.state(event)`. The same family may claim a name again on the same request.
583
+ * @throws `ERR_SESSION_CONFIG_INVALID` if another family already claimed `name` on this request. The message starts with `prefix`.
584
+ */
585
+ export declare function claimSessionName(state: Record<string, unknown>, options: {
586
+ name: string;
587
+ family: string;
588
+ prefix: string;
589
+ }): void;
590
+ export { type CompositeKey, type ContentEncryptionAlgorithm, type CookieAttributes, type CookieJar, type DeriveJWKOptions, type DeriveJWKReturn, type DeriveKeyOptions, type DeriveKeyReturn, type Duration, type ECDHEphemeralKeyInput, type ExpiresIn, type GenerateJWKOptions, type GenerateJWKReturn, type GenerateKeyAlgorithm, type GenerateKeyOptions, type GenerateKeyReturn, type GeneratedJWKMap, type GeneratedKeyMap, type ImportedKey, type ImportedKeyMap, type ImportedPEMJWK, type JOSEConsumerOptions, type JOSELimits, type JOSEPayload, type JOSEPayloadMode, type JOSEPayloadTypeMap, type JWEAsymmetricPrivateJWK, type JWEAsymmetricPublicJWK, type JWEDecryptJWK, type JWEDecryptOptions, type JWEDecryptResult, type JWEEncryptJWK, type JWEEncryptOptions, type JWEFlattenedSerialization, type JWEGeneralRecipient, type JWEGeneralSerialization, type JWEHeaderParameters, type JWEKeyLookup, type JWEKeyManagementHeaderParameters, type JWELibraryHeaderName, type JWEMultiDecryptKey, type JWEMultiDecryptOptions, type JWEMultiDecryptResult, type JWEMultiEncryptOptions, type JWEMultiRecipient, type JWEProtectedHeader, type JWESessionKey, type JWESessionKeyLookup, type JWESessionOctJWK, type JWK, type JWKAlgorithm, type JWKCacheAdapter, type JWKLookupFunction, type JWKLookupFunctionHeader, type JWKMetadata, type JWKPEMAlgorithm, type JWKParameters, type JWKSet, type JWK_AES_Algorithm, type JWK_AES_CBC_HMAC, type JWK_AES_GCM, type JWK_AES_GCM_KW, type JWK_AES_KW, type JWK_Asymmetric, type JWK_Asymmetric_Algorithm, type JWK_EC, type JWK_ECDH_ES, type JWK_ECDSA, type JWK_EC_Private, type JWK_EC_Public, type JWK_HMAC, type JWK_OKP, type JWK_OKP_Private, type JWK_OKP_Public, type JWK_OKP_SIGN, type JWK_PBES2, type JWK_Pair, type JWK_Private, type JWK_Public, type JWK_RSA, type JWK_RSA_ENC, type JWK_RSA_PSS, type JWK_RSA_Private, type JWK_RSA_Public, type JWK_RSA_SIGN, type JWK_Symmetric, type JWK_Symmetric_Algorithm, type JWK_oct, type JWSAlgorithm, type JWSAsymmetricPrivateJWK, type JWSAsymmetricPublicJWK, type JWSFlattenedSerialization, type JWSGeneralSerialization, type JWSGeneralSignature, type JWSHashedAlgorithm, type JWSHeaderParameters, type JWSKeyLookup, type JWSMultiSignOptions, type JWSMultiSigner, type JWSMultiVerifyAllOptions, type JWSMultiVerifyOptions, type JWSMultiVerifyOutcome, type JWSMultiVerifyResult, type JWSProtectedHeader, type JWSSessionKey, type JWSSessionKeyLookup, type JWSSignJWK, type JWSSignOptions, type JWSVerifyJWK, type JWSVerifyOptions, type JWSVerifyResult, type JWT, type JWTClaimValidationOptions, type JWTClaims, type JWTConfirmation, type JWTTimeClaimOptions, type JoseHeaderParameters, type KeyManagementAlgorithm, type KeyWrapAlgorithmOf, type KeyWrappingAlgorithm, type MaxTokenAge, type NotBeforeIn, type OpenedSessionState, type PEMType, type PayloadOf, type SessionClaims, type SessionConfigChecks, type SessionConfigJWE, type SessionConfigJWS, type SessionData, type SessionDecryptOptions, type SessionDefinition, type SessionDriver, type SessionEncryptOptions, type SessionHandedToken, type SessionHooksJWE, type SessionHooksJWS, type SessionIssueOptions, type SessionJWE, type SessionJWS, type SessionManager, type SessionRequestEvent, type SessionSignOptions, type SessionSource, type SessionTransport, type SessionTransportOptions, type SessionUpdate, type SessionUpdateTrigger, type SessionVerifyOptions, type ToPublicJWKReturn, type UnwrapKeyOptions, type UnwrapKeyResult, type UnwrappingKeyFor, type WebSessionEvent, type WrapKeyOptions, type WrapKeyResult, type WrappingKeyFor, deriveJWKFromPassword, exportPEM, generateJWK, importPEM, thumbprint, toPublicJWK };
@@ -47,6 +47,9 @@ function expiredSessionError(options) {
47
47
  function readFailedError(cause, options) {
48
48
  return new JWTError(`${options.prefix} Session could not be read: ${messageOf(cause)}`, "ERR_SESSION_READ_FAILED", cause);
49
49
  }
50
+ function malformedHeaderError(header, options) {
51
+ return invalidTokenError(`${options.prefix} Invalid session token: the "${header}" header holds neither a token nor "Bearer <token>".`);
52
+ }
50
53
  function rethrowAsInvalidToken(error, options) {
51
54
  if (isJWTError(error, "ERR_JWT_EXPIRED") || isJWTError(error, "ERR_SESSION_READ_FAILED")) throw error;
52
55
  throw invalidTokenError(`${options.prefix} Invalid session token: ${messageOf(error)}`, error);
@@ -65,7 +68,7 @@ function assertCookiePrefix(name, attrs, options) {
65
68
  const host = lower.startsWith("__host-");
66
69
  if (!host && !lower.startsWith("__secure-")) return;
67
70
  const rule = host ? "\"__Host-\" cookie must be Secure, with Path=/ and no Domain" : "\"__Secure-\" cookie must be Secure";
68
- if (attrs.secure === false || host && (attrs.domain !== void 0 || attrs.path !== void 0 && attrs.path !== "/")) throw invalidConfigError(`${options.prefix} Session ${quoteTokenValue(name)}: a ${rule}.`);
71
+ if (!attrs.secure || host && (attrs.domain !== void 0 || attrs.path !== void 0 && attrs.path !== "/")) throw invalidConfigError(`${options.prefix} Session ${quoteTokenValue(name)}: a ${rule}.`);
69
72
  }
70
73
  function assertRolling(config, options) {
71
74
  const { rolling } = config;
@@ -161,7 +164,6 @@ function createSessionCodec(spec) {
161
164
  return {
162
165
  family: spec.family,
163
166
  defaultName: spec.defaultName,
164
- defaultCookie: spec.defaultCookie,
165
167
  now,
166
168
  keys: (config) => {
167
169
  const { issueKey, issueAlg, key, algorithms } = spec.resolveKeys(config);
@@ -503,19 +505,12 @@ const RSA_SIGNATURES = [
503
505
  "PS384",
504
506
  "PS512"
505
507
  ];
506
- const JWS_DEFAULT_COOKIE = {
507
- path: "/",
508
- secure: true,
509
- httpOnly: true,
510
- sameSite: "lax"
511
- };
512
508
  function createJWSCodec(options) {
513
509
  const { prefix, defaultName } = options;
514
510
  return createSessionCodec({
515
511
  family: "jws",
516
512
  prefix,
517
513
  defaultName,
518
- defaultCookie: JWS_DEFAULT_COOKIE,
519
514
  issueOptions: (config) => config.jws?.signOptions,
520
515
  openOptions: (config) => config.jws?.verifyOptions,
521
516
  resolveKeys: (config) => resolveSessionKeys$1(config.key, {
@@ -543,6 +538,12 @@ function createJWSCodec(options) {
543
538
  }
544
539
  });
545
540
  }
541
+ const DEFAULT_SESSION_COOKIE = /* @__PURE__ */ Object.freeze({
542
+ path: "/",
543
+ secure: true,
544
+ httpOnly: true,
545
+ sameSite: "lax"
546
+ });
546
547
  const kSessionPending = Symbol("unjwt_session_pending");
547
548
  function sessionInstant(source) {
548
549
  const now = source.codec.now(source.config);
@@ -674,7 +675,7 @@ function decodeValue(value) {
674
675
  return value;
675
676
  }
676
677
  }
677
- function resolveTransport(driver, event, options) {
678
+ function resolveSessionTransport(driver, event, options) {
678
679
  const handed = driver.handed?.(event);
679
680
  if (handed) return {
680
681
  source: "token",
@@ -682,11 +683,11 @@ function resolveTransport(driver, event, options) {
682
683
  readonly: false,
683
684
  writable: () => true
684
685
  };
685
- const { config, name } = options;
686
+ const { name, cookie } = options;
686
687
  const header = presentedHeader(driver, event, options);
687
- if (config.cookie === false) return headerTransport(config, header);
688
+ if (cookie === false) return headerTransport(options, header);
688
689
  const jar = driver.cookies(event);
689
- if (header && (header.token === void 0 || header.token !== readChunked(jar, name))) return headerTransport(config, header);
690
+ if (header && (header.token === void 0 || header.token !== readChunked(jar, name))) return headerTransport(options, header);
690
691
  const store = responseStore(jar);
691
692
  const transport = {
692
693
  source: "cookie",
@@ -695,22 +696,32 @@ function resolveTransport(driver, event, options) {
695
696
  writable: () => driver.writable?.(event) ?? true
696
697
  };
697
698
  if (!store) return transport;
698
- transport.write = (token, { attrs, chunkMaxLength }) => {
699
+ const { attrs, chunkMaxLength } = cookie;
700
+ transport.write = (token, { expiresAt } = {}) => {
701
+ if (!transport.writable()) return false;
702
+ const written = expiresAt === void 0 ? { ...attrs } : {
703
+ ...attrs,
704
+ expires: new Date(expiresAt),
705
+ maxAge: void 0
706
+ };
699
707
  writeChunked(store, name, token, {
700
- attrs,
708
+ attrs: written,
701
709
  chunkMaxLength,
702
710
  prefix: driver.prefix
703
711
  });
712
+ return true;
704
713
  };
705
- transport.drop = ({ attrs }) => {
714
+ transport.drop = () => {
715
+ if (!transport.writable()) return false;
706
716
  removeChunked(store, name, { attrs });
717
+ return true;
707
718
  };
708
719
  return transport;
709
720
  }
710
- function assertTokenSource(transport, options) {
721
+ function assertSessionTokenSource(transport, options) {
711
722
  if (!transport.read) throw invalidConfigError(`${options.prefix} Session has no token source, enable "cookie" or "sessionHeader".`);
712
723
  }
713
- function assertWritable(transport, options) {
724
+ function assertSessionWritable(transport, options) {
714
725
  const { operation, prefix } = options;
715
726
  if (operation === "update" && transport.readonly) throw readOnlyEventError(`${prefix} Cannot update session on read-only event.`);
716
727
  if (!transport.writable()) throw readOnlyEventError(`${prefix} Cannot update/clear session: the response was already sent.`);
@@ -732,29 +743,29 @@ function headerToken(value) {
732
743
  return words.length === 1 && first ? first : void 0;
733
744
  }
734
745
  function presentedHeader(driver, event, options) {
735
- const { config, name } = options;
736
- if (!readsHeader(config)) return void 0;
737
- const header = sessionHeaderName(name, config.sessionHeader);
746
+ const { name, sessionHeader } = options;
747
+ if (!readsHeader(options)) return void 0;
748
+ const header = sessionHeaderName(name, sessionHeader);
738
749
  const value = driver.header(event, header);
739
750
  if (typeof value !== "string") return void 0;
740
751
  const token = headerToken(value);
741
752
  if (token) return { token };
742
753
  return {
743
754
  token: void 0,
744
- malformed: invalidTokenError(`${driver.prefix} Invalid session token: the "${header}" header holds neither a token nor "Bearer <token>".`)
755
+ malformed: { header }
745
756
  };
746
757
  }
747
- function headerTransport(config, header) {
758
+ function headerTransport(options, header) {
748
759
  return {
749
760
  source: "header",
750
- read: readsHeader(config) ? () => header?.token : void 0,
761
+ read: readsHeader(options) ? () => header?.token : void 0,
751
762
  malformed: header?.malformed,
752
763
  readonly: false,
753
764
  writable: () => true
754
765
  };
755
766
  }
756
- function readsHeader(config) {
757
- return config.sessionHeader !== void 0 && config.sessionHeader !== false;
767
+ function readsHeader(options) {
768
+ return options.sessionHeader !== void 0 && options.sessionHeader !== false;
758
769
  }
759
770
  function cookieToken(jar, name) {
760
771
  const written = jar.written ? readChunked({ read: (key) => jar.written?.(key) }, name) : void 0;
@@ -772,15 +783,48 @@ function responseStore(jar) {
772
783
  const kSessionNames = Symbol("unjwt_session_names");
773
784
  const kHeldTokens = Symbol("unjwt_session_held_tokens");
774
785
  function assertSessionConfig(config, options) {
775
- const { name, prefix } = options;
786
+ const { name, prefix, defaultCookie = DEFAULT_SESSION_COOKIE } = options;
776
787
  assertSessionName(name, { prefix });
777
788
  assertSessionHeader(config.sessionHeader, { prefix });
778
789
  if (config.cookie !== false) assertCookiePrefix(name, {
779
- ...options.defaultCookie,
790
+ ...defaultCookie,
780
791
  ...config.cookie
781
792
  }, { prefix });
782
793
  assertRolling(config, { prefix });
783
794
  }
795
+ function sessionTransportOptions(config, options) {
796
+ const { name, defaultCookie = DEFAULT_SESSION_COOKIE } = options;
797
+ const { sessionHeader } = config;
798
+ if (config.cookie === false) return {
799
+ name,
800
+ sessionHeader,
801
+ cookie: false
802
+ };
803
+ const { chunkMaxLength, ...configured } = config.cookie || {};
804
+ return {
805
+ name,
806
+ sessionHeader,
807
+ cookie: {
808
+ attrs: {
809
+ ...defaultCookie,
810
+ ...configured
811
+ },
812
+ chunkMaxLength
813
+ }
814
+ };
815
+ }
816
+ function claimSessionName(state, options) {
817
+ const { name, family, prefix } = options;
818
+ const holder = state;
819
+ const registry = holder[kSessionNames] ||= /* @__PURE__ */ new Map();
820
+ const held = registry.get(name);
821
+ if (held === void 0) {
822
+ registry.set(name, family);
823
+ return;
824
+ }
825
+ if (held === family) return;
826
+ throw invalidConfigError(`${prefix} Session name "${name}" is already used by a ${held.toUpperCase()} session on this request.`);
827
+ }
784
828
  function bindSession(event, options) {
785
829
  const { config, name, driver, codec, keys, actions } = options;
786
830
  const record = driver.state(event);
@@ -801,21 +845,18 @@ function bindSession(event, options) {
801
845
  args: { event },
802
846
  store: recordSlotStore(record, slot),
803
847
  held: heldTokenSlot(record, slot),
804
- transport: resolveTransport(driver, event, {
805
- config,
806
- name
807
- })
848
+ transport: resolveSessionTransport(driver, event, sessionTransportOptions(config, { name }))
808
849
  };
809
850
  }
810
851
  function sessionToken(binding) {
811
852
  const { transport } = binding;
812
- assertTokenSource(transport, { prefix: binding.driver.prefix });
853
+ assertSessionTokenSource(transport, { prefix: binding.driver.prefix });
813
854
  const session = storedSession(binding.store);
814
855
  return session ? session.token : transport.read();
815
856
  }
816
857
  function transportToken(binding) {
817
858
  const { transport } = binding;
818
- assertTokenSource(transport, { prefix: binding.driver.prefix });
859
+ assertSessionTokenSource(transport, { prefix: binding.driver.prefix });
819
860
  const held = binding.held.get();
820
861
  return held ? held.token : transport.read();
821
862
  }
@@ -827,38 +868,6 @@ function recordSlotStore(record, slot) {
827
868
  }
828
869
  };
829
870
  }
830
- function sessionCookie(binding, options = {}) {
831
- const { chunkMaxLength, ...configured } = binding.config.cookie || {};
832
- const attrs = {
833
- ...binding.codec.defaultCookie,
834
- ...configured
835
- };
836
- if (options.expiresAt !== void 0) {
837
- attrs.expires = new Date(options.expiresAt);
838
- attrs.maxAge = void 0;
839
- }
840
- return {
841
- attrs,
842
- chunkMaxLength
843
- };
844
- }
845
- function dropSessionCookie(binding) {
846
- const { transport } = binding;
847
- if (!transport.drop || !transport.writable()) return;
848
- transport.drop(sessionCookie(binding));
849
- }
850
- function claimSessionName(record, options) {
851
- const { name, family, prefix } = options;
852
- const holder = record;
853
- const registry = holder[kSessionNames] ||= /* @__PURE__ */ new Map();
854
- const held = registry.get(name);
855
- if (held === void 0) {
856
- registry.set(name, family);
857
- return;
858
- }
859
- if (held === family) return;
860
- throw invalidConfigError(`${prefix} Session name "${name}" is already used by a ${held.toUpperCase()} session on this request.`);
861
- }
862
871
  function heldTokenSlot(record, slot) {
863
872
  const holder = record;
864
873
  const held = holder[kHeldTokens] ||= /* @__PURE__ */ new Map();
@@ -873,12 +882,12 @@ async function loadSession(binding) {
873
882
  const { config, codec, keys, store, args, transport } = binding;
874
883
  await keys.assertKeys();
875
884
  const open = keys.opener(args);
876
- assertTokenSource(transport, { prefix: binding.driver.prefix });
885
+ assertSessionTokenSource(transport, { prefix: binding.driver.prefix });
877
886
  const existing = storedSession(store);
878
887
  if (existing) {
879
888
  const session = existing[kSessionPending] ? await existing[kSessionPending] : existing;
880
- if (session.token !== void 0 && session.expiresAt !== void 0 && session.expiresAt < codec.now(config)) {
881
- dropSessionCookie(binding);
889
+ if (session.token !== void 0 && session.expiresAt !== void 0 && session.expiresAt <= codec.now(config)) {
890
+ transport.drop?.();
882
891
  const error = expiredSessionError({
883
892
  id: session.id,
884
893
  createdAt: session.createdAt,
@@ -903,8 +912,9 @@ async function loadSession(binding) {
903
912
  const session = emptySession(binding);
904
913
  store.set(session);
905
914
  if (transport.malformed) {
906
- session.error = transport.malformed;
907
- await fireError(binding, transport.malformed);
915
+ const error = malformedHeaderError(transport.malformed.header, { prefix: binding.driver.prefix });
916
+ session.error = error;
917
+ await fireError(binding, error);
908
918
  return session;
909
919
  }
910
920
  const token = transport.read();
@@ -927,7 +937,7 @@ async function loadSession(binding) {
927
937
  if (isJWTError(failure.error)) session.error = failure.error;
928
938
  if (isJWTError(failure.error, "ERR_JWT_EXPIRED")) {
929
939
  const { jti, iat, exp } = failure.error.cause;
930
- dropSessionCookie(binding);
940
+ transport.drop?.();
931
941
  await fireExpire(binding, {
932
942
  session: {
933
943
  id: jti,
@@ -938,7 +948,7 @@ async function loadSession(binding) {
938
948
  error: failure.error
939
949
  });
940
950
  } else {
941
- if (isJWTError(failure.error, "ERR_SESSION_TOKEN_INVALID")) dropSessionCookie(binding);
951
+ if (isJWTError(failure.error, "ERR_SESSION_TOKEN_INVALID")) transport.drop?.();
942
952
  await fireError(binding, failure.error);
943
953
  }
944
954
  return session;
@@ -992,7 +1002,7 @@ async function fireExpire(binding, expired) {
992
1002
  }
993
1003
  async function issueSession(binding, update, options, trigger = "update") {
994
1004
  const { config, keys, driver, store, args, transport } = binding;
995
- assertWritable(transport, {
1005
+ assertSessionWritable(transport, {
996
1006
  operation: "update",
997
1007
  prefix: driver.prefix
998
1008
  });
@@ -1013,7 +1023,7 @@ async function issueSession(binding, update, options, trigger = "update") {
1013
1023
  try {
1014
1024
  await keys.assertKeys();
1015
1025
  const token = await keys.issue(session);
1016
- transport.write?.(token, sessionCookie(binding, { expiresAt: session.expiresAt }));
1026
+ transport.write?.(token, { expiresAt: session.expiresAt });
1017
1027
  binding.held.set(token);
1018
1028
  issued = Object.assign(session, {
1019
1029
  id,
@@ -1047,8 +1057,8 @@ async function issueSession(binding, update, options, trigger = "update") {
1047
1057
  }
1048
1058
  async function dropSession(binding) {
1049
1059
  const { config, driver, store, args, transport } = binding;
1050
- assertTokenSource(transport, { prefix: driver.prefix });
1051
- assertWritable(transport, {
1060
+ assertSessionTokenSource(transport, { prefix: driver.prefix });
1061
+ assertSessionWritable(transport, {
1052
1062
  operation: "clear",
1053
1063
  prefix: driver.prefix
1054
1064
  });
@@ -1057,7 +1067,7 @@ async function dropSession(binding) {
1057
1067
  const oldSession = session && (session.id !== void 0 || session.token !== void 0 || session.error !== void 0) ? snapshotSession(session) : void 0;
1058
1068
  store.set(emptySession(binding));
1059
1069
  binding.held.set(void 0);
1060
- transport.drop?.(sessionCookie(binding));
1070
+ transport.drop?.();
1061
1071
  await config.hooks?.onClear?.({
1062
1072
  oldSession,
1063
1073
  ...args,
@@ -1119,8 +1129,7 @@ function defineSession(config, options) {
1119
1129
  const name = config.name || codec.defaultName;
1120
1130
  assertSessionConfig(config, {
1121
1131
  name,
1122
- prefix: driver.prefix,
1123
- defaultCookie: codec.defaultCookie
1132
+ prefix: driver.prefix
1124
1133
  });
1125
1134
  const keys = codec.keys(config);
1126
1135
  const actions = sessionActions();
@@ -1163,15 +1172,15 @@ function defineSession(config, options) {
1163
1172
  }
1164
1173
  function webCookieJar(request, response) {
1165
1174
  let cookies;
1166
- const jar = { read: (name) => (cookies ||= requestCookies(request))[name] };
1175
+ const jar = { read: (name) => (cookies ||= parseCookie(request.get("cookie") ?? ""))[name] };
1167
1176
  if (response) {
1168
1177
  const { headers, errHeaders } = response;
1169
1178
  jar.written = (name) => setCookieValues(headers.getSetCookie())[name];
1170
1179
  jar.write = (name, value, attrs) => {
1171
1180
  setCookie(headers, {
1181
+ ...attrs,
1172
1182
  name,
1173
- value,
1174
- ...attrs
1183
+ value
1175
1184
  });
1176
1185
  if (errHeaders) mirrorSetCookie(name, {
1177
1186
  from: headers,
@@ -1181,19 +1190,24 @@ function webCookieJar(request, response) {
1181
1190
  }
1182
1191
  return jar;
1183
1192
  }
1184
- function requestCookies(request) {
1185
- const header = request.get("cookie");
1186
- return header ? parse(header) : Object.create(null);
1193
+ function parseCookie(header) {
1194
+ return parse(header);
1187
1195
  }
1188
- function setCookie(headers, cookie) {
1189
- const line = serialize({
1196
+ function serializeCookie(name, value, attrs) {
1197
+ return serialize({
1190
1198
  path: "/",
1191
- ...cookie
1199
+ ...attrs,
1200
+ name,
1201
+ value
1192
1202
  });
1203
+ }
1204
+ function setCookie(headers, cookie) {
1205
+ const { name, value, ...attrs } = cookie;
1206
+ const line = serializeCookie(name, value, attrs);
1193
1207
  const key = cookieKey(cookie);
1194
1208
  const kept = headers.getSetCookie().filter((existing) => {
1195
1209
  const parsed = parseSetCookie(existing);
1196
- return parsed ? cookieKey(parsed) !== key : setCookieName(existing) !== cookie.name;
1210
+ return parsed ? cookieKey(parsed) !== key : setCookieName(existing) !== name;
1197
1211
  });
1198
1212
  headers.delete("set-cookie");
1199
1213
  for (const existing of kept) headers.append("set-cookie", existing);
@@ -1208,33 +1222,36 @@ function cookieKey(cookie) {
1208
1222
  cookie.partitioned ? "1" : "0"
1209
1223
  ].join(";");
1210
1224
  }
1211
- const PREFIX = "[unjwt/session]";
1212
1225
  const requestStates = /* @__PURE__ */ new WeakMap();
1213
- const webSessionDriver = {
1214
- prefix: PREFIX,
1215
- state: (event) => {
1216
- if (isHanded(event)) return sessionsOf(event);
1217
- if (event.context) return contextState(event.context);
1218
- return sessionsOf(event.req ? webRequest(event.req) : event);
1219
- },
1220
- header: (event, name) => isHanded(event) ? void 0 : requestHeaders(event).get(name),
1221
- cookies: (event) => isHanded(event) ? { read: () => void 0 } : webCookieJar(requestHeaders(event), event.res),
1222
- handed: (event) => isHanded(event) ? { token: event.token } : void 0
1223
- };
1224
- function isHanded(event) {
1226
+ function webSessionDriver(options) {
1227
+ const prefix = options?.prefix ?? "[unjwt/session]";
1228
+ return {
1229
+ prefix,
1230
+ state: (event) => {
1231
+ if (isHanded(event, prefix)) return sessionsOf(event);
1232
+ if (event.context) return contextState(event.context);
1233
+ return sessionsOf(event.req ? webRequest(event.req, prefix) : event);
1234
+ },
1235
+ header: (event, name) => isHanded(event, prefix) ? void 0 : requestHeaders(event, prefix).get(name),
1236
+ cookies: (event) => isHanded(event, prefix) ? { read: () => void 0 } : webCookieJar(requestHeaders(event, prefix), event.res),
1237
+ handed: (event) => isHanded(event, prefix) ? { token: event.token } : void 0
1238
+ };
1239
+ }
1240
+ const defaultWebSessionDriver = /* @__PURE__ */ webSessionDriver();
1241
+ function isHanded(event, prefix) {
1225
1242
  if ("req" in event || "headers" in event) return false;
1226
1243
  if ("token" in event) return true;
1227
- throw invalidConfigError(`${PREFIX} use() got neither a request event nor a token.`);
1244
+ throw invalidConfigError(`${prefix} use() got neither a request event nor a token.`);
1228
1245
  }
1229
- function requestHeaders(event) {
1230
- if (event.req) return webRequest(event.req).headers;
1246
+ function requestHeaders(event, prefix) {
1247
+ if (event.req) return webRequest(event.req, prefix).headers;
1231
1248
  if (event.headers) return event.headers;
1232
- throw invalidConfigError(`${PREFIX} use() got a request event with neither "req" nor "headers".`);
1249
+ throw invalidConfigError(`${prefix} use() got a request event with neither "req" nor "headers".`);
1233
1250
  }
1234
- function webRequest(req) {
1251
+ function webRequest(req, prefix) {
1235
1252
  const headers = req.headers;
1236
1253
  if (typeof headers === "object" && headers !== null && "get" in headers) return req;
1237
- throw invalidConfigError(`${PREFIX} use() got an event whose "req" is not a web Request.`);
1254
+ throw invalidConfigError(`${prefix} use() got an event whose "req" is not a web Request.`);
1238
1255
  }
1239
1256
  function contextState(context) {
1240
1257
  const existing = context.unjwt;
@@ -1251,7 +1268,7 @@ function sessionsOf(request) {
1251
1268
  return created;
1252
1269
  }
1253
1270
  function defineJWSSession(config, options) {
1254
- const driver = options?.driver ?? webSessionDriver;
1271
+ const driver = options?.driver ?? defaultWebSessionDriver;
1255
1272
  return defineSession(config, {
1256
1273
  driver,
1257
1274
  codec: createJWSCodec({
@@ -1357,19 +1374,12 @@ function encryptionBits(enc) {
1357
1374
  const bits = Number(enc.slice(1, 4));
1358
1375
  return enc.includes("CBC") ? bits * 2 : bits;
1359
1376
  }
1360
- const JWE_DEFAULT_COOKIE = {
1361
- path: "/",
1362
- secure: true,
1363
- httpOnly: true,
1364
- sameSite: "lax"
1365
- };
1366
1377
  function createJWECodec(options) {
1367
1378
  const { prefix, defaultName } = options;
1368
1379
  return createSessionCodec({
1369
1380
  family: "jwe",
1370
1381
  prefix,
1371
1382
  defaultName,
1372
- defaultCookie: JWE_DEFAULT_COOKIE,
1373
1383
  issueOptions: (config) => config.jwe?.encryptOptions,
1374
1384
  openOptions: (config) => config.jwe?.decryptOptions,
1375
1385
  resolveKeys: (config) => resolveSessionKeys(config.key, {
@@ -1400,7 +1410,7 @@ function sessionMaxIterations(config) {
1400
1410
  return typeof alg === "string" && alg.startsWith("PBES2") ? config.jwe?.encryptOptions?.p2c ?? 6e5 : void 0;
1401
1411
  }
1402
1412
  function defineJWESession(config, options) {
1403
- const driver = options?.driver ?? webSessionDriver;
1413
+ const driver = options?.driver ?? defaultWebSessionDriver;
1404
1414
  return defineSession(config, {
1405
1415
  driver,
1406
1416
  codec: createJWECodec({
@@ -1412,4 +1422,4 @@ function defineJWESession(config, options) {
1412
1422
  function defineSessionDriver(driver) {
1413
1423
  return driver;
1414
1424
  }
1415
- export { defineJWESession, defineJWSSession, defineSessionDriver, deriveJWKFromPassword, exportPEM, generateJWK, importPEM, thumbprint, toPublicJWK };
1425
+ export { DEFAULT_SESSION_COOKIE, assertSessionConfig, assertSessionHeader, assertSessionTokenSource, assertSessionWritable, claimSessionName, defineJWESession, defineJWSSession, defineSessionDriver, deriveJWKFromPassword, exportPEM, generateJWK, importPEM, parseCookie, resolveSessionTransport, serializeCookie, sessionTransportOptions, thumbprint, toPublicJWK, webCookieJar, webSessionDriver };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unjwt",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "A collection of JWT utilities based on the Web Crypto API.",
5
5
  "keywords": [
6
6
  "auth",