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.
- package/dist/_chunks/error.d.mts +1 -1
- package/dist/session/index.d.mts +63 -24
- package/dist/session/index.mjs +169 -66
- package/package.json +2 -1
package/dist/_chunks/error.d.mts
CHANGED
|
@@ -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.
|
package/dist/session/index.d.mts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
*
|
|
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 `
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
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 `
|
|
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
|
|
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
|
|
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 };
|
package/dist/session/index.mjs
CHANGED
|
@@ -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
|
|
607
|
-
const max = chunkMaxLength
|
|
608
|
-
const chunkCount =
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
974
|
-
const { config, codec, transport
|
|
1038
|
+
function isRollDue(binding, session) {
|
|
1039
|
+
const { config, codec, transport } = binding;
|
|
975
1040
|
const { rolling } = config;
|
|
976
|
-
if (rolling === void 0 || transport.source !== "cookie"
|
|
977
|
-
if (session.
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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,
|
|
1052
|
+
async function fireError(binding, failure) {
|
|
985
1053
|
await binding.config.hooks?.onError?.({
|
|
986
|
-
session:
|
|
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
|
|
1011
|
-
const
|
|
1012
|
-
|
|
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
|
|
1096
|
+
expiresAt
|
|
1021
1097
|
});
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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",
|