@blamejs/core 0.4.16 → 0.4.17
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 +1 -0
- package/index.js +1 -0
- package/lib/http-client-cookie-jar.js +400 -0
- package/lib/http-client.js +56 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.4.x
|
|
10
10
|
|
|
11
|
+
- **0.4.16** (2026-04-30) — b.httpClient: interceptors + progress events
|
|
11
12
|
- **0.4.15** (2026-04-30) — b.httpClient: redirect-following + outbound multipart
|
|
12
13
|
- **0.4.14** (2026-04-30) — b.i18n: lazy locales + ordinal plurals + onMissingKey hook
|
|
13
14
|
- **0.4.13** (2026-04-30) — b.db: streaming query results
|
package/index.js
CHANGED
|
@@ -83,6 +83,7 @@ var httpClient = require("./lib/http-client");
|
|
|
83
83
|
// the bare `b.httpClient.request(...)`. The api-encrypt module owns the
|
|
84
84
|
// implementation; httpClient stays free of an api-encrypt dependency.
|
|
85
85
|
httpClient.encrypted = require("./lib/middleware/api-encrypt").httpClient;
|
|
86
|
+
httpClient.cookieJar = require("./lib/http-client-cookie-jar");
|
|
86
87
|
var websocket = require("./lib/websocket");
|
|
87
88
|
var safeUrl = require("./lib/safe-url");
|
|
88
89
|
var ssrfGuard = require("./lib/ssrf-guard");
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.httpClient.cookieJar — outbound HTTP cookie store.
|
|
4
|
+
*
|
|
5
|
+
* Holds Set-Cookie state across requests so successive calls in a flow
|
|
6
|
+
* (login → list → mutate → logout, OAuth code-exchange → userinfo, etc.)
|
|
7
|
+
* carry the right Cookie header without operators threading it by hand.
|
|
8
|
+
* RFC 6265 attribute coverage: Domain / Path / Expires / Max-Age /
|
|
9
|
+
* HttpOnly / Secure / SameSite. Public Suffix List awareness is
|
|
10
|
+
* deliberately deferred — operators wiring jars against trusted
|
|
11
|
+
* domains don't need it; re-open if a real cross-eTLD bug surfaces.
|
|
12
|
+
*
|
|
13
|
+
* var jar = b.httpClient.cookieJar.create(); // in-memory
|
|
14
|
+
* await b.httpClient.request({ url: loginUrl, method: "POST", body, jar });
|
|
15
|
+
* await b.httpClient.request({ url: meUrl, jar }); // session cookie attaches
|
|
16
|
+
*
|
|
17
|
+
* Vault-encrypted persistence — every cookie value is sealed via
|
|
18
|
+
* b.vault.seal before it lands in the jar's store, so a memory dump or
|
|
19
|
+
* core file doesn't expose plaintext values:
|
|
20
|
+
*
|
|
21
|
+
* var jar = b.httpClient.cookieJar.create({ persist: "vault", vault: b.vault });
|
|
22
|
+
*
|
|
23
|
+
* The jar's lifecycle is per-process — restart loses cookies regardless
|
|
24
|
+
* of persist mode. File-backed and cluster-shared persistence are
|
|
25
|
+
* deferred; operators wanting durability serialize via getAll() and
|
|
26
|
+
* restore via setFromSerialized() at boot.
|
|
27
|
+
*
|
|
28
|
+
* Outbound filtering follows RFC 6265 §5.4:
|
|
29
|
+
* - Domain: exact-host match by default; Domain attribute allows
|
|
30
|
+
* subdomain match (host must be a suffix of cookie.domain).
|
|
31
|
+
* - Path: request path must equal cookie.path or be path-below.
|
|
32
|
+
* - Secure: cookie only attaches when the request URL is https:.
|
|
33
|
+
* - Expiry: rows past Expires / Max-Age don't attach.
|
|
34
|
+
* - Sort: longer path first, then earlier creation time.
|
|
35
|
+
*
|
|
36
|
+
* Cookie shape returned from getAll():
|
|
37
|
+
*
|
|
38
|
+
* {
|
|
39
|
+
* name, value, domain, path, hostOnly,
|
|
40
|
+
* expiresAt, // unix ms, or null for session cookies
|
|
41
|
+
* httpOnly, secure, sameSite, // attributes
|
|
42
|
+
* createdAt, updatedAt,
|
|
43
|
+
* }
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
var safeUrl = require("./safe-url");
|
|
47
|
+
var { defineClass } = require("./framework-error");
|
|
48
|
+
|
|
49
|
+
var CookieJarError = defineClass("CookieJarError", { alwaysPermanent: true });
|
|
50
|
+
var _err = CookieJarError.factory;
|
|
51
|
+
|
|
52
|
+
var DEFAULTS = Object.freeze({
|
|
53
|
+
persist: "memory",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
var VALID_PERSIST = new Set(["memory", "vault"]);
|
|
57
|
+
var VALID_SAMESITE = new Set(["Strict", "Lax", "None"]);
|
|
58
|
+
|
|
59
|
+
// ---- Set-Cookie parser ----
|
|
60
|
+
|
|
61
|
+
function _parseHttpDate(s) {
|
|
62
|
+
// node:Date handles RFC 1123 / 850 / asctime — sufficient for HTTP-date.
|
|
63
|
+
var t = Date.parse(s);
|
|
64
|
+
return isNaN(t) ? null : t;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function _parseSetCookie(line) {
|
|
68
|
+
if (typeof line !== "string" || line.length === 0) return null;
|
|
69
|
+
var semi = line.indexOf(";");
|
|
70
|
+
var head = (semi === -1 ? line : line.slice(0, semi)).trim();
|
|
71
|
+
var eq = head.indexOf("=");
|
|
72
|
+
if (eq <= 0) return null;
|
|
73
|
+
var name = head.slice(0, eq).trim();
|
|
74
|
+
var value = head.slice(eq + 1).trim();
|
|
75
|
+
if (!name) return null;
|
|
76
|
+
|
|
77
|
+
var attrs = {};
|
|
78
|
+
if (semi !== -1) {
|
|
79
|
+
var rest = line.slice(semi + 1);
|
|
80
|
+
var parts = rest.split(";");
|
|
81
|
+
for (var i = 0; i < parts.length; i++) {
|
|
82
|
+
var p = parts[i].trim();
|
|
83
|
+
if (!p) continue;
|
|
84
|
+
var pi = p.indexOf("=");
|
|
85
|
+
var k, v;
|
|
86
|
+
if (pi === -1) { k = p; v = ""; }
|
|
87
|
+
else { k = p.slice(0, pi).trim(); v = p.slice(pi + 1).trim(); }
|
|
88
|
+
attrs[k.toLowerCase()] = v;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { name: name, value: value, attrs: attrs };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---- Domain / Path matching ----
|
|
95
|
+
|
|
96
|
+
function _domainMatch(host, cookieDomain) {
|
|
97
|
+
if (host === cookieDomain) return true;
|
|
98
|
+
if (host.length > cookieDomain.length &&
|
|
99
|
+
host.endsWith(cookieDomain) &&
|
|
100
|
+
host.charAt(host.length - cookieDomain.length - 1) === ".") {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function _pathMatch(reqPath, cookiePath) {
|
|
107
|
+
if (cookiePath === reqPath) return true;
|
|
108
|
+
if (reqPath.indexOf(cookiePath) === 0) {
|
|
109
|
+
if (cookiePath.charAt(cookiePath.length - 1) === "/") return true;
|
|
110
|
+
if (reqPath.charAt(cookiePath.length) === "/") return true;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function _defaultPath(reqPath) {
|
|
116
|
+
// RFC 6265 §5.1.4: take everything up to the last "/", or "/" if none.
|
|
117
|
+
if (typeof reqPath !== "string" || reqPath.length === 0) return "/";
|
|
118
|
+
var qm = reqPath.indexOf("?");
|
|
119
|
+
var p = qm === -1 ? reqPath : reqPath.slice(0, qm);
|
|
120
|
+
if (p.charAt(0) !== "/") return "/";
|
|
121
|
+
var lastSlash = p.lastIndexOf("/");
|
|
122
|
+
if (lastSlash <= 0) return "/";
|
|
123
|
+
return p.slice(0, lastSlash);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---- Public create ----
|
|
127
|
+
|
|
128
|
+
function create(opts) {
|
|
129
|
+
opts = opts || {};
|
|
130
|
+
var persist = opts.persist === undefined ? DEFAULTS.persist : opts.persist;
|
|
131
|
+
if (!VALID_PERSIST.has(persist)) {
|
|
132
|
+
throw _err("BAD_OPT", "cookieJar.create: persist must be 'memory' or 'vault', got " +
|
|
133
|
+
JSON.stringify(persist));
|
|
134
|
+
}
|
|
135
|
+
var vault = opts.vault || null;
|
|
136
|
+
if (persist === "vault") {
|
|
137
|
+
if (!vault || typeof vault.seal !== "function" || typeof vault.unseal !== "function") {
|
|
138
|
+
throw _err("BAD_OPT",
|
|
139
|
+
"cookieJar.create: persist: 'vault' requires opts.vault with seal/unseal (pass b.vault)");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
var clock = typeof opts.clock === "function" ? opts.clock : Date.now;
|
|
143
|
+
|
|
144
|
+
// Storage map keyed by `<domain>|<path>|<name>` so a (domain, path)
|
|
145
|
+
// tuple can hold multiple cookies, but a same-tuple-same-name update
|
|
146
|
+
// replaces the prior row per RFC 6265 §5.3.
|
|
147
|
+
var store = new Map();
|
|
148
|
+
|
|
149
|
+
function _seal(plain) {
|
|
150
|
+
if (persist !== "vault" || plain === undefined || plain === null) return String(plain == null ? "" : plain);
|
|
151
|
+
return vault.seal(String(plain));
|
|
152
|
+
}
|
|
153
|
+
function _unseal(blob) {
|
|
154
|
+
if (persist !== "vault" || blob === undefined || blob === null) return blob == null ? "" : String(blob);
|
|
155
|
+
return String(vault.unseal(blob));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function _setOne(reqUrl, parsed) {
|
|
159
|
+
var u;
|
|
160
|
+
try { u = new URL(reqUrl); } catch (_e) { return; }
|
|
161
|
+
var host = u.hostname.toLowerCase();
|
|
162
|
+
var attrs = parsed.attrs || {};
|
|
163
|
+
|
|
164
|
+
// Domain attribute: lower-case, leading-dot stripped (RFC 6265bis).
|
|
165
|
+
var domainAttr = attrs.domain;
|
|
166
|
+
var domain;
|
|
167
|
+
var hostOnly;
|
|
168
|
+
if (domainAttr) {
|
|
169
|
+
var d = String(domainAttr).toLowerCase();
|
|
170
|
+
if (d.charAt(0) === ".") d = d.slice(1);
|
|
171
|
+
// Don't accept a Domain that the request host doesn't match.
|
|
172
|
+
if (!_domainMatch(host, d)) return;
|
|
173
|
+
domain = d;
|
|
174
|
+
hostOnly = false;
|
|
175
|
+
} else {
|
|
176
|
+
domain = host;
|
|
177
|
+
hostOnly = true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
var path = (typeof attrs.path === "string" && attrs.path.charAt(0) === "/")
|
|
181
|
+
? attrs.path : _defaultPath(u.pathname);
|
|
182
|
+
|
|
183
|
+
// Expires / Max-Age. Max-Age wins when both present (RFC 6265 §5.2.2).
|
|
184
|
+
var now = clock();
|
|
185
|
+
var expiresAt = null;
|
|
186
|
+
if (attrs["max-age"] !== undefined) {
|
|
187
|
+
var maxAge = parseInt(attrs["max-age"], 10);
|
|
188
|
+
if (!isNaN(maxAge)) {
|
|
189
|
+
expiresAt = maxAge <= 0 ? 0 : (now + maxAge * 1000);
|
|
190
|
+
}
|
|
191
|
+
} else if (attrs.expires) {
|
|
192
|
+
expiresAt = _parseHttpDate(attrs.expires);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Max-Age=0 / past Expires → delete an existing matching row.
|
|
196
|
+
var key = domain + "|" + path + "|" + parsed.name;
|
|
197
|
+
if (expiresAt !== null && expiresAt <= now) {
|
|
198
|
+
store.delete(key);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
var sameSiteRaw = attrs.samesite;
|
|
203
|
+
var sameSite = null;
|
|
204
|
+
if (typeof sameSiteRaw === "string") {
|
|
205
|
+
var ssLc = sameSiteRaw.toLowerCase();
|
|
206
|
+
if (ssLc === "strict") sameSite = "Strict";
|
|
207
|
+
else if (ssLc === "lax") sameSite = "Lax";
|
|
208
|
+
else if (ssLc === "none") sameSite = "None";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
var prior = store.get(key);
|
|
212
|
+
store.set(key, {
|
|
213
|
+
name: parsed.name,
|
|
214
|
+
value: _seal(parsed.value),
|
|
215
|
+
domain: domain,
|
|
216
|
+
path: path,
|
|
217
|
+
hostOnly: hostOnly,
|
|
218
|
+
expiresAt: expiresAt,
|
|
219
|
+
httpOnly: Object.prototype.hasOwnProperty.call(attrs, "httponly"),
|
|
220
|
+
secure: Object.prototype.hasOwnProperty.call(attrs, "secure"),
|
|
221
|
+
sameSite: sameSite,
|
|
222
|
+
createdAt: prior ? prior.createdAt : now,
|
|
223
|
+
updatedAt: now,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---- Public API ----
|
|
228
|
+
|
|
229
|
+
function setFromResponse(reqUrl, setCookieHeader) {
|
|
230
|
+
if (!setCookieHeader) return;
|
|
231
|
+
var lines = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
|
|
232
|
+
for (var i = 0; i < lines.length; i++) {
|
|
233
|
+
var parsed = _parseSetCookie(lines[i]);
|
|
234
|
+
if (parsed) _setOne(reqUrl, parsed);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function cookieHeaderFor(reqUrl) {
|
|
239
|
+
var u;
|
|
240
|
+
try { u = new URL(reqUrl); } catch (_e) { return null; }
|
|
241
|
+
var host = u.hostname.toLowerCase();
|
|
242
|
+
var path = u.pathname || "/";
|
|
243
|
+
var isSecure = u.protocol === "https:";
|
|
244
|
+
var now = clock();
|
|
245
|
+
|
|
246
|
+
var matches = [];
|
|
247
|
+
for (var entry of store.values()) {
|
|
248
|
+
// Expiry
|
|
249
|
+
if (entry.expiresAt !== null && entry.expiresAt <= now) continue;
|
|
250
|
+
// Domain
|
|
251
|
+
if (entry.hostOnly) {
|
|
252
|
+
if (entry.domain !== host) continue;
|
|
253
|
+
} else {
|
|
254
|
+
if (!_domainMatch(host, entry.domain)) continue;
|
|
255
|
+
}
|
|
256
|
+
// Path
|
|
257
|
+
if (!_pathMatch(path, entry.path)) continue;
|
|
258
|
+
// Secure
|
|
259
|
+
if (entry.secure && !isSecure) continue;
|
|
260
|
+
matches.push(entry);
|
|
261
|
+
}
|
|
262
|
+
if (matches.length === 0) return null;
|
|
263
|
+
|
|
264
|
+
// Sort: longer path first, then earlier creation time.
|
|
265
|
+
matches.sort(function (a, b) {
|
|
266
|
+
if (a.path.length !== b.path.length) return b.path.length - a.path.length;
|
|
267
|
+
return a.createdAt - b.createdAt;
|
|
268
|
+
});
|
|
269
|
+
var pieces = matches.map(function (e) {
|
|
270
|
+
return e.name + "=" + _unseal(e.value);
|
|
271
|
+
});
|
|
272
|
+
return pieces.join("; ");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function getAll() {
|
|
276
|
+
var now = clock();
|
|
277
|
+
var out = [];
|
|
278
|
+
for (var entry of store.values()) {
|
|
279
|
+
if (entry.expiresAt !== null && entry.expiresAt <= now) continue;
|
|
280
|
+
out.push({
|
|
281
|
+
name: entry.name,
|
|
282
|
+
value: _unseal(entry.value),
|
|
283
|
+
domain: entry.domain,
|
|
284
|
+
path: entry.path,
|
|
285
|
+
hostOnly: entry.hostOnly,
|
|
286
|
+
expiresAt: entry.expiresAt,
|
|
287
|
+
httpOnly: entry.httpOnly,
|
|
288
|
+
secure: entry.secure,
|
|
289
|
+
sameSite: entry.sameSite,
|
|
290
|
+
createdAt: entry.createdAt,
|
|
291
|
+
updatedAt: entry.updatedAt,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function clear(filter) {
|
|
298
|
+
if (!filter) {
|
|
299
|
+
var n = store.size;
|
|
300
|
+
store.clear();
|
|
301
|
+
return n;
|
|
302
|
+
}
|
|
303
|
+
if (typeof filter !== "object") {
|
|
304
|
+
throw _err("BAD_OPT", "cookieJar.clear: filter must be an object or undefined");
|
|
305
|
+
}
|
|
306
|
+
var purged = 0;
|
|
307
|
+
var keysToDelete = [];
|
|
308
|
+
for (var pair of store.entries()) {
|
|
309
|
+
var key = pair[0];
|
|
310
|
+
var entry = pair[1];
|
|
311
|
+
if (filter.domain && entry.domain !== filter.domain) continue;
|
|
312
|
+
if (filter.name && entry.name !== filter.name) continue;
|
|
313
|
+
if (filter.path && entry.path !== filter.path) continue;
|
|
314
|
+
keysToDelete.push(key);
|
|
315
|
+
}
|
|
316
|
+
for (var i = 0; i < keysToDelete.length; i++) {
|
|
317
|
+
store.delete(keysToDelete[i]);
|
|
318
|
+
purged++;
|
|
319
|
+
}
|
|
320
|
+
return purged;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function size() {
|
|
324
|
+
var now = clock();
|
|
325
|
+
var n = 0;
|
|
326
|
+
for (var entry of store.values()) {
|
|
327
|
+
if (entry.expiresAt !== null && entry.expiresAt <= now) continue;
|
|
328
|
+
n++;
|
|
329
|
+
}
|
|
330
|
+
return n;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Round-trip helpers — operators with restart-survival needs serialize
|
|
334
|
+
// via getAll(), persist however they like, restore via setFromSerialized.
|
|
335
|
+
function setFromSerialized(rows) {
|
|
336
|
+
if (!Array.isArray(rows)) {
|
|
337
|
+
throw _err("BAD_OPT", "cookieJar.setFromSerialized: rows must be an array");
|
|
338
|
+
}
|
|
339
|
+
var now = clock();
|
|
340
|
+
for (var i = 0; i < rows.length; i++) {
|
|
341
|
+
var r = rows[i];
|
|
342
|
+
if (!r || typeof r.name !== "string" || typeof r.domain !== "string" || typeof r.path !== "string") continue;
|
|
343
|
+
var key = r.domain + "|" + r.path + "|" + r.name;
|
|
344
|
+
if (r.expiresAt !== null && r.expiresAt !== undefined && r.expiresAt <= now) continue;
|
|
345
|
+
store.set(key, {
|
|
346
|
+
name: r.name,
|
|
347
|
+
value: _seal(r.value),
|
|
348
|
+
domain: r.domain,
|
|
349
|
+
path: r.path,
|
|
350
|
+
hostOnly: !!r.hostOnly,
|
|
351
|
+
expiresAt: typeof r.expiresAt === "number" ? r.expiresAt : null,
|
|
352
|
+
httpOnly: !!r.httpOnly,
|
|
353
|
+
secure: !!r.secure,
|
|
354
|
+
sameSite: VALID_SAMESITE.has(r.sameSite) ? r.sameSite : null,
|
|
355
|
+
createdAt: typeof r.createdAt === "number" ? r.createdAt : now,
|
|
356
|
+
updatedAt: typeof r.updatedAt === "number" ? r.updatedAt : now,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Raw-store accessor for tests — returns the literal Map entries with
|
|
362
|
+
// the value field as it sits in memory (sealed when persist === "vault").
|
|
363
|
+
// Operators don't call this; if they need stored state, getAll() returns
|
|
364
|
+
// the unsealed form. Exposed so the no-plaintext assertion is verifiable.
|
|
365
|
+
function _storeForTest() {
|
|
366
|
+
var rows = [];
|
|
367
|
+
for (var entry of store.values()) {
|
|
368
|
+
rows.push({
|
|
369
|
+
name: entry.name,
|
|
370
|
+
valueRaw: entry.value,
|
|
371
|
+
domain: entry.domain,
|
|
372
|
+
path: entry.path,
|
|
373
|
+
expiresAt: entry.expiresAt,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return rows;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
setFromResponse: setFromResponse,
|
|
381
|
+
cookieHeaderFor: cookieHeaderFor,
|
|
382
|
+
getAll: getAll,
|
|
383
|
+
clear: clear,
|
|
384
|
+
size: size,
|
|
385
|
+
setFromSerialized: setFromSerialized,
|
|
386
|
+
persist: persist,
|
|
387
|
+
_storeForTest: _storeForTest,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
module.exports = {
|
|
392
|
+
create: create,
|
|
393
|
+
CookieJarError: CookieJarError,
|
|
394
|
+
DEFAULTS: DEFAULTS,
|
|
395
|
+
// Exposed for tests + advanced operator wiring.
|
|
396
|
+
_parseSetCookie: _parseSetCookie,
|
|
397
|
+
};
|
|
398
|
+
// safeUrl reserved for future scheme validation hooks (e.g. operator-supplied
|
|
399
|
+
// allowedProtocols filter on cookie attachment paths).
|
|
400
|
+
void safeUrl;
|
package/lib/http-client.js
CHANGED
|
@@ -294,6 +294,28 @@ function _fromH2Headers(h2Headers) {
|
|
|
294
294
|
|
|
295
295
|
var REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
296
296
|
|
|
297
|
+
// http-client-cookie-jar is exposed on b.httpClient.cookieJar via index.js.
|
|
298
|
+
// http-client itself only consumes the jar shape passed by operators via
|
|
299
|
+
// the request opt; no direct require is needed here.
|
|
300
|
+
|
|
301
|
+
// Merge a jar-derived Cookie header with any caller-supplied Cookie
|
|
302
|
+
// header. Operators who override Cookie explicitly always win — the jar
|
|
303
|
+
// supplements rather than replaces.
|
|
304
|
+
function _attachJarCookie(headers, jar, url) {
|
|
305
|
+
if (!jar) return headers;
|
|
306
|
+
var jarHeader = jar.cookieHeaderFor(url);
|
|
307
|
+
if (!jarHeader) return headers;
|
|
308
|
+
var merged = Object.assign({}, headers || {});
|
|
309
|
+
var existing = null;
|
|
310
|
+
var keys = Object.keys(merged);
|
|
311
|
+
for (var i = 0; i < keys.length; i++) {
|
|
312
|
+
if (keys[i].toLowerCase() === "cookie") { existing = keys[i]; break; }
|
|
313
|
+
}
|
|
314
|
+
if (existing) merged[existing] = merged[existing] + "; " + jarHeader;
|
|
315
|
+
else merged.Cookie = jarHeader;
|
|
316
|
+
return merged;
|
|
317
|
+
}
|
|
318
|
+
|
|
297
319
|
// Build a multipart/form-data body from { fields, files } shape.
|
|
298
320
|
// Mirrors the wire format that lib/middleware/body-parser.js's multipart
|
|
299
321
|
// parser accepts so round-trip from one blamejs app's outbound to
|
|
@@ -398,6 +420,14 @@ function request(opts) {
|
|
|
398
420
|
return Promise.reject(_makeError(opts.errorClass, "BAD_ARG",
|
|
399
421
|
"onDownloadProgress must be a function", true));
|
|
400
422
|
}
|
|
423
|
+
if (opts.jar !== undefined && opts.jar !== null) {
|
|
424
|
+
if (typeof opts.jar !== "object" ||
|
|
425
|
+
typeof opts.jar.cookieHeaderFor !== "function" ||
|
|
426
|
+
typeof opts.jar.setFromResponse !== "function") {
|
|
427
|
+
return Promise.reject(_makeError(opts.errorClass, "BAD_ARG",
|
|
428
|
+
"jar must be a b.httpClient.cookieJar.create() instance", true));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
401
431
|
|
|
402
432
|
// before interceptors — run in array order. Each may return a modified
|
|
403
433
|
// opts object (or return nothing to leave the running opts as-is).
|
|
@@ -557,6 +587,17 @@ function _requestSingle(opts) {
|
|
|
557
587
|
return Promise.reject(e);
|
|
558
588
|
}
|
|
559
589
|
|
|
590
|
+
// Attach jar-derived Cookie header BEFORE the request fires; record
|
|
591
|
+
// Set-Cookie response headers AFTER. Both halves run when opts.jar
|
|
592
|
+
// is set; redirect-following naturally re-runs both paths per hop
|
|
593
|
+
// because each hop calls _requestSingle.
|
|
594
|
+
if (opts.jar) {
|
|
595
|
+
var headersWithJar = _attachJarCookie(opts.headers, opts.jar, opts.url);
|
|
596
|
+
if (headersWithJar !== opts.headers) {
|
|
597
|
+
opts = Object.assign({}, opts, { headers: headersWithJar });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
560
601
|
// SSRF gate — refuse private / loopback / link-local / cloud-metadata
|
|
561
602
|
// / reserved IP destinations by default. Operators on internal mesh
|
|
562
603
|
// pass `allowInternal: true` (or a CIDR list for narrower bypass).
|
|
@@ -626,6 +667,14 @@ function _requestH1(transport, u, opts) {
|
|
|
626
667
|
var req = transport.lib.request(reqOpts, function (res) {
|
|
627
668
|
if (observer) observer("response:headers", { statusCode: res.statusCode, headers: res.headers });
|
|
628
669
|
|
|
670
|
+
// Save Set-Cookie into the jar (if wired) BEFORE delivering the
|
|
671
|
+
// response object — operator inspecting the response can already
|
|
672
|
+
// count on the jar carrying the new state.
|
|
673
|
+
if (opts.jar && res.headers && res.headers["set-cookie"]) {
|
|
674
|
+
try { opts.jar.setFromResponse(opts.url, res.headers["set-cookie"]); }
|
|
675
|
+
catch (_e) { /* jar is best-effort — never break the response */ }
|
|
676
|
+
}
|
|
677
|
+
|
|
629
678
|
// Download total: Content-Length when present, null otherwise.
|
|
630
679
|
var dlTotal = null;
|
|
631
680
|
if (res.headers && typeof res.headers["content-length"] === "string") {
|
|
@@ -825,6 +874,13 @@ function _requestH2(transport, u, opts) {
|
|
|
825
874
|
|
|
826
875
|
if (observer) observer("response:headers", { statusCode: statusCode, headers: responseHeaders });
|
|
827
876
|
|
|
877
|
+
// Save Set-Cookie to the jar (h2 set-cookie comes through as
|
|
878
|
+
// either a single string or array, same shape as h1).
|
|
879
|
+
if (opts.jar && responseHeaders["set-cookie"]) {
|
|
880
|
+
try { opts.jar.setFromResponse(opts.url, responseHeaders["set-cookie"]); }
|
|
881
|
+
catch (_e) { /* jar best-effort */ }
|
|
882
|
+
}
|
|
883
|
+
|
|
828
884
|
if (responseMode === "stream") {
|
|
829
885
|
if (statusCode >= 400) {
|
|
830
886
|
stream.resume();
|