unjwt 0.8.1 → 0.8.3
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 +4 -4
- package/dist/THIRD-PARTY-LICENSES.md.gz +0 -0
- package/dist/_chunks/_codec-jwe.d.mts +101 -22
- package/dist/_chunks/_codec-jws.mjs +306 -232
- package/dist/_chunks/_cookie-attributes.d.mts +12 -0
- package/dist/_chunks/_jwt-internal.mjs +9 -6
- package/dist/_chunks/digest.d.mts +8 -0
- package/dist/_chunks/{algorithms.mjs → digest.mjs} +78 -8
- package/dist/_chunks/index.d.mts +313 -268
- package/dist/_chunks/index2.d.mts +2 -1
- package/dist/_chunks/index3.d.mts +1 -0
- package/dist/_chunks/jwe.d.mts +4 -4
- package/dist/_chunks/jwe.mjs +21 -3
- package/dist/_chunks/jwk.mjs +219 -173
- package/dist/_chunks/jws.d.mts +10 -10
- package/dist/_chunks/jws.mjs +18 -6
- package/dist/_chunks/jwt.d.mts +2 -1
- package/dist/_chunks/libs/cookie-es.mjs +255 -0
- package/dist/adapters/elysia/index.d.mts +32 -15
- package/dist/adapters/elysia/index.mjs +46 -67
- package/dist/adapters/h3/index.d.mts +135 -0
- package/dist/adapters/h3/index.mjs +119 -0
- package/dist/adapters/h3v1/index.d.mts +100 -27
- package/dist/adapters/h3v1/index.mjs +59 -76
- package/dist/core/jwe.d.mts +2 -2
- package/dist/core/jwk.d.mts +22 -14
- package/dist/core/jwk.mjs +2 -2
- package/dist/core/jws.d.mts +2 -2
- package/dist/core/utils/index.d.mts +2 -1
- package/dist/core/utils/index.mjs +2 -2
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +3 -3
- package/dist/session/index.d.mts +44 -0
- package/dist/session/index.mjs +113 -0
- package/package.json +37 -18
- package/dist/adapters/h3v2/index.d.mts +0 -65
- package/dist/adapters/h3v2/index.mjs +0 -139
|
@@ -36,6 +36,9 @@ function expiredSessionError(options) {
|
|
|
36
36
|
exp
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
|
+
function isTokenFailure(error) {
|
|
40
|
+
return !(isJWTError(error.cause, "ERR_JWK_KEY_NOT_FOUND") && error.cause.cause !== void 0);
|
|
41
|
+
}
|
|
39
42
|
function rethrowAsInvalidToken(error, options) {
|
|
40
43
|
if (isJWTError(error, "ERR_JWT_EXPIRED")) throw error;
|
|
41
44
|
throw invalidTokenError(`${options.prefix} Invalid session token: ${messageOf(error)}`, error);
|
|
@@ -43,9 +46,12 @@ function rethrowAsInvalidToken(error, options) {
|
|
|
43
46
|
const TOKEN = /^[!#$%&'*+\-.^_`|~\w]+$/;
|
|
44
47
|
const CHUNK_SUFFIX = /\.\d+$/;
|
|
45
48
|
function assertSessionName(name, options) {
|
|
46
|
-
if (
|
|
49
|
+
if (!isToken(name)) throw invalidConfigError(`${options.prefix} Session "name" must be a cookie name (RFC 6265 §4.1.1): letters, digits and !#$%&'*+-.^_\`|~ only. Got ${quoteTokenValue(name)}.`);
|
|
47
50
|
if (CHUNK_SUFFIX.test(name)) throw invalidConfigError(`${options.prefix} Session "name" cannot end in a dot and digits: ${quoteTokenValue(name)} is the cookie holding a chunk of the session named ${quoteTokenValue(name.replace(CHUNK_SUFFIX, ""))}.`);
|
|
48
51
|
}
|
|
52
|
+
function isToken(value) {
|
|
53
|
+
return typeof value === "string" && TOKEN.test(value);
|
|
54
|
+
}
|
|
49
55
|
function assertCookiePrefix(name, attrs, options) {
|
|
50
56
|
const lower = name.toLowerCase();
|
|
51
57
|
const host = lower.startsWith("__host-");
|
|
@@ -53,24 +59,13 @@ function assertCookiePrefix(name, attrs, options) {
|
|
|
53
59
|
const rule = host ? "\"__Host-\" cookie must be Secure, with Path=/ and no Domain" : "\"__Secure-\" cookie must be Secure";
|
|
54
60
|
if (attrs.secure === false || host && (attrs.domain !== void 0 || attrs.path !== void 0 && attrs.path !== "/")) throw invalidConfigError(`${options.prefix} Session ${quoteTokenValue(name)}: a ${rule} (RFC 6265bis §4.1.3).`);
|
|
55
61
|
}
|
|
56
|
-
function assertSessionHeader(header, options) {
|
|
57
|
-
if (header === void 0 || header === false) return;
|
|
58
|
-
if (typeof header === "string" && TOKEN.test(header)) return;
|
|
59
|
-
throw invalidConfigError(`${options.prefix} Session "sessionHeader" must be false or a header name: letters, digits and !#$%&'*+-.^_\`|~ only. Got ${quoteTokenValue(header)}.`);
|
|
60
|
-
}
|
|
61
62
|
function assertRolling(config, options) {
|
|
62
63
|
const { rolling } = config;
|
|
63
64
|
if (rolling === void 0) return;
|
|
64
65
|
if (config.maxAge === void 0) throw invalidConfigError(`${options.prefix} "rolling" needs a "maxAge" to extend.`);
|
|
65
66
|
if (rolling === true) return;
|
|
66
67
|
if (!isExpiresIn(rolling)) throw new JWTError(`${options.prefix} "rolling" must be true or a duration.`, "ERR_INVALID_OPTION");
|
|
67
|
-
computeDurationInSeconds(rolling);
|
|
68
|
-
}
|
|
69
|
-
function hasTokenSource(source) {
|
|
70
|
-
return source.cookie !== false || source.sessionHeader !== false;
|
|
71
|
-
}
|
|
72
|
-
function assertTokenSource(source, options) {
|
|
73
|
-
if (!hasTokenSource(source)) throw invalidConfigError(`${options.prefix} Session has no token source: "cookie" and "sessionHeader" are both disabled.`);
|
|
68
|
+
if (computeDurationInSeconds(rolling) >= computeDurationInSeconds(config.maxAge)) throw invalidConfigError(`${options.prefix} "rolling" must be shorter than "maxAge": the session would expire before it rolls.`);
|
|
74
69
|
}
|
|
75
70
|
function assertKeyMaterial(key, options) {
|
|
76
71
|
if (typeof key === "string") throw invalidKeyError(`${options.prefix} Session "key" must be a JWK or a CryptoKey, not a password string. Derive one once with deriveJWKFromPassword() and pass the result.`);
|
|
@@ -101,27 +96,6 @@ async function assertImportableKey(key, options) {
|
|
|
101
96
|
function sessionTyp(configured) {
|
|
102
97
|
return typeof configured === "string" && configured ? configured : SESSION_TYP;
|
|
103
98
|
}
|
|
104
|
-
function sessionHeaderName(sessionName, configured) {
|
|
105
|
-
if (typeof configured === "string") return configured.toLowerCase();
|
|
106
|
-
return `x-${sessionName.replace(/^__(?:host|secure)-/i, "").toLowerCase()}-session`;
|
|
107
|
-
}
|
|
108
|
-
function headerToken(value) {
|
|
109
|
-
if (typeof value !== "string") return void 0;
|
|
110
|
-
const words = value.trim().split(/\s+/);
|
|
111
|
-
const [first = "", second] = words;
|
|
112
|
-
if (/^bearer$/i.test(first)) return words.length === 2 ? second : void 0;
|
|
113
|
-
return words.length === 1 && first ? first : void 0;
|
|
114
|
-
}
|
|
115
|
-
function resolveSessionToken(source, sessionName, read) {
|
|
116
|
-
let cookieValue;
|
|
117
|
-
if (source.cookie !== false) {
|
|
118
|
-
const written = read.written?.(sessionName);
|
|
119
|
-
if (written === "") return void 0;
|
|
120
|
-
cookieValue = written ?? read.cookie(sessionName);
|
|
121
|
-
}
|
|
122
|
-
const headerValue = source.sessionHeader === false ? void 0 : headerToken(read.header(sessionHeaderName(sessionName, source.sessionHeader)));
|
|
123
|
-
return cookieValue || headerValue || void 0;
|
|
124
|
-
}
|
|
125
99
|
function resolveExpiresAt(createdAt, options) {
|
|
126
100
|
return options.maxAge === void 0 ? void 0 : createdAt + computeDurationInSeconds(options.maxAge) * 1e3;
|
|
127
101
|
}
|
|
@@ -153,7 +127,7 @@ function isExpiresIn(value) {
|
|
|
153
127
|
function createSessionCodec(spec) {
|
|
154
128
|
const { prefix } = spec;
|
|
155
129
|
const alg = (config) => spec.issueOptions(config)?.alg;
|
|
156
|
-
const keyFor = (config,
|
|
130
|
+
const keyFor = (config, args) => spec.lookup(config, args) ?? spec.openKey(config);
|
|
157
131
|
const now = (config) => spec.issueOptions(config)?.currentDate?.getTime() ?? Date.now();
|
|
158
132
|
const openWith = async (config, token, key) => {
|
|
159
133
|
const payload = await spec.open(config, token, key).catch((error) => rethrowAsInvalidToken(error, { prefix }));
|
|
@@ -189,13 +163,13 @@ function createSessionCodec(spec) {
|
|
|
189
163
|
});
|
|
190
164
|
if (!isSymmetricJWK(key)) await assertOpenKey(config);
|
|
191
165
|
},
|
|
192
|
-
opener: (config,
|
|
193
|
-
const key = keyFor(config,
|
|
166
|
+
opener: (config, args) => {
|
|
167
|
+
const key = keyFor(config, args);
|
|
194
168
|
return (token) => openWith(config, token, key);
|
|
195
169
|
},
|
|
196
|
-
open: async (config, token,
|
|
170
|
+
open: async (config, token, args) => {
|
|
197
171
|
await assertOpenKey(config);
|
|
198
|
-
return openWith(config, token, keyFor(config,
|
|
172
|
+
return openWith(config, token, keyFor(config, args));
|
|
199
173
|
}
|
|
200
174
|
};
|
|
201
175
|
}
|
|
@@ -277,12 +251,12 @@ function createJWECodec(options) {
|
|
|
277
251
|
openOptions: (config) => config.jwe?.decryptOptions,
|
|
278
252
|
issueKey: (config) => getEncryptKey(config, prefix),
|
|
279
253
|
openKey: (config) => getDecryptKey(config, prefix),
|
|
280
|
-
lookup: (config,
|
|
254
|
+
lookup: (config, args) => {
|
|
281
255
|
const hooks = config.hooks;
|
|
282
256
|
if (!hooks?.onUnsealKeyLookup) return void 0;
|
|
283
257
|
return (header) => hooks.onUnsealKeyLookup?.({
|
|
284
258
|
header,
|
|
285
|
-
...
|
|
259
|
+
...args,
|
|
286
260
|
config
|
|
287
261
|
});
|
|
288
262
|
},
|
|
@@ -353,43 +327,245 @@ function assertPinnedAlgEnc(key, config, prefix) {
|
|
|
353
327
|
hint: "\"jwe.encryptOptions.alg\" and \"enc\""
|
|
354
328
|
});
|
|
355
329
|
}
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
330
|
+
const CHUNK_MARKER = "__chunked__";
|
|
331
|
+
const DEFAULT_CHUNK_MAX_LENGTH = 4e3;
|
|
332
|
+
function readChunked(store, name) {
|
|
333
|
+
const main = store.read(name);
|
|
334
|
+
if (main === void 0) return void 0;
|
|
335
|
+
if (!main.startsWith("__chunked__")) return main;
|
|
336
|
+
const count = parseChunkCount(main);
|
|
337
|
+
if (count === 0) return void 0;
|
|
338
|
+
const parts = [];
|
|
339
|
+
for (let index = 1; index <= count; index++) {
|
|
340
|
+
const part = store.read(chunkName(name, index));
|
|
341
|
+
if (part === void 0) return void 0;
|
|
342
|
+
parts.push(part);
|
|
343
|
+
}
|
|
344
|
+
return parts.join("");
|
|
345
|
+
}
|
|
346
|
+
function writeChunked(store, name, value, options) {
|
|
347
|
+
const { attrs, chunkMaxLength, prefix } = options;
|
|
348
|
+
const max = chunkMaxLength && chunkMaxLength > 0 ? chunkMaxLength : DEFAULT_CHUNK_MAX_LENGTH;
|
|
349
|
+
const chunkCount = Math.ceil(value.length / max);
|
|
350
|
+
if (chunkCount > 16) throw invalidConfigError(`${prefix} Session token requires ${chunkCount} cookie chunks, exceeding the limit of 16.`);
|
|
351
|
+
const previousCount = parseChunkCount(store.read(name));
|
|
352
|
+
const firstSurplus = chunkCount <= 1 ? 1 : chunkCount + 1;
|
|
353
|
+
for (let index = firstSurplus; index <= previousCount; index++) expire(store, chunkName(name, index), attrs);
|
|
354
|
+
if (chunkCount <= 1) {
|
|
355
|
+
store.write(name, value, attrs);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
store.write(name, `${CHUNK_MARKER}${chunkCount}`, attrs);
|
|
359
|
+
for (let index = 1; index <= chunkCount; index++) store.write(chunkName(name, index), value.slice((index - 1) * max, index * max), attrs);
|
|
360
|
+
}
|
|
361
|
+
function removeChunked(store, name, options) {
|
|
362
|
+
const count = parseChunkCount(store.read(name));
|
|
363
|
+
expire(store, name, options.attrs);
|
|
364
|
+
for (let index = 1; index <= count; index++) expire(store, chunkName(name, index), options.attrs);
|
|
365
|
+
}
|
|
366
|
+
function setCookieValues(setCookies) {
|
|
367
|
+
const cookies = Object.create(null);
|
|
368
|
+
for (const setCookie of setCookies) {
|
|
369
|
+
const pair = setCookiePair(setCookie);
|
|
370
|
+
if (pair) cookies[pair.name] = decodeValue(pair.value);
|
|
371
|
+
}
|
|
372
|
+
return cookies;
|
|
373
|
+
}
|
|
374
|
+
function setCookieName(setCookie) {
|
|
375
|
+
return setCookiePair(setCookie)?.name;
|
|
376
|
+
}
|
|
377
|
+
function chunkName(name, index) {
|
|
378
|
+
return `${name}.${index}`;
|
|
379
|
+
}
|
|
380
|
+
function parseChunkCount(main) {
|
|
381
|
+
if (main === void 0 || !main.startsWith("__chunked__")) return 0;
|
|
382
|
+
const suffix = main.slice(11);
|
|
383
|
+
if (!/^\d+$/.test(suffix)) return 0;
|
|
384
|
+
const count = Number.parseInt(suffix, 10);
|
|
385
|
+
if (count < 1 || count > 16) return 0;
|
|
386
|
+
return count;
|
|
387
|
+
}
|
|
388
|
+
function expire(store, name, attrs) {
|
|
389
|
+
store.write(name, "", {
|
|
390
|
+
...attrs,
|
|
391
|
+
maxAge: 0,
|
|
392
|
+
expires: /* @__PURE__ */ new Date(0)
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
function setCookiePair(setCookie) {
|
|
396
|
+
const [pair = ""] = setCookie.split(";", 1);
|
|
397
|
+
const separator = pair.indexOf("=");
|
|
398
|
+
if (separator === -1) return void 0;
|
|
399
|
+
return {
|
|
400
|
+
name: pair.slice(0, separator).trim(),
|
|
401
|
+
value: pair.slice(separator + 1).trim()
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function decodeValue(value) {
|
|
405
|
+
if (!value.includes("%")) return value;
|
|
406
|
+
try {
|
|
407
|
+
return decodeURIComponent(value);
|
|
408
|
+
} catch {
|
|
409
|
+
return value;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function resolveTransport(driver, event, options) {
|
|
413
|
+
const handed = driver.handed?.(event);
|
|
414
|
+
if (handed) return {
|
|
415
|
+
read: () => handed.token,
|
|
416
|
+
readonly: false,
|
|
417
|
+
writable: () => true
|
|
418
|
+
};
|
|
419
|
+
const { config, name } = options;
|
|
420
|
+
const header = headerReader(driver, event, options);
|
|
421
|
+
if (config.cookie === false) return {
|
|
422
|
+
read: header,
|
|
423
|
+
readonly: false,
|
|
424
|
+
writable: () => true
|
|
425
|
+
};
|
|
426
|
+
const jar = driver.cookies(event);
|
|
427
|
+
const store = responseStore(jar);
|
|
428
|
+
const transport = {
|
|
429
|
+
read: () => {
|
|
430
|
+
const written = jar.written ? readChunked({ read: (key) => jar.written?.(key) }, name) : void 0;
|
|
431
|
+
if (written === "") return void 0;
|
|
432
|
+
return (written ?? readChunked(jar, name)) || header?.() || void 0;
|
|
433
|
+
},
|
|
434
|
+
readonly: !store,
|
|
435
|
+
writable: () => driver.writable?.(event) ?? true
|
|
436
|
+
};
|
|
437
|
+
if (!store) return transport;
|
|
438
|
+
transport.write = (token, { attrs, chunkMaxLength }) => {
|
|
439
|
+
writeChunked(store, name, token, {
|
|
440
|
+
attrs,
|
|
441
|
+
chunkMaxLength,
|
|
442
|
+
prefix: driver.prefix
|
|
443
|
+
});
|
|
444
|
+
};
|
|
445
|
+
transport.drop = ({ attrs }) => {
|
|
446
|
+
removeChunked(store, name, { attrs });
|
|
447
|
+
};
|
|
448
|
+
return transport;
|
|
449
|
+
}
|
|
450
|
+
function assertTokenSource(transport, options) {
|
|
451
|
+
if (!transport.read) throw invalidConfigError(`${options.prefix} Session has no token source: "cookie" and "sessionHeader" are both disabled.`);
|
|
452
|
+
}
|
|
453
|
+
function assertWritable(transport, options) {
|
|
454
|
+
const { operation, prefix } = options;
|
|
455
|
+
if (operation === "update" && transport.readonly) throw readOnlyEventError(`${prefix} Cannot update session on read-only event.`);
|
|
456
|
+
if (!transport.writable()) throw readOnlyEventError(`${prefix} Cannot update/clear session: the response was already sent.`);
|
|
457
|
+
}
|
|
458
|
+
function assertSessionHeader(header, options) {
|
|
459
|
+
if (header === void 0 || typeof header === "boolean") return;
|
|
460
|
+
if (isToken(header)) return;
|
|
461
|
+
throw invalidConfigError(`${options.prefix} Session "sessionHeader" must be true, false or a header name: letters, digits and !#$%&'*+-.^_\`|~ only. Got ${quoteTokenValue(header)}.`);
|
|
462
|
+
}
|
|
463
|
+
function sessionHeaderName(sessionName, configured) {
|
|
464
|
+
if (typeof configured === "string") return configured.toLowerCase();
|
|
465
|
+
return `x-${sessionName.replace(/^__(?:host|secure)-/i, "").toLowerCase()}-session`;
|
|
466
|
+
}
|
|
467
|
+
function headerToken(value) {
|
|
468
|
+
if (typeof value !== "string") return void 0;
|
|
469
|
+
const words = value.trim().split(/\s+/);
|
|
470
|
+
const [first = "", second] = words;
|
|
471
|
+
if (/^bearer$/i.test(first)) return words.length === 2 ? second : void 0;
|
|
472
|
+
return words.length === 1 && first ? first : void 0;
|
|
473
|
+
}
|
|
474
|
+
function headerReader(driver, event, options) {
|
|
475
|
+
const { config, name } = options;
|
|
476
|
+
if (config.sessionHeader === false) return void 0;
|
|
477
|
+
const header = sessionHeaderName(name, config.sessionHeader);
|
|
478
|
+
return () => headerToken(driver.header(event, header));
|
|
479
|
+
}
|
|
480
|
+
function responseStore(jar) {
|
|
481
|
+
const write = jar.write?.bind(jar);
|
|
482
|
+
if (!write) return void 0;
|
|
483
|
+
return {
|
|
484
|
+
read: (name) => jar.written?.(name) ?? jar.read(name),
|
|
485
|
+
write
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
const kSessionNames = Symbol("unjwt_session_names");
|
|
489
|
+
function bindSession(event, config, options) {
|
|
490
|
+
const { driver, codec, actions, hookArgs } = options;
|
|
491
|
+
const { prefix } = driver;
|
|
359
492
|
const name = config.name || codec.defaultName;
|
|
360
|
-
assertSessionName(name, { prefix
|
|
361
|
-
assertSessionHeader(config.sessionHeader, { prefix
|
|
493
|
+
assertSessionName(name, { prefix });
|
|
494
|
+
assertSessionHeader(config.sessionHeader, { prefix });
|
|
362
495
|
if (config.cookie !== false) assertCookiePrefix(name, {
|
|
363
496
|
...codec.defaultCookie,
|
|
364
497
|
...config.cookie
|
|
365
|
-
}, { prefix
|
|
366
|
-
assertRolling(config, { prefix
|
|
367
|
-
|
|
498
|
+
}, { prefix });
|
|
499
|
+
assertRolling(config, { prefix });
|
|
500
|
+
const record = driver.state(event);
|
|
501
|
+
claimSessionName(record, {
|
|
502
|
+
name,
|
|
503
|
+
family: codec.family,
|
|
504
|
+
prefix
|
|
505
|
+
});
|
|
368
506
|
return {
|
|
369
|
-
|
|
507
|
+
event,
|
|
370
508
|
config,
|
|
371
|
-
|
|
509
|
+
driver,
|
|
372
510
|
codec,
|
|
373
511
|
actions,
|
|
374
512
|
name,
|
|
375
|
-
args:
|
|
376
|
-
store:
|
|
513
|
+
args: hookArgs(event),
|
|
514
|
+
store: recordSlotStore(record, `${codec.family}:${name}`),
|
|
515
|
+
transport: resolveTransport(driver, event, {
|
|
516
|
+
config,
|
|
517
|
+
name
|
|
518
|
+
})
|
|
377
519
|
};
|
|
378
520
|
}
|
|
379
521
|
function sessionToken(binding) {
|
|
380
|
-
|
|
381
|
-
|
|
522
|
+
const { transport } = binding;
|
|
523
|
+
assertTokenSource(transport, { prefix: binding.driver.prefix });
|
|
524
|
+
return transport.read();
|
|
382
525
|
}
|
|
383
526
|
function recordSlotStore(record, slot) {
|
|
384
527
|
return {
|
|
385
|
-
get: () => record
|
|
528
|
+
get: () => record[slot],
|
|
386
529
|
set: (state) => {
|
|
387
|
-
record
|
|
530
|
+
record[slot] = state;
|
|
388
531
|
}
|
|
389
532
|
};
|
|
390
533
|
}
|
|
391
|
-
function
|
|
392
|
-
const
|
|
534
|
+
function sessionCookie(binding, options = {}) {
|
|
535
|
+
const { chunkMaxLength, ...configured } = binding.config.cookie || {};
|
|
536
|
+
const attrs = {
|
|
537
|
+
...binding.codec.defaultCookie,
|
|
538
|
+
...configured
|
|
539
|
+
};
|
|
540
|
+
if (options.expiresAt !== void 0) {
|
|
541
|
+
attrs.expires = new Date(options.expiresAt);
|
|
542
|
+
attrs.maxAge = void 0;
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
attrs,
|
|
546
|
+
chunkMaxLength
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
function dropSessionCookie(binding) {
|
|
550
|
+
const { transport } = binding;
|
|
551
|
+
if (!transport.drop || !transport.writable()) return;
|
|
552
|
+
transport.drop(sessionCookie(binding));
|
|
553
|
+
}
|
|
554
|
+
function claimSessionName(record, options) {
|
|
555
|
+
const { name, family, prefix } = options;
|
|
556
|
+
const holder = record;
|
|
557
|
+
const registry = holder[kSessionNames] ||= /* @__PURE__ */ new Map();
|
|
558
|
+
const held = registry.get(name);
|
|
559
|
+
if (held === void 0) {
|
|
560
|
+
registry.set(name, family);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (held === family) return;
|
|
564
|
+
throw invalidConfigError(`${prefix} Session name "${name}" is already used by a ${held.toUpperCase()} session on this request; a name is one cookie, so give this session a "name" of its own.`);
|
|
565
|
+
}
|
|
566
|
+
const kSessionPending = Symbol("unjwt_session_pending");
|
|
567
|
+
function sessionInstant(source) {
|
|
568
|
+
const now = source.codec.now(source.config);
|
|
393
569
|
return now - now % 1e3;
|
|
394
570
|
}
|
|
395
571
|
function mintSessionId(config) {
|
|
@@ -416,54 +592,19 @@ function blankSession() {
|
|
|
416
592
|
});
|
|
417
593
|
}
|
|
418
594
|
function openSessionToken(codec, config, options) {
|
|
419
|
-
return codec.open(config, options.token, options.
|
|
420
|
-
}
|
|
421
|
-
function sessionCookie(binding, options = {}) {
|
|
422
|
-
const { chunkMaxLength, ...configured } = binding.config.cookie || {};
|
|
423
|
-
const attrs = {
|
|
424
|
-
...binding.codec.defaultCookie,
|
|
425
|
-
...configured
|
|
426
|
-
};
|
|
427
|
-
if (options.expiresAt !== void 0) {
|
|
428
|
-
attrs.expires = new Date(options.expiresAt);
|
|
429
|
-
attrs.maxAge = void 0;
|
|
430
|
-
}
|
|
431
|
-
return {
|
|
432
|
-
attrs,
|
|
433
|
-
chunkMaxLength
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
function dropSessionCookie(binding) {
|
|
437
|
-
const { config, port, carrier } = binding;
|
|
438
|
-
if (config.cookie === false) return;
|
|
439
|
-
if (!port.writable(carrier)) {
|
|
440
|
-
console.warn(`${port.prefix} Session expired but cookie cannot be cleared on a read-only event.`);
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
port.deleteCookie(carrier, binding.name, sessionCookie(binding));
|
|
444
|
-
}
|
|
445
|
-
const SESSION_NAMES = /* @__PURE__ */ new WeakMap();
|
|
446
|
-
function claimSessionName(scope, name, family, prefix) {
|
|
447
|
-
let registry = SESSION_NAMES.get(scope);
|
|
448
|
-
if (!registry) {
|
|
449
|
-
registry = /* @__PURE__ */ new Map();
|
|
450
|
-
SESSION_NAMES.set(scope, registry);
|
|
451
|
-
}
|
|
452
|
-
const holder = registry.get(name);
|
|
453
|
-
if (holder === void 0) {
|
|
454
|
-
registry.set(name, family);
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
if (holder === family) return;
|
|
458
|
-
throw invalidConfigError(`${prefix} Session name "${name}" is already used by a ${holder.toUpperCase()} session on this request; a name is one cookie, so give this session a "name" of its own.`);
|
|
595
|
+
return codec.open(config, options.token, options.args).then(typedOpened);
|
|
459
596
|
}
|
|
460
597
|
function typedSession(state) {
|
|
461
598
|
return state;
|
|
462
599
|
}
|
|
600
|
+
function typedOpened(opened) {
|
|
601
|
+
return opened;
|
|
602
|
+
}
|
|
463
603
|
async function loadSession(binding) {
|
|
464
|
-
const { config, codec, store, args } = binding;
|
|
604
|
+
const { config, codec, store, args, transport } = binding;
|
|
465
605
|
await codec.assertKeys(config);
|
|
466
606
|
const open = codec.opener(config, args);
|
|
607
|
+
assertTokenSource(transport, { prefix: binding.driver.prefix });
|
|
467
608
|
const existing = storedSession(store);
|
|
468
609
|
if (existing) {
|
|
469
610
|
const session = existing[kSessionPending] ? await existing[kSessionPending] : existing;
|
|
@@ -489,7 +630,7 @@ async function loadSession(binding) {
|
|
|
489
630
|
}
|
|
490
631
|
const session = emptySession(binding);
|
|
491
632
|
store.set(session);
|
|
492
|
-
const token =
|
|
633
|
+
const token = transport.read();
|
|
493
634
|
if (!token) return session;
|
|
494
635
|
session.token = token;
|
|
495
636
|
const pending = open(token).then((opened) => {
|
|
@@ -517,12 +658,15 @@ async function loadSession(binding) {
|
|
|
517
658
|
},
|
|
518
659
|
error: failure.error
|
|
519
660
|
});
|
|
520
|
-
} else
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
661
|
+
} else {
|
|
662
|
+
if (isJWTError(failure.error, "ERR_SESSION_TOKEN_INVALID") && isTokenFailure(failure.error)) dropSessionCookie(binding);
|
|
663
|
+
await config.hooks?.onError?.({
|
|
664
|
+
session: binding.actions.manager(binding),
|
|
665
|
+
...args,
|
|
666
|
+
error: failure.error,
|
|
667
|
+
config
|
|
668
|
+
});
|
|
669
|
+
}
|
|
526
670
|
return session;
|
|
527
671
|
}
|
|
528
672
|
await fireRead(binding, session);
|
|
@@ -538,17 +682,14 @@ async function fireRead(binding, session) {
|
|
|
538
682
|
});
|
|
539
683
|
}
|
|
540
684
|
async function rollSession(binding, session) {
|
|
541
|
-
const { config, codec,
|
|
685
|
+
const { config, codec, transport, store } = binding;
|
|
542
686
|
const { rolling } = config;
|
|
543
687
|
if (rolling === void 0 || store.get() !== session) return;
|
|
544
688
|
if (session.token === void 0 || session.expiresAt === void 0) return;
|
|
545
689
|
if (rolling !== true) {
|
|
546
|
-
if (
|
|
547
|
-
}
|
|
548
|
-
if (config.cookie !== false && !port.writable(carrier)) {
|
|
549
|
-
console.warn(`${port.prefix} Session not rolled: cookie cannot be written on a read-only event.`);
|
|
550
|
-
return;
|
|
690
|
+
if (codec.now(config) - session.createdAt < computeDurationInSeconds(rolling) * 1e3) return;
|
|
551
691
|
}
|
|
692
|
+
if (transport.readonly || !transport.writable()) return;
|
|
552
693
|
await binding.actions.roll(binding);
|
|
553
694
|
}
|
|
554
695
|
async function fireExpire(binding, expired) {
|
|
@@ -560,22 +701,25 @@ async function fireExpire(binding, expired) {
|
|
|
560
701
|
});
|
|
561
702
|
}
|
|
562
703
|
async function encodeSession(binding) {
|
|
563
|
-
const { config, codec, store } = binding;
|
|
704
|
+
const { config, codec, store, transport } = binding;
|
|
564
705
|
await codec.assertKeys(config);
|
|
565
706
|
const issue = codec.issuer(config);
|
|
566
|
-
const session = storedSession(store) || (
|
|
707
|
+
const session = storedSession(store) || (transport.read ? await loadSession(binding) : emptySession(binding));
|
|
567
708
|
return issue(session.id === void 0 ? {
|
|
568
709
|
...session,
|
|
569
710
|
id: mintSessionId(config)
|
|
570
711
|
} : session);
|
|
571
712
|
}
|
|
572
713
|
async function issueSession(binding, update, options, trigger = "update") {
|
|
573
|
-
const { config,
|
|
574
|
-
assertWritable(
|
|
714
|
+
const { config, driver, store, args, transport } = binding;
|
|
715
|
+
assertWritable(transport, {
|
|
716
|
+
operation: "update",
|
|
717
|
+
prefix: driver.prefix
|
|
718
|
+
});
|
|
575
719
|
const session = storedSession(store) || await loadSession(binding);
|
|
576
720
|
const oldSession = snapshotSession(session);
|
|
577
721
|
const resolved = typeof update === "function" ? update(session.data) : update;
|
|
578
|
-
if (resolved) Object.assign(session.data, sanitizeSessionUpdate(resolved, { prefix:
|
|
722
|
+
if (resolved) Object.assign(session.data, sanitizeSessionUpdate(resolved, { prefix: driver.prefix }));
|
|
579
723
|
for (const claim of RESERVED_CLAIMS) delete session.data[claim];
|
|
580
724
|
const maxAge = options?.maxAge ?? config.maxAge;
|
|
581
725
|
const createdAt = sessionInstant(binding);
|
|
@@ -588,7 +732,7 @@ async function issueSession(binding, update, options, trigger = "update") {
|
|
|
588
732
|
let issued;
|
|
589
733
|
try {
|
|
590
734
|
const token = await encodeSession(binding);
|
|
591
|
-
|
|
735
|
+
transport.write?.(token, sessionCookie(binding, { expiresAt: session.expiresAt }));
|
|
592
736
|
issued = Object.assign(session, {
|
|
593
737
|
id,
|
|
594
738
|
token
|
|
@@ -619,14 +763,17 @@ async function issueSession(binding, update, options, trigger = "update") {
|
|
|
619
763
|
return issued;
|
|
620
764
|
}
|
|
621
765
|
async function dropSession(binding) {
|
|
622
|
-
const { config,
|
|
623
|
-
assertTokenSource(
|
|
624
|
-
assertWritable(
|
|
766
|
+
const { config, driver, store, args, transport } = binding;
|
|
767
|
+
assertTokenSource(transport, { prefix: driver.prefix });
|
|
768
|
+
assertWritable(transport, {
|
|
769
|
+
operation: "clear",
|
|
770
|
+
prefix: driver.prefix
|
|
771
|
+
});
|
|
625
772
|
let session = storedSession(store);
|
|
626
773
|
if (session && session[kSessionPending]) session = await session[kSessionPending];
|
|
627
774
|
const oldSession = session && (session.id !== void 0 || session.token !== void 0) ? snapshotSession(session) : void 0;
|
|
628
775
|
store.set(emptySession(binding));
|
|
629
|
-
|
|
776
|
+
transport.drop?.(sessionCookie(binding));
|
|
630
777
|
await config.hooks?.onClear?.({
|
|
631
778
|
oldSession,
|
|
632
779
|
...args,
|
|
@@ -672,122 +819,49 @@ function sessionActions() {
|
|
|
672
819
|
}
|
|
673
820
|
};
|
|
674
821
|
}
|
|
675
|
-
function assertWritable(binding, operation) {
|
|
676
|
-
const { config, port, carrier } = binding;
|
|
677
|
-
if (config.cookie !== false && !port.writable(carrier)) throw readOnlyEventError(`${port.prefix} Cannot ${operation} session on read-only event.`);
|
|
678
|
-
}
|
|
679
822
|
function snapshotSession(session) {
|
|
680
823
|
return {
|
|
681
824
|
...session,
|
|
682
825
|
data: sanitizeObjectCopy(session.data)
|
|
683
826
|
};
|
|
684
827
|
}
|
|
685
|
-
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
function readChunked(store, name) {
|
|
694
|
-
const main = store.read(name);
|
|
695
|
-
if (main === void 0) return void 0;
|
|
696
|
-
if (!main.startsWith("__chunked__")) return main;
|
|
697
|
-
const count = parseChunkCount(main);
|
|
698
|
-
if (count === 0) return void 0;
|
|
699
|
-
const parts = [];
|
|
700
|
-
for (let index = 1; index <= count; index++) {
|
|
701
|
-
const part = store.read(chunkName(name, index));
|
|
702
|
-
if (part === void 0) return void 0;
|
|
703
|
-
parts.push(part);
|
|
704
|
-
}
|
|
705
|
-
return parts.join("");
|
|
706
|
-
}
|
|
707
|
-
function writeChunked(store, name, value, options) {
|
|
708
|
-
const { attrs, chunkMaxLength, prefix } = options;
|
|
709
|
-
const max = chunkMaxLength && chunkMaxLength > 0 ? chunkMaxLength : DEFAULT_CHUNK_MAX_LENGTH;
|
|
710
|
-
const chunkCount = Math.ceil(value.length / max);
|
|
711
|
-
if (chunkCount > 16) throw invalidConfigError(`${prefix} Session token requires ${chunkCount} cookie chunks, exceeding the limit of 16.`);
|
|
712
|
-
const previousCount = parseChunkCount(store.read(name));
|
|
713
|
-
const firstSurplus = chunkCount <= 1 ? 1 : chunkCount + 1;
|
|
714
|
-
for (let index = firstSurplus; index <= previousCount; index++) expire(store, chunkName(name, index), attrs);
|
|
715
|
-
if (chunkCount <= 1) {
|
|
716
|
-
store.write(name, value, attrs);
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
store.write(name, `${CHUNK_MARKER}${chunkCount}`, attrs);
|
|
720
|
-
for (let index = 1; index <= chunkCount; index++) store.write(chunkName(name, index), value.slice((index - 1) * max, index * max), attrs);
|
|
721
|
-
}
|
|
722
|
-
function removeChunked(store, name, options) {
|
|
723
|
-
const count = parseChunkCount(store.read(name));
|
|
724
|
-
expire(store, name, options.attrs);
|
|
725
|
-
for (let index = 1; index <= count; index++) expire(store, chunkName(name, index), options.attrs);
|
|
726
|
-
}
|
|
727
|
-
function readChunkedBy(read, name) {
|
|
728
|
-
return readChunked({
|
|
729
|
-
read,
|
|
730
|
-
write: noop
|
|
731
|
-
}, name);
|
|
732
|
-
}
|
|
733
|
-
function readChunkedFrom(cookies, name) {
|
|
734
|
-
return readChunkedBy((key) => cookies[key], name);
|
|
735
|
-
}
|
|
736
|
-
function readChunkedFromSetCookie(setCookies, name) {
|
|
737
|
-
const cookies = setCookieValues(setCookies);
|
|
738
|
-
if (cookies[name] === void 0) return void 0;
|
|
739
|
-
return readChunkedFrom(cookies, name);
|
|
740
|
-
}
|
|
741
|
-
function setCookieValues(setCookies) {
|
|
742
|
-
const cookies = Object.create(null);
|
|
743
|
-
for (const setCookie of setCookies) {
|
|
744
|
-
const pair = setCookiePair(setCookie);
|
|
745
|
-
if (pair) cookies[pair.name] = decodeValue(pair.value);
|
|
746
|
-
}
|
|
747
|
-
return cookies;
|
|
748
|
-
}
|
|
749
|
-
function setCookieName(setCookie) {
|
|
750
|
-
return setCookiePair(setCookie)?.name;
|
|
751
|
-
}
|
|
752
|
-
function chunkName(name, index) {
|
|
753
|
-
return `${name}.${index}`;
|
|
754
|
-
}
|
|
755
|
-
function isChunkName(cookieName, name) {
|
|
756
|
-
return cookieName.startsWith(`${name}.`) && /^\d+$/.test(cookieName.slice(name.length + 1));
|
|
757
|
-
}
|
|
758
|
-
function parseChunkCount(main) {
|
|
759
|
-
if (main === void 0 || !main.startsWith("__chunked__")) return 0;
|
|
760
|
-
const suffix = main.slice(11);
|
|
761
|
-
if (!/^\d+$/.test(suffix)) return 0;
|
|
762
|
-
const count = Number.parseInt(suffix, 10);
|
|
763
|
-
if (count < 1 || count > 16) return 0;
|
|
764
|
-
return count;
|
|
765
|
-
}
|
|
766
|
-
function expire(store, name, attrs) {
|
|
767
|
-
store.write(name, "", {
|
|
768
|
-
...attrs,
|
|
769
|
-
maxAge: 0,
|
|
770
|
-
expires: /* @__PURE__ */ new Date(0)
|
|
828
|
+
function defineSession(config, options) {
|
|
829
|
+
const { driver, codec, hookArgs } = options;
|
|
830
|
+
const actions = sessionActions();
|
|
831
|
+
const bind = (event) => bindSession(event, config, {
|
|
832
|
+
driver,
|
|
833
|
+
codec,
|
|
834
|
+
actions,
|
|
835
|
+
hookArgs
|
|
771
836
|
});
|
|
772
|
-
}
|
|
773
|
-
function setCookiePair(setCookie) {
|
|
774
|
-
const [pair = ""] = setCookie.split(";", 1);
|
|
775
|
-
const separator = pair.indexOf("=");
|
|
776
|
-
if (separator === -1) return void 0;
|
|
777
837
|
return {
|
|
778
|
-
|
|
779
|
-
|
|
838
|
+
use: async (event) => {
|
|
839
|
+
const binding = bind(event);
|
|
840
|
+
await loadSession(binding);
|
|
841
|
+
return sessionManager(binding);
|
|
842
|
+
},
|
|
843
|
+
token: (event) => sessionToken(bind(event)),
|
|
844
|
+
issue: async (data, issueOptions) => {
|
|
845
|
+
await codec.assertKeys(config);
|
|
846
|
+
const issue = codec.issuer(config);
|
|
847
|
+
const createdAt = sessionInstant({
|
|
848
|
+
codec,
|
|
849
|
+
config
|
|
850
|
+
});
|
|
851
|
+
return issue({
|
|
852
|
+
id: mintSessionId(config),
|
|
853
|
+
createdAt,
|
|
854
|
+
expiresAt: resolveExpiresAt(createdAt, { maxAge: issueOptions?.maxAge ?? config.maxAge }),
|
|
855
|
+
data: sanitizeSessionUpdate(data ?? {}, { prefix: driver.prefix }),
|
|
856
|
+
token: void 0
|
|
857
|
+
});
|
|
858
|
+
},
|
|
859
|
+
open: (token) => openSessionToken(codec, config, {
|
|
860
|
+
token,
|
|
861
|
+
args: {}
|
|
862
|
+
})
|
|
780
863
|
};
|
|
781
864
|
}
|
|
782
|
-
function decodeValue(value) {
|
|
783
|
-
if (!value.includes("%")) return value;
|
|
784
|
-
try {
|
|
785
|
-
return decodeURIComponent(value);
|
|
786
|
-
} catch {
|
|
787
|
-
return value;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
function noop() {}
|
|
791
865
|
const JWS_DEFAULT_COOKIE = {
|
|
792
866
|
path: "/",
|
|
793
867
|
secure: true,
|
|
@@ -805,12 +879,12 @@ function createJWSCodec(options) {
|
|
|
805
879
|
openOptions: (config) => config.jws?.verifyOptions,
|
|
806
880
|
issueKey: (config) => getSignKey(config, prefix),
|
|
807
881
|
openKey: (config) => getVerifyKey(config, prefix),
|
|
808
|
-
lookup: (config,
|
|
882
|
+
lookup: (config, args) => {
|
|
809
883
|
const hooks = config.hooks;
|
|
810
884
|
if (!hooks?.onVerifyKeyLookup) return void 0;
|
|
811
885
|
return (header) => hooks.onVerifyKeyLookup?.({
|
|
812
886
|
header,
|
|
813
|
-
...
|
|
887
|
+
...args,
|
|
814
888
|
config
|
|
815
889
|
});
|
|
816
890
|
},
|
|
@@ -875,4 +949,4 @@ function assertPinnedAlg(key, config, prefix) {
|
|
|
875
949
|
hint: "\"jws.signOptions.alg\""
|
|
876
950
|
});
|
|
877
951
|
}
|
|
878
|
-
export { bindSession, createJWECodec, createJWSCodec, dropSession, encodeSession, invalidConfigError,
|
|
952
|
+
export { bindSession, createJWECodec, createJWSCodec, defineSession, dropSession, encodeSession, invalidConfigError, issueSession, loadSession, openSessionToken, sessionActions, setCookieName, setCookieValues };
|