@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.
@@ -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). csrf-protect builds its
435
- // own Set-Cookie header rather than routing through b.cookies.serialize, so
436
- // this boot check is the only enforcement point. §5.4 requires user agents
437
- // to apply the prefix test case-INSENSITIVELY (the server-side §4.1.3
438
- // description reads "case-sensitive", but the UA is what drops the cookie),
439
- // so `__host-`/`__SECURE-` get the same browser enforcement as
440
- // `__Host-`/`__Secure-` -- case-sensitive matching was itself CVE-2024-5699.
441
- // Compare a lowercased copy so a case-variant name can't dodge the invariant
442
- // here and then be silently rejected by the browser. Catch typos at boot.
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
- if (lowerCookieName.indexOf("__host-") === 0) {
448
- if (cookieCfg.path !== "/") {
449
- throw new Error("middleware.csrfProtect: __Host-* cookie name requires path='/'");
450
- }
451
- if (cookieCfg.secure === false) {
452
- throw new Error("middleware.csrfProtect: __Host-* cookie name requires secure (cannot be explicit false)");
453
- }
454
- } else if (lowerCookieName.indexOf("__secure-") === 0) {
455
- if (cookieCfg.secure === false) {
456
- throw new Error("middleware.csrfProtect: __Secure-* cookie name requires secure (cannot be explicit false)");
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 cookies = _parseCookieHeader(req.headers && req.headers.cookie);
489
- var existing = cookies[cookieName];
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
- var setCookie = _formatSetCookie(cookieName, fresh, {
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
- _appendSetCookie(res, setCookie);
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 db = _resolveDb(opts);
301
- _ensureTable(db);
302
- var q = sql.select(_migrationsTable(), _sqlOpts(db))
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 = db.prepare(q.sql);
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 db = _resolveDb(opts);
323
- _ensureTable(db);
324
- return _withLock(db, opts, function () {
325
- var namesQ = sql.select(_migrationsTable(), _sqlOpts(db)).columns(["name"]).toSql();
326
- var namesStmt = db.prepare(namesQ.sql);
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(db, function () {
340
- mod.up(db);
341
- var insQ = sql.insert(_migrationsTable(), _sqlOpts(db))
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 = db.prepare(insQ.sql);
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 db = _resolveDb(opts);
367
- _ensureTable(db);
368
- return _withLock(db, opts, function () {
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(db)).columns(["name"])
373
+ var downQ = sql.select(_migrationsTable(), _sqlOpts(conn)).columns(["name"])
374
374
  .orderBy("appliedAt", "desc").orderBy("name", "desc").limit(steps).toSql();
375
- var downStmt = db.prepare(downQ.sql);
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(db, function () {
390
- mod.down(db);
391
- var delQ = sql.delete(_migrationsTable(), _sqlOpts(db)).where("name", file).toSql();
392
- var delStmt = db.prepare(delQ.sql);
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 db = _resolveDb(opts);
456
- _ensureTables(db);
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(db, env);
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 db = _resolveDb(opts);
495
- _ensureTables(db);
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(db, lockStaleAfterMs, clock);
507
+ var holder = _acquireLock(conn, lockStaleAfterMs, clock);
508
508
  try {
509
- var appliedSelBuilt = sql.select(_seedersTable(), _sqlOpts(db))
509
+ var appliedSelBuilt = sql.select(_seedersTable(), _sqlOpts(conn))
510
510
  .columns(["name"]).where("env", env).toSql();
511
- var appliedSelStmt = db.prepare(appliedSelBuilt.sql);
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(db, async function () {
543
- await mod.run(db, ctx);
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(db))
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(db))
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(db))
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 = db.prepare(writeBuilt.sql);
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(db, holder);
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
- * res.setHeader("Set-Cookie", "sid=" + s.token + "; HttpOnly; Secure; SameSite=Strict");
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.setHeader("Set-Cookie", "sid=; HttpOnly; Max-Age=0");
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: string, // default: "sid" — the session cookie to expire
803
- * types: string[], // default: the W3C Clear-Site-Data directive set
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
- res.setHeader("Set-Cookie",
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
- * res.setHeader("Set-Cookie", "sid=" + rotated.token + "; HttpOnly; Secure; SameSite=Strict");
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
  */
@@ -21,7 +21,7 @@
21
21
  "server": "sha256:f3325f480cb8eb814fcb0baaa19336cbbf2b993f48624c6aa9600ffd69d0be5e",
22
22
  "browser": "sha256:0ffd91540bcb586a29b56e52ee1c29df69097b50776beb4036a07558f7a4e12e"
23
23
  },
24
- "refreshedAt": "2026-08-20T04:54:41.606Z"
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-20T04:54:41.606Z"
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-20T04:54:41.606Z",
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-20T04:54:41.606Z",
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-20T04:54:41.606Z"
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-20T04:54:41.606Z"
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-20T04:54:41.606Z"
196
+ "refreshedAt": "2026-08-20T15:14:48.311Z"
197
197
  },
198
198
  "@blamejs/pki": {
199
- "version": "0.5.16",
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-19T00:00:00Z",
220
- "cpe": "cpe:2.3:a:blamejs:pki:0.5.16:*:*:*:*:node.js:*:*",
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:96861fcf18c319d2e984cf48aaa0e496e903e447c491c6efcee677e15e87c24e"
222
+ "server": "sha256:dbedb80e1725747a24fdbd3dec9fcab31b507ac048b923462382b6ac117937a0"
223
223
  },
224
- "refreshedAt": "2026-08-20T04:54:41.606Z"
224
+ "refreshedAt": "2026-08-20T15:14:48.311Z"
225
225
  }
226
226
  }
227
227
  }