unjwt 0.9.1 → 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
  /**
@@ -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;
@@ -310,11 +344,16 @@ interface SessionTransport {
310
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
311
345
  * `maxAge`, which would take precedence (RFC 6265 §5.3). Once the response was sent, writes nothing and returns `false`.
312
346
  * 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.
347
+ * @throws `ERR_SESSION_INVALID` if the token needs more than 16 cookie chunks.
314
348
  */
315
349
  write?: (token: string, options?: {
316
350
  expiresAt?: number;
317
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;
318
357
  /**
319
358
  * Expires the cookie and every chunk, and returns `true`. Once the response was sent, writes nothing and returns `false`.
320
359
  * Present when the session reads its token from a cookie the event can write.
@@ -379,10 +418,10 @@ interface JWSSessionConfig<MaxAge extends ExpiresIn | undefined = ExpiresIn | un
379
418
  }
380
419
  /** Session state for the current request, from the claims of its JWS token. */
381
420
  interface SessionJWS<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined> extends SessionState<T, MaxAge> {}
382
- /** 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`. */
383
422
  interface SessionHooksJWS<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TEvent = WebSessionEvent> extends SessionHooks<SessionJWS<T, MaxAge>, SessionManager<T, MaxAge>, {
384
423
  event: TEvent;
385
- }, SessionConfigJWS<T, MaxAge, TEvent>> {
424
+ }, ReadonlyConfig<SessionConfigJWS<T, MaxAge, TEvent>>> {
386
425
  /** Returns the key that verifies an incoming token. See {@link JWSSessionKeyLookup}. */
387
426
  onKeyLookup?: JWSSessionKeyLookup<T, MaxAge, TEvent>;
388
427
  }
@@ -399,7 +438,7 @@ interface SessionHooksJWS<T extends SessionRecord = SessionClaims, MaxAge extend
399
438
  */
400
439
  type JWSSessionKeyLookup<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TEvent = WebSessionEvent> = (lookup: {
401
440
  header: JWSHeaderParameters;
402
- config: SessionConfigJWS<T, MaxAge, TEvent>;
441
+ config: ReadonlyConfig<SessionConfigJWS<T, MaxAge, TEvent>>;
403
442
  event?: TEvent;
404
443
  }) => JWSVerifyJWK | CryptoKey | JWKSet | undefined | Promise<JWSVerifyJWK | CryptoKey | JWKSet | undefined>;
405
444
  /** Configuration of a JWS session, signed but not encrypted. */
@@ -410,14 +449,14 @@ interface SessionConfigJWS<T extends SessionRecord = SessionClaims, MaxAge exten
410
449
  hooks?: SessionHooksJWS<T, MaxAge, TEvent>;
411
450
  }
412
451
  /**
413
- * 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.
414
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.
415
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.
416
455
  * @throws `ERR_INVALID_OPTION` if `rolling` is neither `true` nor a duration.
417
456
  */
418
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>;
419
458
  /**
420
- * 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.
421
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.
422
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.
423
462
  * @throws `ERR_INVALID_OPTION` if `rolling` is neither `true` nor a duration.
@@ -470,10 +509,10 @@ interface JWESessionConfig<MaxAge extends ExpiresIn | undefined = ExpiresIn | un
470
509
  }
471
510
  /** Session state for the current request, from the claims of its JWE token. */
472
511
  interface SessionJWE<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined> extends SessionState<T, MaxAge> {}
473
- /** 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`. */
474
513
  interface SessionHooksJWE<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TEvent = WebSessionEvent> extends SessionHooks<SessionJWE<T, MaxAge>, SessionManager<T, MaxAge>, {
475
514
  event: TEvent;
476
- }, SessionConfigJWE<T, MaxAge, TEvent>> {
515
+ }, ReadonlyConfig<SessionConfigJWE<T, MaxAge, TEvent>>> {
477
516
  /** Returns the key that decrypts an incoming token. See {@link JWESessionKeyLookup}. */
478
517
  onKeyLookup?: JWESessionKeyLookup<T, MaxAge, TEvent>;
479
518
  }
@@ -490,7 +529,7 @@ interface SessionHooksJWE<T extends SessionRecord = SessionClaims, MaxAge extend
490
529
  */
491
530
  type JWESessionKeyLookup<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TEvent = WebSessionEvent> = (lookup: {
492
531
  header: JWEHeaderParameters;
493
- config: SessionConfigJWE<T, MaxAge, TEvent>;
532
+ config: ReadonlyConfig<SessionConfigJWE<T, MaxAge, TEvent>>;
494
533
  event?: TEvent;
495
534
  }) => JWEDecryptJWK | CryptoKey | JWKSet | undefined | Promise<JWEDecryptJWK | CryptoKey | JWKSet | undefined>;
496
535
  /** Configuration of an encrypted JWE session. */
@@ -501,14 +540,14 @@ interface SessionConfigJWE<T extends SessionRecord = SessionClaims, MaxAge exten
501
540
  hooks?: SessionHooksJWE<T, MaxAge, TEvent>;
502
541
  }
503
542
  /**
504
- * 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.
505
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.
506
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`.
507
546
  * @throws `ERR_INVALID_OPTION` if `rolling` is neither `true` nor a duration.
508
547
  */
509
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>;
510
549
  /**
511
- * 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.
512
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.
513
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`.
514
553
  * @throws `ERR_INVALID_OPTION` if `rolling` is neither `true` nor a duration.
@@ -587,4 +626,4 @@ export declare function claimSessionName(state: Record<string, unknown>, options
587
626
  family: string;
588
627
  prefix: string;
589
628
  }): 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 };
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
  }
@@ -574,18 +577,46 @@ function blankSession() {
574
577
  data: {}
575
578
  });
576
579
  }
577
- function openSessionToken(keys, options) {
580
+ async function openSessionToken(keys, options) {
578
581
  return keys.open(options.token, options.args).then((opened) => typedOpened({
579
582
  ...opened,
580
583
  source: "token"
581
584
  }));
582
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
+ }
583
595
  function typedSession(state) {
584
596
  return state;
585
597
  }
586
598
  function typedOpened(opened) {
587
599
  return opened;
588
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
+ }
589
620
  const CHUNK_MARKER = "__chunked__";
590
621
  const DEFAULT_CHUNK_MAX_LENGTH = 4e3;
591
622
  function readChunked(store, name) {
@@ -603,10 +634,9 @@ function readChunked(store, name) {
603
634
  return parts.join("");
604
635
  }
605
636
  function writeChunked(store, name, value, options) {
606
- const { attrs, chunkMaxLength, prefix } = options;
607
- const max = chunkMaxLength && chunkMaxLength > 0 ? chunkMaxLength : DEFAULT_CHUNK_MAX_LENGTH;
608
- const chunkCount = Math.ceil(value.length / max);
609
- 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);
610
640
  const previousCount = parseChunkCount(store.read(name));
611
641
  const firstSurplus = chunkCount <= 1 ? 1 : chunkCount + 1;
612
642
  for (let index = firstSurplus; index <= previousCount; index++) expire(store, chunkName(name, index), attrs);
@@ -617,6 +647,11 @@ function writeChunked(store, name, value, options) {
617
647
  store.write(name, `${CHUNK_MARKER}${chunkCount}`, attrs);
618
648
  for (let index = 1; index <= chunkCount; index++) store.write(chunkName(name, index), value.slice((index - 1) * max, index * max), attrs);
619
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
+ }
620
655
  function removeChunked(store, name, options) {
621
656
  const count = parseChunkCount(store.read(name));
622
657
  expire(store, name, options.attrs);
@@ -651,6 +686,9 @@ function parseChunkCount(main) {
651
686
  if (count < 1 || count > 16) return 0;
652
687
  return count;
653
688
  }
689
+ function chunkLength(chunkMaxLength) {
690
+ return chunkMaxLength && chunkMaxLength > 0 ? chunkMaxLength : DEFAULT_CHUNK_MAX_LENGTH;
691
+ }
654
692
  function expire(store, name, attrs) {
655
693
  store.write(name, "", {
656
694
  ...attrs,
@@ -711,6 +749,12 @@ function resolveSessionTransport(driver, event, options) {
711
749
  });
712
750
  return true;
713
751
  };
752
+ transport.assertFits = (token) => {
753
+ countChunks(token, {
754
+ chunkMaxLength,
755
+ prefix: driver.prefix
756
+ });
757
+ };
714
758
  transport.drop = () => {
715
759
  if (!transport.writable()) return false;
716
760
  removeChunked(store, name, { attrs });
@@ -914,7 +958,10 @@ async function loadSession(binding) {
914
958
  if (transport.malformed) {
915
959
  const error = malformedHeaderError(transport.malformed.header, { prefix: binding.driver.prefix });
916
960
  session.error = error;
917
- await fireError(binding, error);
961
+ await fireError(binding, {
962
+ session,
963
+ error
964
+ });
918
965
  return session;
919
966
  }
920
967
  const token = transport.read();
@@ -949,43 +996,66 @@ async function loadSession(binding) {
949
996
  });
950
997
  } else {
951
998
  if (isJWTError(failure.error, "ERR_SESSION_TOKEN_INVALID")) transport.drop?.();
952
- await fireError(binding, failure.error);
999
+ await fireError(binding, {
1000
+ session,
1001
+ error: failure.error
1002
+ });
953
1003
  }
954
1004
  return session;
955
1005
  }
1006
+ const due = isRollDue(binding, session);
1007
+ let skipped = false;
1008
+ const roll = {
1009
+ due,
1010
+ skip: () => {
1011
+ skipped = true;
1012
+ }
1013
+ };
956
1014
  try {
957
- 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
+ });
958
1023
  } catch (error_) {
959
1024
  store.set(emptySession(binding));
960
1025
  throw error_;
961
1026
  }
962
- await rollSession(binding, session);
963
1027
  return session;
964
1028
  }
965
- async function fireRead(binding, session) {
966
- 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;
967
1031
  await binding.config.hooks?.onRead?.({
968
1032
  session: binding.actions.manager(binding),
969
1033
  ...binding.args,
1034
+ roll: read.roll,
970
1035
  config: binding.config
971
1036
  });
972
1037
  }
973
- async function rollSession(binding, session) {
974
- const { config, codec, transport, store } = binding;
1038
+ function isRollDue(binding, session) {
1039
+ const { config, codec, transport } = binding;
975
1040
  const { rolling } = config;
976
- if (rolling === void 0 || transport.source !== "cookie" || store.get() !== session) return;
977
- if (session.token === void 0 || session.expiresAt === void 0) return;
978
- if (rolling !== true) {
979
- if (codec.now(config) - session.createdAt < computeDurationInSeconds(rolling) * 1e3) return;
980
- }
981
- 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;
982
1050
  await binding.actions.roll(binding);
983
1051
  }
984
- async function fireError(binding, error) {
1052
+ async function fireError(binding, failure) {
985
1053
  await binding.config.hooks?.onError?.({
986
- session: binding.actions.manager(binding),
1054
+ session: snapshotSession(failure.session),
1055
+ oldSession: void 0,
1056
+ trigger: "read",
987
1057
  ...binding.args,
988
- error,
1058
+ error: failure.error,
989
1059
  config: binding.config
990
1060
  });
991
1061
  }
@@ -1007,52 +1077,72 @@ async function issueSession(binding, update, options, trigger = "update") {
1007
1077
  prefix: driver.prefix
1008
1078
  });
1009
1079
  const session = storedSession(store) || await loadSession(binding);
1010
- const oldSession = snapshotSession(session);
1011
- const resolved = typeof update === "function" ? update(session.data) : update;
1012
- 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);
1013
1092
  for (const claim of RESERVED_CLAIMS) delete session.data[claim];
1014
- const maxAge = options?.maxAge ?? config.maxAge;
1015
- const createdAt = sessionInstant(binding);
1016
- const id = mintSessionId(config);
1017
1093
  Object.assign(session, {
1018
1094
  id,
1019
1095
  createdAt,
1020
- expiresAt: resolveExpiresAt(createdAt, { maxAge })
1096
+ expiresAt
1021
1097
  });
1022
- let issued;
1023
- try {
1024
- await keys.assertKeys();
1025
- const token = await keys.issue(session);
1026
- transport.write?.(token, { expiresAt: session.expiresAt });
1027
- binding.held.set(token);
1028
- issued = Object.assign(session, {
1029
- id,
1030
- token,
1098
+ const reportFailure = async (error, failed) => {
1099
+ const failedSession = {
1100
+ ...session,
1101
+ token: failed.token,
1031
1102
  error: void 0
1032
- });
1033
- } catch (error_) {
1034
- Object.assign(session, {
1035
- id: oldSession.id,
1036
- createdAt: oldSession.createdAt,
1037
- expiresAt: oldSession.expiresAt,
1038
- data: oldSession.data
1039
- });
1040
- session.token = oldSession.token;
1103
+ };
1104
+ restoreSession(session, restorePoint);
1041
1105
  await config.hooks?.onError?.({
1042
- 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,
1043
1129
  ...args,
1044
- error: error_,
1045
1130
  config
1046
1131
  });
1132
+ } catch (error_) {
1133
+ restoreSession(session, restorePoint);
1047
1134
  throw error_;
1048
1135
  }
1049
- await config.hooks?.onUpdate?.({
1050
- session: binding.actions.manager(binding),
1051
- oldSession,
1052
- trigger,
1053
- ...args,
1054
- config
1055
- });
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
+ }
1056
1146
  return issued;
1057
1147
  }
1058
1148
  async function dropSession(binding) {
@@ -1118,14 +1208,27 @@ function sessionActions() {
1118
1208
  }
1119
1209
  };
1120
1210
  }
1121
- function snapshotSession(session) {
1122
- return {
1123
- ...session,
1124
- data: safeJsonParse(JSON.stringify(session.data))
1125
- };
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
+ });
1126
1228
  }
1127
- function defineSession(config, options) {
1229
+ function defineSession(configured, options) {
1128
1230
  const { driver, codec } = options;
1231
+ const config = frozenCopy(configured);
1129
1232
  const name = config.name || codec.defaultName;
1130
1233
  assertSessionConfig(config, {
1131
1234
  name,
@@ -1241,17 +1344,17 @@ const defaultWebSessionDriver = /* @__PURE__ */ webSessionDriver();
1241
1344
  function isHanded(event, prefix) {
1242
1345
  if ("req" in event || "headers" in event) return false;
1243
1346
  if ("token" in event) return true;
1244
- 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.`);
1245
1348
  }
1246
1349
  function requestHeaders(event, prefix) {
1247
1350
  if (event.req) return webRequest(event.req, prefix).headers;
1248
1351
  if (event.headers) return event.headers;
1249
- 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".`);
1250
1353
  }
1251
1354
  function webRequest(req, prefix) {
1252
1355
  const headers = req.headers;
1253
1356
  if (typeof headers === "object" && headers !== null && "get" in headers) return req;
1254
- 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.`);
1255
1358
  }
1256
1359
  function contextState(context) {
1257
1360
  const existing = context.unjwt;
@@ -1422,4 +1525,4 @@ function defineJWESession(config, options) {
1422
1525
  function defineSessionDriver(driver) {
1423
1526
  return driver;
1424
1527
  }
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 };
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.1",
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",