kinetex 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1164 -453
- package/dist/browser/kinetex.esm.js +38 -22
- package/dist/browser/kinetex.js +3127 -715
- package/dist/browser/kinetex.min.js +38 -22
- package/dist/cjs/aws-sigv4.js +137 -20
- package/dist/cjs/cache.js +101 -21
- package/dist/cjs/circuit-breaker.js +69 -7
- package/dist/cjs/client.js +838 -191
- package/dist/cjs/cookie-parser.js +110 -9
- package/dist/cjs/cookie-store.js +141 -36
- package/dist/cjs/core.js +501 -63
- package/dist/cjs/dedup.js +58 -18
- package/dist/cjs/digest.js +185 -23
- package/dist/cjs/graphql.js +164 -24
- package/dist/cjs/headers.js +362 -48
- package/dist/cjs/interceptors.js +285 -29
- package/dist/cjs/lifecycle.js +89 -40
- package/dist/cjs/logging.js +169 -16
- package/dist/cjs/mod.js +3 -2
- package/dist/cjs/pagination.js +261 -28
- package/dist/cjs/progress.js +282 -52
- package/dist/cjs/proxy.js +412 -0
- package/dist/cjs/response.js +316 -47
- package/dist/cjs/socks5.js +167 -36
- package/dist/cjs/sse.js +201 -34
- package/dist/cjs/url.js +191 -45
- package/dist/cjs/utils.js +222 -48
- package/dist/cjs/worker.js +6 -6
- package/dist/cjs/ws.js +32 -16
- package/dist/esm/aws-sigv4.js +137 -20
- package/dist/esm/aws-sigv4.js.map +1 -1
- package/dist/esm/cache.js +101 -21
- package/dist/esm/cache.js.map +1 -1
- package/dist/esm/circuit-breaker.js +69 -7
- package/dist/esm/circuit-breaker.js.map +1 -1
- package/dist/esm/client.js +838 -191
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/cookie-parser.js +110 -9
- package/dist/esm/cookie-parser.js.map +1 -1
- package/dist/esm/cookie-store.js +141 -36
- package/dist/esm/cookie-store.js.map +1 -1
- package/dist/esm/core.js +501 -63
- package/dist/esm/core.js.map +1 -1
- package/dist/esm/dedup.js +58 -18
- package/dist/esm/dedup.js.map +1 -1
- package/dist/esm/digest.js +185 -23
- package/dist/esm/digest.js.map +1 -1
- package/dist/esm/graphql.js +164 -24
- package/dist/esm/graphql.js.map +1 -1
- package/dist/esm/headers.js +362 -48
- package/dist/esm/headers.js.map +1 -1
- package/dist/esm/interceptors.js +285 -29
- package/dist/esm/interceptors.js.map +1 -1
- package/dist/esm/lifecycle.js +89 -40
- package/dist/esm/lifecycle.js.map +1 -1
- package/dist/esm/logging.js +169 -16
- package/dist/esm/logging.js.map +1 -1
- package/dist/esm/mod.js +3 -2
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/pagination.js +261 -28
- package/dist/esm/pagination.js.map +1 -1
- package/dist/esm/progress.js +282 -52
- package/dist/esm/progress.js.map +1 -1
- package/dist/esm/proxy.js +413 -0
- package/dist/esm/proxy.js.map +1 -0
- package/dist/esm/response.js +316 -47
- package/dist/esm/response.js.map +1 -1
- package/dist/esm/socks5.js +167 -36
- package/dist/esm/socks5.js.map +1 -1
- package/dist/esm/sse.js +201 -34
- package/dist/esm/sse.js.map +1 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/url.js +191 -45
- package/dist/esm/url.js.map +1 -1
- package/dist/esm/utils.js +222 -48
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/worker.js +6 -6
- package/dist/esm/worker.js.map +1 -1
- package/dist/esm/ws.js +32 -16
- package/dist/esm/ws.js.map +1 -1
- package/dist/types/aws-sigv4.d.ts.map +1 -1
- package/dist/types/cache.d.ts +27 -2
- package/dist/types/cache.d.ts.map +1 -1
- package/dist/types/circuit-breaker.d.ts +14 -1
- package/dist/types/circuit-breaker.d.ts.map +1 -1
- package/dist/types/client.d.ts +98 -23
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/cookie-parser.d.ts +0 -17
- package/dist/types/cookie-parser.d.ts.map +1 -1
- package/dist/types/cookie-store.d.ts.map +1 -1
- package/dist/types/core.d.ts +109 -25
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/dedup.d.ts +0 -7
- package/dist/types/dedup.d.ts.map +1 -1
- package/dist/types/digest.d.ts +31 -37
- package/dist/types/digest.d.ts.map +1 -1
- package/dist/types/graphql.d.ts.map +1 -1
- package/dist/types/headers.d.ts +62 -29
- package/dist/types/headers.d.ts.map +1 -1
- package/dist/types/interceptors.d.ts +102 -0
- package/dist/types/interceptors.d.ts.map +1 -1
- package/dist/types/lifecycle.d.ts +19 -2
- package/dist/types/lifecycle.d.ts.map +1 -1
- package/dist/types/logging.d.ts +23 -4
- package/dist/types/logging.d.ts.map +1 -1
- package/dist/types/mod.d.ts +5 -3
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/pagination.d.ts +0 -25
- package/dist/types/pagination.d.ts.map +1 -1
- package/dist/types/progress.d.ts +1 -1
- package/dist/types/progress.d.ts.map +1 -1
- package/dist/types/proxy.d.ts +50 -0
- package/dist/types/proxy.d.ts.map +1 -0
- package/dist/types/response.d.ts +7 -1
- package/dist/types/response.d.ts.map +1 -1
- package/dist/types/socks5.d.ts.map +1 -1
- package/dist/types/sse.d.ts.map +1 -1
- package/dist/types/types.d.ts +139 -5
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/url.d.ts +0 -14
- package/dist/types/url.d.ts.map +1 -1
- package/dist/types/utils.d.ts.map +1 -1
- package/dist/types/worker.d.ts +6 -6
- package/dist/types/worker.d.ts.map +1 -1
- package/dist/types/ws.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -1513,7 +1513,12 @@ export function getPublicSuffix(hostname) {
|
|
|
1513
1513
|
* For IP addresses, returns the IP itself.
|
|
1514
1514
|
*/
|
|
1515
1515
|
export function getRegistrableDomain(hostname) {
|
|
1516
|
-
|
|
1516
|
+
// The trailing dot is stripped here for the same reason it is in
|
|
1517
|
+
// getPublicSuffix: without it the two disagreed about the same host.
|
|
1518
|
+
// `getPublicSuffix("1.2.3.4.")` correctly returned "4", but this function
|
|
1519
|
+
// split the *unstripped* string and handed back "4." — a fragment that is
|
|
1520
|
+
// not a domain at all, and one a caller could then use as a cookie domain.
|
|
1521
|
+
const h = hostname.toLowerCase().replace(/\.$/, "");
|
|
1517
1522
|
if (isIPAddress(h))
|
|
1518
1523
|
return h;
|
|
1519
1524
|
const suffix = getPublicSuffix(h);
|
|
@@ -1696,6 +1701,43 @@ export function canonicalizeDomainFull(domain) {
|
|
|
1696
1701
|
// ============================================================================
|
|
1697
1702
|
const IPV4_RE = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
1698
1703
|
const IPV6_RE = /^[\da-f:]+$/i;
|
|
1704
|
+
/**
|
|
1705
|
+
* Structural IPv6 check.
|
|
1706
|
+
*
|
|
1707
|
+
* The previous test was `/^[\da-f:]+$/` plus "contains a colon", which is both
|
|
1708
|
+
* too loose and too strict: it accepted `abc:123` and `cafe:babe` (a hostname
|
|
1709
|
+
* happens to be spelled entirely in hex characters) and rejected the
|
|
1710
|
+
* IPv4-mapped forms `::ffff:1.2.3.4` and `::1.2.3.4`, which are real
|
|
1711
|
+
* addresses. `getRegistrableDomain("::ffff:1.2.3.4")` therefore returned the
|
|
1712
|
+
* fragment "3.4". At most one `::` is allowed, a compressed form stands for
|
|
1713
|
+
* at least one omitted group, and an unadorned form must have all eight.
|
|
1714
|
+
*/
|
|
1715
|
+
function isIPv6Literal(h) {
|
|
1716
|
+
let s = h;
|
|
1717
|
+
// An embedded dotted quad occupies the last two hextets.
|
|
1718
|
+
const lastColon = s.lastIndexOf(":");
|
|
1719
|
+
if (lastColon >= 0 && s.slice(lastColon + 1).includes(".")) {
|
|
1720
|
+
const v4 = s.slice(lastColon + 1);
|
|
1721
|
+
if (!IPV4_RE.test(v4))
|
|
1722
|
+
return false;
|
|
1723
|
+
const o = v4.split(".").map(Number);
|
|
1724
|
+
if (!o.every((p) => p >= 0 && p <= 255))
|
|
1725
|
+
return false;
|
|
1726
|
+
s =
|
|
1727
|
+
s.slice(0, lastColon + 1) +
|
|
1728
|
+
(((o[0] << 8) | o[1]).toString(16) + ":" + ((o[2] << 8) | o[3]).toString(16));
|
|
1729
|
+
}
|
|
1730
|
+
if (!IPV6_RE.test(s))
|
|
1731
|
+
return false;
|
|
1732
|
+
if (s.split("::").length > 2)
|
|
1733
|
+
return false; // at most one elision
|
|
1734
|
+
const groupCount = (part) => (part ?? "").split(":").filter(Boolean).length;
|
|
1735
|
+
if (s.includes("::")) {
|
|
1736
|
+
const [head, tail] = s.split("::");
|
|
1737
|
+
return groupCount(head) + groupCount(tail) <= 7;
|
|
1738
|
+
}
|
|
1739
|
+
return groupCount(s) === 8;
|
|
1740
|
+
}
|
|
1699
1741
|
/**
|
|
1700
1742
|
* Check whether a host string is an IPv4 or IPv6 address.
|
|
1701
1743
|
*
|
|
@@ -1708,7 +1750,7 @@ export function isIPAddress(host) {
|
|
|
1708
1750
|
const parts = h.split(".").map(Number);
|
|
1709
1751
|
return parts.every((p) => p >= 0 && p <= 255);
|
|
1710
1752
|
}
|
|
1711
|
-
return
|
|
1753
|
+
return isIPv6Literal(h);
|
|
1712
1754
|
}
|
|
1713
1755
|
// ============================================================================
|
|
1714
1756
|
// 6. RFC 6265 §5.1.3 — DOMAIN MATCHING
|
|
@@ -1722,7 +1764,13 @@ export function isIPAddress(host) {
|
|
|
1722
1764
|
*/
|
|
1723
1765
|
export function domainMatch(requestHost, cookieDomain) {
|
|
1724
1766
|
const rh = requestHost.toLowerCase();
|
|
1725
|
-
|
|
1767
|
+
// RFC 6265 §5.2.3: a leading %x2E (".") in the Domain attribute is ignored.
|
|
1768
|
+
// The parser already strips it, so this mattered only for a caller using
|
|
1769
|
+
// this exported helper directly — and this function's own JSDoc documents
|
|
1770
|
+
// `cookieDomain` as e.g. ".example.com", which it answered `false` for:
|
|
1771
|
+
// neither the exact host nor any subdomain of it ever matched. A cookie set
|
|
1772
|
+
// with the common `Domain=.example.com` spelling was simply not sent.
|
|
1773
|
+
const cd = cookieDomain.toLowerCase().replace(/^\./, "");
|
|
1726
1774
|
if (rh === cd)
|
|
1727
1775
|
return true;
|
|
1728
1776
|
// IP addresses: exact match only
|
|
@@ -1735,10 +1783,13 @@ export function domainMatch(requestHost, cookieDomain) {
|
|
|
1735
1783
|
if (isIPAddress(cd))
|
|
1736
1784
|
return false;
|
|
1737
1785
|
// RFC 6265 §5.3: cookie domain must not be a public suffix
|
|
1738
|
-
//
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1786
|
+
// The COOKIE DOMAIN must not be a public suffix: "com" or "co.uk" must
|
|
1787
|
+
// never be accepted as a cookie domain. This check used to test the request
|
|
1788
|
+
// host instead, so it could never fire and a Domain=com cookie was reported
|
|
1789
|
+
// as matching every .com host. (HTTPCookieJar.setCookie guards it separately
|
|
1790
|
+
// with isPublicSuffix(cd); this exported helper must be correct on its own.)
|
|
1791
|
+
if (isPublicSuffix(cd))
|
|
1792
|
+
return false;
|
|
1742
1793
|
return true;
|
|
1743
1794
|
}
|
|
1744
1795
|
// ============================================================================
|
|
@@ -1870,14 +1921,23 @@ export function parseSetCookieHeader(header) {
|
|
|
1870
1921
|
if (eqIdx === -1) {
|
|
1871
1922
|
// No equals sign at all — treat as value-only (empty name)
|
|
1872
1923
|
name = "";
|
|
1873
|
-
value = nameValueStr
|
|
1924
|
+
value = nameValueStr;
|
|
1874
1925
|
}
|
|
1875
1926
|
else {
|
|
1876
1927
|
name = nameValueStr.slice(0, eqIdx).trim();
|
|
1877
|
-
value = nameValueStr.slice(eqIdx + 1)
|
|
1928
|
+
value = nameValueStr.slice(eqIdx + 1);
|
|
1878
1929
|
}
|
|
1879
1930
|
if (!isValidCookieName(name))
|
|
1880
1931
|
return null;
|
|
1932
|
+
// Validate the raw value, BEFORE trimming. `trim()` runs first, so a control
|
|
1933
|
+
// character at either end was stripped along with the surrounding space and
|
|
1934
|
+
// the check could never see it: `a=\r\nX-Injected: 1` came back as the
|
|
1935
|
+
// cookie `a` with the value "X-Injected: 1" instead of being rejected. The
|
|
1936
|
+
// interior case was already refused, so the rejection applied only from the
|
|
1937
|
+
// second character onwards.
|
|
1938
|
+
if (!isValidCookieValue(value))
|
|
1939
|
+
return null;
|
|
1940
|
+
value = value.trim();
|
|
1881
1941
|
// Strip surrounding double quotes from value (RFC 6265 §5.2 step 3)
|
|
1882
1942
|
if (value.length >= 2 && value[0] === '"' && value[value.length - 1] === '"') {
|
|
1883
1943
|
value = value.slice(1, -1);
|
|
@@ -2060,6 +2120,31 @@ export function formatSetCookieHeader(cookie) {
|
|
|
2060
2120
|
* @param raw - Raw Set-Cookie header string (possibly containing multiple cookies)
|
|
2061
2121
|
* @returns Array of individual Set-Cookie strings
|
|
2062
2122
|
*/
|
|
2123
|
+
/**
|
|
2124
|
+
* Decide whether a comma at `from` separates two cookies or is part of the
|
|
2125
|
+
* value currently being read.
|
|
2126
|
+
*
|
|
2127
|
+
* Looks ahead to the next `,` or `;` — the rest of the current value or
|
|
2128
|
+
* attribute — and reports whether that segment opens a `name=` pair. A
|
|
2129
|
+
* trailing comma with nothing after it also separates, so the empty tail is
|
|
2130
|
+
* dropped rather than kept as a value.
|
|
2131
|
+
*/
|
|
2132
|
+
function startsNewCookie(raw, from) {
|
|
2133
|
+
let j = from;
|
|
2134
|
+
while (j < raw.length && (raw[j] === " " || raw[j] === "\t"))
|
|
2135
|
+
j++;
|
|
2136
|
+
// Nothing but whitespace after the comma: a trailing separator.
|
|
2137
|
+
if (j >= raw.length)
|
|
2138
|
+
return true;
|
|
2139
|
+
for (; j < raw.length; j++) {
|
|
2140
|
+
const c = raw[j];
|
|
2141
|
+
if (c === ";" || c === ",")
|
|
2142
|
+
return false;
|
|
2143
|
+
if (c === "=")
|
|
2144
|
+
return true;
|
|
2145
|
+
}
|
|
2146
|
+
return false;
|
|
2147
|
+
}
|
|
2063
2148
|
export function splitSetCookieHeaders(raw) {
|
|
2064
2149
|
const cookies = [];
|
|
2065
2150
|
let buffer = "";
|
|
@@ -2111,6 +2196,22 @@ export function splitSetCookieHeaders(raw) {
|
|
|
2111
2196
|
continue;
|
|
2112
2197
|
}
|
|
2113
2198
|
if (char === "," && !inQuotedValue && (afterSemicolon || valueComplete || nameComplete)) {
|
|
2199
|
+
// A comma only separates cookies when what follows looks like a new
|
|
2200
|
+
// `name=value` pair. It otherwise belongs to the value being read —
|
|
2201
|
+
// above all `Expires=Thu, 01 Jan 2099 00:00:00 GMT`, the RFC 6265 §5.2
|
|
2202
|
+
// date format, which is the reason this function exists at all.
|
|
2203
|
+
//
|
|
2204
|
+
// The test used to be "have we seen a value or an attribute yet", and
|
|
2205
|
+
// that is true the instant the first `=` is read, so it committed on
|
|
2206
|
+
// EVERY comma: a single cookie carrying an Expires was truncated to
|
|
2207
|
+
// `a=1; Expires=Thu`, silently losing its expiry, and a collapsed
|
|
2208
|
+
// two-cookie header was shredded into fragments such as
|
|
2209
|
+
// `01 Jan 2099 00:00:00 GMT; Path=/; HttpOnly`.
|
|
2210
|
+
if (!startsNewCookie(raw, i + 1)) {
|
|
2211
|
+
buffer += char;
|
|
2212
|
+
i++;
|
|
2213
|
+
continue;
|
|
2214
|
+
}
|
|
2114
2215
|
commitCookie();
|
|
2115
2216
|
while (i + 1 < raw.length && /\s/.test(raw[i + 1])) {
|
|
2116
2217
|
i++;
|
package/dist/cjs/cookie-store.js
CHANGED
|
@@ -79,6 +79,29 @@ function enforcePrefixRules(cookie, requestUrl) {
|
|
|
79
79
|
* @param context - The request's SameSite context
|
|
80
80
|
* @returns true if the cookie may be sent
|
|
81
81
|
*/
|
|
82
|
+
/**
|
|
83
|
+
* Canonicalise a `SameSite` read from outside the library.
|
|
84
|
+
*
|
|
85
|
+
* `SameSite` is a closed union, so anything arriving from JSON is untrusted
|
|
86
|
+
* by construction. An unrecognised value (including a missing one) becomes
|
|
87
|
+
* `"Unset"`, which `sameSiteAllows` treats permissively but still refuses on
|
|
88
|
+
* an explicit cross-site request.
|
|
89
|
+
*/
|
|
90
|
+
function normalizeSameSite(value) {
|
|
91
|
+
if (typeof value !== "string")
|
|
92
|
+
return "Unset";
|
|
93
|
+
switch (value.trim().toLowerCase()) {
|
|
94
|
+
case "strict":
|
|
95
|
+
return "Strict";
|
|
96
|
+
case "lax":
|
|
97
|
+
return "Lax";
|
|
98
|
+
case "none":
|
|
99
|
+
return "None";
|
|
100
|
+
case "unset":
|
|
101
|
+
default:
|
|
102
|
+
return "Unset";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
82
105
|
function sameSiteAllows(sameSite, context) {
|
|
83
106
|
switch (sameSite) {
|
|
84
107
|
case "Strict":
|
|
@@ -180,7 +203,11 @@ export class CookieJar {
|
|
|
180
203
|
let cookieDomain;
|
|
181
204
|
let hostOnly;
|
|
182
205
|
if (parsed.domain !== null && parsed.domain !== "") {
|
|
183
|
-
|
|
206
|
+
// Normalize the case: the Domain attribute is case-insensitive, but the
|
|
207
|
+
// value was stored verbatim while every lookup compares against a
|
|
208
|
+
// lower-cased request host. `Domain=Example.COM` therefore stored a cookie
|
|
209
|
+
// that could never be matched again — silently lost for the jar's lifetime.
|
|
210
|
+
const cd = parsed.domain.toLowerCase();
|
|
184
211
|
// Must domain-match the request host (use custom matcher if provided)
|
|
185
212
|
const matcher = this.domainMatcherFn ?? domainMatch;
|
|
186
213
|
if (!matcher(reqHost, cd))
|
|
@@ -269,10 +296,9 @@ export class CookieJar {
|
|
|
269
296
|
* @returns Array of matching Cookie objects (direct references into storage)
|
|
270
297
|
*/
|
|
271
298
|
getCookies(options) {
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
299
|
+
// Expired cookies are removed by the periodic cleanup timer (and by
|
|
300
|
+
// clearExpired()); the old 1%-per-access coin flip walked the whole store
|
|
301
|
+
// at random and made eviction timing non-deterministic.
|
|
276
302
|
const url = safeParseUrl(options.url);
|
|
277
303
|
if (!url)
|
|
278
304
|
return [];
|
|
@@ -321,6 +347,16 @@ export class CookieJar {
|
|
|
321
347
|
if (cookie.name.startsWith("__Host-") &&
|
|
322
348
|
!(cookie.secure && isSecure && cookie.path === "/" && cookie.hostOnly))
|
|
323
349
|
continue;
|
|
350
|
+
// The same argument applies to RFC 6265 §5.3's "the cookie domain must
|
|
351
|
+
// not be a public suffix", which setCookie() enforces but the storage
|
|
352
|
+
// map does not: fromJSON()/loadCookieJar() write straight into the map
|
|
353
|
+
// and skip setCookie() entirely. A persisted cookie with
|
|
354
|
+
// `domain: "com"` — which setCookie refuses — was therefore sent to
|
|
355
|
+
// every .com host, so anyone able to write the serialized jar (an XSS
|
|
356
|
+
// bug plus localStorage) got a cross-site cookie-tossing primitive.
|
|
357
|
+
// A cookie whose domain is a public suffix is never emitted.
|
|
358
|
+
if (isPublicSuffix(cookie.domain))
|
|
359
|
+
continue;
|
|
324
360
|
result.push(cookie);
|
|
325
361
|
}
|
|
326
362
|
}
|
|
@@ -333,13 +369,23 @@ export class CookieJar {
|
|
|
333
369
|
return a.createdAt - b.createdAt;
|
|
334
370
|
});
|
|
335
371
|
// §5.4 step 3: update last-access time
|
|
372
|
+
//
|
|
373
|
+
// The timestamp is stamped on the STORED cookie, because that is the value
|
|
374
|
+
// LRU eviction orders by, and the caller receives copies. It used to return
|
|
375
|
+
// the stored objects themselves, so
|
|
376
|
+
// `jar.getCookies(opts)[0].value = "x"` rewrote the jar's contents — while
|
|
377
|
+
// getAll() and getForDomain() both return copies, leaving the two halves of
|
|
378
|
+
// the same API disagreeing about whether the result was yours to modify.
|
|
336
379
|
const ts = Date.now();
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
380
|
+
// The snapshot is taken before the stamp, so a caller sees the cookie as it
|
|
381
|
+
// was stored at the moment of the call while the jar records the access.
|
|
382
|
+
// Copying first and stamping after also means a mutation of the returned
|
|
383
|
+
// object cannot write a lastAccessed back into the store.
|
|
384
|
+
return result.map((c) => {
|
|
385
|
+
const snapshot = { ...c };
|
|
340
386
|
c.lastAccessed = ts;
|
|
341
|
-
|
|
342
|
-
|
|
387
|
+
return snapshot;
|
|
388
|
+
});
|
|
343
389
|
}
|
|
344
390
|
// --------------------------------------------------------------------------
|
|
345
391
|
// 7.3 getCookieHeader
|
|
@@ -371,15 +417,38 @@ export class CookieJar {
|
|
|
371
417
|
getCookiesForDomain(domain) {
|
|
372
418
|
const d = canonicalizeDomainFull(domain);
|
|
373
419
|
const result = [];
|
|
420
|
+
const now = Date.now();
|
|
374
421
|
for (const [stored, pm] of this.dm) {
|
|
375
|
-
//
|
|
376
|
-
|
|
422
|
+
// A domain is in scope if it is `d` itself, a subdomain of it, or an
|
|
423
|
+
// ancestor of it. The ancestor case was missing, so the result
|
|
424
|
+
// disagreed with getCookieHeader() for every subdomain query:
|
|
425
|
+
// getCookiesForDomain("sub.example.com") listed only the host-only
|
|
426
|
+
// cookie while the request actually sent "hostonly=1; domainwide=1",
|
|
427
|
+
// and "deep.sub.example.com" returned nothing at all. A parent-domain
|
|
428
|
+
// cookie applies to every host beneath it (RFC 6265 §5.3 step 5), and
|
|
429
|
+
// the host-only check below already keeps an ancestor's host-only
|
|
430
|
+
// cookie out of the result.
|
|
431
|
+
if (stored === d || stored.endsWith("." + d) || d.endsWith("." + stored)) {
|
|
377
432
|
for (const [, nm] of pm) {
|
|
378
433
|
for (const [, cookie] of nm) {
|
|
379
434
|
// Skip expired cookies
|
|
380
|
-
if (cookie.expires !== Infinity && cookie.expires <
|
|
435
|
+
if (cookie.expires !== Infinity && cookie.expires < now)
|
|
436
|
+
continue;
|
|
437
|
+
// A host-only cookie is bound to its own host and is never sent to
|
|
438
|
+
// a sibling or parent domain. The method documents itself as
|
|
439
|
+
// returning "cookies that would be sent to this domain", and it was
|
|
440
|
+
// returning them anyway: asking for "example.com" listed the
|
|
441
|
+
// host-only cookie belonging to "sub.example.com", which never
|
|
442
|
+
// reaches example.com. Only exact-domain host-only cookies apply.
|
|
443
|
+
if (cookie.hostOnly && cookie.domain !== d)
|
|
444
|
+
continue;
|
|
445
|
+
// Same public-suffix rule as getCookies().
|
|
446
|
+
if (isPublicSuffix(cookie.domain))
|
|
381
447
|
continue;
|
|
382
|
-
|
|
448
|
+
// A copy, like every other accessor. Returning the stored object
|
|
449
|
+
// let `jar.getCookiesForDomain(d)[0].value = "x"` rewrite the jar,
|
|
450
|
+
// while the method sitting directly above it returned copies.
|
|
451
|
+
result.push({ ...cookie });
|
|
383
452
|
}
|
|
384
453
|
}
|
|
385
454
|
}
|
|
@@ -590,7 +659,16 @@ export class CookieJar {
|
|
|
590
659
|
maxAge: item.maxAge,
|
|
591
660
|
secure: item.secure,
|
|
592
661
|
httpOnly: item.httpOnly,
|
|
593
|
-
|
|
662
|
+
// Normalized, because this is the one public ingestion path that takes
|
|
663
|
+
// externally authored JSON verbatim. `sameSiteAllows` switches on the
|
|
664
|
+
// canonical `"Strict" | "Lax" | "Unset" | "None"` and returns
|
|
665
|
+
// `undefined` for anything else, and `getCookies` filters on that
|
|
666
|
+
// result — so a persisted `"lax"`, or a store that simply omitted the
|
|
667
|
+
// field, produced a jar whose `count` was 1 and whose cookie was never
|
|
668
|
+
// sent to anything, with no error anywhere. `Unset` is the documented
|
|
669
|
+
// "cannot tell" bucket: everything except an explicit cross-site
|
|
670
|
+
// non-navigation request.
|
|
671
|
+
sameSite: normalizeSameSite(item.sameSite),
|
|
594
672
|
createdAt: item.createdAt,
|
|
595
673
|
lastAccessed: item.lastAccessed,
|
|
596
674
|
hostOnly: item.hostOnly,
|
|
@@ -651,31 +729,53 @@ export class CookieJar {
|
|
|
651
729
|
*/
|
|
652
730
|
putCookie(cookie) {
|
|
653
731
|
const { domain, path, name } = cookie;
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
// total unchanged — same slot
|
|
732
|
+
// Update in place: same domain/path/name replaces the value and keeps the
|
|
733
|
+
// original createdAt (RFC 6265 §5.3 step 11). `total` is unchanged — it is
|
|
734
|
+
// the same slot.
|
|
735
|
+
const existing = this.dm.get(domain)?.get(path)?.get(name);
|
|
736
|
+
if (existing) {
|
|
737
|
+
this.dm
|
|
738
|
+
.get(domain)
|
|
739
|
+
.get(path)
|
|
740
|
+
.set(name, { ...cookie, createdAt: existing.createdAt });
|
|
741
|
+
return;
|
|
665
742
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
743
|
+
// Evict BEFORE this cookie's path map exists.
|
|
744
|
+
//
|
|
745
|
+
// Both eviction helpers prune empty path maps as they go, and evictGlobal
|
|
746
|
+
// can drop a whole domain. Creating the path map up front meant the prune
|
|
747
|
+
// deleted the map that was about to be written into: the new cookie was
|
|
748
|
+
// stored into an orphaned map that nothing could reach, so `setCookie`
|
|
749
|
+
// returned true, the cookie was never sent, `getAll()`/`toJSON()` could
|
|
750
|
+
// not see it, and `this.total` was incremented regardless. The counter
|
|
751
|
+
// then drifted above the number of cookies actually held — measured at
|
|
752
|
+
// `count === 5` with 2 cookies stored — and since evictGlobal() reads
|
|
753
|
+
// `this.total`, the global cap stopped bounding anything either.
|
|
754
|
+
this.evictForDomain(domain);
|
|
755
|
+
this.evictGlobal();
|
|
756
|
+
// Re-fetch: eviction may have removed this domain, or emptied it, while
|
|
757
|
+
// the local reference went stale.
|
|
758
|
+
let pm = this.dm.get(domain);
|
|
759
|
+
if (!pm) {
|
|
760
|
+
pm = new Map();
|
|
761
|
+
this.dm.set(domain, pm);
|
|
762
|
+
}
|
|
763
|
+
let nm = pm.get(path);
|
|
764
|
+
if (!nm) {
|
|
765
|
+
nm = new Map();
|
|
766
|
+
pm.set(path, nm);
|
|
672
767
|
}
|
|
768
|
+
nm.set(name, cookie);
|
|
769
|
+
this.total++;
|
|
673
770
|
}
|
|
674
771
|
/**
|
|
675
772
|
* Evict the least-recently-accessed cookie from a domain when
|
|
676
773
|
* the per-domain cap is exceeded. Evicts down to maxPerDomain - 1.
|
|
677
774
|
*/
|
|
678
|
-
evictForDomain(
|
|
775
|
+
evictForDomain(domain) {
|
|
776
|
+
const pm = this.dm.get(domain);
|
|
777
|
+
if (!pm)
|
|
778
|
+
return;
|
|
679
779
|
let count = 0;
|
|
680
780
|
for (const nm of pm.values())
|
|
681
781
|
count += nm.size;
|
|
@@ -747,11 +847,16 @@ export class CookieJar {
|
|
|
747
847
|
// For domain cookies: domain-match (subdomains allowed).
|
|
748
848
|
// We can't know per-domain whether it's host-only without checking cookies,
|
|
749
849
|
// so we do the broader domain-match here and filter host-only per cookie.
|
|
750
|
-
|
|
850
|
+
// Lower-case both sides: cookie domains are stored from the (already
|
|
851
|
+
// normalized) Domain attribute, but a caller can also insert cookies
|
|
852
|
+
// directly via putCookie() with mixed-case input.
|
|
853
|
+
const host = reqHost.toLowerCase();
|
|
854
|
+
const domain = cookieDomain.toLowerCase();
|
|
855
|
+
if (host === domain)
|
|
751
856
|
return true;
|
|
752
|
-
if (isIPAddress(
|
|
857
|
+
if (isIPAddress(host))
|
|
753
858
|
return false;
|
|
754
|
-
return
|
|
859
|
+
return host.endsWith("." + domain);
|
|
755
860
|
}
|
|
756
861
|
}
|
|
757
862
|
// ============================================================================
|