@blamejs/core 0.18.39 → 0.18.41
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 +118 -0
- package/NOTICE +2 -2
- package/README.md +2 -2
- package/lib/acme.js +5 -7
- package/lib/app.js +14 -5
- 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/file-upload.js +17 -3
- package/lib/gate-contract.js +159 -5
- package/lib/guard-filename.js +93 -18
- package/lib/guard-yaml.js +64 -0
- package/lib/http-client.js +1 -3
- package/lib/mail-auth.js +477 -78
- package/lib/mail.js +1 -2
- package/lib/middleware/csrf-protect.js +45 -51
- package/lib/migrations.js +22 -22
- package/lib/network-dns-resolver.js +23 -23
- package/lib/network-dns.js +205 -43
- package/lib/public-suffix.js +110 -24
- package/lib/seeders.js +15 -15
- package/lib/session.js +99 -8
- package/lib/vendor/MANIFEST.json +14 -14
- package/lib/vendor/blamejs-pki.cjs +1385 -402
- package/lib/vendor/public-suffix-list.dat +4 -3
- package/lib/vendor/public-suffix-list.data.js +2201 -2201
- package/package.json +2 -2
- package/sbom.cdx.json +6 -6
package/lib/public-suffix.js
CHANGED
|
@@ -74,6 +74,38 @@ function _err(code, message) {
|
|
|
74
74
|
return new PublicSuffixError(code, message);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// The index of the first character that cannot appear in a host name, or -1.
|
|
78
|
+
//
|
|
79
|
+
// Control / NUL / whitespace bytes, DEL, and the URL-structural delimiters
|
|
80
|
+
// domainToASCII silently TRUNCATES at — "/" (0x2F), "?" (0x3F), "#" (0x23),
|
|
81
|
+
// "\" (0x5C) reduce "example.com/evil" to "example.com" rather than failing,
|
|
82
|
+
// which would let a hostile host masquerade as a trusted prefix. ":" / "@" /
|
|
83
|
+
// "[" / "]" already make domainToASCII return "", but they are rejected here
|
|
84
|
+
// too so every non-host character fails closed rather than silently.
|
|
85
|
+
//
|
|
86
|
+
// Exported because it is the framework's definition of "a character that may
|
|
87
|
+
// appear in a host", and the DNS wire encoder needs the same answer. When it
|
|
88
|
+
// had its own — a shorter one — `b.network.dns` encoded `a\u0000.com` and
|
|
89
|
+
// `example.com/evil` into query labels that this module refuses outright.
|
|
90
|
+
function _firstNonHostCharacter(name) {
|
|
91
|
+
for (var i = 0; i < name.length; i += 1) {
|
|
92
|
+
var cp = name.charCodeAt(i);
|
|
93
|
+
if (cp < 0x21 || cp === 0x7f ||
|
|
94
|
+
cp === 0x2f || cp === 0x3f || cp === 0x23 || cp === 0x5c || // / ? # \
|
|
95
|
+
cp === 0x3a || cp === 0x40 || cp === 0x5b || cp === 0x5d) { // : @ [ ]
|
|
96
|
+
return i;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return -1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The characters UTS #46 treats as a label separator, and therefore the ones
|
|
103
|
+
// that can mark the root of an absolute name. Scanned against Node's own
|
|
104
|
+
// mapping and found to be exactly this set across the BMP and the SMP.
|
|
105
|
+
function _isRootMarker(ch) {
|
|
106
|
+
return ch === "." || ch === "。" || ch === "." || ch === "。";
|
|
107
|
+
}
|
|
108
|
+
|
|
77
109
|
// _normalizeInput — lowercase + IDN-normalize a candidate domain.
|
|
78
110
|
// Returns a plain ASCII (punycode) string with no leading/trailing
|
|
79
111
|
// dots and no empty labels. Throws PublicSuffixError on bad shape so
|
|
@@ -87,36 +119,41 @@ function _normalizeInput(domain) {
|
|
|
87
119
|
throw _err("public-suffix/invalid-domain",
|
|
88
120
|
"publicSuffix: domain must not be empty");
|
|
89
121
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
//
|
|
122
|
+
// Strip a single trailing root marker (FQDN form) BEFORE measuring. The
|
|
123
|
+
// absolute and relative spellings of one name encode to identical wire
|
|
124
|
+
// bytes, so measuring the marker would refuse a maximum-length name written
|
|
125
|
+
// absolutely while accepting it written relatively.
|
|
126
|
+
//
|
|
127
|
+
// Which character marks the root is not fixed: UTS #46 maps U+3002, U+FF0E
|
|
128
|
+
// and U+FF61 to "." as well, so `münchen.example。` is absolute too.
|
|
129
|
+
// Recognising all four here rather than only the ASCII dot is what lets the
|
|
130
|
+
// `rootStripped` guard below hold — exactly one marker comes off in total,
|
|
131
|
+
// and a second is an empty final label rather than something to remove.
|
|
132
|
+
//
|
|
133
|
+
// A marker that only APPEARS during conversion — because an IDNA-ignored
|
|
134
|
+
// code point trailed it — is handled by the post-conversion strip below,
|
|
135
|
+
// which the same guard keeps mutually exclusive with this one.
|
|
98
136
|
var s = domain.toLowerCase();
|
|
99
|
-
|
|
137
|
+
var rootStripped = false;
|
|
138
|
+
if (_isRootMarker(s.charAt(s.length - 1))) {
|
|
100
139
|
s = s.slice(0, -1);
|
|
140
|
+
rootStripped = true;
|
|
101
141
|
if (s.length === 0) {
|
|
102
142
|
throw _err("public-suffix/invalid-domain",
|
|
103
143
|
"publicSuffix: domain must not be a bare dot");
|
|
104
144
|
}
|
|
105
145
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
throw _err("public-suffix/invalid-domain",
|
|
118
|
-
"publicSuffix: domain contains a control byte or URL delimiter");
|
|
119
|
-
}
|
|
146
|
+
if (s.length > 253) {
|
|
147
|
+
// A cheap bound on the INPUT, so a pathological string is refused before
|
|
148
|
+
// conversion. It is not the authoritative check: an internationalized name
|
|
149
|
+
// grows when it becomes A-labels, so the real test is on the converted form
|
|
150
|
+
// below. Every ASCII name is settled here, since conversion leaves it as-is.
|
|
151
|
+
throw _err("public-suffix/invalid-domain",
|
|
152
|
+
"publicSuffix: domain exceeds 253-octet RFC 1035 limit");
|
|
153
|
+
}
|
|
154
|
+
if (_firstNonHostCharacter(s) !== -1) {
|
|
155
|
+
throw _err("public-suffix/invalid-domain",
|
|
156
|
+
"publicSuffix: domain contains a control byte or URL delimiter");
|
|
120
157
|
}
|
|
121
158
|
// IDN-normalize — non-ASCII labels become xn--… via Node's UTS #46
|
|
122
159
|
// implementation. Empty string back means the input was malformed
|
|
@@ -126,11 +163,55 @@ function _normalizeInput(domain) {
|
|
|
126
163
|
throw _err("public-suffix/invalid-domain",
|
|
127
164
|
"publicSuffix: domain failed IDN normalization");
|
|
128
165
|
}
|
|
129
|
-
// No empty labels (`foo..bar`) and no leading dot.
|
|
166
|
+
// No empty labels (`foo..bar`) and no leading dot. This runs BEFORE the
|
|
167
|
+
// root-marker strip below, so a name carrying an empty final label cannot be
|
|
168
|
+
// turned into a valid one by removing a dot: `münchen.example。。` converts to
|
|
169
|
+
// a trailing `..` and is refused here rather than quietly becoming a
|
|
170
|
+
// different, real domain.
|
|
130
171
|
if (ascii.indexOf("..") !== -1 || ascii.charCodeAt(0) === 46) {
|
|
131
172
|
throw _err("public-suffix/invalid-domain",
|
|
132
173
|
"publicSuffix: domain contains empty label");
|
|
133
174
|
}
|
|
175
|
+
// RFC 1035 §2.3.4 — 253 octets max for the wire form (255 minus the leading
|
|
176
|
+
// length byte and the root's null). This is the AUTHORITATIVE check, and it
|
|
177
|
+
// has to run on the converted name: an internationalized label grows into its
|
|
178
|
+
// `xn--` form, so five 44-character labels are 224 characters going in and
|
|
179
|
+
// 254 octets coming out, with every individual label a legal 50. Measuring
|
|
180
|
+
// the input handed the caller a name that cannot be put on the wire, and a
|
|
181
|
+
// caller cannot tell — it looks like any other domain, and the DMARC walk
|
|
182
|
+
// stepped over the unqueryable target and applied an ancestor's policy.
|
|
183
|
+
//
|
|
184
|
+
// A trailing root marker is still present at this point and does not count:
|
|
185
|
+
// the wire form carries the root as a zero-length label, not a character.
|
|
186
|
+
var withoutRoot = ascii.charCodeAt(ascii.length - 1) === 46
|
|
187
|
+
? ascii.length - 1 : ascii.length;
|
|
188
|
+
if (withoutRoot > 253) {
|
|
189
|
+
throw _err("public-suffix/invalid-domain",
|
|
190
|
+
"publicSuffix: domain exceeds 253-octet RFC 1035 limit once converted " +
|
|
191
|
+
"to A-labels (" + withoutRoot + " octets)");
|
|
192
|
+
}
|
|
193
|
+
// The root marker is stripped here when it was not an ASCII dot on the way
|
|
194
|
+
// in. UTS #46 maps U+3002, U+FF0E and U+FF61 to ".", so `münchen.example。`
|
|
195
|
+
// arrives with no trailing dot and leaves the conversion with one. Returning
|
|
196
|
+
// that from a function whose contract is to strip the trailing dot leaves
|
|
197
|
+
// every caller to compensate, and the ones that do not compare two spellings
|
|
198
|
+
// of the same absolute name as different names.
|
|
199
|
+
//
|
|
200
|
+
// At most ONE root marker is removed in total. A dot still here after one was
|
|
201
|
+
// already taken off means the name ended in two of them — an empty final
|
|
202
|
+
// label — and stripping the second would hand the caller a different, valid
|
|
203
|
+
// domain than the one they asked about.
|
|
204
|
+
if (ascii.charCodeAt(ascii.length - 1) === 46 /* "." */) {
|
|
205
|
+
if (rootStripped) {
|
|
206
|
+
throw _err("public-suffix/invalid-domain",
|
|
207
|
+
"publicSuffix: domain contains empty label");
|
|
208
|
+
}
|
|
209
|
+
ascii = ascii.slice(0, -1);
|
|
210
|
+
if (ascii.length === 0) {
|
|
211
|
+
throw _err("public-suffix/invalid-domain",
|
|
212
|
+
"publicSuffix: domain must not be a bare dot");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
134
215
|
return ascii;
|
|
135
216
|
}
|
|
136
217
|
|
|
@@ -462,4 +543,9 @@ module.exports = {
|
|
|
462
543
|
canonicalDomain: canonicalDomain,
|
|
463
544
|
isPublicSuffix: isPublicSuffix,
|
|
464
545
|
lookupSource: lookupSource,
|
|
546
|
+
_firstNonHostCharacter: _firstNonHostCharacter,
|
|
547
|
+
// Exported for the same reason as the character predicate above: this is the
|
|
548
|
+
// framework's answer to "which characters mark the root of an absolute name",
|
|
549
|
+
// and a second copy of the set drifts from it.
|
|
550
|
+
_isRootMarker: _isRootMarker,
|
|
465
551
|
};
|
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-20T04:54:41.606Z"
|
|
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-20T04:54:41.606Z"
|
|
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-20T04:54:41.606Z",
|
|
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-20T04:54:41.606Z",
|
|
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-20T04:54:41.606Z"
|
|
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-20T04:54:41.606Z"
|
|
177
177
|
},
|
|
178
178
|
"publicsuffix-list": {
|
|
179
179
|
"version": "master",
|
|
@@ -186,17 +186,17 @@
|
|
|
186
186
|
"data_js": "lib/vendor/public-suffix-list.data.js"
|
|
187
187
|
},
|
|
188
188
|
"bundler": "curl https://publicsuffix.org/list/public_suffix_list.dat",
|
|
189
|
-
"bundledAt": "2026-08-
|
|
189
|
+
"bundledAt": "2026-08-19T00:00:00Z",
|
|
190
190
|
"hashes": {
|
|
191
|
-
"server": "sha256:
|
|
192
|
-
"data_js": "sha256:
|
|
191
|
+
"server": "sha256:75142784c0308c8f7cd27f15fca80b9ddc09aeec05ae51c2f54fd0958351d9ad",
|
|
192
|
+
"data_js": "sha256:2435668e6d9964d95b283e5796938aa7c626ea0ca1eed4af8b1abec034f1761b"
|
|
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-20T04:54:41.606Z"
|
|
197
197
|
},
|
|
198
198
|
"@blamejs/pki": {
|
|
199
|
-
"version": "0.5.
|
|
199
|
+
"version": "0.5.16",
|
|
200
200
|
"license": "Apache-2.0",
|
|
201
201
|
"author": "blamejs",
|
|
202
202
|
"source": "https://github.com/blamejs/pki",
|
|
@@ -217,11 +217,11 @@
|
|
|
217
217
|
},
|
|
218
218
|
"bundler": "esbuild --format=cjs --platform=node --external:crypto --external:node:crypto",
|
|
219
219
|
"bundledAt": "2026-08-19T00:00:00Z",
|
|
220
|
-
"cpe": "cpe:2.3:a:blamejs:pki:0.5.
|
|
220
|
+
"cpe": "cpe:2.3:a:blamejs:pki:0.5.16:*:*:*:*:node.js:*:*",
|
|
221
221
|
"hashes": {
|
|
222
|
-
"server": "sha256:
|
|
222
|
+
"server": "sha256:96861fcf18c319d2e984cf48aaa0e496e903e447c491c6efcee677e15e87c24e"
|
|
223
223
|
},
|
|
224
|
-
"refreshedAt": "2026-08-
|
|
224
|
+
"refreshedAt": "2026-08-20T04:54:41.606Z"
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
}
|