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