unjwt 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,7 @@ import { UnsecureError, UnsecureErrorCode } from "./libs/unsecure.mjs";
13
13
  * {@link isJWTError} takes this type, so a misspelled code such as `"ERR_JWS_INVALD"` is a type error.
14
14
  * Use {@link JWTErrorCode} for a value that may hold a code outside this union.
15
15
  */
16
- export type KnownJWTErrorCode = "ERR_JWT_EXPIRED" | "ERR_JWT_NBF" | "ERR_JWT_CLAIM_INVALID" | "ERR_JWT_CLAIM_MISSING" | "ERR_JWS_INVALID" | "ERR_JWS_INVALID_SERIALIZATION" | "ERR_JWS_SIGNATURE_INVALID" | "ERR_JWS_ALG_NOT_ALLOWED" | "ERR_JWS_ALG_MISSING" | "ERR_JWS_SIGNER_ALG_INFERENCE" | "ERR_JWS_NO_MATCHING_SIGNER" | "ERR_JWS_HEADER_PARAMS_NOT_DISJOINT" | "ERR_JWS_B64_INCONSISTENT" | "ERR_JWE_INVALID" | "ERR_JWE_INVALID_SERIALIZATION" | "ERR_JWE_DECRYPTION_FAILED" | "ERR_JWE_ALG_NOT_ALLOWED" | "ERR_JWE_ALG_MISSING" | "ERR_JWE_ALG_FORBIDDEN_IN_MULTI" | "ERR_JWE_ENC_MISSING" | "ERR_JWE_RECIPIENT_ALG_INFERENCE" | "ERR_JWE_NO_MATCHING_RECIPIENT" | "ERR_JWE_HEADER_PARAMS_NOT_DISJOINT" | "ERR_JWK_INVALID" | "ERR_JWK_KEY_NOT_FOUND" | "ERR_JWK_UNSUPPORTED" | "ERR_DPOP_INVALID" | "ERR_DPOP_CLAIM_INVALID" | "ERR_DPOP_NONCE_INVALID" | "ERR_DPOP_REPLAY" | "ERR_SESSION_TOKEN_INVALID" | "ERR_SESSION_READ_FAILED" | "ERR_SESSION_READONLY_EVENT" | "ERR_SESSION_KEY_INVALID" | "ERR_SESSION_CONFIG_INVALID" | "ERR_INVALID_OPTION";
16
+ export type KnownJWTErrorCode = "ERR_JWT_EXPIRED" | "ERR_JWT_NBF" | "ERR_JWT_CLAIM_INVALID" | "ERR_JWT_CLAIM_MISSING" | "ERR_JWS_INVALID" | "ERR_JWS_INVALID_SERIALIZATION" | "ERR_JWS_SIGNATURE_INVALID" | "ERR_JWS_ALG_NOT_ALLOWED" | "ERR_JWS_ALG_MISSING" | "ERR_JWS_SIGNER_ALG_INFERENCE" | "ERR_JWS_NO_MATCHING_SIGNER" | "ERR_JWS_HEADER_PARAMS_NOT_DISJOINT" | "ERR_JWS_B64_INCONSISTENT" | "ERR_JWE_INVALID" | "ERR_JWE_INVALID_SERIALIZATION" | "ERR_JWE_DECRYPTION_FAILED" | "ERR_JWE_ALG_NOT_ALLOWED" | "ERR_JWE_ALG_MISSING" | "ERR_JWE_ALG_FORBIDDEN_IN_MULTI" | "ERR_JWE_ENC_MISSING" | "ERR_JWE_RECIPIENT_ALG_INFERENCE" | "ERR_JWE_NO_MATCHING_RECIPIENT" | "ERR_JWE_HEADER_PARAMS_NOT_DISJOINT" | "ERR_JWK_INVALID" | "ERR_JWK_KEY_NOT_FOUND" | "ERR_JWK_UNSUPPORTED" | "ERR_DPOP_INVALID" | "ERR_DPOP_CLAIM_INVALID" | "ERR_DPOP_NONCE_INVALID" | "ERR_DPOP_REPLAY" | "ERR_SESSION_INVALID" | "ERR_SESSION_TOKEN_INVALID" | "ERR_SESSION_READ_FAILED" | "ERR_SESSION_READONLY_EVENT" | "ERR_SESSION_KEY_INVALID" | "ERR_SESSION_CONFIG_INVALID" | "ERR_INVALID_OPTION";
17
17
  /**
18
18
  * A {@link JWTError} `code`: any {@link KnownJWTErrorCode} or any other string.
19
19
  * A `JWTError` built outside unjwt can hold a code outside the union.
@@ -17,7 +17,16 @@ type SessionData<T extends SessionRecord = SessionClaims> = StrictOmit<T, "jti"
17
17
  type SessionSource = "cookie" | "header" | "token";
18
18
  /** Why `onUpdate` ran: an `update()` call, or `rolling` reissuing a session that was read. */
19
19
  type SessionUpdateTrigger = "update" | "rolling";
20
- /** What `update()` merges: a partial data object, or a function of the current data that returns one or `undefined`. */
20
+ /** Why `onError` ran: `"read"` when the request's token could not be opened, else the {@link SessionUpdateTrigger} of the update that failed. */
21
+ type SessionErrorTrigger = "read" | SessionUpdateTrigger;
22
+ /** The roll `onRead` receives: whether `rolling` reissues the session after the hook, and a way to keep its token on this request. */
23
+ interface SessionRoll {
24
+ /** `true` when `rolling` reissues this session after the hook. An `update()` in the hook is that reissue, and a `clear()` cancels it. */
25
+ readonly due: boolean;
26
+ /** Keeps `rolling` from reissuing the session after the hook on this request. The next request finds the roll due again. Does nothing when no roll is due, or once the hook has returned. */
27
+ readonly skip: () => void;
28
+ }
29
+ /** What `update()` merges: a partial data object, or a function of the current data that returns one, or changes that data in place and returns `undefined`. */
21
30
  type SessionUpdate<T extends SessionRecord = SessionClaims> = Partial<SessionData<T>> | ((oldData: SessionData<T>) => Partial<SessionData<T>> | undefined);
22
31
  /** The session of one request. Its getters read the current state of that request. */
23
32
  interface SessionManager<T extends SessionRecord = SessionClaims, ConfigMaxAge extends ExpiresIn | undefined = ExpiresIn | undefined> {
@@ -54,10 +63,10 @@ interface SessionManager<T extends SessionRecord = SessionClaims, ConfigMaxAge e
54
63
  /**
55
64
  * Merges `update` into the data and issues a token with a fresh `jti` and `iat`. `options.maxAge` overrides the config's `maxAge` for this token.
56
65
  * A `"cookie"` session writes the token to the response's cookie. A `"header"` or `"token"` session writes nothing: the token is on `token` alone, for you to send. Later requests read the data back through JSON.
57
- * If issuing or the cookie write fails, the session is restored, `onError` runs and the call rejects. A throwing `onUpdate` rejects the call and keeps the new session.
66
+ * A failed update rejects and leaves the session as it was. When issuing or the cookie write fails, `onError` runs before it rejects. A throwing `onUpdate` cancels the update the same way, without `onError`.
58
67
  * @throws `ERR_SESSION_READONLY_EVENT` if a `"cookie"` session has no response, or the response was already sent.
59
- * @throws `ERR_INVALID_OPTION` if the update is not a plain object or holds `__proto__`, `constructor` or `prototype`.
60
- * @throws `ERR_SESSION_CONFIG_INVALID` if a `"cookie"` session's token needs more than 16 cookie chunks.
68
+ * @throws `ERR_INVALID_OPTION` if the update is not a plain object or holds `__proto__`, `constructor` or `prototype`, or `options.maxAge` is not a valid duration.
69
+ * @throws `ERR_SESSION_INVALID` if a `"cookie"` session's token needs more than 16 cookie chunks.
61
70
  */
62
71
  update: (update?: SessionUpdate<T>, options?: SessionIssueOptions) => Promise<SessionManager<T, ConfigMaxAge>>;
63
72
  /**
@@ -94,7 +103,9 @@ interface SessionBaseConfig<MaxAge extends ExpiresIn | undefined = ExpiresIn | u
94
103
  /** Session ID (`jti`) generator. @default crypto.randomUUID */
95
104
  generateId?: () => string;
96
105
  }
97
- /** Lifecycle hooks. `onRead`, `onExpire` and `onError` are mutually exclusive per token. `session` is the live manager, open to `update()` and `clear()`. `oldSession` is a snapshot. */
106
+ /** The config a session holds once defined, as its hooks receive it: each property is read-only at every object level. Arrays, functions and `CryptoKey`, `Date` and typed array values keep their types. */
107
+ type ReadonlyConfig<T> = { readonly [K in keyof T]: ReadonlyConfigValue<T[K]>; };
108
+ /** Lifecycle hooks. A read ends in exactly one of `onRead`, `onExpire` and `onError`. In `onRead`, `session` is the live manager, open to `update()` and `clear()`. `onUpdate` and `onError` get copies, and every `oldSession` is a copy. */
98
109
  interface SessionHooks<TSession, TManager, TArgs extends Record<string, unknown>, TConfig> {
99
110
  /** Runs once per request, after a token is read and verified. A throw empties the session and rejects the read. */
100
111
  onRead?: (args: TArgs & {
@@ -102,11 +113,14 @@ interface SessionHooks<TSession, TManager, TArgs extends Record<string, unknown>
102
113
  id: string;
103
114
  token: string;
104
115
  };
116
+ /** Whether `rolling` reissues this session after the hook, and `skip()` to hold that reissue back on this request. */
117
+ roll: SessionRoll;
105
118
  config: TConfig;
106
119
  }) => void | Promise<void>;
107
- /** Runs after `update()` or `rolling` issues a token and, on a cookie session, writes its cookie. A throw rejects the call and keeps the new session. */
120
+ /** Runs after `update()` or `rolling` issues a token, before it is written. A throw cancels the update: nothing is written, the session stays as it was, and the call rejects. */
108
121
  onUpdate?: (args: TArgs & {
109
- session: TManager & {
122
+ /** The session just issued, its data deep-copied. */
123
+ session: TSession & {
110
124
  id: string;
111
125
  token: string;
112
126
  };
@@ -134,12 +148,28 @@ interface SessionHooks<TSession, TManager, TArgs extends Record<string, unknown>
134
148
  config: TConfig;
135
149
  }) => void | Promise<void>;
136
150
  /**
137
- * Runs when a token fails to open for a reason other than expiry, and when an issue or cookie write fails. The session is empty after a read.
151
+ * Runs when the request's token fails to open for a reason other than expiry, and when an `update()` or a roll fails to issue or write its token. `trigger` names which.
152
+ * `session` and `oldSession` are copies, and `use(event)` returns the live session. The session is empty after a read.
138
153
  * - `ERR_SESSION_TOKEN_INVALID`: the token failed its checks, or `onKeyLookup` found no key for it. Its cookie is expired.
139
- * - `ERR_SESSION_READ_FAILED`: `onKeyLookup` threw, its error as `cause`, or returned a key that cannot be used, an `ERR_JWK_INVALID` as `cause`. The cookie is kept, and `clear()` expires it.
154
+ * - `ERR_SESSION_READ_FAILED`: `onKeyLookup` threw, its error as `cause`, or returned a key that cannot be used, an `ERR_JWK_INVALID` as `cause`. The cookie is kept, and `clear()` on the session `use(event)` returns expires it.
140
155
  */
141
- onError?: (args: TArgs & {
142
- session: TManager;
156
+ onError?: (args: TArgs & ({
157
+ /** The session the failed update produced, its data deep-copied. `token` is set when `onUpdate` ran for it. */
158
+ session: TSession & {
159
+ id: string;
160
+ };
161
+ /** The session before the update, its data deep-copied. */
162
+ oldSession: TSession;
163
+ /** The operation that failed: an `update()` call, or the reissue by `rolling`. */
164
+ trigger: SessionUpdateTrigger;
165
+ } | {
166
+ /** An empty copy of the session, with the read's `error`. */
167
+ session: TSession;
168
+ /** `undefined`: a read has no session before it. */
169
+ oldSession: undefined;
170
+ /** The operation that failed: the read of the request's token. */
171
+ trigger: "read";
172
+ }) & {
143
173
  error: any;
144
174
  config: TConfig;
145
175
  }) => void | Promise<void>;
@@ -175,6 +205,7 @@ interface SessionDefinition<T extends SessionRecord = SessionClaims, MaxAge exte
175
205
  * Reads the request's session and returns its manager. Later calls on the same request share that state.
176
206
  * @throws `ERR_SESSION_CONFIG_INVALID` if the session has no token source, or a session of the other family uses its name on this request.
177
207
  * @throws `ERR_SESSION_KEY_INVALID` if a configured key fails to import.
208
+ * @throws `ERR_SESSION_INVALID` if the event is not one the driver can use, or a roll issues a token that needs more than 16 cookie chunks.
178
209
  */
179
210
  use: (event: TEvent) => Promise<SessionManager<T, MaxAge>>;
180
211
  /**
@@ -200,7 +231,7 @@ interface SessionRequestBase {
200
231
  readonly headers: Headers;
201
232
  readonly errHeaders?: Headers;
202
233
  };
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. */
234
+ /** 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
235
  readonly context?: Record<string, unknown>;
205
236
  }
206
237
  /** A request event, by its `Request` or by its `Headers` (a `Request` itself, a WebSocket upgrade request). `req` is read first. */
@@ -215,8 +246,9 @@ type SessionRequestEvent = SessionRequestBase & ({
215
246
  interface SessionHandedToken {
216
247
  token: string | undefined;
217
248
  }
218
- /** What the Web session driver accepts: a request event, or a token in hand. Any other shape throws `ERR_SESSION_CONFIG_INVALID`. */
249
+ /** What the Web session driver accepts: a request event, or a token in hand. Any other shape throws `ERR_SESSION_INVALID`. */
219
250
  type WebSessionEvent = SessionRequestEvent | SessionHandedToken;
251
+ type ReadonlyConfigValue<V> = V extends ((...args: never) => unknown) | CryptoKey | Date | ArrayBufferView ? V : V extends readonly unknown[] ? { [K in keyof V]: ReadonlyConfigValue<V[K]>; } : V extends object ? ReadonlyConfig<V> : V;
220
252
  /**
221
253
  * A symmetric key that signs and verifies, an HMAC `oct` JWK or a secret `CryptoKey`, a JWK Set of HMAC `oct` JWKs, or a pair:
222
254
  * `privateKey` signs and `publicKey` (a JWK, a JWK list, a JWK Set or a `CryptoKey`) verifies. A key without `alg` (a `CryptoKey`, an
@@ -231,6 +263,8 @@ type JWSSessionKey = CryptoKey | JWK_oct<JWK_HMAC> | JWKSet<JWK_oct<JWK_HMAC>[]>
231
263
  privateKey: JWSAsymmetricPrivateJWK | CryptoKey;
232
264
  publicKey: JWSAsymmetricPublicJWK | JWSAsymmetricPublicJWK[] | JWKSet | CryptoKey;
233
265
  };
266
+ /** A copy of `value` whose plain objects and arrays are copied and frozen at every level. Functions and other objects stay as they are. */
267
+ export declare function frozenCopy<T>(value: T): T;
234
268
  interface ExpirableCookieAttributes {
235
269
  expires?: Date;
236
270
  maxAge?: number;
@@ -261,6 +295,96 @@ interface SessionDriver<TEvent> {
261
295
  token: string | undefined;
262
296
  } | undefined;
263
297
  }
298
+ /** The `Set-Cookie` attributes of a session cookie (RFC 6265, RFC 6265bis, and CHIPS: Cookies Having Independent Partitioned State). */
299
+ interface CookieAttributes {
300
+ /** `Domain`. A `__Host-` name refuses it. */
301
+ domain?: string;
302
+ /** `Expires`. An issued token with an `exp` sets it to that time. */
303
+ expires?: Date;
304
+ /** `HttpOnly`: page scripts cannot read the cookie. */
305
+ httpOnly?: boolean;
306
+ /** `Max-Age` in seconds. An issued token with an `exp` replaces it with `expires`. */
307
+ maxAge?: number;
308
+ /** `Path`. A `__Host-` name requires `"/"`. */
309
+ path?: string;
310
+ /** `Priority`. */
311
+ priority?: "low" | "medium" | "high";
312
+ /** `SameSite`. `true` is `Strict`, and `"none"` requires `secure`. */
313
+ sameSite?: boolean | "lax" | "strict" | "none";
314
+ /** `Secure`. A `__Host-` or `__Secure-` name requires it. */
315
+ secure?: boolean;
316
+ /** `Partitioned` (CHIPS). Requires `secure`. */
317
+ partitioned?: boolean;
318
+ }
319
+ /** The attributes a session cookie gets unless its config sets them: `path` `"/"`, `secure`, `httpOnly` and `sameSite` `"lax"`. */
320
+ export declare const DEFAULT_SESSION_COOKIE: Readonly<CookieAttributes>;
321
+ /** The token sources of one session, and the attributes its cookie is written with. */
322
+ interface SessionTransportOptions {
323
+ /** Session name: the cookie name, and the base of the default session header. */
324
+ name: string;
325
+ /** Request header to read the token from, before the cookie: a name, or `true` for `x-<name>-session`. Unset or `false` reads no header. */
326
+ sessionHeader?: boolean | string;
327
+ /** The cookie's attributes and the chunk size its token is split at (`4000` when unset), or `false` for no cookie. */
328
+ cookie: false | {
329
+ attrs: CookieAttributes;
330
+ chunkMaxLength?: number;
331
+ };
332
+ }
333
+ /** How one session reads its token from the request and writes it to the response. */
334
+ interface SessionTransport {
335
+ /** Where the token is read from: the driver's token in hand, the session header, or the cookie. Only a cookie is written back. */
336
+ source: SessionSource;
337
+ /** The token in hand, else the session header's, else the cookie this response wrote, else the request's. Absent without a token source. */
338
+ read?: () => string | undefined;
339
+ /** 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. */
340
+ malformed?: {
341
+ header: string;
342
+ };
343
+ /**
344
+ * Writes the token to the cookie, split into chunks past `chunkMaxLength`, and returns `true`. With `expiresAt` (ms), the cookie gets that `expires` and no
345
+ * `maxAge`, which would take precedence (RFC 6265 §5.3). Once the response was sent, writes nothing and returns `false`.
346
+ * Present when the session reads its token from a cookie the event can write.
347
+ * @throws `ERR_SESSION_INVALID` if the token needs more than 16 cookie chunks.
348
+ */
349
+ write?: (token: string, options?: {
350
+ expiresAt?: number;
351
+ }) => boolean;
352
+ /**
353
+ * Checks that `write` can take the token, and writes nothing. Present when `write` is.
354
+ * @throws `ERR_SESSION_INVALID` if the token needs more than 16 cookie chunks.
355
+ */
356
+ assertFits?: (token: string) => void;
357
+ /**
358
+ * Expires the cookie and every chunk, and returns `true`. Once the response was sent, writes nothing and returns `false`.
359
+ * Present when the session reads its token from a cookie the event can write.
360
+ */
361
+ drop?: () => boolean;
362
+ /** The session reads its token from a cookie, but the event cannot write one (a request without a response). */
363
+ readonly: boolean;
364
+ /** Whether the response can still change. `false` once it was sent. */
365
+ writable: () => boolean;
366
+ }
367
+ /**
368
+ * 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.
369
+ * 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.
370
+ * A token in hand or a header token is reissued onto the session only. Only a cookie session writes back.
371
+ */
372
+ export declare function resolveSessionTransport<TEvent>(driver: SessionDriver<TEvent>, event: TEvent, options: SessionTransportOptions): SessionTransport;
373
+ /** Throws `ERR_SESSION_CONFIG_INVALID` for a transport without a reader: the session has no token source to read or clear. */
374
+ export declare function assertSessionTokenSource(transport: SessionTransport, options: {
375
+ prefix: string;
376
+ }): asserts transport is SessionTransport & {
377
+ read: () => string | undefined;
378
+ };
379
+ /** 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. */
380
+ export declare function assertSessionWritable(transport: SessionTransport, options: {
381
+ operation: "update" | "clear";
382
+ prefix: string;
383
+ }): void;
384
+ /** Refuses a `sessionHeader` that is neither a boolean nor a header name (RFC 7230 §3.2). */
385
+ export declare function assertSessionHeader(header: unknown, options: {
386
+ prefix: string;
387
+ }): void;
264
388
  /**
265
389
  * 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
390
  * without `alg`) verifies, and with `onKeyLookup` set, may also name those only the keys it returns serve. The session sets `typ`,
@@ -292,33 +416,12 @@ interface JWSSessionConfig<MaxAge extends ExpiresIn | undefined = ExpiresIn | un
292
416
  */
293
417
  jws?: JWSSessionOptions;
294
418
  }
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
419
  /** Session state for the current request, from the claims of its JWS token. */
317
420
  interface SessionJWS<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined> extends SessionState<T, MaxAge> {}
318
- /** JWS session hooks. `onRead`, `onExpire` and `onError` are mutually exclusive per token. */
421
+ /** JWS session hooks. A read ends in exactly one of `onRead`, `onExpire` and `onError`. */
319
422
  interface SessionHooksJWS<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TEvent = WebSessionEvent> extends SessionHooks<SessionJWS<T, MaxAge>, SessionManager<T, MaxAge>, {
320
423
  event: TEvent;
321
- }, SessionConfigJWS<T, MaxAge, TEvent>> {
424
+ }, ReadonlyConfig<SessionConfigJWS<T, MaxAge, TEvent>>> {
322
425
  /** Returns the key that verifies an incoming token. See {@link JWSSessionKeyLookup}. */
323
426
  onKeyLookup?: JWSSessionKeyLookup<T, MaxAge, TEvent>;
324
427
  }
@@ -335,7 +438,7 @@ interface SessionHooksJWS<T extends SessionRecord = SessionClaims, MaxAge extend
335
438
  */
336
439
  type JWSSessionKeyLookup<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TEvent = WebSessionEvent> = (lookup: {
337
440
  header: JWSHeaderParameters;
338
- config: SessionConfigJWS<T, MaxAge, TEvent>;
441
+ config: ReadonlyConfig<SessionConfigJWS<T, MaxAge, TEvent>>;
339
442
  event?: TEvent;
340
443
  }) => JWSVerifyJWK | CryptoKey | JWKSet | undefined | Promise<JWSVerifyJWK | CryptoKey | JWKSet | undefined>;
341
444
  /** Configuration of a JWS session, signed but not encrypted. */
@@ -346,14 +449,14 @@ interface SessionConfigJWS<T extends SessionRecord = SessionClaims, MaxAge exten
346
449
  hooks?: SessionHooksJWS<T, MaxAge, TEvent>;
347
450
  }
348
451
  /**
349
- * Defines a JWS session once: signed, not encrypted, so clients can read its data. `use`, `token`, `issue` and `open` serve every request.
452
+ * Defines a JWS session once: signed, not encrypted, so clients can read its data. `use`, `token`, `issue` and `open` serve every request. It keeps a frozen copy of `config`, so later changes to the object passed have no effect.
350
453
  * @throws `ERR_SESSION_CONFIG_INVALID` if `name`, `sessionHeader`, a `__Host-` or `__Secure-` cookie's attributes or `rolling` is invalid, or a key that names no algorithm (a `CryptoKey`, an `oct` JWK without `alg`, a pair when neither half names one) lacks a pinned `jws.signOptions.alg`, or `jws.verifyOptions.algorithms` is empty or lists one no key serves without `onKeyLookup`, or names none of those a `publicKey` entry without `alg` carries, or a token of a JWK Set `key` entry without `kid` meets more than `jws.verifyOptions.limits.keyCandidates` entries.
351
454
  * @throws `ERR_SESSION_KEY_INVALID` if `key` is missing, a string, a lone asymmetric key, not an HMAC `oct` JWK, a JWK Set of them, a secret `CryptoKey` or a key pair, an empty JWK Set or one holding a key that is not an `oct` JWK, a pair with a half that is not what it is named, serves no JWS algorithm, or cannot serve the pinned `alg` or, for a pair's half that names none, the one its other half names, or a pair whose single `publicKey` names another algorithm than the one it issues under, or whose `publicKey` list or JWK Set holds no key that serves it.
352
455
  * @throws `ERR_INVALID_OPTION` if `rolling` is neither `true` nor a duration.
353
456
  */
354
457
  export declare function defineJWSSession<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TEvent extends WebSessionEvent = WebSessionEvent>(config: SessionConfigJWS<T, MaxAge, TEvent>): SessionDefinition<T, MaxAge, TEvent>;
355
458
  /**
356
- * Defines a JWS session over a framework driver of your own, passed as `{ driver }`.
459
+ * Defines a JWS session over a framework driver of your own, passed as `{ driver }`. It keeps a frozen copy of `config`, so later changes to the object passed have no effect.
357
460
  * @throws `ERR_SESSION_CONFIG_INVALID` if `name`, `sessionHeader`, a `__Host-` or `__Secure-` cookie's attributes or `rolling` is invalid, or a key that names no algorithm (a `CryptoKey`, an `oct` JWK without `alg`, a pair when neither half names one) lacks a pinned `jws.signOptions.alg`, or `jws.verifyOptions.algorithms` is empty or lists one no key serves without `onKeyLookup`, or names none of those a `publicKey` entry without `alg` carries, or a token of a JWK Set `key` entry without `kid` meets more than `jws.verifyOptions.limits.keyCandidates` entries.
358
461
  * @throws `ERR_SESSION_KEY_INVALID` if `key` is missing, a string, a lone asymmetric key, not an HMAC `oct` JWK, a JWK Set of them, a secret `CryptoKey` or a key pair, an empty JWK Set or one holding a key that is not an `oct` JWK, a pair with a half that is not what it is named, serves no JWS algorithm, or cannot serve the pinned `alg` or, for a pair's half that names none, the one its other half names, or a pair whose single `publicKey` names another algorithm than the one it issues under, or whose `publicKey` list or JWK Set holds no key that serves it.
359
462
  * @throws `ERR_INVALID_OPTION` if `rolling` is neither `true` nor a duration.
@@ -406,10 +509,10 @@ interface JWESessionConfig<MaxAge extends ExpiresIn | undefined = ExpiresIn | un
406
509
  }
407
510
  /** Session state for the current request, from the claims of its JWE token. */
408
511
  interface SessionJWE<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined> extends SessionState<T, MaxAge> {}
409
- /** JWE session hooks. `onRead`, `onExpire` and `onError` are mutually exclusive per token. */
512
+ /** JWE session hooks. A read ends in exactly one of `onRead`, `onExpire` and `onError`. */
410
513
  interface SessionHooksJWE<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TEvent = WebSessionEvent> extends SessionHooks<SessionJWE<T, MaxAge>, SessionManager<T, MaxAge>, {
411
514
  event: TEvent;
412
- }, SessionConfigJWE<T, MaxAge, TEvent>> {
515
+ }, ReadonlyConfig<SessionConfigJWE<T, MaxAge, TEvent>>> {
413
516
  /** Returns the key that decrypts an incoming token. See {@link JWESessionKeyLookup}. */
414
517
  onKeyLookup?: JWESessionKeyLookup<T, MaxAge, TEvent>;
415
518
  }
@@ -426,7 +529,7 @@ interface SessionHooksJWE<T extends SessionRecord = SessionClaims, MaxAge extend
426
529
  */
427
530
  type JWESessionKeyLookup<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TEvent = WebSessionEvent> = (lookup: {
428
531
  header: JWEHeaderParameters;
429
- config: SessionConfigJWE<T, MaxAge, TEvent>;
532
+ config: ReadonlyConfig<SessionConfigJWE<T, MaxAge, TEvent>>;
430
533
  event?: TEvent;
431
534
  }) => JWEDecryptJWK | CryptoKey | JWKSet | undefined | Promise<JWEDecryptJWK | CryptoKey | JWKSet | undefined>;
432
535
  /** Configuration of an encrypted JWE session. */
@@ -437,14 +540,14 @@ interface SessionConfigJWE<T extends SessionRecord = SessionClaims, MaxAge exten
437
540
  hooks?: SessionHooksJWE<T, MaxAge, TEvent>;
438
541
  }
439
542
  /**
440
- * Defines a JWE session once: encrypted, so clients cannot read its data. `use`, `token`, `issue` and `open` serve every request.
543
+ * Defines a JWE session once: encrypted, so clients cannot read its data. `use`, `token`, `issue` and `open` serve every request. It keeps a frozen copy of `config`, so later changes to the object passed have no effect.
441
544
  * @throws `ERR_SESSION_CONFIG_INVALID` if `name`, `sessionHeader`, a `__Host-` or `__Secure-` cookie's attributes or `rolling` is invalid, or a key that names no algorithm (a `CryptoKey`, an `oct` JWK without `alg`) lacks a pinned `jwe.encryptOptions.alg` and `enc`, or `jwe.decryptOptions.algorithms` is empty or lists one no key serves without `onKeyLookup`, or a token of a JWK Set `key` entry without `kid` meets more than `jwe.decryptOptions.limits.keyCandidates` entries.
442
545
  * @throws `ERR_SESSION_KEY_INVALID` if `key` is missing, a string, asymmetric, not an `oct` JWK, a JWK Set of them or a secret `CryptoKey`, an empty JWK Set or one holding a key that is not an `oct` JWK, serves no JWE algorithm, or cannot serve the pinned `alg` or `enc`.
443
546
  * @throws `ERR_INVALID_OPTION` if `rolling` is neither `true` nor a duration.
444
547
  */
445
548
  export declare function defineJWESession<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TEvent extends WebSessionEvent = WebSessionEvent>(config: SessionConfigJWE<T, MaxAge, TEvent>): SessionDefinition<T, MaxAge, TEvent>;
446
549
  /**
447
- * Defines a JWE session over a framework driver of your own, passed as `{ driver }`.
550
+ * Defines a JWE session over a framework driver of your own, passed as `{ driver }`. It keeps a frozen copy of `config`, so later changes to the object passed have no effect.
448
551
  * @throws `ERR_SESSION_CONFIG_INVALID` if `name`, `sessionHeader`, a `__Host-` or `__Secure-` cookie's attributes or `rolling` is invalid, or a key that names no algorithm (a `CryptoKey`, an `oct` JWK without `alg`) lacks a pinned `jwe.encryptOptions.alg` and `enc`, or `jwe.decryptOptions.algorithms` is empty or lists one no key serves without `onKeyLookup`, or a token of a JWK Set `key` entry without `kid` meets more than `jwe.decryptOptions.limits.keyCandidates` entries.
449
552
  * @throws `ERR_SESSION_KEY_INVALID` if `key` is missing, a string, asymmetric, not an `oct` JWK, a JWK Set of them or a secret `CryptoKey`, an empty JWK Set or one holding a key that is not an `oct` JWK, serves no JWE algorithm, or cannot serve the pinned `alg` or `enc`.
450
553
  * @throws `ERR_INVALID_OPTION` if `rolling` is neither `true` nor a duration.
@@ -452,6 +555,75 @@ export declare function defineJWESession<T extends SessionRecord = SessionClaims
452
555
  export declare function defineJWESession<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TEvent = WebSessionEvent>(config: SessionConfigJWE<T, MaxAge, TEvent>, options: {
453
556
  driver: SessionDriver<TEvent>;
454
557
  }): SessionDefinition<T, MaxAge, TEvent>;
455
- /** Returns `driver` typed as a `SessionDriver`. Pass it to `defineJWSSession` or `defineJWESession` as `{ driver }`. */
558
+ /**
559
+ * Returns `driver` typed as a `SessionDriver`. Pass it to `defineJWSSession` or `defineJWESession` as `{ driver }`.
560
+ * The Web driver, `webSessionDriver()`, is the reference implementation of the contract.
561
+ */
456
562
  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 };
563
+ /**
564
+ * The Web session driver: a request event by its `Request` or its `Headers`, or a token in hand.
565
+ * `prefix` (default `"[unjwt/session]"`) starts the message of every error a session over it throws, the driver's own included.
566
+ * Every driver it returns shares the state of a request with the others.
567
+ */
568
+ export declare function webSessionDriver(options?: {
569
+ prefix?: string;
570
+ }): SessionDriver<WebSessionEvent>;
571
+ /**
572
+ * A cookie jar over a request's `Headers` and, when given, its response's: `read` parses the request's `Cookie` header,
573
+ * `write` sets a `Set-Cookie` line on `response.headers` through `serializeCookie`, replacing the line of the same cookie
574
+ * (name, domain, path, partition) instead of adding a second one, and `written` reads back what the response sets.
575
+ * Each write is copied to `errHeaders` too: h3 and Nitro send only these for a thrown handler.
576
+ * Without `response`, the jar only reads.
577
+ */
578
+ export declare function webCookieJar(request: Headers, response?: {
579
+ headers: Headers;
580
+ errHeaders?: Headers;
581
+ }): CookieJar<CookieAttributes>;
582
+ /** 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. */
583
+ export declare function parseCookie(header: string): Record<string, string | undefined>;
584
+ /** 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. */
585
+ export declare function serializeCookie(name: string, value: string, attrs?: CookieAttributes): string;
586
+ /** The members of a session config that {@link assertSessionConfig} checks. */
587
+ interface SessionConfigChecks {
588
+ /** Request header to read the token from: a name, or `true` for `x-<name>-session`. */
589
+ sessionHeader?: boolean | string;
590
+ /** Cookie attributes over the defaults, or `false` for no cookie. */
591
+ cookie?: false | (CookieAttributes & {
592
+ chunkMaxLength?: number;
593
+ });
594
+ /** Session lifetime. */
595
+ maxAge?: ExpiresIn;
596
+ /** 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. */
597
+ rolling?: true | ExpiresIn;
598
+ }
599
+ /**
600
+ * Checks what the config alone decides: its name, session header, cookie prefix over `defaultCookie`, and `rolling`.
601
+ * `defaultCookie` is {@link DEFAULT_SESSION_COOKIE} when unset.
602
+ * @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,
603
+ * 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`.
604
+ * @throws `ERR_INVALID_OPTION` if `rolling` is neither `true` nor a duration.
605
+ */
606
+ export declare function assertSessionConfig(config: SessionConfigChecks, options: {
607
+ name: string;
608
+ prefix: string;
609
+ defaultCookie?: CookieAttributes;
610
+ }): void;
611
+ /**
612
+ * The transport options of a session config: its session header, and its cookie attributes over `defaultCookie` with `chunkMaxLength` set apart.
613
+ * `defaultCookie` is {@link DEFAULT_SESSION_COOKIE} when unset. `cookie: false` is no cookie.
614
+ */
615
+ export declare function sessionTransportOptions(config: Pick<SessionConfigChecks, "sessionHeader" | "cookie">, options: {
616
+ name: string;
617
+ defaultCookie?: CookieAttributes;
618
+ }): SessionTransportOptions;
619
+ /**
620
+ * 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.
621
+ * `state` is `driver.state(event)`. The same family may claim a name again on the same request.
622
+ * @throws `ERR_SESSION_CONFIG_INVALID` if another family already claimed `name` on this request. The message starts with `prefix`.
623
+ */
624
+ export declare function claimSessionName(state: Record<string, unknown>, options: {
625
+ name: string;
626
+ family: string;
627
+ prefix: string;
628
+ }): void;
629
+ 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 ReadonlyConfig, type SessionClaims, type SessionConfigChecks, type SessionConfigJWE, type SessionConfigJWS, type SessionData, type SessionDecryptOptions, type SessionDefinition, type SessionDriver, type SessionEncryptOptions, type SessionErrorTrigger, type SessionHandedToken, type SessionHooksJWE, type SessionHooksJWS, type SessionIssueOptions, type SessionJWE, type SessionJWS, type SessionManager, type SessionRequestEvent, type SessionRoll, 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 };
@@ -28,6 +28,9 @@ function invalidKeyError(message, cause) {
28
28
  function invalidConfigError(message, cause) {
29
29
  return new JWTError(message, "ERR_SESSION_CONFIG_INVALID", cause);
30
30
  }
31
+ function invalidSessionError(message) {
32
+ return new JWTError(message, "ERR_SESSION_INVALID");
33
+ }
31
34
  function invalidTokenError(message, cause) {
32
35
  return new JWTError(message, "ERR_SESSION_TOKEN_INVALID", cause);
33
36
  }
@@ -47,6 +50,9 @@ function expiredSessionError(options) {
47
50
  function readFailedError(cause, options) {
48
51
  return new JWTError(`${options.prefix} Session could not be read: ${messageOf(cause)}`, "ERR_SESSION_READ_FAILED", cause);
49
52
  }
53
+ function malformedHeaderError(header, options) {
54
+ return invalidTokenError(`${options.prefix} Invalid session token: the "${header}" header holds neither a token nor "Bearer <token>".`);
55
+ }
50
56
  function rethrowAsInvalidToken(error, options) {
51
57
  if (isJWTError(error, "ERR_JWT_EXPIRED") || isJWTError(error, "ERR_SESSION_READ_FAILED")) throw error;
52
58
  throw invalidTokenError(`${options.prefix} Invalid session token: ${messageOf(error)}`, error);
@@ -65,7 +71,7 @@ function assertCookiePrefix(name, attrs, options) {
65
71
  const host = lower.startsWith("__host-");
66
72
  if (!host && !lower.startsWith("__secure-")) return;
67
73
  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}.`);
74
+ if (!attrs.secure || host && (attrs.domain !== void 0 || attrs.path !== void 0 && attrs.path !== "/")) throw invalidConfigError(`${options.prefix} Session ${quoteTokenValue(name)}: a ${rule}.`);
69
75
  }
70
76
  function assertRolling(config, options) {
71
77
  const { rolling } = config;
@@ -161,7 +167,6 @@ function createSessionCodec(spec) {
161
167
  return {
162
168
  family: spec.family,
163
169
  defaultName: spec.defaultName,
164
- defaultCookie: spec.defaultCookie,
165
170
  now,
166
171
  keys: (config) => {
167
172
  const { issueKey, issueAlg, key, algorithms } = spec.resolveKeys(config);
@@ -503,19 +508,12 @@ const RSA_SIGNATURES = [
503
508
  "PS384",
504
509
  "PS512"
505
510
  ];
506
- const JWS_DEFAULT_COOKIE = {
507
- path: "/",
508
- secure: true,
509
- httpOnly: true,
510
- sameSite: "lax"
511
- };
512
511
  function createJWSCodec(options) {
513
512
  const { prefix, defaultName } = options;
514
513
  return createSessionCodec({
515
514
  family: "jws",
516
515
  prefix,
517
516
  defaultName,
518
- defaultCookie: JWS_DEFAULT_COOKIE,
519
517
  issueOptions: (config) => config.jws?.signOptions,
520
518
  openOptions: (config) => config.jws?.verifyOptions,
521
519
  resolveKeys: (config) => resolveSessionKeys$1(config.key, {
@@ -543,6 +541,12 @@ function createJWSCodec(options) {
543
541
  }
544
542
  });
545
543
  }
544
+ const DEFAULT_SESSION_COOKIE = /* @__PURE__ */ Object.freeze({
545
+ path: "/",
546
+ secure: true,
547
+ httpOnly: true,
548
+ sameSite: "lax"
549
+ });
546
550
  const kSessionPending = Symbol("unjwt_session_pending");
547
551
  function sessionInstant(source) {
548
552
  const now = source.codec.now(source.config);
@@ -573,18 +577,46 @@ function blankSession() {
573
577
  data: {}
574
578
  });
575
579
  }
576
- function openSessionToken(keys, options) {
580
+ async function openSessionToken(keys, options) {
577
581
  return keys.open(options.token, options.args).then((opened) => typedOpened({
578
582
  ...opened,
579
583
  source: "token"
580
584
  }));
581
585
  }
586
+ function snapshotSession(session) {
587
+ return {
588
+ ...session,
589
+ data: safeJsonParse(JSON.stringify(session.data))
590
+ };
591
+ }
592
+ function frozenCopy(value) {
593
+ return frozenValue(value, /* @__PURE__ */ new Map());
594
+ }
582
595
  function typedSession(state) {
583
596
  return state;
584
597
  }
585
598
  function typedOpened(opened) {
586
599
  return opened;
587
600
  }
601
+ function frozenValue(value, seen) {
602
+ if (typeof value !== "object" || value === null) return value;
603
+ const known = seen.get(value);
604
+ if (known) return known;
605
+ if (Array.isArray(value)) {
606
+ const copy = [];
607
+ seen.set(value, copy);
608
+ for (const item of value) copy.push(frozenValue(item, seen));
609
+ return Object.freeze(copy);
610
+ }
611
+ if (!isPlainObject(value)) return value;
612
+ const copy = Object.create(Object.getPrototypeOf(value));
613
+ seen.set(value, copy);
614
+ for (const key of Object.keys(value)) Object.defineProperty(copy, key, {
615
+ value: frozenValue(value[key], seen),
616
+ enumerable: true
617
+ });
618
+ return Object.freeze(copy);
619
+ }
588
620
  const CHUNK_MARKER = "__chunked__";
589
621
  const DEFAULT_CHUNK_MAX_LENGTH = 4e3;
590
622
  function readChunked(store, name) {
@@ -602,10 +634,9 @@ function readChunked(store, name) {
602
634
  return parts.join("");
603
635
  }
604
636
  function writeChunked(store, name, value, options) {
605
- const { attrs, chunkMaxLength, prefix } = options;
606
- const max = chunkMaxLength && chunkMaxLength > 0 ? chunkMaxLength : DEFAULT_CHUNK_MAX_LENGTH;
607
- const chunkCount = Math.ceil(value.length / max);
608
- if (chunkCount > 16) throw invalidConfigError(`${prefix} Session token requires ${chunkCount} cookie chunks, exceeding the limit of 16.`);
637
+ const { attrs, chunkMaxLength } = options;
638
+ const max = chunkLength(chunkMaxLength);
639
+ const chunkCount = countChunks(value, options);
609
640
  const previousCount = parseChunkCount(store.read(name));
610
641
  const firstSurplus = chunkCount <= 1 ? 1 : chunkCount + 1;
611
642
  for (let index = firstSurplus; index <= previousCount; index++) expire(store, chunkName(name, index), attrs);
@@ -616,6 +647,11 @@ function writeChunked(store, name, value, options) {
616
647
  store.write(name, `${CHUNK_MARKER}${chunkCount}`, attrs);
617
648
  for (let index = 1; index <= chunkCount; index++) store.write(chunkName(name, index), value.slice((index - 1) * max, index * max), attrs);
618
649
  }
650
+ function countChunks(value, options) {
651
+ const chunkCount = Math.ceil(value.length / chunkLength(options.chunkMaxLength));
652
+ if (chunkCount > 16) throw invalidSessionError(`${options.prefix} Session token requires ${chunkCount} cookie chunks, exceeding the limit of 16.`);
653
+ return chunkCount;
654
+ }
619
655
  function removeChunked(store, name, options) {
620
656
  const count = parseChunkCount(store.read(name));
621
657
  expire(store, name, options.attrs);
@@ -650,6 +686,9 @@ function parseChunkCount(main) {
650
686
  if (count < 1 || count > 16) return 0;
651
687
  return count;
652
688
  }
689
+ function chunkLength(chunkMaxLength) {
690
+ return chunkMaxLength && chunkMaxLength > 0 ? chunkMaxLength : DEFAULT_CHUNK_MAX_LENGTH;
691
+ }
653
692
  function expire(store, name, attrs) {
654
693
  store.write(name, "", {
655
694
  ...attrs,
@@ -674,7 +713,7 @@ function decodeValue(value) {
674
713
  return value;
675
714
  }
676
715
  }
677
- function resolveTransport(driver, event, options) {
716
+ function resolveSessionTransport(driver, event, options) {
678
717
  const handed = driver.handed?.(event);
679
718
  if (handed) return {
680
719
  source: "token",
@@ -682,11 +721,11 @@ function resolveTransport(driver, event, options) {
682
721
  readonly: false,
683
722
  writable: () => true
684
723
  };
685
- const { config, name } = options;
724
+ const { name, cookie } = options;
686
725
  const header = presentedHeader(driver, event, options);
687
- if (config.cookie === false) return headerTransport(config, header);
726
+ if (cookie === false) return headerTransport(options, header);
688
727
  const jar = driver.cookies(event);
689
- if (header && (header.token === void 0 || header.token !== readChunked(jar, name))) return headerTransport(config, header);
728
+ if (header && (header.token === void 0 || header.token !== readChunked(jar, name))) return headerTransport(options, header);
690
729
  const store = responseStore(jar);
691
730
  const transport = {
692
731
  source: "cookie",
@@ -695,22 +734,38 @@ function resolveTransport(driver, event, options) {
695
734
  writable: () => driver.writable?.(event) ?? true
696
735
  };
697
736
  if (!store) return transport;
698
- transport.write = (token, { attrs, chunkMaxLength }) => {
737
+ const { attrs, chunkMaxLength } = cookie;
738
+ transport.write = (token, { expiresAt } = {}) => {
739
+ if (!transport.writable()) return false;
740
+ const written = expiresAt === void 0 ? { ...attrs } : {
741
+ ...attrs,
742
+ expires: new Date(expiresAt),
743
+ maxAge: void 0
744
+ };
699
745
  writeChunked(store, name, token, {
700
- attrs,
746
+ attrs: written,
701
747
  chunkMaxLength,
702
748
  prefix: driver.prefix
703
749
  });
750
+ return true;
704
751
  };
705
- transport.drop = ({ attrs }) => {
752
+ transport.assertFits = (token) => {
753
+ countChunks(token, {
754
+ chunkMaxLength,
755
+ prefix: driver.prefix
756
+ });
757
+ };
758
+ transport.drop = () => {
759
+ if (!transport.writable()) return false;
706
760
  removeChunked(store, name, { attrs });
761
+ return true;
707
762
  };
708
763
  return transport;
709
764
  }
710
- function assertTokenSource(transport, options) {
765
+ function assertSessionTokenSource(transport, options) {
711
766
  if (!transport.read) throw invalidConfigError(`${options.prefix} Session has no token source, enable "cookie" or "sessionHeader".`);
712
767
  }
713
- function assertWritable(transport, options) {
768
+ function assertSessionWritable(transport, options) {
714
769
  const { operation, prefix } = options;
715
770
  if (operation === "update" && transport.readonly) throw readOnlyEventError(`${prefix} Cannot update session on read-only event.`);
716
771
  if (!transport.writable()) throw readOnlyEventError(`${prefix} Cannot update/clear session: the response was already sent.`);
@@ -732,29 +787,29 @@ function headerToken(value) {
732
787
  return words.length === 1 && first ? first : void 0;
733
788
  }
734
789
  function presentedHeader(driver, event, options) {
735
- const { config, name } = options;
736
- if (!readsHeader(config)) return void 0;
737
- const header = sessionHeaderName(name, config.sessionHeader);
790
+ const { name, sessionHeader } = options;
791
+ if (!readsHeader(options)) return void 0;
792
+ const header = sessionHeaderName(name, sessionHeader);
738
793
  const value = driver.header(event, header);
739
794
  if (typeof value !== "string") return void 0;
740
795
  const token = headerToken(value);
741
796
  if (token) return { token };
742
797
  return {
743
798
  token: void 0,
744
- malformed: invalidTokenError(`${driver.prefix} Invalid session token: the "${header}" header holds neither a token nor "Bearer <token>".`)
799
+ malformed: { header }
745
800
  };
746
801
  }
747
- function headerTransport(config, header) {
802
+ function headerTransport(options, header) {
748
803
  return {
749
804
  source: "header",
750
- read: readsHeader(config) ? () => header?.token : void 0,
805
+ read: readsHeader(options) ? () => header?.token : void 0,
751
806
  malformed: header?.malformed,
752
807
  readonly: false,
753
808
  writable: () => true
754
809
  };
755
810
  }
756
- function readsHeader(config) {
757
- return config.sessionHeader !== void 0 && config.sessionHeader !== false;
811
+ function readsHeader(options) {
812
+ return options.sessionHeader !== void 0 && options.sessionHeader !== false;
758
813
  }
759
814
  function cookieToken(jar, name) {
760
815
  const written = jar.written ? readChunked({ read: (key) => jar.written?.(key) }, name) : void 0;
@@ -772,15 +827,48 @@ function responseStore(jar) {
772
827
  const kSessionNames = Symbol("unjwt_session_names");
773
828
  const kHeldTokens = Symbol("unjwt_session_held_tokens");
774
829
  function assertSessionConfig(config, options) {
775
- const { name, prefix } = options;
830
+ const { name, prefix, defaultCookie = DEFAULT_SESSION_COOKIE } = options;
776
831
  assertSessionName(name, { prefix });
777
832
  assertSessionHeader(config.sessionHeader, { prefix });
778
833
  if (config.cookie !== false) assertCookiePrefix(name, {
779
- ...options.defaultCookie,
834
+ ...defaultCookie,
780
835
  ...config.cookie
781
836
  }, { prefix });
782
837
  assertRolling(config, { prefix });
783
838
  }
839
+ function sessionTransportOptions(config, options) {
840
+ const { name, defaultCookie = DEFAULT_SESSION_COOKIE } = options;
841
+ const { sessionHeader } = config;
842
+ if (config.cookie === false) return {
843
+ name,
844
+ sessionHeader,
845
+ cookie: false
846
+ };
847
+ const { chunkMaxLength, ...configured } = config.cookie || {};
848
+ return {
849
+ name,
850
+ sessionHeader,
851
+ cookie: {
852
+ attrs: {
853
+ ...defaultCookie,
854
+ ...configured
855
+ },
856
+ chunkMaxLength
857
+ }
858
+ };
859
+ }
860
+ function claimSessionName(state, options) {
861
+ const { name, family, prefix } = options;
862
+ const holder = state;
863
+ const registry = holder[kSessionNames] ||= /* @__PURE__ */ new Map();
864
+ const held = registry.get(name);
865
+ if (held === void 0) {
866
+ registry.set(name, family);
867
+ return;
868
+ }
869
+ if (held === family) return;
870
+ throw invalidConfigError(`${prefix} Session name "${name}" is already used by a ${held.toUpperCase()} session on this request.`);
871
+ }
784
872
  function bindSession(event, options) {
785
873
  const { config, name, driver, codec, keys, actions } = options;
786
874
  const record = driver.state(event);
@@ -801,21 +889,18 @@ function bindSession(event, options) {
801
889
  args: { event },
802
890
  store: recordSlotStore(record, slot),
803
891
  held: heldTokenSlot(record, slot),
804
- transport: resolveTransport(driver, event, {
805
- config,
806
- name
807
- })
892
+ transport: resolveSessionTransport(driver, event, sessionTransportOptions(config, { name }))
808
893
  };
809
894
  }
810
895
  function sessionToken(binding) {
811
896
  const { transport } = binding;
812
- assertTokenSource(transport, { prefix: binding.driver.prefix });
897
+ assertSessionTokenSource(transport, { prefix: binding.driver.prefix });
813
898
  const session = storedSession(binding.store);
814
899
  return session ? session.token : transport.read();
815
900
  }
816
901
  function transportToken(binding) {
817
902
  const { transport } = binding;
818
- assertTokenSource(transport, { prefix: binding.driver.prefix });
903
+ assertSessionTokenSource(transport, { prefix: binding.driver.prefix });
819
904
  const held = binding.held.get();
820
905
  return held ? held.token : transport.read();
821
906
  }
@@ -827,38 +912,6 @@ function recordSlotStore(record, slot) {
827
912
  }
828
913
  };
829
914
  }
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
915
  function heldTokenSlot(record, slot) {
863
916
  const holder = record;
864
917
  const held = holder[kHeldTokens] ||= /* @__PURE__ */ new Map();
@@ -873,12 +926,12 @@ async function loadSession(binding) {
873
926
  const { config, codec, keys, store, args, transport } = binding;
874
927
  await keys.assertKeys();
875
928
  const open = keys.opener(args);
876
- assertTokenSource(transport, { prefix: binding.driver.prefix });
929
+ assertSessionTokenSource(transport, { prefix: binding.driver.prefix });
877
930
  const existing = storedSession(store);
878
931
  if (existing) {
879
932
  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);
933
+ if (session.token !== void 0 && session.expiresAt !== void 0 && session.expiresAt <= codec.now(config)) {
934
+ transport.drop?.();
882
935
  const error = expiredSessionError({
883
936
  id: session.id,
884
937
  createdAt: session.createdAt,
@@ -903,8 +956,12 @@ async function loadSession(binding) {
903
956
  const session = emptySession(binding);
904
957
  store.set(session);
905
958
  if (transport.malformed) {
906
- session.error = transport.malformed;
907
- await fireError(binding, transport.malformed);
959
+ const error = malformedHeaderError(transport.malformed.header, { prefix: binding.driver.prefix });
960
+ session.error = error;
961
+ await fireError(binding, {
962
+ session,
963
+ error
964
+ });
908
965
  return session;
909
966
  }
910
967
  const token = transport.read();
@@ -927,7 +984,7 @@ async function loadSession(binding) {
927
984
  if (isJWTError(failure.error)) session.error = failure.error;
928
985
  if (isJWTError(failure.error, "ERR_JWT_EXPIRED")) {
929
986
  const { jti, iat, exp } = failure.error.cause;
930
- dropSessionCookie(binding);
987
+ transport.drop?.();
931
988
  await fireExpire(binding, {
932
989
  session: {
933
990
  id: jti,
@@ -938,44 +995,67 @@ async function loadSession(binding) {
938
995
  error: failure.error
939
996
  });
940
997
  } else {
941
- if (isJWTError(failure.error, "ERR_SESSION_TOKEN_INVALID")) dropSessionCookie(binding);
942
- await fireError(binding, failure.error);
998
+ if (isJWTError(failure.error, "ERR_SESSION_TOKEN_INVALID")) transport.drop?.();
999
+ await fireError(binding, {
1000
+ session,
1001
+ error: failure.error
1002
+ });
943
1003
  }
944
1004
  return session;
945
1005
  }
1006
+ const due = isRollDue(binding, session);
1007
+ let skipped = false;
1008
+ const roll = {
1009
+ due,
1010
+ skip: () => {
1011
+ skipped = true;
1012
+ }
1013
+ };
946
1014
  try {
947
- await fireRead(binding, session);
1015
+ await fireRead(binding, {
1016
+ session,
1017
+ roll
1018
+ });
1019
+ if (due && !skipped) await rollSession(binding, {
1020
+ session,
1021
+ token
1022
+ });
948
1023
  } catch (error_) {
949
1024
  store.set(emptySession(binding));
950
1025
  throw error_;
951
1026
  }
952
- await rollSession(binding, session);
953
1027
  return session;
954
1028
  }
955
- async function fireRead(binding, session) {
956
- if (session.id === void 0 || session.token === void 0) return;
1029
+ async function fireRead(binding, read) {
1030
+ if (read.session.id === void 0 || read.session.token === void 0) return;
957
1031
  await binding.config.hooks?.onRead?.({
958
1032
  session: binding.actions.manager(binding),
959
1033
  ...binding.args,
1034
+ roll: read.roll,
960
1035
  config: binding.config
961
1036
  });
962
1037
  }
963
- async function rollSession(binding, session) {
964
- const { config, codec, transport, store } = binding;
1038
+ function isRollDue(binding, session) {
1039
+ const { config, codec, transport } = binding;
965
1040
  const { rolling } = config;
966
- if (rolling === void 0 || transport.source !== "cookie" || store.get() !== session) return;
967
- if (session.token === void 0 || session.expiresAt === void 0) return;
968
- if (rolling !== true) {
969
- if (codec.now(config) - session.createdAt < computeDurationInSeconds(rolling) * 1e3) return;
970
- }
971
- if (transport.readonly || !transport.writable()) return;
1041
+ if (rolling === void 0 || transport.source !== "cookie") return false;
1042
+ if (session.expiresAt === void 0) return false;
1043
+ const age = codec.now(config) - session.createdAt;
1044
+ if (rolling !== true && age < computeDurationInSeconds(rolling) * 1e3) return false;
1045
+ return !transport.readonly && transport.writable();
1046
+ }
1047
+ async function rollSession(binding, read) {
1048
+ if (binding.store.get() !== read.session || read.session.token !== read.token) return;
1049
+ if (!binding.transport.writable()) return;
972
1050
  await binding.actions.roll(binding);
973
1051
  }
974
- async function fireError(binding, error) {
1052
+ async function fireError(binding, failure) {
975
1053
  await binding.config.hooks?.onError?.({
976
- session: binding.actions.manager(binding),
1054
+ session: snapshotSession(failure.session),
1055
+ oldSession: void 0,
1056
+ trigger: "read",
977
1057
  ...binding.args,
978
- error,
1058
+ error: failure.error,
979
1059
  config: binding.config
980
1060
  });
981
1061
  }
@@ -992,63 +1072,83 @@ async function fireExpire(binding, expired) {
992
1072
  }
993
1073
  async function issueSession(binding, update, options, trigger = "update") {
994
1074
  const { config, keys, driver, store, args, transport } = binding;
995
- assertWritable(transport, {
1075
+ assertSessionWritable(transport, {
996
1076
  operation: "update",
997
1077
  prefix: driver.prefix
998
1078
  });
999
1079
  const session = storedSession(store) || await loadSession(binding);
1000
- const oldSession = snapshotSession(session);
1001
- const resolved = typeof update === "function" ? update(session.data) : update;
1002
- if (resolved) Object.assign(session.data, sanitizeSessionUpdate(resolved, { prefix: driver.prefix }));
1080
+ const restorePoint = snapshotSession(session);
1081
+ const { changes, createdAt, id, expiresAt } = restoringOnThrow(session, restorePoint, () => {
1082
+ const resolved = typeof update === "function" ? update(session.data) : update;
1083
+ const instant = sessionInstant(binding);
1084
+ return {
1085
+ changes: resolved && sanitizeSessionUpdate(resolved, { prefix: driver.prefix }),
1086
+ createdAt: instant,
1087
+ id: mintSessionId(config),
1088
+ expiresAt: resolveExpiresAt(instant, { maxAge: options?.maxAge ?? config.maxAge })
1089
+ };
1090
+ });
1091
+ if (changes) Object.assign(session.data, changes);
1003
1092
  for (const claim of RESERVED_CLAIMS) delete session.data[claim];
1004
- const maxAge = options?.maxAge ?? config.maxAge;
1005
- const createdAt = sessionInstant(binding);
1006
- const id = mintSessionId(config);
1007
1093
  Object.assign(session, {
1008
1094
  id,
1009
1095
  createdAt,
1010
- expiresAt: resolveExpiresAt(createdAt, { maxAge })
1096
+ expiresAt
1011
1097
  });
1012
- let issued;
1013
- try {
1014
- await keys.assertKeys();
1015
- const token = await keys.issue(session);
1016
- transport.write?.(token, sessionCookie(binding, { expiresAt: session.expiresAt }));
1017
- binding.held.set(token);
1018
- issued = Object.assign(session, {
1019
- id,
1020
- token,
1098
+ const reportFailure = async (error, failed) => {
1099
+ const failedSession = {
1100
+ ...session,
1101
+ token: failed.token,
1021
1102
  error: void 0
1022
- });
1023
- } catch (error_) {
1024
- Object.assign(session, {
1025
- id: oldSession.id,
1026
- createdAt: oldSession.createdAt,
1027
- expiresAt: oldSession.expiresAt,
1028
- data: oldSession.data
1029
- });
1030
- session.token = oldSession.token;
1103
+ };
1104
+ restoreSession(session, restorePoint);
1031
1105
  await config.hooks?.onError?.({
1032
- session: binding.actions.manager(binding),
1106
+ session: snapshotSession(failedSession),
1107
+ oldSession: snapshotSession(restorePoint),
1108
+ trigger,
1109
+ ...args,
1110
+ error,
1111
+ config
1112
+ });
1113
+ throw error;
1114
+ };
1115
+ const token = await keys.assertKeys().then(() => keys.issue(session)).then((issuedToken) => {
1116
+ transport.assertFits?.(issuedToken);
1117
+ return issuedToken;
1118
+ }).catch((error) => reportFailure(error, { token: void 0 }));
1119
+ const issued = Object.assign(session, {
1120
+ id,
1121
+ token,
1122
+ error: void 0
1123
+ });
1124
+ try {
1125
+ await config.hooks?.onUpdate?.({
1126
+ session: snapshotSession(issued),
1127
+ oldSession: snapshotSession(restorePoint),
1128
+ trigger,
1033
1129
  ...args,
1034
- error: error_,
1035
1130
  config
1036
1131
  });
1132
+ } catch (error_) {
1133
+ restoreSession(session, restorePoint);
1037
1134
  throw error_;
1038
1135
  }
1039
- await config.hooks?.onUpdate?.({
1040
- session: binding.actions.manager(binding),
1041
- oldSession,
1042
- trigger,
1043
- ...args,
1044
- config
1045
- });
1136
+ try {
1137
+ assertSessionWritable(transport, {
1138
+ operation: "update",
1139
+ prefix: driver.prefix
1140
+ });
1141
+ transport.write?.(token, { expiresAt: issued.expiresAt });
1142
+ binding.held.set(token);
1143
+ } catch (error_) {
1144
+ return reportFailure(error_, { token });
1145
+ }
1046
1146
  return issued;
1047
1147
  }
1048
1148
  async function dropSession(binding) {
1049
1149
  const { config, driver, store, args, transport } = binding;
1050
- assertTokenSource(transport, { prefix: driver.prefix });
1051
- assertWritable(transport, {
1150
+ assertSessionTokenSource(transport, { prefix: driver.prefix });
1151
+ assertSessionWritable(transport, {
1052
1152
  operation: "clear",
1053
1153
  prefix: driver.prefix
1054
1154
  });
@@ -1057,7 +1157,7 @@ async function dropSession(binding) {
1057
1157
  const oldSession = session && (session.id !== void 0 || session.token !== void 0 || session.error !== void 0) ? snapshotSession(session) : void 0;
1058
1158
  store.set(emptySession(binding));
1059
1159
  binding.held.set(void 0);
1060
- transport.drop?.(sessionCookie(binding));
1160
+ transport.drop?.();
1061
1161
  await config.hooks?.onClear?.({
1062
1162
  oldSession,
1063
1163
  ...args,
@@ -1108,19 +1208,31 @@ function sessionActions() {
1108
1208
  }
1109
1209
  };
1110
1210
  }
1111
- function snapshotSession(session) {
1112
- return {
1113
- ...session,
1114
- data: safeJsonParse(JSON.stringify(session.data))
1115
- };
1211
+ function restoringOnThrow(session, oldSession, run) {
1212
+ try {
1213
+ return run();
1214
+ } catch (error) {
1215
+ restoreSession(session, oldSession);
1216
+ throw error;
1217
+ }
1218
+ }
1219
+ function restoreSession(session, oldSession) {
1220
+ Object.assign(session, {
1221
+ id: oldSession.id,
1222
+ createdAt: oldSession.createdAt,
1223
+ expiresAt: oldSession.expiresAt,
1224
+ data: oldSession.data,
1225
+ token: oldSession.token,
1226
+ error: oldSession.error
1227
+ });
1116
1228
  }
1117
- function defineSession(config, options) {
1229
+ function defineSession(configured, options) {
1118
1230
  const { driver, codec } = options;
1231
+ const config = frozenCopy(configured);
1119
1232
  const name = config.name || codec.defaultName;
1120
1233
  assertSessionConfig(config, {
1121
1234
  name,
1122
- prefix: driver.prefix,
1123
- defaultCookie: codec.defaultCookie
1235
+ prefix: driver.prefix
1124
1236
  });
1125
1237
  const keys = codec.keys(config);
1126
1238
  const actions = sessionActions();
@@ -1163,15 +1275,15 @@ function defineSession(config, options) {
1163
1275
  }
1164
1276
  function webCookieJar(request, response) {
1165
1277
  let cookies;
1166
- const jar = { read: (name) => (cookies ||= requestCookies(request))[name] };
1278
+ const jar = { read: (name) => (cookies ||= parseCookie(request.get("cookie") ?? ""))[name] };
1167
1279
  if (response) {
1168
1280
  const { headers, errHeaders } = response;
1169
1281
  jar.written = (name) => setCookieValues(headers.getSetCookie())[name];
1170
1282
  jar.write = (name, value, attrs) => {
1171
1283
  setCookie(headers, {
1284
+ ...attrs,
1172
1285
  name,
1173
- value,
1174
- ...attrs
1286
+ value
1175
1287
  });
1176
1288
  if (errHeaders) mirrorSetCookie(name, {
1177
1289
  from: headers,
@@ -1181,19 +1293,24 @@ function webCookieJar(request, response) {
1181
1293
  }
1182
1294
  return jar;
1183
1295
  }
1184
- function requestCookies(request) {
1185
- const header = request.get("cookie");
1186
- return header ? parse(header) : Object.create(null);
1296
+ function parseCookie(header) {
1297
+ return parse(header);
1187
1298
  }
1188
- function setCookie(headers, cookie) {
1189
- const line = serialize({
1299
+ function serializeCookie(name, value, attrs) {
1300
+ return serialize({
1190
1301
  path: "/",
1191
- ...cookie
1302
+ ...attrs,
1303
+ name,
1304
+ value
1192
1305
  });
1306
+ }
1307
+ function setCookie(headers, cookie) {
1308
+ const { name, value, ...attrs } = cookie;
1309
+ const line = serializeCookie(name, value, attrs);
1193
1310
  const key = cookieKey(cookie);
1194
1311
  const kept = headers.getSetCookie().filter((existing) => {
1195
1312
  const parsed = parseSetCookie(existing);
1196
- return parsed ? cookieKey(parsed) !== key : setCookieName(existing) !== cookie.name;
1313
+ return parsed ? cookieKey(parsed) !== key : setCookieName(existing) !== name;
1197
1314
  });
1198
1315
  headers.delete("set-cookie");
1199
1316
  for (const existing of kept) headers.append("set-cookie", existing);
@@ -1208,33 +1325,36 @@ function cookieKey(cookie) {
1208
1325
  cookie.partitioned ? "1" : "0"
1209
1326
  ].join(";");
1210
1327
  }
1211
- const PREFIX = "[unjwt/session]";
1212
1328
  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) {
1329
+ function webSessionDriver(options) {
1330
+ const prefix = options?.prefix ?? "[unjwt/session]";
1331
+ return {
1332
+ prefix,
1333
+ state: (event) => {
1334
+ if (isHanded(event, prefix)) return sessionsOf(event);
1335
+ if (event.context) return contextState(event.context);
1336
+ return sessionsOf(event.req ? webRequest(event.req, prefix) : event);
1337
+ },
1338
+ header: (event, name) => isHanded(event, prefix) ? void 0 : requestHeaders(event, prefix).get(name),
1339
+ cookies: (event) => isHanded(event, prefix) ? { read: () => void 0 } : webCookieJar(requestHeaders(event, prefix), event.res),
1340
+ handed: (event) => isHanded(event, prefix) ? { token: event.token } : void 0
1341
+ };
1342
+ }
1343
+ const defaultWebSessionDriver = /* @__PURE__ */ webSessionDriver();
1344
+ function isHanded(event, prefix) {
1225
1345
  if ("req" in event || "headers" in event) return false;
1226
1346
  if ("token" in event) return true;
1227
- throw invalidConfigError(`${PREFIX} use() got neither a request event nor a token.`);
1347
+ throw invalidSessionError(`${prefix} use() got neither a request event nor a token.`);
1228
1348
  }
1229
- function requestHeaders(event) {
1230
- if (event.req) return webRequest(event.req).headers;
1349
+ function requestHeaders(event, prefix) {
1350
+ if (event.req) return webRequest(event.req, prefix).headers;
1231
1351
  if (event.headers) return event.headers;
1232
- throw invalidConfigError(`${PREFIX} use() got a request event with neither "req" nor "headers".`);
1352
+ throw invalidSessionError(`${prefix} use() got a request event with neither "req" nor "headers".`);
1233
1353
  }
1234
- function webRequest(req) {
1354
+ function webRequest(req, prefix) {
1235
1355
  const headers = req.headers;
1236
1356
  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.`);
1357
+ throw invalidSessionError(`${prefix} use() got an event whose "req" is not a web Request.`);
1238
1358
  }
1239
1359
  function contextState(context) {
1240
1360
  const existing = context.unjwt;
@@ -1251,7 +1371,7 @@ function sessionsOf(request) {
1251
1371
  return created;
1252
1372
  }
1253
1373
  function defineJWSSession(config, options) {
1254
- const driver = options?.driver ?? webSessionDriver;
1374
+ const driver = options?.driver ?? defaultWebSessionDriver;
1255
1375
  return defineSession(config, {
1256
1376
  driver,
1257
1377
  codec: createJWSCodec({
@@ -1357,19 +1477,12 @@ function encryptionBits(enc) {
1357
1477
  const bits = Number(enc.slice(1, 4));
1358
1478
  return enc.includes("CBC") ? bits * 2 : bits;
1359
1479
  }
1360
- const JWE_DEFAULT_COOKIE = {
1361
- path: "/",
1362
- secure: true,
1363
- httpOnly: true,
1364
- sameSite: "lax"
1365
- };
1366
1480
  function createJWECodec(options) {
1367
1481
  const { prefix, defaultName } = options;
1368
1482
  return createSessionCodec({
1369
1483
  family: "jwe",
1370
1484
  prefix,
1371
1485
  defaultName,
1372
- defaultCookie: JWE_DEFAULT_COOKIE,
1373
1486
  issueOptions: (config) => config.jwe?.encryptOptions,
1374
1487
  openOptions: (config) => config.jwe?.decryptOptions,
1375
1488
  resolveKeys: (config) => resolveSessionKeys(config.key, {
@@ -1400,7 +1513,7 @@ function sessionMaxIterations(config) {
1400
1513
  return typeof alg === "string" && alg.startsWith("PBES2") ? config.jwe?.encryptOptions?.p2c ?? 6e5 : void 0;
1401
1514
  }
1402
1515
  function defineJWESession(config, options) {
1403
- const driver = options?.driver ?? webSessionDriver;
1516
+ const driver = options?.driver ?? defaultWebSessionDriver;
1404
1517
  return defineSession(config, {
1405
1518
  driver,
1406
1519
  codec: createJWECodec({
@@ -1412,4 +1525,4 @@ function defineJWESession(config, options) {
1412
1525
  function defineSessionDriver(driver) {
1413
1526
  return driver;
1414
1527
  }
1415
- export { defineJWESession, defineJWSSession, defineSessionDriver, deriveJWKFromPassword, exportPEM, generateJWK, importPEM, thumbprint, toPublicJWK };
1528
+ export { DEFAULT_SESSION_COOKIE, assertSessionConfig, assertSessionHeader, assertSessionTokenSource, assertSessionWritable, claimSessionName, defineJWESession, defineJWSSession, defineSessionDriver, deriveJWKFromPassword, exportPEM, frozenCopy, 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.2",
4
4
  "description": "A collection of JWT utilities based on the Web Crypto API.",
5
5
  "keywords": [
6
6
  "auth",
@@ -121,6 +121,7 @@
121
121
  "build": "obuild",
122
122
  "dev": "pnpm --dir playgrounds/h3 dev",
123
123
  "docs": "pnpm --filter unjwt-docs dev",
124
+ "docs:automd": "automd --dir docs",
124
125
  "dev:nuxt": "pnpm --dir playgrounds/nuxt-4 dev",
125
126
  "dev:nitro": "pnpm --dir playgrounds/nitro dev",
126
127
  "dev:prepare": "obuild --stub && pnpm --dir playgrounds/nuxt-4 prepare && pnpm simple-git-hooks",