unjwt 0.8.4 → 0.8.5
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/README.md +3 -9
- package/dist/_chunks/{_import-key.mjs → algorithms.mjs} +194 -399
- package/dist/_chunks/{_codec-jwe.d.mts → codec-jwe.d.mts} +40 -9
- package/dist/_chunks/{_codec-jws.mjs → codec-jws.mjs} +133 -80
- package/dist/_chunks/cookie.mjs +50 -0
- package/dist/_chunks/digest.mjs +10 -3
- package/dist/_chunks/error.d.mts +103 -0
- package/dist/_chunks/export.mjs +97 -0
- package/dist/_chunks/index.d.mts +5 -103
- package/dist/_chunks/index2.d.mts +206 -12
- package/dist/_chunks/index3.d.mts +12 -109
- package/dist/_chunks/{jwe.d.mts → index4.d.mts} +2 -1
- package/dist/_chunks/{jws.d.mts → index5.d.mts} +2 -1
- package/dist/_chunks/index6.d.mts +113 -0
- package/dist/_chunks/jwe.mjs +205 -72
- package/dist/{core → _chunks}/jwk.mjs +210 -495
- package/dist/_chunks/jws.mjs +280 -193
- package/dist/_chunks/jwt.d.mts +2 -1
- package/dist/_chunks/{_jwt-internal.mjs → jwt.mjs} +110 -54
- package/dist/_chunks/libs/unsecure.d.mts +35 -25
- package/dist/_chunks/libs/unsecure.mjs +101 -268
- package/dist/_chunks/{_internal.d.mts → limits.d.mts} +1 -1
- package/dist/_chunks/payload.mjs +23 -0
- package/dist/_chunks/wrap.mjs +209 -0
- package/dist/adapters/elysia/index.d.mts +4 -4
- package/dist/adapters/elysia/index.mjs +4 -3
- package/dist/adapters/h3/index.d.mts +13 -13
- package/dist/adapters/h3/index.mjs +8 -11
- package/dist/adapters/h3v1/index.d.mts +5 -5
- package/dist/adapters/h3v1/index.mjs +6 -9
- package/dist/core/internal.mjs +1 -1
- package/dist/core/{jwe.d.mts → jwe/index.d.mts} +5 -4
- package/dist/core/jwe/index.mjs +5 -0
- package/dist/core/{jwk.d.mts → jwk/index.d.mts} +5 -4
- package/dist/core/jwk/index.mjs +7 -0
- package/dist/core/{jws.d.mts → jws/index.d.mts} +5 -4
- package/dist/core/jws/index.mjs +5 -0
- package/dist/dpop/index.d.mts +73 -0
- package/dist/dpop/index.mjs +296 -0
- package/dist/error.d.mts +3 -0
- package/dist/error.mjs +20 -0
- package/dist/index.d.mts +10 -9
- package/dist/index.mjs +7 -4
- package/dist/session/index.d.mts +4 -4
- package/dist/session/index.mjs +6 -47
- package/dist/utils/index.d.mts +7 -0
- package/dist/{core/utils → utils}/index.mjs +4 -3
- package/package.json +21 -10
- package/dist/_chunks/jwk.d.mts +0 -205
- package/dist/core/jwe.mjs +0 -5
- package/dist/core/jws.mjs +0 -5
- package/dist/core/utils/index.d.mts +0 -6
- /package/dist/_chunks/{_cookie-attributes.d.mts → cookie-attributes.d.mts} +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import "./error.mjs";
|
|
1
2
|
import { ExpiresIn, JWEAsymmetricPrivateJWK, JWEAsymmetricPublicJWK, JWEDecryptJWK, JWEDecryptOptions, JWEEncryptJWK, JWEEncryptOptions, JWEHeaderParameters, JWKSet, JWK_HMAC, JWK_oct, JWSAsymmetricPrivateJWK, JWSAsymmetricPublicJWK, JWSHeaderParameters, JWSSignOptions, JWSVerifyJWK, JWSVerifyOptions, JWTClaims, StrictOmit } from "./index.mjs";
|
|
2
3
|
interface ExpirableCookieAttributes {
|
|
3
4
|
expires?: Date;
|
|
@@ -14,7 +15,7 @@ export type SessionUpdate<T extends SessionRecord = SessionClaims> = Partial<Ses
|
|
|
14
15
|
export interface SessionManager<T extends SessionRecord = SessionClaims, ConfigMaxAge extends ExpiresIn | undefined = ExpiresIn | undefined> {
|
|
15
16
|
readonly id: string | undefined;
|
|
16
17
|
readonly createdAt: number;
|
|
17
|
-
/** Expiry in ms
|
|
18
|
+
/** Expiry in ms: `exp`, or `iat + maxAge` when sooner; always a number when the session has a `maxAge`. */
|
|
18
19
|
readonly expiresAt: ConfigMaxAge extends ExpiresIn ? number : number | undefined;
|
|
19
20
|
readonly data: SessionData<T>;
|
|
20
21
|
readonly token: string | undefined;
|
|
@@ -28,7 +29,7 @@ export interface SessionManager<T extends SessionRecord = SessionClaims, ConfigM
|
|
|
28
29
|
}
|
|
29
30
|
/** The config members every session shares, whether it signs or seals its token. */
|
|
30
31
|
interface SessionBaseConfig<MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined> {
|
|
31
|
-
/** Sets `exp = iat + maxAge`, requires `exp` on incoming tokens, drives cookie expiry; unset never expires. */
|
|
32
|
+
/** Sets `exp = iat + maxAge`, requires `exp` on incoming tokens and ends them at `iat + maxAge` at the latest, drives cookie expiry; unset never expires. */
|
|
32
33
|
maxAge?: MaxAge;
|
|
33
34
|
/**
|
|
34
35
|
* Reissue the session when it is read, needs `maxAge`.
|
|
@@ -49,6 +50,7 @@ interface SessionBaseConfig<MaxAge extends ExpiresIn | undefined = ExpiresIn | u
|
|
|
49
50
|
name?: string;
|
|
50
51
|
/**
|
|
51
52
|
* Header carrying a bare or `Bearer` token: a header name, `true` for `x-<name>-session` (cookie prefix stripped), or `false` for none.
|
|
53
|
+
* A request that carries it is read from it and never from the cookie, and writes no cookie back; any other value is a token failure (`onError`).
|
|
52
54
|
* Left unset, a session with cookies still reads `x-<name>-session`, and so does one with `cookie: false`.
|
|
53
55
|
*/
|
|
54
56
|
sessionHeader?: boolean | string;
|
|
@@ -68,7 +70,7 @@ interface SessionHooks<TSession, TManager, TArgs extends Record<string, unknown>
|
|
|
68
70
|
};
|
|
69
71
|
config: TConfig;
|
|
70
72
|
}) => void | Promise<void>;
|
|
71
|
-
/** Fires after a token is issued and its cookie written, by `update()` or by `rolling`; a throw rejects without rollback. */
|
|
73
|
+
/** Fires after a token is issued and, on a cookie session, its cookie written, by `update()` or by `rolling`; a throw rejects without rollback. */
|
|
72
74
|
onUpdate?: (args: TArgs & {
|
|
73
75
|
session: TManager & {
|
|
74
76
|
id: string;
|
|
@@ -104,7 +106,7 @@ interface SessionHooks<TSession, TManager, TArgs extends Record<string, unknown>
|
|
|
104
106
|
}
|
|
105
107
|
/** Per-issue overrides for a single `update()` call. */
|
|
106
108
|
export interface SessionIssueOptions {
|
|
107
|
-
/** Lifetime for this issue only, overriding the config's `maxAge`. */
|
|
109
|
+
/** Lifetime for this issue only, overriding the config's `maxAge`; a read under that `maxAge` still ends the session at `iat + maxAge`. */
|
|
108
110
|
maxAge?: ExpiresIn;
|
|
109
111
|
}
|
|
110
112
|
/** In-request session state, mirroring the claims of the underlying token. */
|
|
@@ -113,7 +115,7 @@ export interface SessionState<T extends SessionRecord, MaxAge extends ExpiresIn
|
|
|
113
115
|
id: string | undefined;
|
|
114
116
|
/** In ms; mirrors the JWT `iat` claim (seconds). */
|
|
115
117
|
createdAt: number;
|
|
116
|
-
/** In ms; mirrors the JWT `exp` claim (seconds). */
|
|
118
|
+
/** In ms; mirrors the JWT `exp` claim (seconds), or `iat + maxAge` when sooner. */
|
|
117
119
|
expiresAt: MaxAge extends ExpiresIn ? number : number | undefined;
|
|
118
120
|
data: SessionData<T>;
|
|
119
121
|
token: string | undefined;
|
|
@@ -127,7 +129,7 @@ export type OpenedSessionState<T extends SessionRecord, MaxAge extends ExpiresIn
|
|
|
127
129
|
export interface SessionDefinition<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TEvent = unknown> {
|
|
128
130
|
/** Reads the request's session once and returns its manager; later calls on the same request share that state. */
|
|
129
131
|
use: (event: TEvent) => Promise<SessionManager<T, MaxAge>>;
|
|
130
|
-
/** The token the request carries (
|
|
132
|
+
/** The session's token once `use()` read or wrote it, else the one the request carries (the session header before the cookie), unopened. */
|
|
131
133
|
token: (event: TEvent) => string | undefined;
|
|
132
134
|
/** Mints a token for `data` with a fresh `jti` and `iat`, and `exp` from `maxAge` (`options.maxAge` overrides it); reads and writes nothing. */
|
|
133
135
|
issue: (data?: Partial<SessionData<T>>, options?: SessionIssueOptions) => Promise<string>;
|
|
@@ -135,9 +137,13 @@ export interface SessionDefinition<T extends SessionRecord = SessionClaims, MaxA
|
|
|
135
137
|
open: (token: string) => Promise<OpenedSessionState<T, MaxAge>>;
|
|
136
138
|
}
|
|
137
139
|
interface SessionRequestBase {
|
|
138
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* The response being prepared; without it the session is read-only.
|
|
142
|
+
* With `errHeaders` too (h3 v2, which sends only those for a thrown handler), every cookie write lands in both.
|
|
143
|
+
*/
|
|
139
144
|
readonly res?: {
|
|
140
145
|
readonly headers: Headers;
|
|
146
|
+
readonly errHeaders?: Headers;
|
|
141
147
|
};
|
|
142
148
|
/** Per-request state shared with the framework, where `sessions` is kept; without it state is keyed on `req`, else on this object. */
|
|
143
149
|
readonly context?: Record<string, unknown>;
|
|
@@ -213,10 +219,35 @@ export interface JWSSessionHooks<TSession, TManager, TArgs extends Record<string
|
|
|
213
219
|
config: TConfig;
|
|
214
220
|
}) => JWKSet | JWSVerifyJWK | Promise<JWKSet | JWSVerifyJWK>;
|
|
215
221
|
}
|
|
216
|
-
/**
|
|
222
|
+
/**
|
|
223
|
+
* A symmetric `oct` JWK, or a `CryptoKey` (requires pinned `alg` and `enc`): the one key seals and unseals.
|
|
224
|
+
*
|
|
225
|
+
* The `{ privateKey, publicKey? }` pair members are deprecated: a pair seals with its public half,
|
|
226
|
+
* so anyone holding the public key can mint a session the server accepts. Use an `oct` JWK, or
|
|
227
|
+
* `defineJWSSession` for a key pair.
|
|
228
|
+
*/
|
|
217
229
|
type JWESessionKey = CryptoKey | JWEEncryptJWK | {
|
|
218
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Unseals; a private JWK alone seals with its own public half.
|
|
232
|
+
* @deprecated A pair seals with its public half, so anyone holding the public key can mint a session the server accepts. Use an `oct` JWK, or `defineJWSSession` for a key pair.
|
|
233
|
+
*/
|
|
234
|
+
privateKey: JWEAsymmetricPrivateJWK;
|
|
235
|
+
/**
|
|
236
|
+
* Seals.
|
|
237
|
+
* @deprecated A pair seals with its public half, so anyone holding the public key can mint a session the server accepts. Use an `oct` JWK, or `defineJWSSession` for a key pair.
|
|
238
|
+
*/
|
|
219
239
|
publicKey?: JWEAsymmetricPublicJWK | CryptoKey;
|
|
240
|
+
} | {
|
|
241
|
+
/**
|
|
242
|
+
* Unseals; a private `CryptoKey` has no public half to derive, so it comes with one.
|
|
243
|
+
* @deprecated A pair seals with its public half, so anyone holding the public key can mint a session the server accepts. Use an `oct` JWK, or `defineJWSSession` for a key pair.
|
|
244
|
+
*/
|
|
245
|
+
privateKey: CryptoKey;
|
|
246
|
+
/**
|
|
247
|
+
* Seals.
|
|
248
|
+
* @deprecated A pair seals with its public half, so anyone holding the public key can mint a session the server accepts. Use an `oct` JWK, or `defineJWSSession` for a key pair.
|
|
249
|
+
*/
|
|
250
|
+
publicKey: JWEAsymmetricPublicJWK | CryptoKey;
|
|
220
251
|
};
|
|
221
252
|
/** JWE decrypt overrides, minus the options the adapter owns. */
|
|
222
253
|
export type SessionDecryptOptions = StrictOmit<JWEDecryptOptions, "algorithms" | "encryptionAlgorithms" | "typ" | "maxTokenAge" | "validateClaims" | "forceUint8Array" | "payload">;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { JWTError,
|
|
3
|
-
import {
|
|
1
|
+
import { safeJsonParse } from "./libs/unsecure.mjs";
|
|
2
|
+
import { JWTError, isJWTError } from "../error.mjs";
|
|
3
|
+
import { computeDurationInSeconds, expiredClaimMessage, isCryptoKey, isJWK, isJWKSet, isPlainObject, isPrivateJWK, isPublicJWK, isSymmetricJWK, quoteTokenValue } from "./jwt.mjs";
|
|
4
|
+
import { importKey } from "./algorithms.mjs";
|
|
4
5
|
import { sign, verify } from "./jws.mjs";
|
|
5
6
|
import { decrypt, encrypt } from "./jwe.mjs";
|
|
7
|
+
import { toPublicJWK } from "./export.mjs";
|
|
6
8
|
const RESERVED_CLAIMS = [
|
|
7
9
|
"jti",
|
|
8
10
|
"iat",
|
|
@@ -29,10 +31,12 @@ function readOnlyEventError(message) {
|
|
|
29
31
|
return new JWTError(message, "ERR_SESSION_READONLY_EVENT");
|
|
30
32
|
}
|
|
31
33
|
function expiredSessionError(options) {
|
|
32
|
-
const { id, expiresAt } = options;
|
|
34
|
+
const { id, createdAt, expiresAt } = options;
|
|
33
35
|
const exp = Math.floor(expiresAt / 1e3);
|
|
36
|
+
const iat = createdAt === void 0 ? void 0 : Math.floor(createdAt / 1e3);
|
|
34
37
|
return new JWTError(expiredClaimMessage(exp), "ERR_JWT_EXPIRED", {
|
|
35
38
|
jti: id,
|
|
39
|
+
iat,
|
|
36
40
|
exp
|
|
37
41
|
});
|
|
38
42
|
}
|
|
@@ -131,12 +135,17 @@ function createSessionCodec(spec) {
|
|
|
131
135
|
const now = (config) => spec.issueOptions(config)?.currentDate?.getTime() ?? Date.now();
|
|
132
136
|
const openWith = async (config, token, key) => {
|
|
133
137
|
const payload = await spec.open(config, token, key).catch((error) => rethrowAsInvalidToken(error, { prefix }));
|
|
134
|
-
|
|
138
|
+
const clock = {
|
|
135
139
|
now: now(config),
|
|
136
|
-
clockTolerance: spec.openOptions(config)?.clockTolerance
|
|
140
|
+
clockTolerance: spec.openOptions(config)?.clockTolerance
|
|
141
|
+
};
|
|
142
|
+
assertNotFromTheFuture(payload, {
|
|
143
|
+
...clock,
|
|
137
144
|
prefix
|
|
138
145
|
});
|
|
139
|
-
|
|
146
|
+
const opened = openedSession(payload, { maxAge: config.maxAge });
|
|
147
|
+
assertNotPastMaxAge(opened, clock);
|
|
148
|
+
return opened;
|
|
140
149
|
};
|
|
141
150
|
const assertOpenKey = async (config) => {
|
|
142
151
|
if (spec.lookup(config, {})) return;
|
|
@@ -218,19 +227,33 @@ function sessionPayload(state) {
|
|
|
218
227
|
return payload;
|
|
219
228
|
}
|
|
220
229
|
function assertNotFromTheFuture(payload, options) {
|
|
221
|
-
|
|
222
|
-
|
|
230
|
+
if (payload.iat > Math.floor(options.now / 1e3) + toleranceSeconds(options.clockTolerance)) throw invalidTokenError(`${options.prefix} Invalid session token: "iat" is in the future (${(/* @__PURE__ */ new Date(payload.iat * 1e3)).toISOString()}).`);
|
|
231
|
+
}
|
|
232
|
+
function assertNotPastMaxAge(opened, options) {
|
|
233
|
+
const { expiresAt } = opened;
|
|
234
|
+
if (expiresAt === void 0) return;
|
|
235
|
+
if (expiresAt <= options.now - toleranceSeconds(options.clockTolerance) * 1e3) throw expiredSessionError({
|
|
236
|
+
id: opened.id,
|
|
237
|
+
createdAt: opened.createdAt,
|
|
238
|
+
expiresAt
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function toleranceSeconds(clockTolerance) {
|
|
242
|
+
return clockTolerance === void 0 ? 0 : computeDurationInSeconds(clockTolerance);
|
|
223
243
|
}
|
|
224
|
-
function openedSession(payload) {
|
|
244
|
+
function openedSession(payload, options) {
|
|
225
245
|
const data = Object.create(null);
|
|
226
246
|
for (const key of Object.keys(payload)) {
|
|
227
247
|
if (RESERVED_CLAIMS.includes(key)) continue;
|
|
228
248
|
data[key] = payload[key];
|
|
229
249
|
}
|
|
250
|
+
const createdAt = payload.iat * 1e3;
|
|
251
|
+
const exp = payload.exp ? payload.exp * 1e3 : void 0;
|
|
252
|
+
const bound = resolveExpiresAt(createdAt, options);
|
|
230
253
|
return {
|
|
231
254
|
id: payload.jti,
|
|
232
|
-
createdAt
|
|
233
|
-
expiresAt:
|
|
255
|
+
createdAt,
|
|
256
|
+
expiresAt: exp === void 0 || bound === void 0 ? exp ?? bound : Math.min(exp, bound),
|
|
234
257
|
data
|
|
235
258
|
};
|
|
236
259
|
}
|
|
@@ -290,10 +313,7 @@ function getEncryptKey(config, prefix) {
|
|
|
290
313
|
assertKeyMaterial(key, { prefix });
|
|
291
314
|
let resolved;
|
|
292
315
|
if (isCryptoKey(key) || isSymmetricJWK(key)) resolved = key;
|
|
293
|
-
else if (isKeyPair(key))
|
|
294
|
-
assertJWEKeyPair(key, prefix);
|
|
295
|
-
resolved = key.publicKey;
|
|
296
|
-
}
|
|
316
|
+
else if (isKeyPair(key)) resolved = jweKeyPair(key, prefix).publicKey;
|
|
297
317
|
if (!resolved) throw invalidKeyError(`${prefix} Invalid JWE key, expected a JWK or a CryptoKey.`, key);
|
|
298
318
|
assertPinnedAlgEnc(resolved, config, prefix);
|
|
299
319
|
return resolved;
|
|
@@ -303,21 +323,27 @@ function getDecryptKey(config, prefix) {
|
|
|
303
323
|
assertKeyMaterial(key, { prefix });
|
|
304
324
|
let resolved;
|
|
305
325
|
if (isCryptoKey(key) || isSymmetricJWK(key)) resolved = key;
|
|
306
|
-
else if (isKeyPair(key))
|
|
307
|
-
assertJWEKeyPair(key, prefix);
|
|
308
|
-
resolved = key.privateKey;
|
|
309
|
-
}
|
|
326
|
+
else if (isKeyPair(key)) resolved = jweKeyPair(key, prefix).privateKey;
|
|
310
327
|
if (!resolved) throw invalidKeyError(`${prefix} Invalid JWE key, expected a JWK, a private JWK or a CryptoKey.`, key);
|
|
311
328
|
assertPinnedAlgEnc(resolved, config, prefix);
|
|
312
329
|
return resolved;
|
|
313
330
|
}
|
|
314
|
-
function
|
|
315
|
-
|
|
331
|
+
function jweKeyPair(key, prefix) {
|
|
332
|
+
const pair = {
|
|
333
|
+
privateKey: key.privateKey,
|
|
334
|
+
publicKey: key.publicKey ?? derivedPublicHalf(key.privateKey, prefix)
|
|
335
|
+
};
|
|
336
|
+
assertKeyPair(pair, {
|
|
316
337
|
prefix,
|
|
317
338
|
family: "JWE",
|
|
318
339
|
isPublic: (half) => isCryptoKey(half) || isPublicJWK(half),
|
|
319
340
|
publicHalf: "a public JWK or a CryptoKey"
|
|
320
341
|
});
|
|
342
|
+
return pair;
|
|
343
|
+
}
|
|
344
|
+
function derivedPublicHalf(privateKey, prefix) {
|
|
345
|
+
if (isPrivateJWK(privateKey)) return toPublicJWK(privateKey);
|
|
346
|
+
throw invalidKeyError(`${prefix} Invalid JWE key: a CryptoKey "privateKey" needs its "publicKey".`, privateKey);
|
|
321
347
|
}
|
|
322
348
|
function assertPinnedAlgEnc(key, config, prefix) {
|
|
323
349
|
const { alg, enc } = config.jwe?.encryptOptions ?? {};
|
|
@@ -327,6 +353,43 @@ function assertPinnedAlgEnc(key, config, prefix) {
|
|
|
327
353
|
hint: "\"jwe.encryptOptions.alg\" and \"enc\""
|
|
328
354
|
});
|
|
329
355
|
}
|
|
356
|
+
const kSessionPending = Symbol("unjwt_session_pending");
|
|
357
|
+
function sessionInstant(source) {
|
|
358
|
+
const now = source.codec.now(source.config);
|
|
359
|
+
return now - now % 1e3;
|
|
360
|
+
}
|
|
361
|
+
function mintSessionId(config) {
|
|
362
|
+
return config.generateId?.() || crypto.randomUUID();
|
|
363
|
+
}
|
|
364
|
+
function emptySession(binding) {
|
|
365
|
+
const createdAt = sessionInstant(binding);
|
|
366
|
+
return typedSession({
|
|
367
|
+
id: void 0,
|
|
368
|
+
createdAt,
|
|
369
|
+
expiresAt: resolveExpiresAt(createdAt, { maxAge: binding.config.maxAge }),
|
|
370
|
+
data: Object.create(null),
|
|
371
|
+
token: void 0
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
function storedSession(store) {
|
|
375
|
+
const state = store.get();
|
|
376
|
+
return state === void 0 ? void 0 : typedSession(state);
|
|
377
|
+
}
|
|
378
|
+
function blankSession() {
|
|
379
|
+
return typedSession({
|
|
380
|
+
expiresAt: void 0,
|
|
381
|
+
data: {}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
function openSessionToken(codec, config, options) {
|
|
385
|
+
return codec.open(config, options.token, options.args).then(typedOpened);
|
|
386
|
+
}
|
|
387
|
+
function typedSession(state) {
|
|
388
|
+
return state;
|
|
389
|
+
}
|
|
390
|
+
function typedOpened(opened) {
|
|
391
|
+
return opened;
|
|
392
|
+
}
|
|
330
393
|
const CHUNK_MARKER = "__chunked__";
|
|
331
394
|
const DEFAULT_CHUNK_MAX_LENGTH = 4e3;
|
|
332
395
|
function readChunked(store, name) {
|
|
@@ -374,6 +437,13 @@ function setCookieValues(setCookies) {
|
|
|
374
437
|
function setCookieName(setCookie) {
|
|
375
438
|
return setCookiePair(setCookie)?.name;
|
|
376
439
|
}
|
|
440
|
+
function mirrorSetCookie(name, headers) {
|
|
441
|
+
const { from, to } = headers;
|
|
442
|
+
const isThisCookie = (line) => setCookieName(line) === name;
|
|
443
|
+
const lines = [...to.getSetCookie().filter((line) => !isThisCookie(line)), ...from.getSetCookie().filter((line) => isThisCookie(line))];
|
|
444
|
+
to.delete("set-cookie");
|
|
445
|
+
for (const line of lines) to.append("set-cookie", line);
|
|
446
|
+
}
|
|
377
447
|
function chunkName(name, index) {
|
|
378
448
|
return `${name}.${index}`;
|
|
379
449
|
}
|
|
@@ -412,25 +482,25 @@ function decodeValue(value) {
|
|
|
412
482
|
function resolveTransport(driver, event, options) {
|
|
413
483
|
const handed = driver.handed?.(event);
|
|
414
484
|
if (handed) return {
|
|
485
|
+
source: "handed",
|
|
415
486
|
read: () => handed.token,
|
|
416
487
|
readonly: false,
|
|
417
488
|
writable: () => true
|
|
418
489
|
};
|
|
419
490
|
const { config, name } = options;
|
|
420
|
-
const header =
|
|
421
|
-
if (config.cookie === false) return {
|
|
422
|
-
|
|
491
|
+
const header = presentedHeader(driver, event, options);
|
|
492
|
+
if (header || config.cookie === false) return {
|
|
493
|
+
source: "header",
|
|
494
|
+
read: config.sessionHeader === false ? void 0 : () => header?.token,
|
|
495
|
+
malformed: header?.malformed,
|
|
423
496
|
readonly: false,
|
|
424
497
|
writable: () => true
|
|
425
498
|
};
|
|
426
499
|
const jar = driver.cookies(event);
|
|
427
500
|
const store = responseStore(jar);
|
|
428
501
|
const transport = {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (written === "") return void 0;
|
|
432
|
-
return (written ?? readChunked(jar, name)) || header?.() || void 0;
|
|
433
|
-
},
|
|
502
|
+
source: "cookie",
|
|
503
|
+
read: () => cookieToken(jar, name),
|
|
434
504
|
readonly: !store,
|
|
435
505
|
writable: () => driver.writable?.(event) ?? true
|
|
436
506
|
};
|
|
@@ -471,11 +541,23 @@ function headerToken(value) {
|
|
|
471
541
|
if (/^bearer$/i.test(first)) return words.length === 2 ? second : void 0;
|
|
472
542
|
return words.length === 1 && first ? first : void 0;
|
|
473
543
|
}
|
|
474
|
-
function
|
|
544
|
+
function presentedHeader(driver, event, options) {
|
|
475
545
|
const { config, name } = options;
|
|
476
546
|
if (config.sessionHeader === false) return void 0;
|
|
477
547
|
const header = sessionHeaderName(name, config.sessionHeader);
|
|
478
|
-
|
|
548
|
+
const value = driver.header(event, header);
|
|
549
|
+
if (typeof value !== "string") return void 0;
|
|
550
|
+
const token = headerToken(value);
|
|
551
|
+
if (token) return { token };
|
|
552
|
+
return {
|
|
553
|
+
token: void 0,
|
|
554
|
+
malformed: invalidTokenError(`${driver.prefix} Invalid session token: the "${header}" header holds neither a token nor "Bearer <token>".`)
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function cookieToken(jar, name) {
|
|
558
|
+
const written = jar.written ? readChunked({ read: (key) => jar.written?.(key) }, name) : void 0;
|
|
559
|
+
if (written === "") return void 0;
|
|
560
|
+
return (written ?? readChunked(jar, name)) || void 0;
|
|
479
561
|
}
|
|
480
562
|
function responseStore(jar) {
|
|
481
563
|
const write = jar.write?.bind(jar);
|
|
@@ -521,7 +603,8 @@ function bindSession(event, config, options) {
|
|
|
521
603
|
function sessionToken(binding) {
|
|
522
604
|
const { transport } = binding;
|
|
523
605
|
assertTokenSource(transport, { prefix: binding.driver.prefix });
|
|
524
|
-
|
|
606
|
+
const session = storedSession(binding.store);
|
|
607
|
+
return session ? session.token : transport.read();
|
|
525
608
|
}
|
|
526
609
|
function recordSlotStore(record, slot) {
|
|
527
610
|
return {
|
|
@@ -563,43 +646,6 @@ function claimSessionName(record, options) {
|
|
|
563
646
|
if (held === family) return;
|
|
564
647
|
throw invalidConfigError(`${prefix} Session name "${name}" is already used by a ${held.toUpperCase()} session on this request.`);
|
|
565
648
|
}
|
|
566
|
-
const kSessionPending = Symbol("unjwt_session_pending");
|
|
567
|
-
function sessionInstant(source) {
|
|
568
|
-
const now = source.codec.now(source.config);
|
|
569
|
-
return now - now % 1e3;
|
|
570
|
-
}
|
|
571
|
-
function mintSessionId(config) {
|
|
572
|
-
return config.generateId?.() || crypto.randomUUID();
|
|
573
|
-
}
|
|
574
|
-
function emptySession(binding) {
|
|
575
|
-
const createdAt = sessionInstant(binding);
|
|
576
|
-
return typedSession({
|
|
577
|
-
id: void 0,
|
|
578
|
-
createdAt,
|
|
579
|
-
expiresAt: resolveExpiresAt(createdAt, { maxAge: binding.config.maxAge }),
|
|
580
|
-
data: Object.create(null),
|
|
581
|
-
token: void 0
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
function storedSession(store) {
|
|
585
|
-
const state = store.get();
|
|
586
|
-
return state === void 0 ? void 0 : typedSession(state);
|
|
587
|
-
}
|
|
588
|
-
function blankSession() {
|
|
589
|
-
return typedSession({
|
|
590
|
-
expiresAt: void 0,
|
|
591
|
-
data: {}
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
function openSessionToken(codec, config, options) {
|
|
595
|
-
return codec.open(config, options.token, options.args).then(typedOpened);
|
|
596
|
-
}
|
|
597
|
-
function typedSession(state) {
|
|
598
|
-
return state;
|
|
599
|
-
}
|
|
600
|
-
function typedOpened(opened) {
|
|
601
|
-
return opened;
|
|
602
|
-
}
|
|
603
649
|
async function loadSession(binding) {
|
|
604
650
|
const { config, codec, store, args, transport } = binding;
|
|
605
651
|
await codec.assertKeys(config);
|
|
@@ -621,6 +667,7 @@ async function loadSession(binding) {
|
|
|
621
667
|
},
|
|
622
668
|
error: expiredSessionError({
|
|
623
669
|
id: session.id,
|
|
670
|
+
createdAt: session.createdAt,
|
|
624
671
|
expiresAt: session.expiresAt
|
|
625
672
|
})
|
|
626
673
|
});
|
|
@@ -630,6 +677,10 @@ async function loadSession(binding) {
|
|
|
630
677
|
}
|
|
631
678
|
const session = emptySession(binding);
|
|
632
679
|
store.set(session);
|
|
680
|
+
if (transport.malformed) {
|
|
681
|
+
await fireError(binding, transport.malformed);
|
|
682
|
+
return session;
|
|
683
|
+
}
|
|
633
684
|
const token = transport.read();
|
|
634
685
|
if (!token) return session;
|
|
635
686
|
session.token = token;
|
|
@@ -660,12 +711,7 @@ async function loadSession(binding) {
|
|
|
660
711
|
});
|
|
661
712
|
} else {
|
|
662
713
|
if (isJWTError(failure.error, "ERR_SESSION_TOKEN_INVALID") && isTokenFailure(failure.error)) dropSessionCookie(binding);
|
|
663
|
-
await
|
|
664
|
-
session: binding.actions.manager(binding),
|
|
665
|
-
...args,
|
|
666
|
-
error: failure.error,
|
|
667
|
-
config
|
|
668
|
-
});
|
|
714
|
+
await fireError(binding, failure.error);
|
|
669
715
|
}
|
|
670
716
|
return session;
|
|
671
717
|
}
|
|
@@ -692,6 +738,14 @@ async function rollSession(binding, session) {
|
|
|
692
738
|
if (transport.readonly || !transport.writable()) return;
|
|
693
739
|
await binding.actions.roll(binding);
|
|
694
740
|
}
|
|
741
|
+
async function fireError(binding, error) {
|
|
742
|
+
await binding.config.hooks?.onError?.({
|
|
743
|
+
session: binding.actions.manager(binding),
|
|
744
|
+
...binding.args,
|
|
745
|
+
error,
|
|
746
|
+
config: binding.config
|
|
747
|
+
});
|
|
748
|
+
}
|
|
695
749
|
async function fireExpire(binding, expired) {
|
|
696
750
|
await binding.config.hooks?.onExpire?.({
|
|
697
751
|
session: expired.session,
|
|
@@ -797,8 +851,7 @@ function sessionManager(binding) {
|
|
|
797
851
|
return (state() ?? blankSession()).data;
|
|
798
852
|
},
|
|
799
853
|
get token() {
|
|
800
|
-
|
|
801
|
-
return session ? session.token : sessionToken(binding);
|
|
854
|
+
return sessionToken(binding);
|
|
802
855
|
},
|
|
803
856
|
update: async (update, options) => {
|
|
804
857
|
await issueSession(binding, update, options);
|
|
@@ -822,7 +875,7 @@ function sessionActions() {
|
|
|
822
875
|
function snapshotSession(session) {
|
|
823
876
|
return {
|
|
824
877
|
...session,
|
|
825
|
-
data:
|
|
878
|
+
data: safeJsonParse(JSON.stringify(session.data))
|
|
826
879
|
};
|
|
827
880
|
}
|
|
828
881
|
function defineSession(config, options) {
|
|
@@ -949,4 +1002,4 @@ function assertPinnedAlg(key, config, prefix) {
|
|
|
949
1002
|
hint: "\"jws.signOptions.alg\""
|
|
950
1003
|
});
|
|
951
1004
|
}
|
|
952
|
-
export { bindSession, createJWECodec, createJWSCodec, defineSession, dropSession, encodeSession, invalidConfigError, issueSession, loadSession, openSessionToken, sessionActions, setCookieName, setCookieValues };
|
|
1005
|
+
export { bindSession, createJWECodec, createJWSCodec, defineSession, dropSession, encodeSession, invalidConfigError, issueSession, loadSession, mirrorSetCookie, openSessionToken, sessionActions, setCookieName, setCookieValues };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { mirrorSetCookie, setCookieName, setCookieValues } from "./codec-jws.mjs";
|
|
2
|
+
import { parse, parseSetCookie, serialize } from "./libs/cookie-es.mjs";
|
|
3
|
+
function webCookieJar(request, response) {
|
|
4
|
+
let cookies;
|
|
5
|
+
const jar = { read: (name) => (cookies ||= requestCookies(request))[name] };
|
|
6
|
+
if (response) {
|
|
7
|
+
const { headers, errHeaders } = response;
|
|
8
|
+
jar.written = (name) => setCookieValues(headers.getSetCookie())[name];
|
|
9
|
+
jar.write = (name, value, attrs) => {
|
|
10
|
+
setCookie(headers, {
|
|
11
|
+
name,
|
|
12
|
+
value,
|
|
13
|
+
...attrs
|
|
14
|
+
});
|
|
15
|
+
if (errHeaders) mirrorSetCookie(name, {
|
|
16
|
+
from: headers,
|
|
17
|
+
to: errHeaders
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return jar;
|
|
22
|
+
}
|
|
23
|
+
function requestCookies(request) {
|
|
24
|
+
const header = request.get("cookie");
|
|
25
|
+
return header ? parse(header) : Object.create(null);
|
|
26
|
+
}
|
|
27
|
+
function setCookie(headers, cookie) {
|
|
28
|
+
const line = serialize({
|
|
29
|
+
path: "/",
|
|
30
|
+
...cookie
|
|
31
|
+
});
|
|
32
|
+
const key = cookieKey(cookie);
|
|
33
|
+
const kept = headers.getSetCookie().filter((existing) => {
|
|
34
|
+
const parsed = parseSetCookie(existing);
|
|
35
|
+
return parsed ? cookieKey(parsed) !== key : setCookieName(existing) !== cookie.name;
|
|
36
|
+
});
|
|
37
|
+
headers.delete("set-cookie");
|
|
38
|
+
for (const existing of kept) headers.append("set-cookie", existing);
|
|
39
|
+
headers.append("set-cookie", line);
|
|
40
|
+
}
|
|
41
|
+
function cookieKey(cookie) {
|
|
42
|
+
const domain = (cookie.domain || "").replace(/^\./, "").toLowerCase();
|
|
43
|
+
return [
|
|
44
|
+
cookie.name,
|
|
45
|
+
domain,
|
|
46
|
+
cookie.path || "/",
|
|
47
|
+
cookie.partitioned ? "1" : "0"
|
|
48
|
+
].join(";");
|
|
49
|
+
}
|
|
50
|
+
export { requestCookies, webCookieJar };
|
package/dist/_chunks/digest.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { textEncoder } from "./libs/unsecure.mjs";
|
|
2
|
-
import { JWTError
|
|
2
|
+
import { JWTError } from "../error.mjs";
|
|
3
|
+
import { base64UrlStringify, hasSymmetricSecret, isCryptoKey, isCryptoKeyPair, isJWK, isJWKSet } from "./jwt.mjs";
|
|
3
4
|
const ECDSA_CURVES = /* @__PURE__ */ Object.freeze({
|
|
4
5
|
ES256: "P-256",
|
|
5
6
|
ES384: "P-384",
|
|
@@ -149,7 +150,7 @@ function unionFromJWKSet(keys, mapper, options) {
|
|
|
149
150
|
const algs = /* @__PURE__ */ new Set();
|
|
150
151
|
for (const k of keys) {
|
|
151
152
|
if (!isJWK(k)) continue;
|
|
152
|
-
if (
|
|
153
|
+
if (!options?.allowSymmetric && hasSymmetricSecret(k)) continue;
|
|
153
154
|
const mapped = mapper(k);
|
|
154
155
|
if (!mapped) continue;
|
|
155
156
|
for (const a of mapped) algs.add(a);
|
|
@@ -159,6 +160,12 @@ function unionFromJWKSet(keys, mapper, options) {
|
|
|
159
160
|
function isEd25519Signature(alg) {
|
|
160
161
|
return alg === "EdDSA" || alg === "Ed25519";
|
|
161
162
|
}
|
|
163
|
+
function isSameAlgorithm(a, b) {
|
|
164
|
+
return a === b || isEd25519Signature(a) && isEd25519Signature(b);
|
|
165
|
+
}
|
|
166
|
+
function jwkAlgServes(jwk, alg) {
|
|
167
|
+
return isSameAlgorithm(jwk.alg, alg) || jwk.kty === "oct" && alg === "dir" && isContentEncryptionAlgorithm(jwk.alg);
|
|
168
|
+
}
|
|
162
169
|
function jwsAlgsFromJWK(jwk) {
|
|
163
170
|
if (jwk.alg && isEd25519Signature(jwk.alg)) return ["EdDSA", "Ed25519"];
|
|
164
171
|
if (jwk.alg) return isJWSAlgorithm(jwk.alg) ? [jwk.alg] : void 0;
|
|
@@ -253,4 +260,4 @@ async function digestUrl(data, hash = "SHA-256") {
|
|
|
253
260
|
const bytes = typeof data === "string" ? textEncoder.encode(data) : data;
|
|
254
261
|
return base64UrlStringify(new Uint8Array(await crypto.subtle.digest(hash, bytes)));
|
|
255
262
|
}
|
|
256
|
-
export { ECDSA_CURVES, PBES2_ALGORITHMS, aesBitsOf, checkKeyUsage, cryptoKeyCurve, digestUrl, hashBitsOf, inferJWEAllowedAlgorithms, inferJWSAllowedAlgorithms, innerKeyWrapAlg, isAlgorithm, isContentEncryptionAlgorithm, isECDHESAlgorithm,
|
|
263
|
+
export { ECDSA_CURVES, PBES2_ALGORITHMS, aesBitsOf, checkKeyUsage, cryptoKeyCurve, digestUrl, hashBitsOf, inferJWEAllowedAlgorithms, inferJWSAllowedAlgorithms, innerKeyWrapAlg, isAlgorithm, isContentEncryptionAlgorithm, isECDHESAlgorithm, isJWSAlgorithm, isKeyManagementAlgorithm, isPBES2Algorithm, isSameAlgorithm, jwkAlgServes, jwsHashOf, lengthOf, modulusLengthOf, namedCurveOf, pbes2HashOf, rsaOaepHashOf, shaBitsOf, unusableKey };
|