unjwt 0.8.5 → 0.9.1
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 +34 -21
- package/dist/_chunks/algorithms.mjs +44 -20
- package/dist/_chunks/digest.d.mts +4 -4
- package/dist/_chunks/digest.mjs +23 -15
- package/dist/_chunks/error.d.mts +29 -42
- package/dist/_chunks/export.mjs +1 -4
- package/dist/_chunks/index.d.mts +270 -234
- package/dist/_chunks/index2.d.mts +82 -189
- package/dist/_chunks/index4.d.mts +223 -60
- package/dist/_chunks/index5.d.mts +68 -31
- package/dist/_chunks/index6.d.mts +45 -64
- package/dist/_chunks/jwe.mjs +53 -48
- package/dist/_chunks/jwk.mjs +9 -3
- package/dist/_chunks/jws.mjs +55 -50
- package/dist/_chunks/jwt.d.mts +13 -49
- package/dist/_chunks/jwt.mjs +4 -158
- package/dist/_chunks/jwt2.mjs +162 -0
- package/dist/_chunks/libs/unsecure.d.mts +0 -6
- package/dist/_chunks/limits.d.mts +3 -2
- package/dist/_chunks/payload.mjs +13 -7
- package/dist/_chunks/wrap.mjs +3 -1
- package/dist/core/internal.d.mts +50 -2
- package/dist/core/internal.mjs +3 -1
- package/dist/core/jwe/index.d.mts +1 -1
- package/dist/core/jwk/index.d.mts +2 -2
- package/dist/core/jwk/index.mjs +2 -2
- package/dist/dpop/index.d.mts +77 -18
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +4 -4
- package/dist/session/index.d.mts +568 -22
- package/dist/session/index.mjs +1386 -34
- package/dist/utils/index.d.mts +4 -4
- package/dist/utils/index.mjs +2 -10
- package/package.json +4 -32
- package/dist/_chunks/codec-jwe.d.mts +0 -280
- package/dist/_chunks/codec-jws.mjs +0 -1005
- package/dist/_chunks/cookie-attributes.d.mts +0 -12
- package/dist/_chunks/cookie.mjs +0 -50
- package/dist/adapters/elysia/index.d.mts +0 -123
- package/dist/adapters/elysia/index.mjs +0 -120
- package/dist/adapters/h3/index.d.mts +0 -135
- package/dist/adapters/h3/index.mjs +0 -117
- package/dist/adapters/h3v1/index.d.mts +0 -148
- package/dist/adapters/h3v1/index.mjs +0 -128
package/dist/session/index.mjs
CHANGED
|
@@ -1,73 +1,1425 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { safeJsonParse } from "../_chunks/libs/unsecure.mjs";
|
|
2
|
+
import { JWTError, isJWTError } from "../error.mjs";
|
|
3
|
+
import { computeDurationInSeconds, expiredClaimMessage, isAsymmetricJWK, isCryptoKey, isJWK, isJWKSet, isPlainObject, isPrivateJWK, isPublicJWK, isSymmetricJWK } from "../_chunks/jwt.mjs";
|
|
4
|
+
import { quoteTokenValue } from "../_chunks/jwt2.mjs";
|
|
5
|
+
import { importKey, resolveLimit, thumbprint } from "../_chunks/algorithms.mjs";
|
|
6
|
+
import { PBES2_ALGORITHMS, inferJWEAllowedAlgorithms, inferJWSAllowedAlgorithms, isContentEncryptionAlgorithm, lengthOf } from "../_chunks/digest.mjs";
|
|
7
|
+
import { sign, verify } from "../_chunks/jws.mjs";
|
|
8
|
+
import { decrypt, encrypt } from "../_chunks/jwe.mjs";
|
|
2
9
|
import { toPublicJWK } from "../_chunks/export.mjs";
|
|
3
10
|
import { deriveJWKFromPassword, exportPEM, generateJWK, importPEM } from "../_chunks/jwk.mjs";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
import { parse, parseSetCookie, serialize } from "../_chunks/libs/cookie-es.mjs";
|
|
12
|
+
const RESERVED_CLAIMS = [
|
|
13
|
+
"jti",
|
|
14
|
+
"iat",
|
|
15
|
+
"exp",
|
|
16
|
+
"nbf"
|
|
17
|
+
];
|
|
18
|
+
const SESSION_TYP = "session+jwt";
|
|
19
|
+
const FORBIDDEN_KEYS = [
|
|
20
|
+
"__proto__",
|
|
21
|
+
"constructor",
|
|
22
|
+
"prototype"
|
|
23
|
+
];
|
|
24
|
+
const importedKeys = /* @__PURE__ */ new WeakMap();
|
|
25
|
+
function invalidKeyError(message, cause) {
|
|
26
|
+
return new JWTError(message, "ERR_SESSION_KEY_INVALID", cause);
|
|
27
|
+
}
|
|
28
|
+
function invalidConfigError(message, cause) {
|
|
29
|
+
return new JWTError(message, "ERR_SESSION_CONFIG_INVALID", cause);
|
|
30
|
+
}
|
|
31
|
+
function invalidTokenError(message, cause) {
|
|
32
|
+
return new JWTError(message, "ERR_SESSION_TOKEN_INVALID", cause);
|
|
33
|
+
}
|
|
34
|
+
function readOnlyEventError(message) {
|
|
35
|
+
return new JWTError(message, "ERR_SESSION_READONLY_EVENT");
|
|
36
|
+
}
|
|
37
|
+
function expiredSessionError(options) {
|
|
38
|
+
const { id, createdAt, expiresAt } = options;
|
|
39
|
+
const exp = Math.floor(expiresAt / 1e3);
|
|
40
|
+
const iat = createdAt === void 0 ? void 0 : Math.floor(createdAt / 1e3);
|
|
41
|
+
return new JWTError(expiredClaimMessage(exp), "ERR_JWT_EXPIRED", {
|
|
42
|
+
jti: id,
|
|
43
|
+
iat,
|
|
44
|
+
exp
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function readFailedError(cause, options) {
|
|
48
|
+
return new JWTError(`${options.prefix} Session could not be read: ${messageOf(cause)}`, "ERR_SESSION_READ_FAILED", cause);
|
|
49
|
+
}
|
|
50
|
+
function malformedHeaderError(header, options) {
|
|
51
|
+
return invalidTokenError(`${options.prefix} Invalid session token: the "${header}" header holds neither a token nor "Bearer <token>".`);
|
|
52
|
+
}
|
|
53
|
+
function rethrowAsInvalidToken(error, options) {
|
|
54
|
+
if (isJWTError(error, "ERR_JWT_EXPIRED") || isJWTError(error, "ERR_SESSION_READ_FAILED")) throw error;
|
|
55
|
+
throw invalidTokenError(`${options.prefix} Invalid session token: ${messageOf(error)}`, error);
|
|
56
|
+
}
|
|
57
|
+
const TOKEN = /^[!#$%&'*+\-.^_`|~\w]+$/;
|
|
58
|
+
const CHUNK_SUFFIX = /\.\d+$/;
|
|
59
|
+
function assertSessionName(name, options) {
|
|
60
|
+
if (!isToken(name)) throw invalidConfigError(`${options.prefix} Session "name" must be a valid cookie name (got ${quoteTokenValue(name)}).`);
|
|
61
|
+
if (CHUNK_SUFFIX.test(name)) throw invalidConfigError(`${options.prefix} Session "name" ${quoteTokenValue(name)} cannot end in a dot and digits.`);
|
|
62
|
+
}
|
|
63
|
+
function isToken(value) {
|
|
64
|
+
return typeof value === "string" && TOKEN.test(value);
|
|
65
|
+
}
|
|
66
|
+
function assertCookiePrefix(name, attrs, options) {
|
|
67
|
+
const lower = name.toLowerCase();
|
|
68
|
+
const host = lower.startsWith("__host-");
|
|
69
|
+
if (!host && !lower.startsWith("__secure-")) return;
|
|
70
|
+
const rule = host ? "\"__Host-\" cookie must be Secure, with Path=/ and no Domain" : "\"__Secure-\" cookie must be Secure";
|
|
71
|
+
if (!attrs.secure || host && (attrs.domain !== void 0 || attrs.path !== void 0 && attrs.path !== "/")) throw invalidConfigError(`${options.prefix} Session ${quoteTokenValue(name)}: a ${rule}.`);
|
|
72
|
+
}
|
|
73
|
+
function assertRolling(config, options) {
|
|
74
|
+
const { rolling } = config;
|
|
75
|
+
if (rolling === void 0) return;
|
|
76
|
+
if (config.maxAge === void 0) throw invalidConfigError(`${options.prefix} "rolling" needs a "maxAge" to extend.`);
|
|
77
|
+
if (rolling === true) return;
|
|
78
|
+
if (!isExpiresIn(rolling)) throw new JWTError(`${options.prefix} "rolling" must be true or a duration.`, "ERR_INVALID_OPTION");
|
|
79
|
+
if (computeDurationInSeconds(rolling) >= computeDurationInSeconds(config.maxAge)) throw invalidConfigError(`${options.prefix} "rolling" must be shorter than "maxAge".`);
|
|
80
|
+
}
|
|
81
|
+
function assertKeyMaterial(key, options) {
|
|
82
|
+
if (typeof key === "string") throw invalidKeyError(`${options.prefix} Session "key" cannot be a string, use deriveJWKFromPassword().`);
|
|
83
|
+
if (!key) throw invalidKeyError(`${options.prefix} Session "key" is required.`);
|
|
84
|
+
}
|
|
85
|
+
async function assertImportableKey(key, options) {
|
|
86
|
+
if (!isJWK(key)) return;
|
|
87
|
+
const alg = key.alg ?? options.alg;
|
|
88
|
+
if (key.kty !== "oct" && !alg) return;
|
|
89
|
+
let imports = importedKeys.get(key);
|
|
90
|
+
if (!imports) {
|
|
91
|
+
imports = /* @__PURE__ */ new Map();
|
|
92
|
+
importedKeys.set(key, imports);
|
|
93
|
+
}
|
|
94
|
+
const slot = alg ?? "";
|
|
95
|
+
let pending = imports.get(slot);
|
|
96
|
+
if (!pending) {
|
|
97
|
+
pending = importKey(key, { alg }).then(() => void 0);
|
|
98
|
+
imports.set(slot, pending);
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
await pending;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (imports.get(slot) === pending) imports.delete(slot);
|
|
104
|
+
throw invalidKeyError(`${options.prefix} Invalid session key: ${messageOf(error)}`, error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function sessionTyp(configured) {
|
|
108
|
+
return typeof configured === "string" && configured ? configured : SESSION_TYP;
|
|
109
|
+
}
|
|
110
|
+
function resolveExpiresAt(createdAt, options) {
|
|
111
|
+
return options.maxAge === void 0 ? void 0 : createdAt + computeDurationInSeconds(options.maxAge) * 1e3;
|
|
112
|
+
}
|
|
113
|
+
function sessionRequiredClaims(configured, options) {
|
|
114
|
+
const required = /* @__PURE__ */ new Set([
|
|
115
|
+
...configured || [],
|
|
116
|
+
"jti",
|
|
117
|
+
"iat"
|
|
118
|
+
]);
|
|
119
|
+
if (options.maxAge !== void 0) required.add("exp");
|
|
120
|
+
return [...required];
|
|
121
|
+
}
|
|
122
|
+
function sanitizeSessionUpdate(update, options) {
|
|
123
|
+
if (!isPlainObject(update)) throw new JWTError(`${options.prefix} Session data must be a plain object.`, "ERR_INVALID_OPTION");
|
|
124
|
+
const sanitized = Object.create(null);
|
|
125
|
+
for (const key of Object.keys(update)) {
|
|
126
|
+
if (FORBIDDEN_KEYS.includes(key)) throw new JWTError(`${options.prefix} Session data cannot carry the key "${key}".`, "ERR_INVALID_OPTION");
|
|
127
|
+
if (RESERVED_CLAIMS.includes(key)) continue;
|
|
128
|
+
sanitized[key] = update[key];
|
|
129
|
+
}
|
|
130
|
+
return sanitized;
|
|
131
|
+
}
|
|
132
|
+
function messageOf(error) {
|
|
133
|
+
return error instanceof Error ? error.message : String(error);
|
|
134
|
+
}
|
|
135
|
+
function isExpiresIn(value) {
|
|
136
|
+
return typeof value === "number" || typeof value === "string";
|
|
137
|
+
}
|
|
138
|
+
function createSessionCodec(spec) {
|
|
139
|
+
const { prefix } = spec;
|
|
140
|
+
const now = (config) => spec.issueOptions(config)?.currentDate?.getTime() ?? Date.now();
|
|
141
|
+
const openWith = async (config, token, key, opening) => {
|
|
142
|
+
const options = spec.openOptions(config);
|
|
143
|
+
const payload = await spec.open(config, token, key, sessionOpenOptions({
|
|
144
|
+
maxAge: config.maxAge,
|
|
145
|
+
requiredClaims: options?.requiredClaims,
|
|
146
|
+
typ: spec.issueOptions(config)?.protectedHeader?.typ,
|
|
147
|
+
algorithms: opening.algorithms
|
|
148
|
+
})).catch((error) => rethrowOpenFailure(error, {
|
|
149
|
+
lookup: opening.lookup,
|
|
150
|
+
prefix
|
|
151
|
+
}));
|
|
152
|
+
const clock = {
|
|
153
|
+
now: now(config),
|
|
154
|
+
clockTolerance: options?.clockTolerance
|
|
155
|
+
};
|
|
156
|
+
assertNotFromTheFuture(payload, {
|
|
157
|
+
...clock,
|
|
158
|
+
prefix
|
|
159
|
+
});
|
|
160
|
+
const opened = openedSession(payload, { maxAge: config.maxAge });
|
|
161
|
+
assertNotPastMaxAge(opened, clock);
|
|
162
|
+
return opened;
|
|
163
|
+
};
|
|
164
|
+
return {
|
|
165
|
+
family: spec.family,
|
|
166
|
+
defaultName: spec.defaultName,
|
|
167
|
+
now,
|
|
168
|
+
keys: (config) => {
|
|
169
|
+
const { issueKey, issueAlg, key, algorithms } = spec.resolveKeys(config);
|
|
170
|
+
const hook = spec.lookup(config);
|
|
171
|
+
const keyFor = (args) => hook ? ({ header }) => lookUpKey(hook, {
|
|
172
|
+
header,
|
|
173
|
+
...args,
|
|
174
|
+
config
|
|
175
|
+
}, { prefix }) : key;
|
|
176
|
+
const importable = hook ? [] : isJWKSet(key) ? key.keys : [key];
|
|
177
|
+
const opening = {
|
|
178
|
+
algorithms,
|
|
179
|
+
lookup: hook !== void 0
|
|
180
|
+
};
|
|
181
|
+
const assertOpenKeys = async () => {
|
|
182
|
+
for (const entry of importable) await assertImportableKey(entry, {
|
|
183
|
+
alg: void 0,
|
|
184
|
+
prefix
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
return {
|
|
188
|
+
assertKeys: async () => {
|
|
189
|
+
await assertImportableKey(issueKey, {
|
|
190
|
+
alg: issueAlg ?? spec.issueOptions(config)?.alg,
|
|
191
|
+
prefix
|
|
192
|
+
});
|
|
193
|
+
await assertOpenKeys();
|
|
194
|
+
},
|
|
195
|
+
issue: (state) => spec.issue(config, sessionPayload(state), issueKey, issueAlg),
|
|
196
|
+
opener: (args) => {
|
|
197
|
+
const keyed = keyFor(args);
|
|
198
|
+
return (token) => openWith(config, token, keyed, opening);
|
|
199
|
+
},
|
|
200
|
+
open: async (token, args) => {
|
|
201
|
+
await assertOpenKeys();
|
|
202
|
+
return openWith(config, token, keyFor(args), opening);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function sessionIssueOptions(options) {
|
|
209
|
+
return Object.assign({}, options, {
|
|
210
|
+
expiresIn: void 0,
|
|
211
|
+
expiresAt: void 0,
|
|
212
|
+
notBeforeIn: void 0,
|
|
213
|
+
notBeforeAt: void 0,
|
|
214
|
+
protectedHeader: {
|
|
215
|
+
...options?.protectedHeader,
|
|
216
|
+
typ: sessionTyp(options?.protectedHeader?.typ)
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function sessionOpenOptions(options) {
|
|
221
|
+
const { maxAge, requiredClaims, typ, algorithms } = options;
|
|
222
|
+
return {
|
|
223
|
+
requiredClaims: sessionRequiredClaims(requiredClaims, { maxAge }),
|
|
224
|
+
typ: sessionTyp(typ),
|
|
225
|
+
maxTokenAge: void 0,
|
|
226
|
+
algorithms,
|
|
227
|
+
payload: "json",
|
|
228
|
+
validateClaims: true
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function sessionPayload(state) {
|
|
232
|
+
const iat = Math.floor(state.createdAt / 1e3);
|
|
233
|
+
const exp = state.expiresAt ? Math.floor(state.expiresAt / 1e3) : void 0;
|
|
234
|
+
const payload = {};
|
|
235
|
+
for (const key of Object.keys(state.data)) if (!RESERVED_CLAIMS.includes(key)) payload[key] = state.data[key];
|
|
236
|
+
payload.jti = state.id;
|
|
237
|
+
payload.iat = iat;
|
|
238
|
+
if (exp) payload.exp = exp;
|
|
239
|
+
return payload;
|
|
240
|
+
}
|
|
241
|
+
function assertNotFromTheFuture(payload, options) {
|
|
242
|
+
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()}).`);
|
|
243
|
+
}
|
|
244
|
+
function assertNotPastMaxAge(opened, options) {
|
|
245
|
+
const { expiresAt } = opened;
|
|
246
|
+
if (expiresAt === void 0) return;
|
|
247
|
+
if (expiresAt <= options.now - toleranceSeconds(options.clockTolerance) * 1e3) throw expiredSessionError({
|
|
248
|
+
id: opened.id,
|
|
249
|
+
createdAt: opened.createdAt,
|
|
250
|
+
expiresAt
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
async function lookUpKey(hook, args, options) {
|
|
254
|
+
try {
|
|
255
|
+
return await hook(args);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
throw readFailedError(error, options);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function rethrowOpenFailure(error, options) {
|
|
261
|
+
if (options.lookup && isJWTError(error, "ERR_JWK_INVALID")) throw readFailedError(error, options);
|
|
262
|
+
return rethrowAsInvalidToken(error, options);
|
|
263
|
+
}
|
|
264
|
+
function toleranceSeconds(clockTolerance) {
|
|
265
|
+
return clockTolerance === void 0 ? 0 : computeDurationInSeconds(clockTolerance);
|
|
266
|
+
}
|
|
267
|
+
function openedSession(payload, options) {
|
|
268
|
+
const data = Object.create(null);
|
|
269
|
+
for (const key of Object.keys(payload)) {
|
|
270
|
+
if (RESERVED_CLAIMS.includes(key)) continue;
|
|
271
|
+
data[key] = payload[key];
|
|
272
|
+
}
|
|
273
|
+
const createdAt = payload.iat * 1e3;
|
|
274
|
+
const exp = payload.exp ? payload.exp * 1e3 : void 0;
|
|
275
|
+
const bound = resolveExpiresAt(createdAt, options);
|
|
276
|
+
return {
|
|
277
|
+
id: payload.jti,
|
|
278
|
+
createdAt,
|
|
279
|
+
expiresAt: exp === void 0 || bound === void 0 ? exp ?? bound : Math.min(exp, bound),
|
|
280
|
+
data
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function assertJWSKeyPair(pair, prefix) {
|
|
284
|
+
const { privateKey, publicKey } = pair;
|
|
285
|
+
if (!isPrivateJWK(privateKey) && !isCryptoKeyOf(privateKey, "private")) throw invalidKeyError(`${prefix} Invalid JWS session key: "privateKey" must be a private JWK or a private CryptoKey.`, pair);
|
|
286
|
+
if (!isPublicHalf(publicKey)) throw invalidKeyError(`${prefix} Invalid JWS session key: "publicKey" must be a public JWK, a non-empty list or JWK Set of them, or a public CryptoKey.`, pair);
|
|
287
|
+
}
|
|
288
|
+
function assertVerifiesIssued(entries, options) {
|
|
289
|
+
const { alg, by, list, rules, prefix } = options;
|
|
290
|
+
const served = entries.map(({ key }) => rules.own(key) ?? rules.carried(key));
|
|
291
|
+
if (served.some((algorithms) => algorithms.includes(alg))) return;
|
|
292
|
+
const [{ key }] = entries;
|
|
293
|
+
const own = list ? void 0 : rules.own(key);
|
|
294
|
+
throw invalidKeyError(`${prefix} The session key "publicKey" ${own ? `names "${(isJWK(key) ? key.alg : void 0) ?? own[0]}"` : `serves ${[...new Set(served.flat())].join(", ") || "no algorithm"}`}, not the "${alg}" that ${by}.`, list ? entries.map((entry) => entry.key) : key);
|
|
295
|
+
}
|
|
296
|
+
function isPublicHalf(half) {
|
|
297
|
+
if (isCryptoKeyOf(half, "public") || isPublicJWK(half)) return true;
|
|
298
|
+
const keys = Array.isArray(half) ? half : isJWKSet(half) ? half.keys : void 0;
|
|
299
|
+
return keys !== void 0 && keys.length > 0 && keys.every((candidate) => isPublicJWK(candidate));
|
|
300
|
+
}
|
|
301
|
+
function isCryptoKeyOf(key, type) {
|
|
302
|
+
return isCryptoKey(key) && key.type === type;
|
|
303
|
+
}
|
|
304
|
+
function sessionKeySetEntries(keys, options) {
|
|
305
|
+
const { rules, prefix } = options;
|
|
306
|
+
if (keys.length === 0) throw invalidKeyError(`${prefix} The session key is an empty JWK Set.`);
|
|
307
|
+
const entries = keys.map((key, index) => {
|
|
308
|
+
const subject = `The session key "keys[${index}]"${kidLabel(key)}`;
|
|
309
|
+
if (!isSymmetricJWK(key)) throw invalidKeyError(`${prefix} ${subject} must be an "oct" JWK.`, key);
|
|
310
|
+
return {
|
|
311
|
+
key,
|
|
312
|
+
subject,
|
|
313
|
+
pin: index ? void 0 : options.pin
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
const limit = resolveLimit(options.limits, "keyCandidates");
|
|
317
|
+
for (const entry of entries) {
|
|
318
|
+
if (entry.key.kid !== void 0) continue;
|
|
319
|
+
for (const alg of servedBy(entry, options)) {
|
|
320
|
+
const met = entries.filter(({ key }) => rules.own(key)?.includes(alg) ?? true).length;
|
|
321
|
+
if (met > limit) throw invalidConfigError(`${prefix} ${entry.subject} carries no "kid", so a token of "${alg}" meets ${met} keys, over the limit of ${limit} ("${rules.limitPath}").`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return entries;
|
|
325
|
+
}
|
|
326
|
+
function assertPinnedKey(entry, options) {
|
|
327
|
+
const { rules, prefix } = options;
|
|
328
|
+
const { key, pin, subject } = entry;
|
|
329
|
+
const own = rules.own(key);
|
|
330
|
+
if (own === void 0 && (!pin || !options.pinned)) throw invalidConfigError(`${prefix} ${subject} names no algorithm, set ${rules.hint}${isJWK(key) ? ", or its \"alg\"" : ""}.`);
|
|
331
|
+
const served = own ?? rules.carried(key);
|
|
332
|
+
if (!pin || served.includes(pin)) return;
|
|
333
|
+
throw invalidKeyError(`${prefix} ${subject} serves ${served.join(", ") || "no algorithm"}, not the "${pin}" that ${entry.pinnedBy ?? `"${rules.pinPath}" pins`}.`, key);
|
|
334
|
+
}
|
|
335
|
+
function servedAlgorithms(entries, options) {
|
|
336
|
+
const { rules, listed, prefix } = options;
|
|
337
|
+
const { listPath } = rules;
|
|
338
|
+
if (listed?.length === 0) throw invalidConfigError(`${prefix} "${listPath}" is empty, list the algorithms the session keys serve or leave it unset.`);
|
|
339
|
+
const all = /* @__PURE__ */ new Set();
|
|
340
|
+
const each = entries.map((entry) => {
|
|
341
|
+
const served = servedBy(entry, options);
|
|
342
|
+
if (served.length === 0) throw unservedKeyError(entry, {
|
|
343
|
+
rules,
|
|
344
|
+
prefix
|
|
345
|
+
});
|
|
346
|
+
for (const alg of served) all.add(alg);
|
|
347
|
+
return served;
|
|
348
|
+
});
|
|
349
|
+
for (const alg of listed ?? []) {
|
|
350
|
+
if (all.has(alg)) continue;
|
|
351
|
+
if (!options.lookup) throw invalidConfigError(`${prefix} "${listPath}" lists "${alg}", which no session key serves. Remove it, or add a key for it.`);
|
|
352
|
+
all.add(alg);
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
each,
|
|
356
|
+
all: [...all]
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function isKeyPair(key) {
|
|
360
|
+
return typeof key === "object" && key !== null && ("privateKey" in key || "publicKey" in key);
|
|
361
|
+
}
|
|
362
|
+
function kidLabel(key) {
|
|
363
|
+
return isJWK(key) && key.kid !== void 0 ? ` (kid ${quoteTokenValue(key.kid)})` : "";
|
|
364
|
+
}
|
|
365
|
+
function octBits(key) {
|
|
366
|
+
return Math.floor(key.k.length * 6 / 8) * 8;
|
|
367
|
+
}
|
|
368
|
+
function servedBy(entry, options) {
|
|
369
|
+
const { rules, listed } = options;
|
|
370
|
+
return rules.own(entry.key) ?? rules.carried(entry.key).filter((alg) => alg === entry.pin || listed?.includes(alg));
|
|
371
|
+
}
|
|
372
|
+
function unservedKeyError(entry, options) {
|
|
373
|
+
const { rules, prefix } = options;
|
|
374
|
+
const carried = rules.own(entry.key) ? [] : rules.carried(entry.key);
|
|
375
|
+
if (carried.length === 0) return invalidKeyError(`${prefix} ${entry.subject} serves no algorithm.`, entry.key);
|
|
376
|
+
return invalidConfigError(`${prefix} ${entry.subject} names no algorithm, list one it serves (${carried.join(", ")}) in "${rules.listPath}", or set its "alg".`);
|
|
377
|
+
}
|
|
378
|
+
const JWS_KEY_RULES = {
|
|
379
|
+
own: (key) => {
|
|
380
|
+
if (!isJWK(key)) return void 0;
|
|
381
|
+
const inferred = inferJWSAllowedAlgorithms(key);
|
|
382
|
+
return key.alg ? inferred ?? [] : inferred;
|
|
14
383
|
},
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
384
|
+
carried: (key) => {
|
|
385
|
+
if (isCryptoKey(key)) return inferJWSAllowedAlgorithms(key) ?? [];
|
|
386
|
+
if (isJWK(key) && key.kty === "RSA") return RSA_SIGNATURES;
|
|
387
|
+
if (!isSymmetricJWK(key)) return [];
|
|
388
|
+
const bits = octBits(key);
|
|
389
|
+
return HMAC_SIGNATURES.filter((alg) => bits >= Number(alg.slice(2)));
|
|
390
|
+
},
|
|
391
|
+
pinPath: "jws.signOptions.alg",
|
|
392
|
+
hint: "\"jws.signOptions.alg\"",
|
|
393
|
+
listPath: "jws.verifyOptions.algorithms",
|
|
394
|
+
limitPath: "jws.verifyOptions.limits.keyCandidates"
|
|
18
395
|
};
|
|
19
|
-
function
|
|
396
|
+
function resolveSessionKeys$1(key, options) {
|
|
397
|
+
const { alg: pin, prefix } = options;
|
|
398
|
+
assertKeyMaterial(key, { prefix });
|
|
399
|
+
let issuing;
|
|
400
|
+
let entries;
|
|
401
|
+
let list;
|
|
402
|
+
let issued;
|
|
403
|
+
if (isCryptoKey(key) && key.type === "secret" || isSymmetricJWK(key)) {
|
|
404
|
+
issuing = {
|
|
405
|
+
key,
|
|
406
|
+
subject: "The session key",
|
|
407
|
+
pin
|
|
408
|
+
};
|
|
409
|
+
entries = [issuing];
|
|
410
|
+
} else if (isJWKSet(key)) {
|
|
411
|
+
const set = sessionKeySetEntries(key.keys, {
|
|
412
|
+
...options,
|
|
413
|
+
pin,
|
|
414
|
+
rules: JWS_KEY_RULES
|
|
415
|
+
});
|
|
416
|
+
[issuing] = set;
|
|
417
|
+
entries = set;
|
|
418
|
+
} else if (isKeyPair(key)) {
|
|
419
|
+
assertJWSKeyPair(key, prefix);
|
|
420
|
+
const half = key.publicKey;
|
|
421
|
+
list = Array.isArray(half) ? half : isJWKSet(half) ? half.keys : void 0;
|
|
422
|
+
const signing = namedAlgorithm(key.privateKey);
|
|
423
|
+
const alg = pin ?? signing;
|
|
424
|
+
if (alg) issued = {
|
|
425
|
+
alg,
|
|
426
|
+
by: pin ? `"${JWS_KEY_RULES.pinPath}" pins` : "the session key \"privateKey\" names"
|
|
427
|
+
};
|
|
428
|
+
issuing = {
|
|
429
|
+
key: key.privateKey,
|
|
430
|
+
subject: "The session key \"privateKey\"",
|
|
431
|
+
...pairPin(key.privateKey, {
|
|
432
|
+
pin,
|
|
433
|
+
named: list ? void 0 : namedAlgorithm(half),
|
|
434
|
+
by: "publicKey"
|
|
435
|
+
})
|
|
436
|
+
};
|
|
437
|
+
entries = (list ?? [half]).map((entry, index) => ({
|
|
438
|
+
key: entry,
|
|
439
|
+
subject: list ? `The session key "publicKey[${index}]"${kidLabel(entry)}` : "The session key \"publicKey\"",
|
|
440
|
+
...pairPin(entry, {
|
|
441
|
+
pin,
|
|
442
|
+
named: signing,
|
|
443
|
+
by: "privateKey"
|
|
444
|
+
})
|
|
445
|
+
}));
|
|
446
|
+
} else throw invalidJWSKeyError(key, prefix);
|
|
447
|
+
const pinning = {
|
|
448
|
+
rules: JWS_KEY_RULES,
|
|
449
|
+
pinned: true,
|
|
450
|
+
prefix
|
|
451
|
+
};
|
|
452
|
+
assertPinnedKey(issuing, pinning);
|
|
453
|
+
if (!list && issuing !== entries[0]) assertPinnedKey(entries[0], pinning);
|
|
454
|
+
if (issued) assertVerifiesIssued(entries, {
|
|
455
|
+
...issued,
|
|
456
|
+
list: list !== void 0,
|
|
457
|
+
rules: JWS_KEY_RULES,
|
|
458
|
+
prefix
|
|
459
|
+
});
|
|
460
|
+
const { each, all } = servedAlgorithms(entries, {
|
|
461
|
+
...options,
|
|
462
|
+
rules: JWS_KEY_RULES
|
|
463
|
+
});
|
|
464
|
+
const verifying = isJWKSet(key) ? key : list ? { keys: entries.flatMap(({ key: entry }, index) => servedCopies(entry, each[index])) } : entries[0].key;
|
|
465
|
+
const issueAlg = issuing.pin;
|
|
466
|
+
return {
|
|
467
|
+
issueKey: issuing.key,
|
|
468
|
+
issueAlg,
|
|
469
|
+
key: verifying,
|
|
470
|
+
algorithms: all
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function namedAlgorithm(key) {
|
|
474
|
+
return isJWK(key) ? key.alg ?? JWS_KEY_RULES.own(key)?.[0] : void 0;
|
|
475
|
+
}
|
|
476
|
+
function pairPin(key, options) {
|
|
477
|
+
const { pin, named, by } = options;
|
|
478
|
+
if (pin || !named || JWS_KEY_RULES.own(key)) return { pin };
|
|
479
|
+
return {
|
|
480
|
+
pin: named,
|
|
481
|
+
pinnedBy: `the session key "${by}" names`
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function servedCopies(key, served) {
|
|
485
|
+
if (!isJWK(key)) return [];
|
|
486
|
+
if (JWS_KEY_RULES.own(key)) return [key];
|
|
487
|
+
return served.map((alg) => ({
|
|
488
|
+
...key,
|
|
489
|
+
alg
|
|
490
|
+
}));
|
|
491
|
+
}
|
|
492
|
+
function invalidJWSKeyError(key, prefix) {
|
|
493
|
+
return invalidKeyError(isCryptoKey(key) || isAsymmetricJWK(key) ? `${prefix} Invalid JWS session key: an asymmetric key signs with its private half and verifies with its public half, pass both as { privateKey, publicKey }.` : `${prefix} Invalid JWS session key: expected an HMAC "oct" JWK, a JWK Set of them, a secret CryptoKey or a key pair.`, key);
|
|
494
|
+
}
|
|
495
|
+
const HMAC_SIGNATURES = [
|
|
496
|
+
"HS256",
|
|
497
|
+
"HS384",
|
|
498
|
+
"HS512"
|
|
499
|
+
];
|
|
500
|
+
const RSA_SIGNATURES = [
|
|
501
|
+
"RS256",
|
|
502
|
+
"RS384",
|
|
503
|
+
"RS512",
|
|
504
|
+
"PS256",
|
|
505
|
+
"PS384",
|
|
506
|
+
"PS512"
|
|
507
|
+
];
|
|
508
|
+
function createJWSCodec(options) {
|
|
509
|
+
const { prefix, defaultName } = options;
|
|
510
|
+
return createSessionCodec({
|
|
511
|
+
family: "jws",
|
|
512
|
+
prefix,
|
|
513
|
+
defaultName,
|
|
514
|
+
issueOptions: (config) => config.jws?.signOptions,
|
|
515
|
+
openOptions: (config) => config.jws?.verifyOptions,
|
|
516
|
+
resolveKeys: (config) => resolveSessionKeys$1(config.key, {
|
|
517
|
+
alg: config.jws?.signOptions?.alg,
|
|
518
|
+
listed: config.jws?.verifyOptions?.algorithms,
|
|
519
|
+
limits: config.jws?.verifyOptions?.limits,
|
|
520
|
+
lookup: typeof config.hooks?.onKeyLookup === "function",
|
|
521
|
+
prefix
|
|
522
|
+
}),
|
|
523
|
+
lookup: (config) => config.hooks?.onKeyLookup,
|
|
524
|
+
issue: (config, payload, key, alg) => {
|
|
525
|
+
const signOptions = config.jws?.signOptions;
|
|
526
|
+
const issuing = sessionIssueOptions(alg ? {
|
|
527
|
+
...signOptions,
|
|
528
|
+
alg
|
|
529
|
+
} : signOptions);
|
|
530
|
+
return sign(payload, key, issuing);
|
|
531
|
+
},
|
|
532
|
+
open: async (config, token, key, overrides) => {
|
|
533
|
+
const { payload } = await verify(token, key, {
|
|
534
|
+
...config.jws?.verifyOptions,
|
|
535
|
+
...overrides
|
|
536
|
+
});
|
|
537
|
+
return payload;
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
const DEFAULT_SESSION_COOKIE = /* @__PURE__ */ Object.freeze({
|
|
542
|
+
path: "/",
|
|
543
|
+
secure: true,
|
|
544
|
+
httpOnly: true,
|
|
545
|
+
sameSite: "lax"
|
|
546
|
+
});
|
|
547
|
+
const kSessionPending = Symbol("unjwt_session_pending");
|
|
548
|
+
function sessionInstant(source) {
|
|
549
|
+
const now = source.codec.now(source.config);
|
|
550
|
+
return now - now % 1e3;
|
|
551
|
+
}
|
|
552
|
+
function mintSessionId(config) {
|
|
553
|
+
return config.generateId?.() || crypto.randomUUID();
|
|
554
|
+
}
|
|
555
|
+
function emptySession(binding) {
|
|
556
|
+
const createdAt = sessionInstant(binding);
|
|
557
|
+
return typedSession({
|
|
558
|
+
id: void 0,
|
|
559
|
+
createdAt,
|
|
560
|
+
expiresAt: resolveExpiresAt(createdAt, { maxAge: binding.config.maxAge }),
|
|
561
|
+
data: Object.create(null),
|
|
562
|
+
token: void 0,
|
|
563
|
+
source: binding.transport.source,
|
|
564
|
+
error: void 0
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
function storedSession(store) {
|
|
568
|
+
const state = store.get();
|
|
569
|
+
return state === void 0 ? void 0 : typedSession(state);
|
|
570
|
+
}
|
|
571
|
+
function blankSession() {
|
|
572
|
+
return typedSession({
|
|
573
|
+
expiresAt: void 0,
|
|
574
|
+
data: {}
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
function openSessionToken(keys, options) {
|
|
578
|
+
return keys.open(options.token, options.args).then((opened) => typedOpened({
|
|
579
|
+
...opened,
|
|
580
|
+
source: "token"
|
|
581
|
+
}));
|
|
582
|
+
}
|
|
583
|
+
function typedSession(state) {
|
|
584
|
+
return state;
|
|
585
|
+
}
|
|
586
|
+
function typedOpened(opened) {
|
|
587
|
+
return opened;
|
|
588
|
+
}
|
|
589
|
+
const CHUNK_MARKER = "__chunked__";
|
|
590
|
+
const DEFAULT_CHUNK_MAX_LENGTH = 4e3;
|
|
591
|
+
function readChunked(store, name) {
|
|
592
|
+
const main = store.read(name);
|
|
593
|
+
if (main === void 0) return void 0;
|
|
594
|
+
if (!main.startsWith("__chunked__")) return main;
|
|
595
|
+
const count = parseChunkCount(main);
|
|
596
|
+
if (count === 0) return void 0;
|
|
597
|
+
const parts = [];
|
|
598
|
+
for (let index = 1; index <= count; index++) {
|
|
599
|
+
const part = store.read(chunkName(name, index));
|
|
600
|
+
if (part === void 0) return void 0;
|
|
601
|
+
parts.push(part);
|
|
602
|
+
}
|
|
603
|
+
return parts.join("");
|
|
604
|
+
}
|
|
605
|
+
function writeChunked(store, name, value, options) {
|
|
606
|
+
const { attrs, chunkMaxLength, prefix } = options;
|
|
607
|
+
const max = chunkMaxLength && chunkMaxLength > 0 ? chunkMaxLength : DEFAULT_CHUNK_MAX_LENGTH;
|
|
608
|
+
const chunkCount = Math.ceil(value.length / max);
|
|
609
|
+
if (chunkCount > 16) throw invalidConfigError(`${prefix} Session token requires ${chunkCount} cookie chunks, exceeding the limit of 16.`);
|
|
610
|
+
const previousCount = parseChunkCount(store.read(name));
|
|
611
|
+
const firstSurplus = chunkCount <= 1 ? 1 : chunkCount + 1;
|
|
612
|
+
for (let index = firstSurplus; index <= previousCount; index++) expire(store, chunkName(name, index), attrs);
|
|
613
|
+
if (chunkCount <= 1) {
|
|
614
|
+
store.write(name, value, attrs);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
store.write(name, `${CHUNK_MARKER}${chunkCount}`, attrs);
|
|
618
|
+
for (let index = 1; index <= chunkCount; index++) store.write(chunkName(name, index), value.slice((index - 1) * max, index * max), attrs);
|
|
619
|
+
}
|
|
620
|
+
function removeChunked(store, name, options) {
|
|
621
|
+
const count = parseChunkCount(store.read(name));
|
|
622
|
+
expire(store, name, options.attrs);
|
|
623
|
+
for (let index = 1; index <= count; index++) expire(store, chunkName(name, index), options.attrs);
|
|
624
|
+
}
|
|
625
|
+
function setCookieValues(setCookies) {
|
|
626
|
+
const cookies = Object.create(null);
|
|
627
|
+
for (const setCookie of setCookies) {
|
|
628
|
+
const pair = setCookiePair(setCookie);
|
|
629
|
+
if (pair) cookies[pair.name] = decodeValue(pair.value);
|
|
630
|
+
}
|
|
631
|
+
return cookies;
|
|
632
|
+
}
|
|
633
|
+
function setCookieName(setCookie) {
|
|
634
|
+
return setCookiePair(setCookie)?.name;
|
|
635
|
+
}
|
|
636
|
+
function mirrorSetCookie(name, headers) {
|
|
637
|
+
const { from, to } = headers;
|
|
638
|
+
const isThisCookie = (line) => setCookieName(line) === name;
|
|
639
|
+
const lines = [...to.getSetCookie().filter((line) => !isThisCookie(line)), ...from.getSetCookie().filter((line) => isThisCookie(line))];
|
|
640
|
+
to.delete("set-cookie");
|
|
641
|
+
for (const line of lines) to.append("set-cookie", line);
|
|
642
|
+
}
|
|
643
|
+
function chunkName(name, index) {
|
|
644
|
+
return `${name}.${index}`;
|
|
645
|
+
}
|
|
646
|
+
function parseChunkCount(main) {
|
|
647
|
+
if (main === void 0 || !main.startsWith("__chunked__")) return 0;
|
|
648
|
+
const suffix = main.slice(11);
|
|
649
|
+
if (!/^\d+$/.test(suffix)) return 0;
|
|
650
|
+
const count = Number.parseInt(suffix, 10);
|
|
651
|
+
if (count < 1 || count > 16) return 0;
|
|
652
|
+
return count;
|
|
653
|
+
}
|
|
654
|
+
function expire(store, name, attrs) {
|
|
655
|
+
store.write(name, "", {
|
|
656
|
+
...attrs,
|
|
657
|
+
maxAge: 0,
|
|
658
|
+
expires: /* @__PURE__ */ new Date(0)
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
function setCookiePair(setCookie) {
|
|
662
|
+
const [pair = ""] = setCookie.split(";", 1);
|
|
663
|
+
const separator = pair.indexOf("=");
|
|
664
|
+
if (separator === -1) return void 0;
|
|
665
|
+
return {
|
|
666
|
+
name: pair.slice(0, separator).trim(),
|
|
667
|
+
value: pair.slice(separator + 1).trim()
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function decodeValue(value) {
|
|
671
|
+
if (!value.includes("%")) return value;
|
|
672
|
+
try {
|
|
673
|
+
return decodeURIComponent(value);
|
|
674
|
+
} catch {
|
|
675
|
+
return value;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
function resolveSessionTransport(driver, event, options) {
|
|
679
|
+
const handed = driver.handed?.(event);
|
|
680
|
+
if (handed) return {
|
|
681
|
+
source: "token",
|
|
682
|
+
read: () => handed.token,
|
|
683
|
+
readonly: false,
|
|
684
|
+
writable: () => true
|
|
685
|
+
};
|
|
686
|
+
const { name, cookie } = options;
|
|
687
|
+
const header = presentedHeader(driver, event, options);
|
|
688
|
+
if (cookie === false) return headerTransport(options, header);
|
|
689
|
+
const jar = driver.cookies(event);
|
|
690
|
+
if (header && (header.token === void 0 || header.token !== readChunked(jar, name))) return headerTransport(options, header);
|
|
691
|
+
const store = responseStore(jar);
|
|
692
|
+
const transport = {
|
|
693
|
+
source: "cookie",
|
|
694
|
+
read: () => cookieToken(jar, name),
|
|
695
|
+
readonly: !store,
|
|
696
|
+
writable: () => driver.writable?.(event) ?? true
|
|
697
|
+
};
|
|
698
|
+
if (!store) return transport;
|
|
699
|
+
const { attrs, chunkMaxLength } = cookie;
|
|
700
|
+
transport.write = (token, { expiresAt } = {}) => {
|
|
701
|
+
if (!transport.writable()) return false;
|
|
702
|
+
const written = expiresAt === void 0 ? { ...attrs } : {
|
|
703
|
+
...attrs,
|
|
704
|
+
expires: new Date(expiresAt),
|
|
705
|
+
maxAge: void 0
|
|
706
|
+
};
|
|
707
|
+
writeChunked(store, name, token, {
|
|
708
|
+
attrs: written,
|
|
709
|
+
chunkMaxLength,
|
|
710
|
+
prefix: driver.prefix
|
|
711
|
+
});
|
|
712
|
+
return true;
|
|
713
|
+
};
|
|
714
|
+
transport.drop = () => {
|
|
715
|
+
if (!transport.writable()) return false;
|
|
716
|
+
removeChunked(store, name, { attrs });
|
|
717
|
+
return true;
|
|
718
|
+
};
|
|
719
|
+
return transport;
|
|
720
|
+
}
|
|
721
|
+
function assertSessionTokenSource(transport, options) {
|
|
722
|
+
if (!transport.read) throw invalidConfigError(`${options.prefix} Session has no token source, enable "cookie" or "sessionHeader".`);
|
|
723
|
+
}
|
|
724
|
+
function assertSessionWritable(transport, options) {
|
|
725
|
+
const { operation, prefix } = options;
|
|
726
|
+
if (operation === "update" && transport.readonly) throw readOnlyEventError(`${prefix} Cannot update session on read-only event.`);
|
|
727
|
+
if (!transport.writable()) throw readOnlyEventError(`${prefix} Cannot update/clear session: the response was already sent.`);
|
|
728
|
+
}
|
|
729
|
+
function assertSessionHeader(header, options) {
|
|
730
|
+
if (header === void 0 || typeof header === "boolean") return;
|
|
731
|
+
if (isToken(header)) return;
|
|
732
|
+
throw invalidConfigError(`${options.prefix} Session "sessionHeader" must be true, false or a header name (got ${quoteTokenValue(header)}).`);
|
|
733
|
+
}
|
|
734
|
+
function sessionHeaderName(sessionName, configured) {
|
|
735
|
+
if (typeof configured === "string") return configured.toLowerCase();
|
|
736
|
+
return `x-${sessionName.replace(/^__(?:host|secure)-/i, "").toLowerCase()}-session`;
|
|
737
|
+
}
|
|
738
|
+
function headerToken(value) {
|
|
739
|
+
if (typeof value !== "string") return void 0;
|
|
740
|
+
const words = value.trim().split(/\s+/);
|
|
741
|
+
const [first = "", second] = words;
|
|
742
|
+
if (/^bearer$/i.test(first)) return words.length === 2 ? second : void 0;
|
|
743
|
+
return words.length === 1 && first ? first : void 0;
|
|
744
|
+
}
|
|
745
|
+
function presentedHeader(driver, event, options) {
|
|
746
|
+
const { name, sessionHeader } = options;
|
|
747
|
+
if (!readsHeader(options)) return void 0;
|
|
748
|
+
const header = sessionHeaderName(name, sessionHeader);
|
|
749
|
+
const value = driver.header(event, header);
|
|
750
|
+
if (typeof value !== "string") return void 0;
|
|
751
|
+
const token = headerToken(value);
|
|
752
|
+
if (token) return { token };
|
|
753
|
+
return {
|
|
754
|
+
token: void 0,
|
|
755
|
+
malformed: { header }
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function headerTransport(options, header) {
|
|
759
|
+
return {
|
|
760
|
+
source: "header",
|
|
761
|
+
read: readsHeader(options) ? () => header?.token : void 0,
|
|
762
|
+
malformed: header?.malformed,
|
|
763
|
+
readonly: false,
|
|
764
|
+
writable: () => true
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function readsHeader(options) {
|
|
768
|
+
return options.sessionHeader !== void 0 && options.sessionHeader !== false;
|
|
769
|
+
}
|
|
770
|
+
function cookieToken(jar, name) {
|
|
771
|
+
const written = jar.written ? readChunked({ read: (key) => jar.written?.(key) }, name) : void 0;
|
|
772
|
+
if (written === "") return void 0;
|
|
773
|
+
return (written ?? readChunked(jar, name)) || void 0;
|
|
774
|
+
}
|
|
775
|
+
function responseStore(jar) {
|
|
776
|
+
const write = jar.write?.bind(jar);
|
|
777
|
+
if (!write) return void 0;
|
|
778
|
+
return {
|
|
779
|
+
read: (name) => jar.written?.(name) ?? jar.read(name),
|
|
780
|
+
write
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
const kSessionNames = Symbol("unjwt_session_names");
|
|
784
|
+
const kHeldTokens = Symbol("unjwt_session_held_tokens");
|
|
785
|
+
function assertSessionConfig(config, options) {
|
|
786
|
+
const { name, prefix, defaultCookie = DEFAULT_SESSION_COOKIE } = options;
|
|
787
|
+
assertSessionName(name, { prefix });
|
|
788
|
+
assertSessionHeader(config.sessionHeader, { prefix });
|
|
789
|
+
if (config.cookie !== false) assertCookiePrefix(name, {
|
|
790
|
+
...defaultCookie,
|
|
791
|
+
...config.cookie
|
|
792
|
+
}, { prefix });
|
|
793
|
+
assertRolling(config, { prefix });
|
|
794
|
+
}
|
|
795
|
+
function sessionTransportOptions(config, options) {
|
|
796
|
+
const { name, defaultCookie = DEFAULT_SESSION_COOKIE } = options;
|
|
797
|
+
const { sessionHeader } = config;
|
|
798
|
+
if (config.cookie === false) return {
|
|
799
|
+
name,
|
|
800
|
+
sessionHeader,
|
|
801
|
+
cookie: false
|
|
802
|
+
};
|
|
803
|
+
const { chunkMaxLength, ...configured } = config.cookie || {};
|
|
804
|
+
return {
|
|
805
|
+
name,
|
|
806
|
+
sessionHeader,
|
|
807
|
+
cookie: {
|
|
808
|
+
attrs: {
|
|
809
|
+
...defaultCookie,
|
|
810
|
+
...configured
|
|
811
|
+
},
|
|
812
|
+
chunkMaxLength
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function claimSessionName(state, options) {
|
|
817
|
+
const { name, family, prefix } = options;
|
|
818
|
+
const holder = state;
|
|
819
|
+
const registry = holder[kSessionNames] ||= /* @__PURE__ */ new Map();
|
|
820
|
+
const held = registry.get(name);
|
|
821
|
+
if (held === void 0) {
|
|
822
|
+
registry.set(name, family);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (held === family) return;
|
|
826
|
+
throw invalidConfigError(`${prefix} Session name "${name}" is already used by a ${held.toUpperCase()} session on this request.`);
|
|
827
|
+
}
|
|
828
|
+
function bindSession(event, options) {
|
|
829
|
+
const { config, name, driver, codec, keys, actions } = options;
|
|
830
|
+
const record = driver.state(event);
|
|
831
|
+
claimSessionName(record, {
|
|
832
|
+
name,
|
|
833
|
+
family: codec.family,
|
|
834
|
+
prefix: driver.prefix
|
|
835
|
+
});
|
|
836
|
+
const slot = `${codec.family}:${name}`;
|
|
837
|
+
return {
|
|
838
|
+
event,
|
|
839
|
+
config,
|
|
840
|
+
driver,
|
|
841
|
+
codec,
|
|
842
|
+
keys,
|
|
843
|
+
actions,
|
|
844
|
+
name,
|
|
845
|
+
args: { event },
|
|
846
|
+
store: recordSlotStore(record, slot),
|
|
847
|
+
held: heldTokenSlot(record, slot),
|
|
848
|
+
transport: resolveSessionTransport(driver, event, sessionTransportOptions(config, { name }))
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
function sessionToken(binding) {
|
|
852
|
+
const { transport } = binding;
|
|
853
|
+
assertSessionTokenSource(transport, { prefix: binding.driver.prefix });
|
|
854
|
+
const session = storedSession(binding.store);
|
|
855
|
+
return session ? session.token : transport.read();
|
|
856
|
+
}
|
|
857
|
+
function transportToken(binding) {
|
|
858
|
+
const { transport } = binding;
|
|
859
|
+
assertSessionTokenSource(transport, { prefix: binding.driver.prefix });
|
|
860
|
+
const held = binding.held.get();
|
|
861
|
+
return held ? held.token : transport.read();
|
|
862
|
+
}
|
|
863
|
+
function recordSlotStore(record, slot) {
|
|
864
|
+
return {
|
|
865
|
+
get: () => record[slot],
|
|
866
|
+
set: (state) => {
|
|
867
|
+
record[slot] = state;
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
function heldTokenSlot(record, slot) {
|
|
872
|
+
const holder = record;
|
|
873
|
+
const held = holder[kHeldTokens] ||= /* @__PURE__ */ new Map();
|
|
874
|
+
return {
|
|
875
|
+
get: () => held.has(slot) ? { token: held.get(slot) } : void 0,
|
|
876
|
+
set: (token) => {
|
|
877
|
+
held.set(slot, token);
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
async function loadSession(binding) {
|
|
882
|
+
const { config, codec, keys, store, args, transport } = binding;
|
|
883
|
+
await keys.assertKeys();
|
|
884
|
+
const open = keys.opener(args);
|
|
885
|
+
assertSessionTokenSource(transport, { prefix: binding.driver.prefix });
|
|
886
|
+
const existing = storedSession(store);
|
|
887
|
+
if (existing) {
|
|
888
|
+
const session = existing[kSessionPending] ? await existing[kSessionPending] : existing;
|
|
889
|
+
if (session.token !== void 0 && session.expiresAt !== void 0 && session.expiresAt <= codec.now(config)) {
|
|
890
|
+
transport.drop?.();
|
|
891
|
+
const error = expiredSessionError({
|
|
892
|
+
id: session.id,
|
|
893
|
+
createdAt: session.createdAt,
|
|
894
|
+
expiresAt: session.expiresAt
|
|
895
|
+
});
|
|
896
|
+
const freshSession = emptySession(binding);
|
|
897
|
+
freshSession.error = error;
|
|
898
|
+
store.set(freshSession);
|
|
899
|
+
await fireExpire(binding, {
|
|
900
|
+
session: {
|
|
901
|
+
id: session.id,
|
|
902
|
+
createdAt: session.createdAt,
|
|
903
|
+
expiresAt: session.expiresAt,
|
|
904
|
+
token: session.token
|
|
905
|
+
},
|
|
906
|
+
error
|
|
907
|
+
});
|
|
908
|
+
return freshSession;
|
|
909
|
+
}
|
|
910
|
+
return session;
|
|
911
|
+
}
|
|
912
|
+
const session = emptySession(binding);
|
|
913
|
+
store.set(session);
|
|
914
|
+
if (transport.malformed) {
|
|
915
|
+
const error = malformedHeaderError(transport.malformed.header, { prefix: binding.driver.prefix });
|
|
916
|
+
session.error = error;
|
|
917
|
+
await fireError(binding, error);
|
|
918
|
+
return session;
|
|
919
|
+
}
|
|
920
|
+
const token = transport.read();
|
|
921
|
+
if (!token) return session;
|
|
922
|
+
binding.held.set(token);
|
|
923
|
+
session.token = token;
|
|
924
|
+
const pending = open(token).then((opened) => {
|
|
925
|
+
Object.assign(session, opened);
|
|
926
|
+
return { failure: void 0 };
|
|
927
|
+
}, (error) => {
|
|
928
|
+
session.token = void 0;
|
|
929
|
+
return { failure: { error } };
|
|
930
|
+
}).then((outcome) => {
|
|
931
|
+
delete session[kSessionPending];
|
|
932
|
+
return outcome;
|
|
933
|
+
});
|
|
934
|
+
session[kSessionPending] = pending.then(() => session);
|
|
935
|
+
const { failure } = await pending;
|
|
936
|
+
if (failure) {
|
|
937
|
+
if (isJWTError(failure.error)) session.error = failure.error;
|
|
938
|
+
if (isJWTError(failure.error, "ERR_JWT_EXPIRED")) {
|
|
939
|
+
const { jti, iat, exp } = failure.error.cause;
|
|
940
|
+
transport.drop?.();
|
|
941
|
+
await fireExpire(binding, {
|
|
942
|
+
session: {
|
|
943
|
+
id: jti,
|
|
944
|
+
createdAt: iat ? iat * 1e3 : void 0,
|
|
945
|
+
expiresAt: exp ? exp * 1e3 : void 0,
|
|
946
|
+
token
|
|
947
|
+
},
|
|
948
|
+
error: failure.error
|
|
949
|
+
});
|
|
950
|
+
} else {
|
|
951
|
+
if (isJWTError(failure.error, "ERR_SESSION_TOKEN_INVALID")) transport.drop?.();
|
|
952
|
+
await fireError(binding, failure.error);
|
|
953
|
+
}
|
|
954
|
+
return session;
|
|
955
|
+
}
|
|
956
|
+
try {
|
|
957
|
+
await fireRead(binding, session);
|
|
958
|
+
} catch (error_) {
|
|
959
|
+
store.set(emptySession(binding));
|
|
960
|
+
throw error_;
|
|
961
|
+
}
|
|
962
|
+
await rollSession(binding, session);
|
|
963
|
+
return session;
|
|
964
|
+
}
|
|
965
|
+
async function fireRead(binding, session) {
|
|
966
|
+
if (session.id === void 0 || session.token === void 0) return;
|
|
967
|
+
await binding.config.hooks?.onRead?.({
|
|
968
|
+
session: binding.actions.manager(binding),
|
|
969
|
+
...binding.args,
|
|
970
|
+
config: binding.config
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
async function rollSession(binding, session) {
|
|
974
|
+
const { config, codec, transport, store } = binding;
|
|
975
|
+
const { rolling } = config;
|
|
976
|
+
if (rolling === void 0 || transport.source !== "cookie" || store.get() !== session) return;
|
|
977
|
+
if (session.token === void 0 || session.expiresAt === void 0) return;
|
|
978
|
+
if (rolling !== true) {
|
|
979
|
+
if (codec.now(config) - session.createdAt < computeDurationInSeconds(rolling) * 1e3) return;
|
|
980
|
+
}
|
|
981
|
+
if (transport.readonly || !transport.writable()) return;
|
|
982
|
+
await binding.actions.roll(binding);
|
|
983
|
+
}
|
|
984
|
+
async function fireError(binding, error) {
|
|
985
|
+
await binding.config.hooks?.onError?.({
|
|
986
|
+
session: binding.actions.manager(binding),
|
|
987
|
+
...binding.args,
|
|
988
|
+
error,
|
|
989
|
+
config: binding.config
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
async function fireExpire(binding, expired) {
|
|
993
|
+
await binding.config.hooks?.onExpire?.({
|
|
994
|
+
session: {
|
|
995
|
+
...expired.session,
|
|
996
|
+
source: binding.transport.source
|
|
997
|
+
},
|
|
998
|
+
...binding.args,
|
|
999
|
+
error: expired.error,
|
|
1000
|
+
config: binding.config
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
async function issueSession(binding, update, options, trigger = "update") {
|
|
1004
|
+
const { config, keys, driver, store, args, transport } = binding;
|
|
1005
|
+
assertSessionWritable(transport, {
|
|
1006
|
+
operation: "update",
|
|
1007
|
+
prefix: driver.prefix
|
|
1008
|
+
});
|
|
1009
|
+
const session = storedSession(store) || await loadSession(binding);
|
|
1010
|
+
const oldSession = snapshotSession(session);
|
|
1011
|
+
const resolved = typeof update === "function" ? update(session.data) : update;
|
|
1012
|
+
if (resolved) Object.assign(session.data, sanitizeSessionUpdate(resolved, { prefix: driver.prefix }));
|
|
1013
|
+
for (const claim of RESERVED_CLAIMS) delete session.data[claim];
|
|
1014
|
+
const maxAge = options?.maxAge ?? config.maxAge;
|
|
1015
|
+
const createdAt = sessionInstant(binding);
|
|
1016
|
+
const id = mintSessionId(config);
|
|
1017
|
+
Object.assign(session, {
|
|
1018
|
+
id,
|
|
1019
|
+
createdAt,
|
|
1020
|
+
expiresAt: resolveExpiresAt(createdAt, { maxAge })
|
|
1021
|
+
});
|
|
1022
|
+
let issued;
|
|
1023
|
+
try {
|
|
1024
|
+
await keys.assertKeys();
|
|
1025
|
+
const token = await keys.issue(session);
|
|
1026
|
+
transport.write?.(token, { expiresAt: session.expiresAt });
|
|
1027
|
+
binding.held.set(token);
|
|
1028
|
+
issued = Object.assign(session, {
|
|
1029
|
+
id,
|
|
1030
|
+
token,
|
|
1031
|
+
error: void 0
|
|
1032
|
+
});
|
|
1033
|
+
} catch (error_) {
|
|
1034
|
+
Object.assign(session, {
|
|
1035
|
+
id: oldSession.id,
|
|
1036
|
+
createdAt: oldSession.createdAt,
|
|
1037
|
+
expiresAt: oldSession.expiresAt,
|
|
1038
|
+
data: oldSession.data
|
|
1039
|
+
});
|
|
1040
|
+
session.token = oldSession.token;
|
|
1041
|
+
await config.hooks?.onError?.({
|
|
1042
|
+
session: binding.actions.manager(binding),
|
|
1043
|
+
...args,
|
|
1044
|
+
error: error_,
|
|
1045
|
+
config
|
|
1046
|
+
});
|
|
1047
|
+
throw error_;
|
|
1048
|
+
}
|
|
1049
|
+
await config.hooks?.onUpdate?.({
|
|
1050
|
+
session: binding.actions.manager(binding),
|
|
1051
|
+
oldSession,
|
|
1052
|
+
trigger,
|
|
1053
|
+
...args,
|
|
1054
|
+
config
|
|
1055
|
+
});
|
|
1056
|
+
return issued;
|
|
1057
|
+
}
|
|
1058
|
+
async function dropSession(binding) {
|
|
1059
|
+
const { config, driver, store, args, transport } = binding;
|
|
1060
|
+
assertSessionTokenSource(transport, { prefix: driver.prefix });
|
|
1061
|
+
assertSessionWritable(transport, {
|
|
1062
|
+
operation: "clear",
|
|
1063
|
+
prefix: driver.prefix
|
|
1064
|
+
});
|
|
1065
|
+
let session = storedSession(store);
|
|
1066
|
+
if (session && session[kSessionPending]) session = await session[kSessionPending];
|
|
1067
|
+
const oldSession = session && (session.id !== void 0 || session.token !== void 0 || session.error !== void 0) ? snapshotSession(session) : void 0;
|
|
1068
|
+
store.set(emptySession(binding));
|
|
1069
|
+
binding.held.set(void 0);
|
|
1070
|
+
transport.drop?.();
|
|
1071
|
+
await config.hooks?.onClear?.({
|
|
1072
|
+
oldSession,
|
|
1073
|
+
...args,
|
|
1074
|
+
config
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
function sessionManager(binding) {
|
|
1078
|
+
const { store } = binding;
|
|
1079
|
+
const state = () => storedSession(store);
|
|
1080
|
+
const manager = {
|
|
1081
|
+
get id() {
|
|
1082
|
+
return state()?.id;
|
|
1083
|
+
},
|
|
1084
|
+
get createdAt() {
|
|
1085
|
+
return state()?.createdAt ?? Date.now();
|
|
1086
|
+
},
|
|
1087
|
+
get expiresAt() {
|
|
1088
|
+
return (state() ?? blankSession()).expiresAt;
|
|
1089
|
+
},
|
|
1090
|
+
get data() {
|
|
1091
|
+
return (state() ?? blankSession()).data;
|
|
1092
|
+
},
|
|
1093
|
+
get token() {
|
|
1094
|
+
return sessionToken(binding);
|
|
1095
|
+
},
|
|
1096
|
+
get source() {
|
|
1097
|
+
return binding.transport.source;
|
|
1098
|
+
},
|
|
1099
|
+
get error() {
|
|
1100
|
+
return state()?.error;
|
|
1101
|
+
},
|
|
1102
|
+
update: async (update, options) => {
|
|
1103
|
+
await issueSession(binding, update, options);
|
|
1104
|
+
return manager;
|
|
1105
|
+
},
|
|
1106
|
+
clear: async () => {
|
|
1107
|
+
await dropSession(binding);
|
|
1108
|
+
return manager;
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
return manager;
|
|
1112
|
+
}
|
|
1113
|
+
function sessionActions() {
|
|
1114
|
+
return {
|
|
1115
|
+
manager: (binding) => sessionManager(binding),
|
|
1116
|
+
roll: async (binding) => {
|
|
1117
|
+
await issueSession(binding, void 0, void 0, "rolling");
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
function snapshotSession(session) {
|
|
1122
|
+
return {
|
|
1123
|
+
...session,
|
|
1124
|
+
data: safeJsonParse(JSON.stringify(session.data))
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
function defineSession(config, options) {
|
|
1128
|
+
const { driver, codec } = options;
|
|
1129
|
+
const name = config.name || codec.defaultName;
|
|
1130
|
+
assertSessionConfig(config, {
|
|
1131
|
+
name,
|
|
1132
|
+
prefix: driver.prefix
|
|
1133
|
+
});
|
|
1134
|
+
const keys = codec.keys(config);
|
|
1135
|
+
const actions = sessionActions();
|
|
1136
|
+
const bind = (event) => bindSession(event, {
|
|
1137
|
+
config,
|
|
1138
|
+
name,
|
|
1139
|
+
driver,
|
|
1140
|
+
codec,
|
|
1141
|
+
keys,
|
|
1142
|
+
actions
|
|
1143
|
+
});
|
|
1144
|
+
return {
|
|
1145
|
+
use: async (event) => {
|
|
1146
|
+
const binding = bind(event);
|
|
1147
|
+
await loadSession(binding);
|
|
1148
|
+
return sessionManager(binding);
|
|
1149
|
+
},
|
|
1150
|
+
token: (event) => transportToken(bind(event)),
|
|
1151
|
+
issue: async (data, issueOptions) => {
|
|
1152
|
+
await keys.assertKeys();
|
|
1153
|
+
const createdAt = sessionInstant({
|
|
1154
|
+
codec,
|
|
1155
|
+
config
|
|
1156
|
+
});
|
|
1157
|
+
return keys.issue({
|
|
1158
|
+
id: mintSessionId(config),
|
|
1159
|
+
createdAt,
|
|
1160
|
+
expiresAt: resolveExpiresAt(createdAt, { maxAge: issueOptions?.maxAge ?? config.maxAge }),
|
|
1161
|
+
data: sanitizeSessionUpdate(data ?? {}, { prefix: driver.prefix }),
|
|
1162
|
+
token: void 0,
|
|
1163
|
+
source: "token",
|
|
1164
|
+
error: void 0
|
|
1165
|
+
});
|
|
1166
|
+
},
|
|
1167
|
+
open: (token) => openSessionToken(keys, {
|
|
1168
|
+
token,
|
|
1169
|
+
args: {}
|
|
1170
|
+
})
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
function webCookieJar(request, response) {
|
|
1174
|
+
let cookies;
|
|
1175
|
+
const jar = { read: (name) => (cookies ||= parseCookie(request.get("cookie") ?? ""))[name] };
|
|
1176
|
+
if (response) {
|
|
1177
|
+
const { headers, errHeaders } = response;
|
|
1178
|
+
jar.written = (name) => setCookieValues(headers.getSetCookie())[name];
|
|
1179
|
+
jar.write = (name, value, attrs) => {
|
|
1180
|
+
setCookie(headers, {
|
|
1181
|
+
...attrs,
|
|
1182
|
+
name,
|
|
1183
|
+
value
|
|
1184
|
+
});
|
|
1185
|
+
if (errHeaders) mirrorSetCookie(name, {
|
|
1186
|
+
from: headers,
|
|
1187
|
+
to: errHeaders
|
|
1188
|
+
});
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
return jar;
|
|
1192
|
+
}
|
|
1193
|
+
function parseCookie(header) {
|
|
1194
|
+
return parse(header);
|
|
1195
|
+
}
|
|
1196
|
+
function serializeCookie(name, value, attrs) {
|
|
1197
|
+
return serialize({
|
|
1198
|
+
path: "/",
|
|
1199
|
+
...attrs,
|
|
1200
|
+
name,
|
|
1201
|
+
value
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
function setCookie(headers, cookie) {
|
|
1205
|
+
const { name, value, ...attrs } = cookie;
|
|
1206
|
+
const line = serializeCookie(name, value, attrs);
|
|
1207
|
+
const key = cookieKey(cookie);
|
|
1208
|
+
const kept = headers.getSetCookie().filter((existing) => {
|
|
1209
|
+
const parsed = parseSetCookie(existing);
|
|
1210
|
+
return parsed ? cookieKey(parsed) !== key : setCookieName(existing) !== name;
|
|
1211
|
+
});
|
|
1212
|
+
headers.delete("set-cookie");
|
|
1213
|
+
for (const existing of kept) headers.append("set-cookie", existing);
|
|
1214
|
+
headers.append("set-cookie", line);
|
|
1215
|
+
}
|
|
1216
|
+
function cookieKey(cookie) {
|
|
1217
|
+
const domain = (cookie.domain || "").replace(/^\./, "").toLowerCase();
|
|
1218
|
+
return [
|
|
1219
|
+
cookie.name,
|
|
1220
|
+
domain,
|
|
1221
|
+
cookie.path || "/",
|
|
1222
|
+
cookie.partitioned ? "1" : "0"
|
|
1223
|
+
].join(";");
|
|
1224
|
+
}
|
|
1225
|
+
const requestStates = /* @__PURE__ */ new WeakMap();
|
|
1226
|
+
function webSessionDriver(options) {
|
|
1227
|
+
const prefix = options?.prefix ?? "[unjwt/session]";
|
|
1228
|
+
return {
|
|
1229
|
+
prefix,
|
|
1230
|
+
state: (event) => {
|
|
1231
|
+
if (isHanded(event, prefix)) return sessionsOf(event);
|
|
1232
|
+
if (event.context) return contextState(event.context);
|
|
1233
|
+
return sessionsOf(event.req ? webRequest(event.req, prefix) : event);
|
|
1234
|
+
},
|
|
1235
|
+
header: (event, name) => isHanded(event, prefix) ? void 0 : requestHeaders(event, prefix).get(name),
|
|
1236
|
+
cookies: (event) => isHanded(event, prefix) ? { read: () => void 0 } : webCookieJar(requestHeaders(event, prefix), event.res),
|
|
1237
|
+
handed: (event) => isHanded(event, prefix) ? { token: event.token } : void 0
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
const defaultWebSessionDriver = /* @__PURE__ */ webSessionDriver();
|
|
1241
|
+
function isHanded(event, prefix) {
|
|
20
1242
|
if ("req" in event || "headers" in event) return false;
|
|
21
1243
|
if ("token" in event) return true;
|
|
22
|
-
throw invalidConfigError(`${
|
|
1244
|
+
throw invalidConfigError(`${prefix} use() got neither a request event nor a token.`);
|
|
23
1245
|
}
|
|
24
|
-
function requestHeaders(event) {
|
|
25
|
-
if (event.req) return webRequest(event.req).headers;
|
|
1246
|
+
function requestHeaders(event, prefix) {
|
|
1247
|
+
if (event.req) return webRequest(event.req, prefix).headers;
|
|
26
1248
|
if (event.headers) return event.headers;
|
|
27
|
-
throw invalidConfigError(`${
|
|
1249
|
+
throw invalidConfigError(`${prefix} use() got a request event with neither "req" nor "headers".`);
|
|
28
1250
|
}
|
|
29
|
-
function webRequest(req) {
|
|
1251
|
+
function webRequest(req, prefix) {
|
|
30
1252
|
const headers = req.headers;
|
|
31
1253
|
if (typeof headers === "object" && headers !== null && "get" in headers) return req;
|
|
32
|
-
throw invalidConfigError(`${
|
|
1254
|
+
throw invalidConfigError(`${prefix} use() got an event whose "req" is not a web Request.`);
|
|
33
1255
|
}
|
|
34
|
-
function
|
|
35
|
-
const existing = context.
|
|
1256
|
+
function contextState(context) {
|
|
1257
|
+
const existing = context.unjwt;
|
|
36
1258
|
if (typeof existing === "object" && existing !== null) return existing;
|
|
37
1259
|
const created = Object.create(null);
|
|
38
|
-
context.
|
|
1260
|
+
context.unjwt = created;
|
|
39
1261
|
return created;
|
|
40
1262
|
}
|
|
41
|
-
function sessionsOf(
|
|
42
|
-
const existing =
|
|
1263
|
+
function sessionsOf(request) {
|
|
1264
|
+
const existing = requestStates.get(request);
|
|
43
1265
|
if (existing) return existing;
|
|
44
1266
|
const created = Object.create(null);
|
|
45
|
-
|
|
1267
|
+
requestStates.set(request, created);
|
|
46
1268
|
return created;
|
|
47
1269
|
}
|
|
48
1270
|
function defineJWSSession(config, options) {
|
|
49
|
-
const driver = options?.driver ??
|
|
1271
|
+
const driver = options?.driver ?? defaultWebSessionDriver;
|
|
50
1272
|
return defineSession(config, {
|
|
51
1273
|
driver,
|
|
52
|
-
hookArgs: (event) => ({ event }),
|
|
53
1274
|
codec: createJWSCodec({
|
|
54
1275
|
prefix: driver.prefix,
|
|
55
|
-
defaultName: "unjwt-jws"
|
|
1276
|
+
defaultName: "__Host-unjwt-jws"
|
|
56
1277
|
})
|
|
57
1278
|
});
|
|
58
1279
|
}
|
|
1280
|
+
const JWE_KEY_RULES = {
|
|
1281
|
+
own: (key) => isJWK(key) && key.alg ? inferJWEAllowedAlgorithms(key) ?? [] : void 0,
|
|
1282
|
+
carried: (key) => {
|
|
1283
|
+
if (isCryptoKey(key)) {
|
|
1284
|
+
const name = key.algorithm.name;
|
|
1285
|
+
if (name === "PBKDF2") return PBES2_ALGORITHMS;
|
|
1286
|
+
const wraps = inferJWEAllowedAlgorithms(key) ?? [];
|
|
1287
|
+
return name === "AES-GCM" && wraps.length > 0 ? [...wraps, "dir"] : wraps;
|
|
1288
|
+
}
|
|
1289
|
+
if (!isSymmetricJWK(key)) return [];
|
|
1290
|
+
const algs = aesWraps(octBits(key));
|
|
1291
|
+
return directEncryptions(key).length > 0 ? [...algs, "dir"] : algs;
|
|
1292
|
+
},
|
|
1293
|
+
pinPath: "jwe.encryptOptions.alg",
|
|
1294
|
+
hint: "\"jwe.encryptOptions.alg\" and \"enc\"",
|
|
1295
|
+
listPath: "jwe.decryptOptions.algorithms",
|
|
1296
|
+
limitPath: "jwe.decryptOptions.limits.keyCandidates"
|
|
1297
|
+
};
|
|
1298
|
+
function resolveSessionKeys(key, options) {
|
|
1299
|
+
const { alg: pin, enc, prefix } = options;
|
|
1300
|
+
assertKeyMaterial(key, { prefix });
|
|
1301
|
+
const entries = isJWKSet(key) ? sessionKeySetEntries(key.keys, {
|
|
1302
|
+
...options,
|
|
1303
|
+
pin,
|
|
1304
|
+
rules: JWE_KEY_RULES
|
|
1305
|
+
}) : [{
|
|
1306
|
+
key: sessionSecret(key, prefix),
|
|
1307
|
+
subject: "The session key",
|
|
1308
|
+
pin
|
|
1309
|
+
}];
|
|
1310
|
+
assertIssuingKey(entries[0], {
|
|
1311
|
+
enc,
|
|
1312
|
+
prefix
|
|
1313
|
+
});
|
|
1314
|
+
const { all } = servedAlgorithms(entries, {
|
|
1315
|
+
...options,
|
|
1316
|
+
rules: JWE_KEY_RULES
|
|
1317
|
+
});
|
|
1318
|
+
return {
|
|
1319
|
+
issueKey: entries[0].key,
|
|
1320
|
+
key,
|
|
1321
|
+
algorithms: all
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
function assertIssuingKey(entry, options) {
|
|
1325
|
+
const { enc, prefix } = options;
|
|
1326
|
+
const { key, subject, pin } = entry;
|
|
1327
|
+
assertPinnedKey(entry, {
|
|
1328
|
+
rules: JWE_KEY_RULES,
|
|
1329
|
+
pinned: Boolean(pin && enc),
|
|
1330
|
+
prefix
|
|
1331
|
+
});
|
|
1332
|
+
if ((pin ?? JWE_KEY_RULES.own(key)?.[0]) !== "dir" || !enc) return;
|
|
1333
|
+
const encs = directEncryptions(key);
|
|
1334
|
+
if (encs.includes(enc)) return;
|
|
1335
|
+
throw invalidKeyError(`${prefix} ${subject} serves "dir" with ${encs.join(", ") || "no content encryption"}, not the "${enc}" that "jwe.encryptOptions.enc" pins.`, key);
|
|
1336
|
+
}
|
|
1337
|
+
function directEncryptions(key) {
|
|
1338
|
+
if (isJWK(key) && typeof key.alg === "string" && isContentEncryptionAlgorithm(key.alg)) return [key.alg];
|
|
1339
|
+
if (isCryptoKey(key)) {
|
|
1340
|
+
const bits = lengthOf(key.algorithm) ?? 0;
|
|
1341
|
+
return key.algorithm.name === "AES-GCM" ? GCM_ENCRYPTIONS.filter((enc) => enc === `A${bits}GCM`) : [];
|
|
1342
|
+
}
|
|
1343
|
+
if (!isSymmetricJWK(key)) return [];
|
|
1344
|
+
const bits = octBits(key);
|
|
1345
|
+
return [...GCM_ENCRYPTIONS, ...CBC_ENCRYPTIONS].filter((enc) => encryptionBits(enc) === bits);
|
|
1346
|
+
}
|
|
1347
|
+
function sessionSecret(key, prefix) {
|
|
1348
|
+
if (isOctJWK(key)) return key;
|
|
1349
|
+
if (isCryptoKey(key) && key.type === "secret") return key;
|
|
1350
|
+
throw invalidKeyError((isCryptoKey(key) ? key.type !== "secret" : isAsymmetricJWK(key) || isKeyPair(key)) ? `${prefix} Invalid JWE session key: an asymmetric key seals with its public half, so anyone holding that half could mint sessions. Use an "oct" JWK, or defineJWSSession for a key pair.` : `${prefix} Invalid JWE session key: expected an "oct" JWK, a JWK Set of them or a secret CryptoKey.`, key);
|
|
1351
|
+
}
|
|
1352
|
+
function isOctJWK(key) {
|
|
1353
|
+
return isSymmetricJWK(key);
|
|
1354
|
+
}
|
|
1355
|
+
const GCM_ENCRYPTIONS = [
|
|
1356
|
+
"A128GCM",
|
|
1357
|
+
"A192GCM",
|
|
1358
|
+
"A256GCM"
|
|
1359
|
+
];
|
|
1360
|
+
const CBC_ENCRYPTIONS = [
|
|
1361
|
+
"A128CBC-HS256",
|
|
1362
|
+
"A192CBC-HS384",
|
|
1363
|
+
"A256CBC-HS512"
|
|
1364
|
+
];
|
|
1365
|
+
function aesWraps(bits) {
|
|
1366
|
+
switch (bits) {
|
|
1367
|
+
case 128: return ["A128KW", "A128GCMKW"];
|
|
1368
|
+
case 192: return ["A192KW", "A192GCMKW"];
|
|
1369
|
+
case 256: return ["A256KW", "A256GCMKW"];
|
|
1370
|
+
default: return [];
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
function encryptionBits(enc) {
|
|
1374
|
+
const bits = Number(enc.slice(1, 4));
|
|
1375
|
+
return enc.includes("CBC") ? bits * 2 : bits;
|
|
1376
|
+
}
|
|
1377
|
+
function createJWECodec(options) {
|
|
1378
|
+
const { prefix, defaultName } = options;
|
|
1379
|
+
return createSessionCodec({
|
|
1380
|
+
family: "jwe",
|
|
1381
|
+
prefix,
|
|
1382
|
+
defaultName,
|
|
1383
|
+
issueOptions: (config) => config.jwe?.encryptOptions,
|
|
1384
|
+
openOptions: (config) => config.jwe?.decryptOptions,
|
|
1385
|
+
resolveKeys: (config) => resolveSessionKeys(config.key, {
|
|
1386
|
+
alg: config.jwe?.encryptOptions?.alg,
|
|
1387
|
+
enc: config.jwe?.encryptOptions?.enc,
|
|
1388
|
+
listed: config.jwe?.decryptOptions?.algorithms,
|
|
1389
|
+
limits: config.jwe?.decryptOptions?.limits,
|
|
1390
|
+
lookup: typeof config.hooks?.onKeyLookup === "function",
|
|
1391
|
+
prefix
|
|
1392
|
+
}),
|
|
1393
|
+
lookup: (config) => config.hooks?.onKeyLookup,
|
|
1394
|
+
issue: (config, payload, key) => encrypt(payload, key, sessionIssueOptions(config.jwe?.encryptOptions)),
|
|
1395
|
+
open: async (config, sealed, key, overrides) => {
|
|
1396
|
+
const { payload } = await decrypt(sealed, key, {
|
|
1397
|
+
...config.jwe?.decryptOptions,
|
|
1398
|
+
maxIterations: sessionMaxIterations(config),
|
|
1399
|
+
...overrides
|
|
1400
|
+
});
|
|
1401
|
+
return payload;
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
function sessionMaxIterations(config) {
|
|
1406
|
+
const configured = config.jwe?.decryptOptions?.maxIterations;
|
|
1407
|
+
if (configured !== void 0) return configured;
|
|
1408
|
+
const issuing = isJWKSet(config.key) ? config.key.keys[0] : config.key;
|
|
1409
|
+
const alg = config.jwe?.encryptOptions?.alg ?? (isJWK(issuing) ? issuing.alg : void 0);
|
|
1410
|
+
return typeof alg === "string" && alg.startsWith("PBES2") ? config.jwe?.encryptOptions?.p2c ?? 6e5 : void 0;
|
|
1411
|
+
}
|
|
59
1412
|
function defineJWESession(config, options) {
|
|
60
|
-
const driver = options?.driver ??
|
|
1413
|
+
const driver = options?.driver ?? defaultWebSessionDriver;
|
|
61
1414
|
return defineSession(config, {
|
|
62
1415
|
driver,
|
|
63
|
-
hookArgs: (event) => ({ event }),
|
|
64
1416
|
codec: createJWECodec({
|
|
65
1417
|
prefix: driver.prefix,
|
|
66
|
-
defaultName: "unjwt-jwe"
|
|
1418
|
+
defaultName: "__Host-unjwt-jwe"
|
|
67
1419
|
})
|
|
68
1420
|
});
|
|
69
1421
|
}
|
|
70
1422
|
function defineSessionDriver(driver) {
|
|
71
1423
|
return driver;
|
|
72
1424
|
}
|
|
73
|
-
export { defineJWESession, defineJWSSession, defineSessionDriver, deriveJWKFromPassword, exportPEM, generateJWK, importPEM, thumbprint, toPublicJWK };
|
|
1425
|
+
export { DEFAULT_SESSION_COOKIE, assertSessionConfig, assertSessionHeader, assertSessionTokenSource, assertSessionWritable, claimSessionName, defineJWESession, defineJWSSession, defineSessionDriver, deriveJWKFromPassword, exportPEM, generateJWK, importPEM, parseCookie, resolveSessionTransport, serializeCookie, sessionTransportOptions, thumbprint, toPublicJWK, webCookieJar, webSessionDriver };
|