@blamejs/core 0.18.40 → 0.18.42
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/CHANGELOG.md +28 -0
- package/NOTICE +1 -1
- package/README.md +2 -2
- package/lib/acme.js +5 -7
- package/lib/audit.js +4 -4
- package/lib/auth/dpop.js +6 -6
- package/lib/cert.js +12 -12
- package/lib/compliance-sanctions.js +4 -4
- package/lib/cookies.js +98 -14
- package/lib/http-client.js +1 -3
- package/lib/mail-auth.js +4 -5
- package/lib/mail.js +1 -2
- package/lib/middleware/bot-guard.js +33 -10
- package/lib/middleware/csrf-protect.js +45 -51
- package/lib/migrations.js +22 -22
- package/lib/seeders.js +15 -15
- package/lib/session.js +99 -8
- package/lib/vendor/MANIFEST.json +12 -12
- package/lib/vendor/blamejs-pki.cjs +104 -73
- package/package.json +2 -2
- package/sbom.cdx.json +6 -6
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
* }
|
|
70
70
|
*/
|
|
71
71
|
var C = require("../constants");
|
|
72
|
+
var cookies = require("../cookies");
|
|
72
73
|
var lazyRequire = require("../lazy-require");
|
|
73
74
|
var pick = require("../pick");
|
|
74
75
|
var forms = require("../forms");
|
|
@@ -130,33 +131,6 @@ function _parseCookieHeader(header) {
|
|
|
130
131
|
return Object.assign(Object.create(null), Object.fromEntries(pairs));
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
function _formatSetCookie(name, value, opts) {
|
|
134
|
-
var parts = [name + "=" + value];
|
|
135
|
-
parts.push("Path=" + (opts.path || "/"));
|
|
136
|
-
parts.push("SameSite=" + (opts.sameSite || "Lax"));
|
|
137
|
-
if (opts.httpOnly) parts.push("HttpOnly");
|
|
138
|
-
if (opts.secure) parts.push("Secure");
|
|
139
|
-
if (opts.maxAge != null) parts.push("Max-Age=" + opts.maxAge);
|
|
140
|
-
return parts.join("; ");
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function _appendSetCookie(res, value) {
|
|
144
|
-
// Don't clobber other Set-Cookie headers the route may have already
|
|
145
|
-
// queued (login session cookie, etc.). Use res.appendHeader when
|
|
146
|
-
// available; else array-merge manually.
|
|
147
|
-
if (typeof res.appendHeader === "function") {
|
|
148
|
-
res.appendHeader("Set-Cookie", value);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
var existing = typeof res.getHeader === "function" ? res.getHeader("Set-Cookie") : undefined;
|
|
152
|
-
if (existing == null) {
|
|
153
|
-
res.setHeader("Set-Cookie", value);
|
|
154
|
-
} else if (Array.isArray(existing)) {
|
|
155
|
-
res.setHeader("Set-Cookie", existing.concat(value));
|
|
156
|
-
} else {
|
|
157
|
-
res.setHeader("Set-Cookie", [existing, value]);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
134
|
|
|
161
135
|
// csrf-protect does NOT buffer or parse the request body itself.
|
|
162
136
|
// Operators who use form-urlencoded POSTs MUST register
|
|
@@ -431,30 +405,44 @@ function create(opts) {
|
|
|
431
405
|
if (["Lax", "Strict", "None"].indexOf(cookieCfg.sameSite) === -1) {
|
|
432
406
|
throw new Error("middleware.csrfProtect: opts.cookie.sameSite must be Lax|Strict|None");
|
|
433
407
|
}
|
|
434
|
-
// Cookie name-prefix safety (RFC 6265bis §4.1.3).
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
// `
|
|
441
|
-
//
|
|
442
|
-
//
|
|
408
|
+
// Cookie name-prefix safety (RFC 6265bis §4.1.3). b.cookies.serialize
|
|
409
|
+
// enforces the same invariants when the header is built, but that is a
|
|
410
|
+
// per-request throw inside the middleware; catching the bad configuration
|
|
411
|
+
// at boot turns it into a startup error the operator can read. §5.4
|
|
412
|
+
// requires user agents to apply the prefix test case-INSENSITIVELY (the
|
|
413
|
+
// server-side §4.1.3 description reads "case-sensitive", but the UA is what
|
|
414
|
+
// drops the cookie), so `__host-`/`__SECURE-` get the same browser
|
|
415
|
+
// enforcement as `__Host-`/`__Secure-` -- case-sensitive matching was itself
|
|
416
|
+
// CVE-2024-5699. Compare a lowercased copy so a case-variant name can't
|
|
417
|
+
// dodge the invariant here and then be silently rejected by the browser.
|
|
443
418
|
// __Host-* — Path must be "/", no Domain (we never set one), Secure.
|
|
444
419
|
// __Secure-* — Secure.
|
|
445
420
|
if (cookieCfg.name) {
|
|
446
421
|
var lowerCookieName = cookieCfg.name.toLowerCase();
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
422
|
+
var isHostPrefix = lowerCookieName.indexOf("__host-") === 0;
|
|
423
|
+
var isSecurePrefix = lowerCookieName.indexOf("__secure-") === 0;
|
|
424
|
+
if (isHostPrefix && cookieCfg.path !== "/") {
|
|
425
|
+
throw new Error("middleware.csrfProtect: __Host-* cookie name requires path='/'");
|
|
426
|
+
}
|
|
427
|
+
if (isHostPrefix && cookieCfg.secure === false) {
|
|
428
|
+
throw new Error("middleware.csrfProtect: __Host-* cookie name requires secure (cannot be explicit false)");
|
|
429
|
+
}
|
|
430
|
+
if (isSecurePrefix && cookieCfg.secure === false) {
|
|
431
|
+
throw new Error("middleware.csrfProtect: __Secure-* cookie name requires secure (cannot be explicit false)");
|
|
432
|
+
}
|
|
433
|
+
// A prefixed name is a promise to the browser that the cookie is always
|
|
434
|
+
// Secure. Leaving `secure` to per-request auto-detection breaks that
|
|
435
|
+
// promise on any cleartext request: the cookie goes out prefixed and
|
|
436
|
+
// WITHOUT Secure, the browser drops it, and the double-submit token
|
|
437
|
+
// silently never persists. The name and the auto-detect cannot both
|
|
438
|
+
// stand — the operator picking the prefix is the one asserting HTTPS,
|
|
439
|
+
// so make them say it.
|
|
440
|
+
if ((isHostPrefix || isSecurePrefix) && cookieCfg.secure == null) {
|
|
441
|
+
throw new Error("middleware.csrfProtect: " +
|
|
442
|
+
(isHostPrefix ? "__Host-*" : "__Secure-*") +
|
|
443
|
+
" cookie name requires an explicit cookie.secure: true — " +
|
|
444
|
+
"auto-detected secure emits the prefix without Secure on a plain-HTTP " +
|
|
445
|
+
"request, which browsers reject");
|
|
458
446
|
}
|
|
459
447
|
}
|
|
460
448
|
}
|
|
@@ -485,8 +473,8 @@ function create(opts) {
|
|
|
485
473
|
function _issueIfNeeded(req, res) {
|
|
486
474
|
if (!cookieCfg) return null;
|
|
487
475
|
var cookieName = _resolveCookieName(req);
|
|
488
|
-
var
|
|
489
|
-
var existing =
|
|
476
|
+
var requestCookies = _parseCookieHeader(req.headers && req.headers.cookie);
|
|
477
|
+
var existing = requestCookies[cookieName];
|
|
490
478
|
// Strict 64-hex-char check matches the byte-length of every token
|
|
491
479
|
// forms.generateCsrfToken() produces (CSRF_TOKEN_BYTES = 32 bytes
|
|
492
480
|
// → 64 hex chars). The previous {2,} floor accepted any 2-char
|
|
@@ -522,14 +510,20 @@ function create(opts) {
|
|
|
522
510
|
} catch (_e) { /* drop-silent */ }
|
|
523
511
|
}
|
|
524
512
|
var fresh = forms.generateCsrfToken();
|
|
525
|
-
|
|
513
|
+
// b.cookies owns Set-Cookie: it validates the name as an RFC 6265 token,
|
|
514
|
+
// refuses CRLF / NUL in the value, scrubs the Path and Domain attributes
|
|
515
|
+
// before they reach a response header, and enforces the RFC 6265bis
|
|
516
|
+
// prefix invariants. The token is 64 hex characters, so the percent-
|
|
517
|
+
// encoding serialize applies to the value is a no-op on it and the cookie
|
|
518
|
+
// the browser echoes back still matches the double-submit compare.
|
|
519
|
+
var setCookie = cookies.serialize(cookieName, fresh, {
|
|
526
520
|
path: cookieCfg.path,
|
|
527
521
|
sameSite: cookieCfg.sameSite,
|
|
528
522
|
secure: cookieCfg.secure == null ? _isHttps(req) : !!cookieCfg.secure,
|
|
529
523
|
httpOnly: cookieCfg.httpOnly,
|
|
530
524
|
maxAge: cookieCfg.maxAge,
|
|
531
525
|
});
|
|
532
|
-
|
|
526
|
+
cookies.appendSetCookie(res, setCookie);
|
|
533
527
|
req._csrfIssuedCookies[cookieName] = fresh;
|
|
534
528
|
req.csrfToken = fresh;
|
|
535
529
|
return fresh;
|
package/lib/migrations.js
CHANGED
|
@@ -297,12 +297,12 @@ function create(opts) {
|
|
|
297
297
|
var dir = opts.dir;
|
|
298
298
|
|
|
299
299
|
function _appliedRows() {
|
|
300
|
-
var
|
|
301
|
-
_ensureTable(
|
|
302
|
-
var q = sql.select(_migrationsTable(), _sqlOpts(
|
|
300
|
+
var conn = _resolveDb(opts);
|
|
301
|
+
_ensureTable(conn);
|
|
302
|
+
var q = sql.select(_migrationsTable(), _sqlOpts(conn))
|
|
303
303
|
.columns(["name", "description", "appliedAt"])
|
|
304
304
|
.orderBy("appliedAt", "asc").orderBy("name", "asc").toSql();
|
|
305
|
-
var stmt =
|
|
305
|
+
var stmt = conn.prepare(q.sql);
|
|
306
306
|
return stmt.all.apply(stmt, q.params);
|
|
307
307
|
}
|
|
308
308
|
|
|
@@ -319,11 +319,11 @@ function create(opts) {
|
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
function up() {
|
|
322
|
-
var
|
|
323
|
-
_ensureTable(
|
|
324
|
-
return _withLock(
|
|
325
|
-
var namesQ = sql.select(_migrationsTable(), _sqlOpts(
|
|
326
|
-
var namesStmt =
|
|
322
|
+
var conn = _resolveDb(opts);
|
|
323
|
+
_ensureTable(conn);
|
|
324
|
+
return _withLock(conn, opts, function () {
|
|
325
|
+
var namesQ = sql.select(_migrationsTable(), _sqlOpts(conn)).columns(["name"]).toSql();
|
|
326
|
+
var namesStmt = conn.prepare(namesQ.sql);
|
|
327
327
|
var appliedSet = new Set(
|
|
328
328
|
namesStmt.all.apply(namesStmt, namesQ.params)
|
|
329
329
|
.map(function (r) { return r.name; })
|
|
@@ -336,12 +336,12 @@ function create(opts) {
|
|
|
336
336
|
if (appliedSet.has(file)) { skipped.push(file); continue; }
|
|
337
337
|
var mod = _loadMigration(file, dir);
|
|
338
338
|
try {
|
|
339
|
-
_txn(
|
|
340
|
-
mod.up(
|
|
341
|
-
var insQ = sql.insert(_migrationsTable(), _sqlOpts(
|
|
339
|
+
_txn(conn, function () {
|
|
340
|
+
mod.up(conn);
|
|
341
|
+
var insQ = sql.insert(_migrationsTable(), _sqlOpts(conn))
|
|
342
342
|
.values({ name: file, description: mod.description || "",
|
|
343
343
|
appliedAt: new Date().toISOString() }).toSql();
|
|
344
|
-
var insStmt =
|
|
344
|
+
var insStmt = conn.prepare(insQ.sql);
|
|
345
345
|
insStmt.run.apply(insStmt, insQ.params);
|
|
346
346
|
});
|
|
347
347
|
} catch (e) {
|
|
@@ -363,16 +363,16 @@ function create(opts) {
|
|
|
363
363
|
"down: steps must be a positive integer (got " + opts2.steps + ")",
|
|
364
364
|
true);
|
|
365
365
|
}
|
|
366
|
-
var
|
|
367
|
-
_ensureTable(
|
|
368
|
-
return _withLock(
|
|
366
|
+
var conn = _resolveDb(opts);
|
|
367
|
+
_ensureTable(conn);
|
|
368
|
+
return _withLock(conn, opts, function () {
|
|
369
369
|
// Most-recent applied first (reverse chronological by appliedAt
|
|
370
370
|
// then by name as a stable tiebreaker for fixtures with identical
|
|
371
371
|
// timestamps). steps is a validated positive integer, so b.sql
|
|
372
372
|
// inlines the LIMIT.
|
|
373
|
-
var downQ = sql.select(_migrationsTable(), _sqlOpts(
|
|
373
|
+
var downQ = sql.select(_migrationsTable(), _sqlOpts(conn)).columns(["name"])
|
|
374
374
|
.orderBy("appliedAt", "desc").orderBy("name", "desc").limit(steps).toSql();
|
|
375
|
-
var downStmt =
|
|
375
|
+
var downStmt = conn.prepare(downQ.sql);
|
|
376
376
|
var rows = downStmt.all.apply(downStmt, downQ.params);
|
|
377
377
|
|
|
378
378
|
var reverted = [];
|
|
@@ -386,10 +386,10 @@ function create(opts) {
|
|
|
386
386
|
true);
|
|
387
387
|
}
|
|
388
388
|
try {
|
|
389
|
-
_txn(
|
|
390
|
-
mod.down(
|
|
391
|
-
var delQ = sql.delete(_migrationsTable(), _sqlOpts(
|
|
392
|
-
var delStmt =
|
|
389
|
+
_txn(conn, function () {
|
|
390
|
+
mod.down(conn);
|
|
391
|
+
var delQ = sql.delete(_migrationsTable(), _sqlOpts(conn)).where("name", file).toSql();
|
|
392
|
+
var delStmt = conn.prepare(delQ.sql);
|
|
393
393
|
delStmt.run.apply(delStmt, delQ.params);
|
|
394
394
|
});
|
|
395
395
|
} catch (e) {
|
package/lib/seeders.js
CHANGED
|
@@ -452,11 +452,11 @@ function create(opts) {
|
|
|
452
452
|
function status(callerOpts) {
|
|
453
453
|
callerOpts = callerOpts || {};
|
|
454
454
|
_validateEnv("seeders.status: env", callerOpts.env);
|
|
455
|
-
var
|
|
456
|
-
_ensureTables(
|
|
455
|
+
var conn = _resolveDb(opts);
|
|
456
|
+
_ensureTables(conn);
|
|
457
457
|
var env = callerOpts.env;
|
|
458
458
|
var loaded = _loadAllForEnv(dir, env);
|
|
459
|
-
var applied = _appliedRows(
|
|
459
|
+
var applied = _appliedRows(conn, env);
|
|
460
460
|
var appliedNames = new Set(applied.map(function (r) { return r.name; }));
|
|
461
461
|
var pending = loaded.ordered.filter(function (n) {
|
|
462
462
|
var mod = loaded.modByName[n];
|
|
@@ -491,8 +491,8 @@ function create(opts) {
|
|
|
491
491
|
}
|
|
492
492
|
}
|
|
493
493
|
|
|
494
|
-
var
|
|
495
|
-
_ensureTables(
|
|
494
|
+
var conn = _resolveDb(opts);
|
|
495
|
+
_ensureTables(conn);
|
|
496
496
|
|
|
497
497
|
var loaded = _loadAllForEnv(dir, env);
|
|
498
498
|
|
|
@@ -504,11 +504,11 @@ function create(opts) {
|
|
|
504
504
|
var startedAt = clock();
|
|
505
505
|
observability().safeEvent("seeders.run.start", 1, { env: env, count: loaded.ordered.length });
|
|
506
506
|
|
|
507
|
-
var holder = _acquireLock(
|
|
507
|
+
var holder = _acquireLock(conn, lockStaleAfterMs, clock);
|
|
508
508
|
try {
|
|
509
|
-
var appliedSelBuilt = sql.select(_seedersTable(), _sqlOpts(
|
|
509
|
+
var appliedSelBuilt = sql.select(_seedersTable(), _sqlOpts(conn))
|
|
510
510
|
.columns(["name"]).where("env", env).toSql();
|
|
511
|
-
var appliedSelStmt =
|
|
511
|
+
var appliedSelStmt = conn.prepare(appliedSelBuilt.sql);
|
|
512
512
|
var appliedSet = new Set(
|
|
513
513
|
appliedSelStmt.all.apply(appliedSelStmt, appliedSelBuilt.params)
|
|
514
514
|
.map(function (r) { return r.name; })
|
|
@@ -539,26 +539,26 @@ function create(opts) {
|
|
|
539
539
|
// Per-seed transaction: SQLite txns are sync, but the seed's
|
|
540
540
|
// run() may be async — runInTransactionAsync wraps BEGIN/COMMIT
|
|
541
541
|
// around the awaited body and rolls back this seed only on failure.
|
|
542
|
-
await dbSchema.runInTransactionAsync(
|
|
543
|
-
await mod.run(
|
|
542
|
+
await dbSchema.runInTransactionAsync(conn, async function () {
|
|
543
|
+
await mod.run(conn, ctx);
|
|
544
544
|
var nowIso = new Date(clock()).toISOString();
|
|
545
545
|
var writeBuilt;
|
|
546
546
|
if (alreadyApplied && mod.rerunnable) {
|
|
547
|
-
writeBuilt = sql.update(_seedersTable(), _sqlOpts(
|
|
547
|
+
writeBuilt = sql.update(_seedersTable(), _sqlOpts(conn))
|
|
548
548
|
.set({ appliedAt: nowIso, description: mod.description || "",
|
|
549
549
|
rerunnable: mod.rerunnable ? 1 : 0 })
|
|
550
550
|
.where("env", env).where("name", name).toSql();
|
|
551
551
|
} else if (alreadyApplied && force) {
|
|
552
|
-
writeBuilt = sql.update(_seedersTable(), _sqlOpts(
|
|
552
|
+
writeBuilt = sql.update(_seedersTable(), _sqlOpts(conn))
|
|
553
553
|
.set({ appliedAt: nowIso, description: mod.description || "" })
|
|
554
554
|
.where("env", env).where("name", name).toSql();
|
|
555
555
|
} else {
|
|
556
|
-
writeBuilt = sql.insert(_seedersTable(), _sqlOpts(
|
|
556
|
+
writeBuilt = sql.insert(_seedersTable(), _sqlOpts(conn))
|
|
557
557
|
.values({ env: env, name: name, description: mod.description || "",
|
|
558
558
|
appliedAt: nowIso, rerunnable: mod.rerunnable ? 1 : 0 })
|
|
559
559
|
.toSql();
|
|
560
560
|
}
|
|
561
|
-
var writeStmt =
|
|
561
|
+
var writeStmt = conn.prepare(writeBuilt.sql);
|
|
562
562
|
writeStmt.run.apply(writeStmt, writeBuilt.params);
|
|
563
563
|
}, {
|
|
564
564
|
onRollbackFail: function (rollbackErr) {
|
|
@@ -629,7 +629,7 @@ function create(opts) {
|
|
|
629
629
|
}
|
|
630
630
|
return result;
|
|
631
631
|
} finally {
|
|
632
|
-
_releaseLock(
|
|
632
|
+
_releaseLock(conn, holder);
|
|
633
633
|
}
|
|
634
634
|
}
|
|
635
635
|
|
package/lib/session.js
CHANGED
|
@@ -54,6 +54,7 @@ var validateOpts = require("./validate-opts");
|
|
|
54
54
|
var cluster = require("./cluster");
|
|
55
55
|
var clusterStorage = require("./cluster-storage");
|
|
56
56
|
var C = require("./constants");
|
|
57
|
+
var cookies = require("./cookies");
|
|
57
58
|
var { generateToken, sha3Hash } = require("./crypto");
|
|
58
59
|
var cryptoField = require("./crypto-field");
|
|
59
60
|
var frameworkSchema = require("./framework-schema");
|
|
@@ -410,7 +411,9 @@ function _hashFingerprint(sid, inputs) {
|
|
|
410
411
|
* data: { roles: ["admin"] },
|
|
411
412
|
* ttlMs: b.constants.TIME.hours(8),
|
|
412
413
|
* });
|
|
413
|
-
*
|
|
414
|
+
* b.cookies.appendSetCookie(res, b.cookies.serialize("sid", s.token, {
|
|
415
|
+
* httpOnly: true, secure: true, sameSite: "Strict", path: "/",
|
|
416
|
+
* }));
|
|
414
417
|
* // → { token: "9f2c…", expiresAt: 1735689600000 }
|
|
415
418
|
*/
|
|
416
419
|
// Anonymous-session prefix. b.session.create({ anonymous: true })
|
|
@@ -770,7 +773,9 @@ async function verify(token, verifyOpts) {
|
|
|
770
773
|
*
|
|
771
774
|
* @example
|
|
772
775
|
* await b.session.destroy(req.cookies.sid);
|
|
773
|
-
* res.
|
|
776
|
+
* b.cookies.appendSetCookie(res, b.cookies.serialize("sid", "", {
|
|
777
|
+
* httpOnly: true, sameSite: "Strict", path: "/", maxAge: 0,
|
|
778
|
+
* }));
|
|
774
779
|
* res.end("logged out");
|
|
775
780
|
* // → true
|
|
776
781
|
*/
|
|
@@ -798,13 +803,29 @@ async function destroy(token) {
|
|
|
798
803
|
* this composes the secure-default logout the middleware otherwise had to be
|
|
799
804
|
* mounted by hand. Returns whether a session was destroyed. Leader-only.
|
|
800
805
|
*
|
|
806
|
+
* A browser deletes a cookie by MATCHING the expiry cookie's name, path and
|
|
807
|
+
* domain against the one in its jar, and it refuses a `Secure` cookie
|
|
808
|
+
* altogether when the response came over plain HTTP. So the expiry cookie has
|
|
809
|
+
* to describe the same scope the session cookie was written with, or the
|
|
810
|
+
* logout leaves it in place. Pass the `req` and the scheme is resolved through
|
|
811
|
+
* `b.requestHelpers.trustedProtocol` (a forwarded scheme counts only from a
|
|
812
|
+
* peer you declared trusted); pass `secure` to state it outright. With neither,
|
|
813
|
+
* the cookie is `Secure` — the secure default, unchanged.
|
|
814
|
+
*
|
|
801
815
|
* @opts
|
|
802
|
-
* cookieName:
|
|
803
|
-
* types:
|
|
816
|
+
* cookieName: string, // default: "sid" — the session cookie to expire
|
|
817
|
+
* types: string[], // default: the W3C Clear-Site-Data directive set
|
|
818
|
+
* req: object, // resolve Secure from this request's scheme
|
|
819
|
+
* secure: boolean, // default: true — state the scheme outright
|
|
820
|
+
* sameSite: string, // default: "Strict" — Strict / Lax / None
|
|
821
|
+
* path: string, // default: "/" — must match the cookie's Path
|
|
822
|
+
* domain: string, // must match the cookie's Domain, if it had one
|
|
823
|
+
* trustedProxies: string | string[], // CIDRs, for the `req` scheme resolve
|
|
824
|
+
* protocolResolver: function(req), // own the scheme decision instead
|
|
804
825
|
*
|
|
805
826
|
* @example
|
|
806
827
|
* app.post("/logout", async function (req, res) {
|
|
807
|
-
* await b.session.logout(res, req.cookies.sid);
|
|
828
|
+
* await b.session.logout(res, req.cookies.sid, { req: req });
|
|
808
829
|
* res.end("logged out");
|
|
809
830
|
* });
|
|
810
831
|
* // → emits Clear-Site-Data + expires the sid cookie + destroys the session
|
|
@@ -814,7 +835,19 @@ async function logout(res, token, opts) {
|
|
|
814
835
|
throw new SessionError("session/bad-res",
|
|
815
836
|
"b.session.logout: res must be an HTTP response with setHeader()");
|
|
816
837
|
}
|
|
838
|
+
// The expiry cookie is queued with b.cookies.appendSetCookie, which needs to
|
|
839
|
+
// READ the response as well as write it. Assert that contract here, with the
|
|
840
|
+
// other validation, rather than discovering it at queue time: the queue
|
|
841
|
+
// happens after destroy(), so a throw there would leave the session revoked,
|
|
842
|
+
// Clear-Site-Data queued, no expiry cookie and a failed request. The
|
|
843
|
+
// response's shape is the caller's and fixed for the process, so it is
|
|
844
|
+
// knowable before any of that.
|
|
845
|
+
cookies.assertAppendable(res);
|
|
817
846
|
opts = opts || {};
|
|
847
|
+
validateOpts(opts, [
|
|
848
|
+
"cookieName", "types", "req", "secure", "sameSite", "path", "domain",
|
|
849
|
+
"trustedProxies", "protocolResolver",
|
|
850
|
+
], "b.session.logout");
|
|
818
851
|
var cookieName = opts.cookieName === undefined ? "sid" : opts.cookieName;
|
|
819
852
|
if (typeof cookieName !== "string" || cookieName.length === 0) {
|
|
820
853
|
throw new SessionError("session/bad-cookie-name",
|
|
@@ -826,6 +859,19 @@ async function logout(res, token, opts) {
|
|
|
826
859
|
// unknown directive throws here, queuing nothing.
|
|
827
860
|
var clearSiteDataValue = csd.headerValue(types, "b.session.logout");
|
|
828
861
|
|
|
862
|
+
// Same ordering for the cookie: b.cookies.serialize validates the name, the
|
|
863
|
+
// attributes and the RFC 6265bis prefix invariants, so it is a throwing call
|
|
864
|
+
// and must run before the row is revoked — otherwise a `__Host-` typo leaves
|
|
865
|
+
// the session destroyed and the browser still holding its cookie.
|
|
866
|
+
var expiryCookie = cookies.serialize(cookieName, "", {
|
|
867
|
+
httpOnly: true,
|
|
868
|
+
secure: _logoutCookieSecure(opts, cookieName),
|
|
869
|
+
sameSite: opts.sameSite === undefined ? "Strict" : opts.sameSite,
|
|
870
|
+
path: opts.path === undefined ? "/" : opts.path,
|
|
871
|
+
domain: opts.domain,
|
|
872
|
+
maxAge: 0,
|
|
873
|
+
});
|
|
874
|
+
|
|
829
875
|
// Revoke the server-side session FIRST. If destroy() throws (a follower
|
|
830
876
|
// failing cluster.requireLeader(), or a store/DB error), no client-wipe
|
|
831
877
|
// headers have been queued — an error response can't then expire the
|
|
@@ -836,12 +882,55 @@ async function logout(res, token, opts) {
|
|
|
836
882
|
// Now wipe the client-side state: W3C Clear-Site-Data (cookies /
|
|
837
883
|
// storage / cache) + expire the session cookie (belt-and-suspenders with the
|
|
838
884
|
// "cookies" directive, and effective even if the client ignores the header).
|
|
885
|
+
// Append rather than set: a route that already queued a cookie of its own
|
|
886
|
+
// (a rotated CSRF token, a locale) keeps it.
|
|
839
887
|
res.setHeader("Clear-Site-Data", clearSiteDataValue);
|
|
840
|
-
|
|
841
|
-
cookieName + "=; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=0");
|
|
888
|
+
cookies.appendSetCookie(res, expiryCookie);
|
|
842
889
|
return destroyed;
|
|
843
890
|
}
|
|
844
891
|
|
|
892
|
+
// Whether logout's expiry cookie carries Secure. Explicit `secure` wins; a
|
|
893
|
+
// `req` resolves through the peer-gated protocol helper; with neither the
|
|
894
|
+
// answer is the secure default. A browser drops a Secure cookie arriving over
|
|
895
|
+
// plain HTTP, so answering "true" for an HTTP deployment does not fail safe —
|
|
896
|
+
// it fails to clear the cookie at all.
|
|
897
|
+
function _logoutCookieSecure(opts, cookieName) {
|
|
898
|
+
if (opts.secure !== undefined) {
|
|
899
|
+
if (typeof opts.secure !== "boolean") {
|
|
900
|
+
throw new SessionError("session/bad-secure",
|
|
901
|
+
"b.session.logout: opts.secure must be a boolean");
|
|
902
|
+
}
|
|
903
|
+
// An explicit `false` against a `__Host-`/`__Secure-` name is a
|
|
904
|
+
// contradiction, and serialize() refuses it. That is deliberate: the
|
|
905
|
+
// operator stated both halves, it fails on the first logout in every
|
|
906
|
+
// environment, and no request can provoke or avoid it.
|
|
907
|
+
return opts.secure;
|
|
908
|
+
}
|
|
909
|
+
// A `__Host-` / `__Secure-` name is a statement about the COOKIE, not about
|
|
910
|
+
// this request: a browser will only ever have stored such a cookie on a
|
|
911
|
+
// secure origin, so on a cleartext request there is nothing of that name to
|
|
912
|
+
// clear. Letting the request's scheme resolve `secure` to false here would
|
|
913
|
+
// make serialize() refuse the name — and the cookie is built BEFORE the row
|
|
914
|
+
// is revoked, so that refusal would abort the logout entirely and whoever
|
|
915
|
+
// chose the scheme would decide whether the session died. The prefix wins.
|
|
916
|
+
var lowerName = cookieName.toLowerCase();
|
|
917
|
+
if (lowerName.indexOf("__host-") === 0 || lowerName.indexOf("__secure-") === 0) {
|
|
918
|
+
return true;
|
|
919
|
+
}
|
|
920
|
+
if (opts.req === undefined) return true;
|
|
921
|
+
if (opts.req === null || typeof opts.req !== "object") {
|
|
922
|
+
// trustedProtocol answers "http" for a non-request rather than throwing, so
|
|
923
|
+
// a mistyped `req` would quietly drop Secure. Refuse it instead.
|
|
924
|
+
throw new SessionError("session/bad-req",
|
|
925
|
+
"b.session.logout: opts.req must be an HTTP request object");
|
|
926
|
+
}
|
|
927
|
+
var resolver = requestHelpers.trustedProtocol({
|
|
928
|
+
trustedProxies: opts.trustedProxies,
|
|
929
|
+
protocolResolver: opts.protocolResolver,
|
|
930
|
+
});
|
|
931
|
+
return resolver.resolve(opts.req) === "https";
|
|
932
|
+
}
|
|
933
|
+
|
|
845
934
|
async function _deleteBySidHash(sidHash) {
|
|
846
935
|
var built = sql.delete(_sessionSqlTable(), _sessionSqlOpts())
|
|
847
936
|
.where("sidHash", sidHash)
|
|
@@ -1057,7 +1146,9 @@ async function touch(token, opts) {
|
|
|
1057
1146
|
* reason: "mfa",
|
|
1058
1147
|
* });
|
|
1059
1148
|
* if (rotated) {
|
|
1060
|
-
*
|
|
1149
|
+
* b.cookies.appendSetCookie(res, b.cookies.serialize("sid", rotated.token, {
|
|
1150
|
+
* httpOnly: true, secure: true, sameSite: "Strict", path: "/",
|
|
1151
|
+
* }));
|
|
1061
1152
|
* }
|
|
1062
1153
|
* // → { token: "7a1e…", expiresAt: 1735689600000 }
|
|
1063
1154
|
*/
|
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"server": "sha256:f3325f480cb8eb814fcb0baaa19336cbbf2b993f48624c6aa9600ffd69d0be5e",
|
|
22
22
|
"browser": "sha256:0ffd91540bcb586a29b56e52ee1c29df69097b50776beb4036a07558f7a4e12e"
|
|
23
23
|
},
|
|
24
|
-
"refreshedAt": "2026-08-
|
|
24
|
+
"refreshedAt": "2026-08-20T15:14:48.311Z"
|
|
25
25
|
},
|
|
26
26
|
"@noble/hashes": {
|
|
27
27
|
"version": "2.3.0",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"hashes": {
|
|
49
49
|
"browser": "sha256:dfe4b7ae3c9880e388c8da4b68f44742b229b53afacd1e674179527e33da62b0"
|
|
50
50
|
},
|
|
51
|
-
"refreshedAt": "2026-08-
|
|
51
|
+
"refreshedAt": "2026-08-20T15:14:48.311Z"
|
|
52
52
|
},
|
|
53
53
|
"@noble/curves": {
|
|
54
54
|
"version": "2.3.0",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"hashes": {
|
|
71
71
|
"server": "sha256:b5fe88d1ea780d0581dee6145d666f89d46fc9531b5db35db2e5b16627840890"
|
|
72
72
|
},
|
|
73
|
-
"refreshedAt": "2026-08-
|
|
73
|
+
"refreshedAt": "2026-08-20T15:14:48.311Z",
|
|
74
74
|
"components": {
|
|
75
75
|
"@noble/hashes": {
|
|
76
76
|
"url": "https://github.com/paulmillr/noble-hashes",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"server": "sha256:fab7ebe5737793862c473444f4ee5912f79dd1edec86683acbb4eecbca0f5892",
|
|
115
115
|
"browser": "sha256:cae1d5bbdc7184b202b6ca68df6e1db7b0d0f668c77809ded189ca7f271accc9"
|
|
116
116
|
},
|
|
117
|
-
"refreshedAt": "2026-08-
|
|
117
|
+
"refreshedAt": "2026-08-20T15:14:48.311Z",
|
|
118
118
|
"components": {
|
|
119
119
|
"@noble/hashes": {
|
|
120
120
|
"url": "https://github.com/paulmillr/noble-hashes",
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
},
|
|
149
149
|
"runtime_artifact": "lib/vendor/common-passwords-top-10000.data.js",
|
|
150
150
|
"integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
|
|
151
|
-
"refreshedAt": "2026-08-
|
|
151
|
+
"refreshedAt": "2026-08-20T15:14:48.311Z"
|
|
152
152
|
},
|
|
153
153
|
"bimi-trust-anchors": {
|
|
154
154
|
"version": "operator-managed",
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
},
|
|
174
174
|
"runtime_artifact": "lib/vendor/bimi-trust-anchors.data.js",
|
|
175
175
|
"integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
|
|
176
|
-
"refreshedAt": "2026-08-
|
|
176
|
+
"refreshedAt": "2026-08-20T15:14:48.311Z"
|
|
177
177
|
},
|
|
178
178
|
"publicsuffix-list": {
|
|
179
179
|
"version": "master",
|
|
@@ -193,10 +193,10 @@
|
|
|
193
193
|
},
|
|
194
194
|
"runtime_artifact": "lib/vendor/public-suffix-list.data.js",
|
|
195
195
|
"integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
|
|
196
|
-
"refreshedAt": "2026-08-
|
|
196
|
+
"refreshedAt": "2026-08-20T15:14:48.311Z"
|
|
197
197
|
},
|
|
198
198
|
"@blamejs/pki": {
|
|
199
|
-
"version": "0.5.
|
|
199
|
+
"version": "0.5.17",
|
|
200
200
|
"license": "Apache-2.0",
|
|
201
201
|
"author": "blamejs",
|
|
202
202
|
"source": "https://github.com/blamejs/pki",
|
|
@@ -216,12 +216,12 @@
|
|
|
216
216
|
"server": "lib/vendor/blamejs-pki.cjs"
|
|
217
217
|
},
|
|
218
218
|
"bundler": "esbuild --format=cjs --platform=node --external:crypto --external:node:crypto",
|
|
219
|
-
"bundledAt": "2026-08-
|
|
220
|
-
"cpe": "cpe:2.3:a:blamejs:pki:0.5.
|
|
219
|
+
"bundledAt": "2026-08-20T00:00:00Z",
|
|
220
|
+
"cpe": "cpe:2.3:a:blamejs:pki:0.5.17:*:*:*:*:node.js:*:*",
|
|
221
221
|
"hashes": {
|
|
222
|
-
"server": "sha256:
|
|
222
|
+
"server": "sha256:dbedb80e1725747a24fdbd3dec9fcab31b507ac048b923462382b6ac117937a0"
|
|
223
223
|
},
|
|
224
|
-
"refreshedAt": "2026-08-
|
|
224
|
+
"refreshedAt": "2026-08-20T15:14:48.311Z"
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
}
|