kinetex 1.3.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 +246 -9
- package/dist/browser/kinetex.esm.js +38 -22
- package/dist/browser/kinetex.js +2545 -550
- package/dist/browser/kinetex.min.js +38 -22
- package/dist/cjs/aws-sigv4.js +133 -19
- package/dist/cjs/cache.js +49 -7
- package/dist/cjs/circuit-breaker.js +45 -3
- package/dist/cjs/client.js +387 -104
- package/dist/cjs/cookie-parser.js +103 -5
- package/dist/cjs/cookie-store.js +125 -28
- package/dist/cjs/core.js +465 -66
- package/dist/cjs/dedup.js +49 -11
- package/dist/cjs/digest.js +160 -24
- package/dist/cjs/graphql.js +164 -24
- package/dist/cjs/headers.js +303 -45
- package/dist/cjs/interceptors.js +221 -7
- package/dist/cjs/lifecycle.js +89 -40
- package/dist/cjs/logging.js +168 -15
- package/dist/cjs/mod.js +3 -2
- package/dist/cjs/pagination.js +247 -22
- package/dist/cjs/progress.js +177 -27
- package/dist/cjs/proxy.js +412 -0
- package/dist/cjs/response.js +316 -47
- package/dist/cjs/socks5.js +131 -15
- package/dist/cjs/sse.js +173 -43
- package/dist/cjs/url.js +191 -45
- package/dist/cjs/utils.js +222 -48
- package/dist/cjs/ws.js +19 -10
- package/dist/esm/aws-sigv4.js +133 -19
- package/dist/esm/aws-sigv4.js.map +1 -1
- package/dist/esm/cache.js +49 -7
- package/dist/esm/cache.js.map +1 -1
- package/dist/esm/circuit-breaker.js +45 -3
- package/dist/esm/circuit-breaker.js.map +1 -1
- package/dist/esm/client.js +387 -104
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/cookie-parser.js +103 -5
- package/dist/esm/cookie-parser.js.map +1 -1
- package/dist/esm/cookie-store.js +125 -28
- package/dist/esm/cookie-store.js.map +1 -1
- package/dist/esm/core.js +465 -66
- package/dist/esm/core.js.map +1 -1
- package/dist/esm/dedup.js +49 -11
- package/dist/esm/dedup.js.map +1 -1
- package/dist/esm/digest.js +160 -24
- 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 +303 -45
- package/dist/esm/headers.js.map +1 -1
- package/dist/esm/interceptors.js +221 -7
- 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 +168 -15
- 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 +247 -22
- package/dist/esm/pagination.js.map +1 -1
- package/dist/esm/progress.js +177 -27
- 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 +131 -15
- package/dist/esm/socks5.js.map +1 -1
- package/dist/esm/sse.js +173 -43
- 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/ws.js +19 -10
- package/dist/esm/ws.js.map +1 -1
- package/dist/types/aws-sigv4.d.ts.map +1 -1
- package/dist/types/cache.d.ts +19 -1
- 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 +69 -11
- 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 +103 -25
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/dedup.d.ts.map +1 -1
- package/dist/types/digest.d.ts +17 -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 +45 -27
- 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 +22 -3
- 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 +114 -3
- 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/ws.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
@@ -1873,14 +1921,23 @@ export function parseSetCookieHeader(header) {
|
|
|
1873
1921
|
if (eqIdx === -1) {
|
|
1874
1922
|
// No equals sign at all — treat as value-only (empty name)
|
|
1875
1923
|
name = "";
|
|
1876
|
-
value = nameValueStr
|
|
1924
|
+
value = nameValueStr;
|
|
1877
1925
|
}
|
|
1878
1926
|
else {
|
|
1879
1927
|
name = nameValueStr.slice(0, eqIdx).trim();
|
|
1880
|
-
value = nameValueStr.slice(eqIdx + 1)
|
|
1928
|
+
value = nameValueStr.slice(eqIdx + 1);
|
|
1881
1929
|
}
|
|
1882
1930
|
if (!isValidCookieName(name))
|
|
1883
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();
|
|
1884
1941
|
// Strip surrounding double quotes from value (RFC 6265 §5.2 step 3)
|
|
1885
1942
|
if (value.length >= 2 && value[0] === '"' && value[value.length - 1] === '"') {
|
|
1886
1943
|
value = value.slice(1, -1);
|
|
@@ -2063,6 +2120,31 @@ export function formatSetCookieHeader(cookie) {
|
|
|
2063
2120
|
* @param raw - Raw Set-Cookie header string (possibly containing multiple cookies)
|
|
2064
2121
|
* @returns Array of individual Set-Cookie strings
|
|
2065
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
|
+
}
|
|
2066
2148
|
export function splitSetCookieHeaders(raw) {
|
|
2067
2149
|
const cookies = [];
|
|
2068
2150
|
let buffer = "";
|
|
@@ -2114,6 +2196,22 @@ export function splitSetCookieHeaders(raw) {
|
|
|
2114
2196
|
continue;
|
|
2115
2197
|
}
|
|
2116
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
|
+
}
|
|
2117
2215
|
commitCookie();
|
|
2118
2216
|
while (i + 1 < raw.length && /\s/.test(raw[i + 1])) {
|
|
2119
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":
|
|
@@ -324,6 +347,16 @@ export class CookieJar {
|
|
|
324
347
|
if (cookie.name.startsWith("__Host-") &&
|
|
325
348
|
!(cookie.secure && isSecure && cookie.path === "/" && cookie.hostOnly))
|
|
326
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;
|
|
327
360
|
result.push(cookie);
|
|
328
361
|
}
|
|
329
362
|
}
|
|
@@ -336,13 +369,23 @@ export class CookieJar {
|
|
|
336
369
|
return a.createdAt - b.createdAt;
|
|
337
370
|
});
|
|
338
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.
|
|
339
379
|
const ts = Date.now();
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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 };
|
|
343
386
|
c.lastAccessed = ts;
|
|
344
|
-
|
|
345
|
-
|
|
387
|
+
return snapshot;
|
|
388
|
+
});
|
|
346
389
|
}
|
|
347
390
|
// --------------------------------------------------------------------------
|
|
348
391
|
// 7.3 getCookieHeader
|
|
@@ -374,15 +417,38 @@ export class CookieJar {
|
|
|
374
417
|
getCookiesForDomain(domain) {
|
|
375
418
|
const d = canonicalizeDomainFull(domain);
|
|
376
419
|
const result = [];
|
|
420
|
+
const now = Date.now();
|
|
377
421
|
for (const [stored, pm] of this.dm) {
|
|
378
|
-
//
|
|
379
|
-
|
|
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)) {
|
|
380
432
|
for (const [, nm] of pm) {
|
|
381
433
|
for (const [, cookie] of nm) {
|
|
382
434
|
// Skip expired cookies
|
|
383
|
-
if (cookie.expires !== Infinity && cookie.expires <
|
|
435
|
+
if (cookie.expires !== Infinity && cookie.expires < now)
|
|
384
436
|
continue;
|
|
385
|
-
|
|
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))
|
|
447
|
+
continue;
|
|
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 });
|
|
386
452
|
}
|
|
387
453
|
}
|
|
388
454
|
}
|
|
@@ -593,7 +659,16 @@ export class CookieJar {
|
|
|
593
659
|
maxAge: item.maxAge,
|
|
594
660
|
secure: item.secure,
|
|
595
661
|
httpOnly: item.httpOnly,
|
|
596
|
-
|
|
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),
|
|
597
672
|
createdAt: item.createdAt,
|
|
598
673
|
lastAccessed: item.lastAccessed,
|
|
599
674
|
hostOnly: item.hostOnly,
|
|
@@ -654,31 +729,53 @@ export class CookieJar {
|
|
|
654
729
|
*/
|
|
655
730
|
putCookie(cookie) {
|
|
656
731
|
const { domain, path, name } = cookie;
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
// 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;
|
|
668
742
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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);
|
|
675
767
|
}
|
|
768
|
+
nm.set(name, cookie);
|
|
769
|
+
this.total++;
|
|
676
770
|
}
|
|
677
771
|
/**
|
|
678
772
|
* Evict the least-recently-accessed cookie from a domain when
|
|
679
773
|
* the per-domain cap is exceeded. Evicts down to maxPerDomain - 1.
|
|
680
774
|
*/
|
|
681
|
-
evictForDomain(
|
|
775
|
+
evictForDomain(domain) {
|
|
776
|
+
const pm = this.dm.get(domain);
|
|
777
|
+
if (!pm)
|
|
778
|
+
return;
|
|
682
779
|
let count = 0;
|
|
683
780
|
for (const nm of pm.values())
|
|
684
781
|
count += nm.size;
|