kinetex 0.0.2 → 1.0.0-rc.2
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/LICENSE +16 -4
- package/README.md +2434 -701
- package/dist/browser/kinetex.esm.js +86 -0
- package/dist/browser/kinetex.js +19056 -1999
- package/dist/browser/kinetex.min.js +85 -8
- package/dist/cjs/aws-sigv4.js +903 -0
- package/dist/cjs/cache.js +1099 -0
- package/dist/cjs/circuit-breaker.js +362 -0
- package/dist/cjs/client.js +2890 -0
- package/dist/cjs/cookie-parser.js +2182 -0
- package/dist/cjs/cookie-store.js +783 -0
- package/dist/cjs/cookiejar.js +175 -0
- package/dist/cjs/core.js +1267 -0
- package/dist/cjs/dedup.js +267 -0
- package/dist/cjs/digest.js +552 -0
- package/dist/cjs/graphql.js +1086 -0
- package/dist/cjs/headers.js +2264 -0
- package/dist/cjs/interceptors.js +1115 -0
- package/dist/cjs/lifecycle.js +886 -0
- package/dist/cjs/logging.js +887 -0
- package/dist/cjs/mod.js +98 -0
- package/dist/cjs/package.json +1 -3
- package/dist/cjs/pagination.js +535 -0
- package/dist/cjs/progress.js +694 -0
- package/dist/cjs/response.js +1212 -0
- package/dist/cjs/socks5.js +681 -0
- package/dist/cjs/sse.js +827 -0
- package/dist/cjs/types.js +217 -0
- package/dist/cjs/url.js +1269 -0
- package/dist/cjs/utils.js +901 -0
- package/dist/cjs/worker.js +56 -0
- package/dist/cjs/ws.js +1099 -0
- package/dist/esm/aws-sigv4.js +904 -0
- package/dist/esm/aws-sigv4.js.map +1 -0
- package/dist/esm/cache.js +1100 -0
- package/dist/esm/cache.js.map +1 -0
- package/dist/esm/circuit-breaker.js +363 -0
- package/dist/esm/circuit-breaker.js.map +1 -0
- package/dist/esm/client.js +2891 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/cookie-parser.js +2183 -0
- package/dist/esm/cookie-parser.js.map +1 -0
- package/dist/esm/cookie-store.js +784 -0
- package/dist/esm/cookie-store.js.map +1 -0
- package/dist/esm/cookiejar.js +176 -0
- package/dist/esm/cookiejar.js.map +1 -0
- package/dist/esm/core.js +1268 -0
- package/dist/esm/core.js.map +1 -0
- package/dist/esm/dedup.js +268 -0
- package/dist/esm/dedup.js.map +1 -0
- package/dist/esm/digest.js +553 -0
- package/dist/esm/digest.js.map +1 -0
- package/dist/esm/graphql.js +1087 -0
- package/dist/esm/graphql.js.map +1 -0
- package/dist/esm/headers.js +2265 -0
- package/dist/esm/headers.js.map +1 -0
- package/dist/esm/interceptors.js +1116 -0
- package/dist/esm/interceptors.js.map +1 -0
- package/dist/esm/lifecycle.js +887 -0
- package/dist/esm/lifecycle.js.map +1 -0
- package/dist/esm/logging.js +888 -0
- package/dist/esm/logging.js.map +1 -0
- package/dist/esm/mod.js +99 -0
- package/dist/esm/mod.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/pagination.js +536 -0
- package/dist/esm/pagination.js.map +1 -0
- package/dist/esm/progress.js +695 -0
- package/dist/esm/progress.js.map +1 -0
- package/dist/esm/response.js +1213 -0
- package/dist/esm/response.js.map +1 -0
- package/dist/esm/socks5.js +682 -0
- package/dist/esm/socks5.js.map +1 -0
- package/dist/esm/sse.js +828 -0
- package/dist/esm/sse.js.map +1 -0
- package/dist/esm/types.js +218 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/url.js +1270 -0
- package/dist/esm/url.js.map +1 -0
- package/dist/esm/utils.js +902 -0
- package/dist/esm/utils.js.map +1 -0
- package/dist/esm/worker.js +57 -0
- package/dist/esm/worker.js.map +1 -0
- package/dist/esm/ws.js +1100 -0
- package/dist/esm/ws.js.map +1 -0
- package/dist/types/aws-sigv4.d.ts +382 -0
- package/dist/types/aws-sigv4.d.ts.map +1 -0
- package/dist/types/cache.d.ts +397 -0
- package/dist/types/cache.d.ts.map +1 -0
- package/dist/types/circuit-breaker.d.ts +241 -0
- package/dist/types/circuit-breaker.d.ts.map +1 -0
- package/dist/types/client.d.ts +616 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/cookie-parser.d.ts +203 -0
- package/dist/types/cookie-parser.d.ts.map +1 -0
- package/dist/types/cookie-store.d.ts +316 -0
- package/dist/types/cookie-store.d.ts.map +1 -0
- package/dist/types/cookiejar.d.ts +165 -0
- package/dist/types/cookiejar.d.ts.map +1 -0
- package/dist/types/core.d.ts +297 -0
- package/dist/types/core.d.ts.map +1 -0
- package/dist/types/dedup.d.ts +164 -0
- package/dist/types/dedup.d.ts.map +1 -0
- package/dist/types/digest.d.ts +136 -0
- package/dist/types/digest.d.ts.map +1 -0
- package/dist/types/graphql.d.ts +530 -0
- package/dist/types/graphql.d.ts.map +1 -0
- package/dist/types/headers.d.ts +1119 -0
- package/dist/types/headers.d.ts.map +1 -0
- package/dist/types/interceptors.d.ts +681 -0
- package/dist/types/interceptors.d.ts.map +1 -0
- package/dist/types/lifecycle.d.ts +553 -0
- package/dist/types/lifecycle.d.ts.map +1 -0
- package/dist/types/logging.d.ts +518 -0
- package/dist/types/logging.d.ts.map +1 -0
- package/dist/types/mod.d.ts +92 -0
- package/dist/types/mod.d.ts.map +1 -0
- package/dist/types/pagination.d.ts +401 -0
- package/dist/types/pagination.d.ts.map +1 -0
- package/dist/types/progress.d.ts +344 -0
- package/dist/types/progress.d.ts.map +1 -0
- package/dist/types/response.d.ts +484 -0
- package/dist/types/response.d.ts.map +1 -0
- package/dist/types/socks5.d.ts +218 -0
- package/dist/types/socks5.d.ts.map +1 -0
- package/dist/types/sse.d.ts +399 -0
- package/dist/types/sse.d.ts.map +1 -0
- package/dist/types/types.d.ts +967 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/url.d.ts +530 -0
- package/dist/types/url.d.ts.map +1 -0
- package/dist/types/utils.d.ts +300 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/dist/types/worker.d.ts +55 -0
- package/dist/types/worker.d.ts.map +1 -0
- package/dist/types/ws.d.ts +480 -0
- package/dist/types/ws.d.ts.map +1 -0
- package/package.json +194 -127
- package/CHANGELOG.md +0 -50
- package/SECURITY.md +0 -31
- package/dist/browser/kinetex.cjs.js +0 -9
- package/dist/browser/kinetex.esm.min.js +0 -9
- package/dist/browser/kinetex.js.map +0 -7
- package/dist/cjs/auth/index.cjs +0 -333
- package/dist/cjs/auth/index.d.cts +0 -41
- package/dist/cjs/auth/index.d.ts.map +0 -1
- package/dist/cjs/auth/index.js.map +0 -1
- package/dist/cjs/core/chain.cjs +0 -127
- package/dist/cjs/core/chain.d.cts +0 -44
- package/dist/cjs/core/chain.d.ts.map +0 -1
- package/dist/cjs/core/chain.js.map +0 -1
- package/dist/cjs/core/transport.cjs +0 -544
- package/dist/cjs/core/transport.d.cts +0 -11
- package/dist/cjs/core/transport.d.ts.map +0 -1
- package/dist/cjs/core/transport.js.map +0 -1
- package/dist/cjs/core/types.cjs +0 -54
- package/dist/cjs/core/types.d.cts +0 -322
- package/dist/cjs/core/types.d.ts.map +0 -1
- package/dist/cjs/core/types.js.map +0 -1
- package/dist/cjs/index.cjs +0 -272
- package/dist/cjs/index.d.cts +0 -12
- package/dist/cjs/index.d.ts.map +0 -1
- package/dist/cjs/index.js.map +0 -1
- package/dist/cjs/middleware/pipeline.cjs +0 -451
- package/dist/cjs/middleware/pipeline.d.cts +0 -32
- package/dist/cjs/middleware/pipeline.d.ts.map +0 -1
- package/dist/cjs/middleware/pipeline.js.map +0 -1
- package/dist/cjs/plugins/cookies.cjs +0 -192
- package/dist/cjs/plugins/cookies.d.cts +0 -57
- package/dist/cjs/plugins/cookies.d.ts.map +0 -1
- package/dist/cjs/plugins/cookies.js.map +0 -1
- package/dist/cjs/plugins/extras.cjs +0 -316
- package/dist/cjs/plugins/extras.d.cts +0 -87
- package/dist/cjs/plugins/extras.d.ts.map +0 -1
- package/dist/cjs/plugins/extras.js.map +0 -1
- package/dist/cjs/plugins/index.cjs +0 -18
- package/dist/cjs/plugins/index.d.cts +0 -7
- package/dist/cjs/plugins/index.d.ts.map +0 -1
- package/dist/cjs/plugins/index.js.map +0 -1
- package/dist/cjs/plugins/proxy.cjs +0 -161
- package/dist/cjs/plugins/proxy.d.cts +0 -25
- package/dist/cjs/plugins/proxy.d.ts.map +0 -1
- package/dist/cjs/plugins/proxy.js.map +0 -1
- package/dist/cjs/utils/index.cjs +0 -12
- package/dist/cjs/utils/index.d.cts +0 -6
- package/dist/cjs/utils/index.d.ts.map +0 -1
- package/dist/cjs/utils/index.js.map +0 -1
- package/dist/esm/auth/index.js +0 -324
- package/dist/esm/auth/index.js.map +0 -1
- package/dist/esm/core/chain.js +0 -123
- package/dist/esm/core/chain.js.map +0 -1
- package/dist/esm/core/transport.js +0 -536
- package/dist/esm/core/transport.js.map +0 -1
- package/dist/esm/core/types.js +0 -46
- package/dist/esm/core/types.js.map +0 -1
- package/dist/esm/index.js +0 -241
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/middleware/pipeline.js +0 -438
- package/dist/esm/middleware/pipeline.js.map +0 -1
- package/dist/esm/plugins/cookies.js +0 -187
- package/dist/esm/plugins/cookies.js.map +0 -1
- package/dist/esm/plugins/extras.js +0 -306
- package/dist/esm/plugins/extras.js.map +0 -1
- package/dist/esm/plugins/index.js +0 -4
- package/dist/esm/plugins/index.js.map +0 -1
- package/dist/esm/plugins/proxy.js +0 -124
- package/dist/esm/plugins/proxy.js.map +0 -1
- package/dist/esm/utils/index.js +0 -6
- package/dist/esm/utils/index.js.map +0 -1
- package/dist/types/auth/index.d.ts +0 -41
- package/dist/types/auth/index.d.ts.map +0 -1
- package/dist/types/core/chain.d.ts +0 -44
- package/dist/types/core/chain.d.ts.map +0 -1
- package/dist/types/core/transport.d.ts +0 -11
- package/dist/types/core/transport.d.ts.map +0 -1
- package/dist/types/core/types.d.ts +0 -322
- package/dist/types/core/types.d.ts.map +0 -1
- package/dist/types/index.d.ts +0 -12
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/middleware/pipeline.d.ts +0 -32
- package/dist/types/middleware/pipeline.d.ts.map +0 -1
- package/dist/types/plugins/cookies.d.ts +0 -57
- package/dist/types/plugins/cookies.d.ts.map +0 -1
- package/dist/types/plugins/extras.d.ts +0 -87
- package/dist/types/plugins/extras.d.ts.map +0 -1
- package/dist/types/plugins/index.d.ts +0 -7
- package/dist/types/plugins/index.d.ts.map +0 -1
- package/dist/types/plugins/proxy.d.ts +0 -25
- package/dist/types/plugins/proxy.d.ts.map +0 -1
- package/dist/types/utils/index.d.ts +0 -6
- package/dist/types/utils/index.d.ts.map +0 -1
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cookie-store.ts
|
|
3
|
+
*
|
|
4
|
+
* Full RFC 6265 §5.3 + §5.4 implementation:
|
|
5
|
+
* - Storage model (creation time, last access, persistent vs session)
|
|
6
|
+
* - Cookie retrieval algorithm (domain, path, secure, httpOnly, SameSite)
|
|
7
|
+
* - __Secure- / __Host- prefix enforcement (RFC 6265bis §4.1.3)
|
|
8
|
+
* - SameSite Strict / Lax / None enforcement (full state machine)
|
|
9
|
+
* - LRU eviction: per-domain cap (50) + global cap (3000)
|
|
10
|
+
* - Atomic update semantics (update preserves createdAt)
|
|
11
|
+
* - Full JSON serialization / deserialization with expiry filtering
|
|
12
|
+
* - clear / clearExpired / clearSession / clearForDomain / clearForUrl
|
|
13
|
+
*/
|
|
14
|
+
import { parseSetCookieHeader, canonicalizeDomainFull, defaultPath, pathMatch, domainMatch, isPublicSuffix, isIPAddress, extractSetCookieHeaders, } from "./cookie-parser.js";
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// 3. CONSTANTS
|
|
17
|
+
// ============================================================================
|
|
18
|
+
/**
|
|
19
|
+
* Maximum cookie size in bytes per RFC 6265.
|
|
20
|
+
* User agents may impose a 4096-byte limit on the combined size of
|
|
21
|
+
* the cookie name, "=" separator, and value.
|
|
22
|
+
*/
|
|
23
|
+
const MAX_COOKIE_SIZE = 4096;
|
|
24
|
+
const MAX_PER_DOMAIN = 50; // Max cookies per domain
|
|
25
|
+
const MAX_TOTAL = 3000; // Max total cookies across all domains
|
|
26
|
+
const MAX_COOKIE_AGE = 400 * 24 * 60 * 60 * 1000; // 400 days in ms (Chrome cap)
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// 4. PREFIX ENFORCEMENT — RFC 6265bis §4.1.3
|
|
29
|
+
// ============================================================================
|
|
30
|
+
/**
|
|
31
|
+
* Enforce __Secure- and __Host- prefix rules per RFC 6265bis §4.1.3.
|
|
32
|
+
*
|
|
33
|
+
* __Secure- requires Secure flag + secure context.
|
|
34
|
+
* __Host- requires Secure + secure context + Path=/ + no Domain attribute.
|
|
35
|
+
*
|
|
36
|
+
* @param cookie - The cookie to validate
|
|
37
|
+
* @param requestUrl - URL of the request that set the cookie
|
|
38
|
+
* @returns true if the cookie passes all prefix rules
|
|
39
|
+
*/
|
|
40
|
+
function enforcePrefixRules(cookie, requestUrl) {
|
|
41
|
+
const name = cookie.name;
|
|
42
|
+
if (name.startsWith("__Secure-")) {
|
|
43
|
+
// Must be Secure
|
|
44
|
+
if (!cookie.secure)
|
|
45
|
+
return false;
|
|
46
|
+
// Must be from a secure context
|
|
47
|
+
if (requestUrl.protocol !== "https:")
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (name.startsWith("__Host-")) {
|
|
51
|
+
// Must be Secure
|
|
52
|
+
if (!cookie.secure)
|
|
53
|
+
return false;
|
|
54
|
+
// Must be from a secure context
|
|
55
|
+
if (requestUrl.protocol !== "https:")
|
|
56
|
+
return false;
|
|
57
|
+
// Must have Path=/
|
|
58
|
+
if (cookie.path !== "/")
|
|
59
|
+
return false;
|
|
60
|
+
// Must NOT have a Domain attribute (i.e. hostOnly must be true)
|
|
61
|
+
if (!cookie.hostOnly)
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// 5. SAMESITE ENFORCEMENT — RFC 6265bis §5.3.7
|
|
68
|
+
// ============================================================================
|
|
69
|
+
/**
|
|
70
|
+
* Determines whether a cookie with the given SameSite attribute
|
|
71
|
+
* should be sent in a request with the given context.
|
|
72
|
+
*
|
|
73
|
+
* sameSite = Strict → only send in strict same-site context
|
|
74
|
+
* sameSite = Lax → send in strict + lax (cross-site safe top-level nav)
|
|
75
|
+
* sameSite = None → send in all contexts (requires Secure)
|
|
76
|
+
* sameSite = Unset → treated as Lax by modern browsers (we follow suit)
|
|
77
|
+
*
|
|
78
|
+
* @param sameSite - The cookie's SameSite attribute
|
|
79
|
+
* @param context - The request's SameSite context
|
|
80
|
+
* @returns true if the cookie may be sent
|
|
81
|
+
*/
|
|
82
|
+
function sameSiteAllows(sameSite, context) {
|
|
83
|
+
switch (sameSite) {
|
|
84
|
+
case "Strict":
|
|
85
|
+
return context === "strict";
|
|
86
|
+
case "Lax":
|
|
87
|
+
// Lax allows strict context and cross-site safe top-level navigation
|
|
88
|
+
return context === "strict" || context === "lax";
|
|
89
|
+
case "Unset":
|
|
90
|
+
// Unset: treated permissively — allow same-site and "none" (navigation/direct)
|
|
91
|
+
// Block only explicit cross-site non-navigation requests
|
|
92
|
+
return context !== "cross-site";
|
|
93
|
+
case "None":
|
|
94
|
+
// None: send in all contexts (Secure requirement enforced at set time)
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* RFC 6265 §5.3 + §5.4 cookie jar implementation.
|
|
100
|
+
*
|
|
101
|
+
* Stores cookies in a three-level map (domain → path → name) with:
|
|
102
|
+
* - LRU eviction (per-domain cap of 50, global cap of 3000)
|
|
103
|
+
* - __Secure- / __Host- prefix enforcement (RFC 6265bis §4.1.3)
|
|
104
|
+
* - SameSite Strict / Lax / None enforcement
|
|
105
|
+
* - JSON serialization / deserialization
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* const jar = new CookieJar();
|
|
109
|
+
* jar.setCookie("session=abc; Path=/; Secure", { url: "https://example.com/" });
|
|
110
|
+
* const cookies = jar.getCookies({ url: "https://example.com/page" });
|
|
111
|
+
*/
|
|
112
|
+
export class CookieJar {
|
|
113
|
+
dm = new Map();
|
|
114
|
+
total = 0;
|
|
115
|
+
cleanupTimer = null;
|
|
116
|
+
// Customizable limits
|
|
117
|
+
maxTotal;
|
|
118
|
+
maxPerDomain;
|
|
119
|
+
domainMatcherFn;
|
|
120
|
+
/**
|
|
121
|
+
* @param options - Cookie jar limits and optional custom domain matcher
|
|
122
|
+
*/
|
|
123
|
+
constructor(options = {}) {
|
|
124
|
+
this.maxTotal = options.maxTotal ?? MAX_TOTAL;
|
|
125
|
+
this.maxPerDomain = options.maxPerDomain ?? MAX_PER_DOMAIN;
|
|
126
|
+
this.domainMatcherFn = options.domainMatcher ?? null;
|
|
127
|
+
// Set up periodic cleanup (1% chance on each cookie access + every 5 minutes)
|
|
128
|
+
this.cleanupTimer = setInterval(() => {
|
|
129
|
+
this.clearExpired();
|
|
130
|
+
}, 5 * 60 * 1000);
|
|
131
|
+
// Allow process to exit even if timer is running (Node.js only)
|
|
132
|
+
if (typeof this.cleanupTimer === "object" &&
|
|
133
|
+
this.cleanupTimer !== null &&
|
|
134
|
+
"unref" in this.cleanupTimer) {
|
|
135
|
+
this.cleanupTimer.unref();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Destroy the jar — clears the periodic cleanup timer.
|
|
140
|
+
* Call this when the jar is no longer needed to prevent memory leaks.
|
|
141
|
+
*/
|
|
142
|
+
destroy() {
|
|
143
|
+
if (this.cleanupTimer) {
|
|
144
|
+
clearInterval(this.cleanupTimer);
|
|
145
|
+
this.cleanupTimer = null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// --------------------------------------------------------------------------
|
|
149
|
+
// 7.1 setCookie — RFC 6265 §5.3
|
|
150
|
+
// --------------------------------------------------------------------------
|
|
151
|
+
/**
|
|
152
|
+
* Set a cookie per RFC 6265 §5.3.
|
|
153
|
+
*
|
|
154
|
+
* Rejects cookies that exceed size limits, fail domain/path validation,
|
|
155
|
+
* violate prefix rules, or would be immediately expired (treats those as deletes).
|
|
156
|
+
*
|
|
157
|
+
* @param header - Raw Set-Cookie header value (e.g. "session=abc; Path=/; Secure")
|
|
158
|
+
* @param options - URL the cookie was received from + SameSite context
|
|
159
|
+
* @returns true if the cookie was stored, false if rejected
|
|
160
|
+
*/
|
|
161
|
+
setCookie(header, options) {
|
|
162
|
+
const url = safeParseUrl(options.url);
|
|
163
|
+
if (!url)
|
|
164
|
+
return false;
|
|
165
|
+
const parsed = parseSetCookieHeader(header);
|
|
166
|
+
if (!parsed)
|
|
167
|
+
return false;
|
|
168
|
+
const now = Date.now();
|
|
169
|
+
const reqHost = url.hostname.toLowerCase();
|
|
170
|
+
const reqHostBare = reqHost.replace(/^\[|\]$/g, "");
|
|
171
|
+
const isSecureCtx = url.protocol === "https:" ||
|
|
172
|
+
reqHostBare === "localhost" ||
|
|
173
|
+
reqHostBare === "127.0.0.1" ||
|
|
174
|
+
reqHostBare === "::1";
|
|
175
|
+
// §5.3 step 1: validate name/value size
|
|
176
|
+
const rawSize = parsed.name.length + 1 + parsed.value.length;
|
|
177
|
+
if (rawSize > MAX_COOKIE_SIZE)
|
|
178
|
+
return false;
|
|
179
|
+
// §5.3 step 2: determine cookie-domain and host-only-flag
|
|
180
|
+
let cookieDomain;
|
|
181
|
+
let hostOnly;
|
|
182
|
+
if (parsed.domain !== null && parsed.domain !== "") {
|
|
183
|
+
const cd = parsed.domain;
|
|
184
|
+
// Must domain-match the request host (use custom matcher if provided)
|
|
185
|
+
const matcher = this.domainMatcherFn ?? domainMatch;
|
|
186
|
+
if (!matcher(reqHost, cd))
|
|
187
|
+
return false;
|
|
188
|
+
// Must not be a public suffix
|
|
189
|
+
if (isPublicSuffix(cd))
|
|
190
|
+
return false;
|
|
191
|
+
cookieDomain = cd;
|
|
192
|
+
hostOnly = false;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
cookieDomain = reqHost;
|
|
196
|
+
hostOnly = true;
|
|
197
|
+
}
|
|
198
|
+
// §5.3 step 3: determine cookie-path
|
|
199
|
+
const cookiePath = parsed.path ?? defaultPath(url.pathname);
|
|
200
|
+
// §5.3 step 4: Secure attribute
|
|
201
|
+
const secure = parsed.secure;
|
|
202
|
+
if (secure && !isSecureCtx)
|
|
203
|
+
return false;
|
|
204
|
+
// §5.3 step 5: HttpOnly — caller signals HTTP context implicitly by calling us
|
|
205
|
+
// §5.3 step 6: SameSite
|
|
206
|
+
const sameSite = parsed.sameSite;
|
|
207
|
+
// SameSite=None requires Secure
|
|
208
|
+
if (sameSite === "None" && !secure)
|
|
209
|
+
return false;
|
|
210
|
+
// §5.3 step 7: compute expiry
|
|
211
|
+
let expires;
|
|
212
|
+
if (parsed.maxAge !== null) {
|
|
213
|
+
// Max-Age takes precedence over Expires
|
|
214
|
+
if (parsed.maxAge <= 0) {
|
|
215
|
+
// Delete the cookie
|
|
216
|
+
this.removeCookie(cookieDomain, cookiePath, parsed.name);
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
// Cap at 400 days (Chrome / Firefox behaviour)
|
|
220
|
+
const ageMs = Math.min(parsed.maxAge * 1000, MAX_COOKIE_AGE);
|
|
221
|
+
expires = now + ageMs;
|
|
222
|
+
}
|
|
223
|
+
else if (parsed.expires !== null) {
|
|
224
|
+
expires = parsed.expires;
|
|
225
|
+
if (expires <= now) {
|
|
226
|
+
// Already expired — treat as delete
|
|
227
|
+
this.removeCookie(cookieDomain, cookiePath, parsed.name);
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
// Cap at 400 days from now
|
|
231
|
+
expires = Math.min(expires, now + MAX_COOKIE_AGE);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
expires = Infinity; // session cookie
|
|
235
|
+
}
|
|
236
|
+
// §5.3 step 8: build cookie object
|
|
237
|
+
const cookie = {
|
|
238
|
+
name: parsed.name,
|
|
239
|
+
value: parsed.value,
|
|
240
|
+
domain: cookieDomain,
|
|
241
|
+
path: cookiePath,
|
|
242
|
+
expires,
|
|
243
|
+
maxAge: parsed.maxAge,
|
|
244
|
+
secure,
|
|
245
|
+
httpOnly: parsed.httpOnly,
|
|
246
|
+
sameSite,
|
|
247
|
+
createdAt: now,
|
|
248
|
+
lastAccessed: now,
|
|
249
|
+
hostOnly,
|
|
250
|
+
};
|
|
251
|
+
// RFC 6265bis prefix rules
|
|
252
|
+
if (!enforcePrefixRules(cookie, url))
|
|
253
|
+
return false;
|
|
254
|
+
// §5.3 step 11-12: store (evicting old entry if same name/domain/path)
|
|
255
|
+
this.putCookie(cookie);
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
// --------------------------------------------------------------------------
|
|
259
|
+
// 7.2 getCookies — RFC 6265 §5.4
|
|
260
|
+
// --------------------------------------------------------------------------
|
|
261
|
+
/**
|
|
262
|
+
* Retrieve matching cookies per RFC 6265 §5.4.
|
|
263
|
+
*
|
|
264
|
+
* Filters by domain, path, secure/httpOnly flags, and SameSite context.
|
|
265
|
+
* Results are sorted by longest path first, then oldest creation time.
|
|
266
|
+
* Updates lastAccessed on returned cookies.
|
|
267
|
+
*
|
|
268
|
+
* @param options - URL, protocol context, and SameSite context
|
|
269
|
+
* @returns Array of matching Cookie objects (direct references into storage)
|
|
270
|
+
*/
|
|
271
|
+
getCookies(options) {
|
|
272
|
+
// Lazy cleanup: 1% chance to clear expired cookies on each access
|
|
273
|
+
if (Math.random() < 0.01) {
|
|
274
|
+
this.clearExpired();
|
|
275
|
+
}
|
|
276
|
+
const url = safeParseUrl(options.url);
|
|
277
|
+
if (!url)
|
|
278
|
+
return [];
|
|
279
|
+
const reqHost = url.hostname.toLowerCase();
|
|
280
|
+
const reqHostBare = reqHost.replace(/^\[|\]$/g, "");
|
|
281
|
+
const reqPath = url.pathname || "/";
|
|
282
|
+
const isSecure = url.protocol === "https:" ||
|
|
283
|
+
reqHostBare === "localhost" ||
|
|
284
|
+
reqHostBare === "127.0.0.1" ||
|
|
285
|
+
reqHostBare === "::1";
|
|
286
|
+
const isHttp = options.http !== false;
|
|
287
|
+
const context = options.sameSiteContext ?? "none";
|
|
288
|
+
const now = Date.now();
|
|
289
|
+
const result = [];
|
|
290
|
+
for (const [domain, pm] of this.dm) {
|
|
291
|
+
// §5.4 step 1: domain match
|
|
292
|
+
if (!this.matchDomain(reqHost, domain))
|
|
293
|
+
continue;
|
|
294
|
+
for (const [, nm] of pm) {
|
|
295
|
+
for (const [, cookie] of nm) {
|
|
296
|
+
// §5.4 step 2a: hostOnly check
|
|
297
|
+
if (cookie.hostOnly && reqHost !== cookie.domain)
|
|
298
|
+
continue;
|
|
299
|
+
// §5.4 step 2b: expiry check
|
|
300
|
+
if (cookie.expires !== Infinity && cookie.expires < now)
|
|
301
|
+
continue;
|
|
302
|
+
// §5.4 step 2b: path match
|
|
303
|
+
if (!pathMatch(reqPath, cookie.path))
|
|
304
|
+
continue;
|
|
305
|
+
// §5.4 step 2c: secure flag
|
|
306
|
+
if (cookie.secure && !isSecure)
|
|
307
|
+
continue;
|
|
308
|
+
// §5.4 step 2d: httpOnly flag
|
|
309
|
+
if (cookie.httpOnly && !isHttp)
|
|
310
|
+
continue;
|
|
311
|
+
// §5.4 step 2e: SameSite
|
|
312
|
+
if (!sameSiteAllows(cookie.sameSite, context))
|
|
313
|
+
continue;
|
|
314
|
+
result.push(cookie);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// §5.4 step 2 sort: longer path first, then older createdAt
|
|
319
|
+
result.sort((a, b) => {
|
|
320
|
+
const pd = b.path.length - a.path.length;
|
|
321
|
+
if (pd !== 0)
|
|
322
|
+
return pd;
|
|
323
|
+
return a.createdAt - b.createdAt;
|
|
324
|
+
});
|
|
325
|
+
// §5.4 step 3: update last-access time
|
|
326
|
+
const ts = Date.now();
|
|
327
|
+
for (const c of result) {
|
|
328
|
+
// Note: getCookies returns direct references to stored cookies, not copies.
|
|
329
|
+
// Modifying lastAccessed here directly updates the stored cookie.
|
|
330
|
+
c.lastAccessed = ts;
|
|
331
|
+
}
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
// --------------------------------------------------------------------------
|
|
335
|
+
// 7.3 getCookieHeader
|
|
336
|
+
// --------------------------------------------------------------------------
|
|
337
|
+
/**
|
|
338
|
+
* Get a Cookie header string for a request.
|
|
339
|
+
*
|
|
340
|
+
* @param options - URL and request context
|
|
341
|
+
* @returns "name=value; name=value" string suitable for a Cookie header
|
|
342
|
+
*/
|
|
343
|
+
getCookieHeader(options) {
|
|
344
|
+
return this.getCookies(options)
|
|
345
|
+
.map((c) => (c.name ? `${c.name}=${c.value}` : c.value))
|
|
346
|
+
.join("; ");
|
|
347
|
+
}
|
|
348
|
+
// --------------------------------------------------------------------------
|
|
349
|
+
// 7.4a getCookiesForDomain
|
|
350
|
+
// --------------------------------------------------------------------------
|
|
351
|
+
/**
|
|
352
|
+
* Get all cookies for a specific domain.
|
|
353
|
+
*
|
|
354
|
+
* @param domain - The domain to get cookies for (will be canonicalized)
|
|
355
|
+
* @returns Array of cookies that would be sent to this domain
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* jar.setCookie("session=abc", { url: "https://example.com/" });
|
|
359
|
+
* jar.getCookiesForDomain("example.com"); // Returns cookies for example.com
|
|
360
|
+
*/
|
|
361
|
+
getCookiesForDomain(domain) {
|
|
362
|
+
const d = canonicalizeDomainFull(domain);
|
|
363
|
+
const result = [];
|
|
364
|
+
for (const [stored, pm] of this.dm) {
|
|
365
|
+
// Match domain or its subdomains
|
|
366
|
+
if (stored === d || stored.endsWith("." + d)) {
|
|
367
|
+
for (const [, nm] of pm) {
|
|
368
|
+
for (const [, cookie] of nm) {
|
|
369
|
+
// Skip expired cookies
|
|
370
|
+
if (cookie.expires !== Infinity && cookie.expires < Date.now())
|
|
371
|
+
continue;
|
|
372
|
+
result.push(cookie);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
// --------------------------------------------------------------------------
|
|
380
|
+
// 7.4 processResponseHeaders
|
|
381
|
+
// --------------------------------------------------------------------------
|
|
382
|
+
/**
|
|
383
|
+
* Process Set-Cookie headers from an HTTP response.
|
|
384
|
+
* Extracts all Set-Cookie values and calls setCookie() for each.
|
|
385
|
+
*
|
|
386
|
+
* @param headers - Response headers (Headers object or plain record)
|
|
387
|
+
* @param options - URL and SameSite context for all cookies
|
|
388
|
+
*/
|
|
389
|
+
processResponseHeaders(headers, options) {
|
|
390
|
+
for (const h of extractSetCookieHeaders(headers)) {
|
|
391
|
+
this.setCookie(h, options);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// --------------------------------------------------------------------------
|
|
395
|
+
// 7.5 removeCookie
|
|
396
|
+
// --------------------------------------------------------------------------
|
|
397
|
+
/**
|
|
398
|
+
* Remove a specific cookie by domain, path, and name.
|
|
399
|
+
*
|
|
400
|
+
* @param domain - Domain the cookie was stored under
|
|
401
|
+
* @param path - Cookie path
|
|
402
|
+
* @param name - Cookie name
|
|
403
|
+
* @returns true if a cookie was removed
|
|
404
|
+
*/
|
|
405
|
+
removeCookie(domain, path, name) {
|
|
406
|
+
const d = canonicalizeDomainFull(domain);
|
|
407
|
+
const pm = this.dm.get(d);
|
|
408
|
+
if (!pm)
|
|
409
|
+
return false;
|
|
410
|
+
const nm = pm.get(path);
|
|
411
|
+
if (!nm)
|
|
412
|
+
return false;
|
|
413
|
+
const deleted = nm.delete(name);
|
|
414
|
+
if (deleted) {
|
|
415
|
+
this.total--;
|
|
416
|
+
if (nm.size === 0)
|
|
417
|
+
pm.delete(path);
|
|
418
|
+
if (pm.size === 0)
|
|
419
|
+
this.dm.delete(d);
|
|
420
|
+
}
|
|
421
|
+
return deleted;
|
|
422
|
+
}
|
|
423
|
+
// --------------------------------------------------------------------------
|
|
424
|
+
// 7.6 Clear operations
|
|
425
|
+
// --------------------------------------------------------------------------
|
|
426
|
+
/**
|
|
427
|
+
* Remove every cookie from the jar.
|
|
428
|
+
*/
|
|
429
|
+
clear() {
|
|
430
|
+
this.dm.clear();
|
|
431
|
+
this.total = 0;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Remove all expired cookies from the jar.
|
|
435
|
+
*
|
|
436
|
+
* @returns Number of cookies removed
|
|
437
|
+
*/
|
|
438
|
+
clearExpired() {
|
|
439
|
+
const now = Date.now();
|
|
440
|
+
let removed = 0;
|
|
441
|
+
for (const [domain, pm] of this.dm) {
|
|
442
|
+
for (const [path, nm] of pm) {
|
|
443
|
+
for (const [name, cookie] of nm) {
|
|
444
|
+
if (cookie.expires !== Infinity && cookie.expires < now) {
|
|
445
|
+
nm.delete(name);
|
|
446
|
+
this.total--;
|
|
447
|
+
removed++;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (nm.size === 0)
|
|
451
|
+
pm.delete(path);
|
|
452
|
+
}
|
|
453
|
+
if (pm.size === 0)
|
|
454
|
+
this.dm.delete(domain);
|
|
455
|
+
}
|
|
456
|
+
return removed;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Remove all session cookies (cookies with no expiry / Infinity).
|
|
460
|
+
*
|
|
461
|
+
* @returns Number of cookies removed
|
|
462
|
+
*/
|
|
463
|
+
clearSession() {
|
|
464
|
+
let removed = 0;
|
|
465
|
+
for (const [domain, pm] of this.dm) {
|
|
466
|
+
for (const [path, nm] of pm) {
|
|
467
|
+
for (const [name, cookie] of nm) {
|
|
468
|
+
if (cookie.expires === Infinity) {
|
|
469
|
+
nm.delete(name);
|
|
470
|
+
this.total--;
|
|
471
|
+
removed++;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (nm.size === 0)
|
|
475
|
+
pm.delete(path);
|
|
476
|
+
}
|
|
477
|
+
if (pm.size === 0)
|
|
478
|
+
this.dm.delete(domain);
|
|
479
|
+
}
|
|
480
|
+
return removed;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Remove all cookies for a domain and its subdomains.
|
|
484
|
+
*
|
|
485
|
+
* @param domain - Domain to clear (e.g., "example.com" clears sub.example.com too)
|
|
486
|
+
* @returns Number of cookies removed
|
|
487
|
+
*/
|
|
488
|
+
clearForDomain(domain) {
|
|
489
|
+
const d = canonicalizeDomainFull(domain);
|
|
490
|
+
let removed = 0;
|
|
491
|
+
for (const [stored, pm] of this.dm) {
|
|
492
|
+
if (stored === d || stored.endsWith("." + d)) {
|
|
493
|
+
let n = 0;
|
|
494
|
+
for (const nm of pm.values())
|
|
495
|
+
n += nm.size;
|
|
496
|
+
this.dm.delete(stored);
|
|
497
|
+
this.total -= n;
|
|
498
|
+
removed += n;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return removed;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Remove all cookies that would be sent to a given URL.
|
|
505
|
+
*
|
|
506
|
+
* @param url - Full URL whose hostname cookies should be cleared
|
|
507
|
+
* @returns Number of cookies removed
|
|
508
|
+
*/
|
|
509
|
+
clearForUrl(url) {
|
|
510
|
+
const parsed = safeParseUrl(url);
|
|
511
|
+
if (!parsed)
|
|
512
|
+
return 0;
|
|
513
|
+
return this.clearForDomain(parsed.hostname);
|
|
514
|
+
}
|
|
515
|
+
// --------------------------------------------------------------------------
|
|
516
|
+
// 7.7 Serialization
|
|
517
|
+
// --------------------------------------------------------------------------
|
|
518
|
+
/**
|
|
519
|
+
* Serialize all non-expired cookies to a JSON-compatible array.
|
|
520
|
+
* Session cookies (Infinity) are stored with `expires: null`.
|
|
521
|
+
*
|
|
522
|
+
* @returns Array of serialized cookies
|
|
523
|
+
*/
|
|
524
|
+
toJSON() {
|
|
525
|
+
const now = Date.now();
|
|
526
|
+
const out = [];
|
|
527
|
+
for (const [, pm] of this.dm) {
|
|
528
|
+
for (const [, nm] of pm) {
|
|
529
|
+
for (const [, cookie] of nm) {
|
|
530
|
+
if (cookie.expires !== Infinity && cookie.expires < now)
|
|
531
|
+
continue;
|
|
532
|
+
out.push({
|
|
533
|
+
name: cookie.name,
|
|
534
|
+
value: cookie.value,
|
|
535
|
+
domain: cookie.domain,
|
|
536
|
+
path: cookie.path,
|
|
537
|
+
expires: cookie.expires === Infinity ? null : cookie.expires,
|
|
538
|
+
maxAge: cookie.maxAge,
|
|
539
|
+
secure: cookie.secure,
|
|
540
|
+
httpOnly: cookie.httpOnly,
|
|
541
|
+
sameSite: cookie.sameSite,
|
|
542
|
+
createdAt: cookie.createdAt,
|
|
543
|
+
lastAccessed: cookie.lastAccessed,
|
|
544
|
+
hostOnly: cookie.hostOnly,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return out;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Get a pretty-printed JSON string of all cookies.
|
|
553
|
+
*
|
|
554
|
+
* @returns Formatted JSON string
|
|
555
|
+
*/
|
|
556
|
+
toString() {
|
|
557
|
+
return JSON.stringify(this.toJSON(), null, 2);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Deserialize cookies from JSON and create a new CookieJar.
|
|
561
|
+
* Expired cookies in the input are silently skipped.
|
|
562
|
+
*
|
|
563
|
+
* @param data - Array of serialized cookies or a JSON string
|
|
564
|
+
* @returns A new CookieJar populated with the deserialized cookies
|
|
565
|
+
*/
|
|
566
|
+
static fromJSON(data) {
|
|
567
|
+
const jar = new CookieJar();
|
|
568
|
+
const items = (typeof data === "string" ? JSON.parse(data) : data);
|
|
569
|
+
const now = Date.now();
|
|
570
|
+
for (const item of items) {
|
|
571
|
+
const expires = item.expires === null ? Infinity : item.expires;
|
|
572
|
+
if (expires !== Infinity && expires < now)
|
|
573
|
+
continue; // skip expired
|
|
574
|
+
const cookie = {
|
|
575
|
+
name: item.name,
|
|
576
|
+
value: item.value,
|
|
577
|
+
domain: item.domain,
|
|
578
|
+
path: item.path,
|
|
579
|
+
expires,
|
|
580
|
+
maxAge: item.maxAge,
|
|
581
|
+
secure: item.secure,
|
|
582
|
+
httpOnly: item.httpOnly,
|
|
583
|
+
sameSite: item.sameSite,
|
|
584
|
+
createdAt: item.createdAt,
|
|
585
|
+
lastAccessed: item.lastAccessed,
|
|
586
|
+
hostOnly: item.hostOnly,
|
|
587
|
+
};
|
|
588
|
+
jar.putCookie(cookie);
|
|
589
|
+
}
|
|
590
|
+
return jar;
|
|
591
|
+
}
|
|
592
|
+
// --------------------------------------------------------------------------
|
|
593
|
+
// 7.8 Inspection
|
|
594
|
+
// --------------------------------------------------------------------------
|
|
595
|
+
/**
|
|
596
|
+
* Total number of cookies currently stored in the jar.
|
|
597
|
+
*/
|
|
598
|
+
get count() {
|
|
599
|
+
return this.total;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Return a snapshot of all cookies (copies, not references to internal storage).
|
|
603
|
+
*
|
|
604
|
+
* @returns Array of cloned Cookie objects
|
|
605
|
+
*/
|
|
606
|
+
getAll() {
|
|
607
|
+
const out = [];
|
|
608
|
+
for (const [, pm] of this.dm) {
|
|
609
|
+
for (const [, nm] of pm) {
|
|
610
|
+
for (const [, c] of nm)
|
|
611
|
+
out.push({ ...c });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return out;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Return all cookies for a given domain (exact match + subdomains).
|
|
618
|
+
*
|
|
619
|
+
* @param domain - Domain to query (e.g., "example.com")
|
|
620
|
+
* @returns Array of cloned Cookie objects for matching domains
|
|
621
|
+
*/
|
|
622
|
+
getForDomain(domain) {
|
|
623
|
+
const d = canonicalizeDomainFull(domain);
|
|
624
|
+
const out = [];
|
|
625
|
+
for (const [stored, pm] of this.dm) {
|
|
626
|
+
if (stored === d || stored.endsWith("." + d)) {
|
|
627
|
+
for (const [, nm] of pm) {
|
|
628
|
+
for (const [, c] of nm)
|
|
629
|
+
out.push({ ...c });
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return out;
|
|
634
|
+
}
|
|
635
|
+
// --------------------------------------------------------------------------
|
|
636
|
+
// 7.9 Private: storage internals
|
|
637
|
+
// --------------------------------------------------------------------------
|
|
638
|
+
/**
|
|
639
|
+
* Store or update a cookie in the internal map.
|
|
640
|
+
* Preserves original createdAt on update per RFC 6265 §5.3 step 11.
|
|
641
|
+
*/
|
|
642
|
+
putCookie(cookie) {
|
|
643
|
+
const { domain, path, name } = cookie;
|
|
644
|
+
if (!this.dm.has(domain))
|
|
645
|
+
this.dm.set(domain, new Map());
|
|
646
|
+
const pm = this.dm.get(domain);
|
|
647
|
+
if (!pm.has(path))
|
|
648
|
+
pm.set(path, new Map());
|
|
649
|
+
const nm = pm.get(path);
|
|
650
|
+
if (nm.has(name)) {
|
|
651
|
+
// Update: preserve original createdAt (RFC 6265 §5.3 step 11)
|
|
652
|
+
const old = nm.get(name);
|
|
653
|
+
nm.set(name, { ...cookie, createdAt: old.createdAt });
|
|
654
|
+
// total unchanged — same slot
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
// New cookie: enforce caps first
|
|
658
|
+
this.evictForDomain(domain, pm);
|
|
659
|
+
this.evictGlobal();
|
|
660
|
+
nm.set(name, cookie);
|
|
661
|
+
this.total++;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Evict the least-recently-accessed cookie from a domain when
|
|
666
|
+
* the per-domain cap is exceeded. Evicts down to maxPerDomain - 1.
|
|
667
|
+
*/
|
|
668
|
+
evictForDomain(_domain, pm) {
|
|
669
|
+
let count = 0;
|
|
670
|
+
for (const nm of pm.values())
|
|
671
|
+
count += nm.size;
|
|
672
|
+
if (count < this.maxPerDomain)
|
|
673
|
+
return;
|
|
674
|
+
// Collect all cookies for this domain, sort by lastAccessed ascending (LRU first)
|
|
675
|
+
const candidates = [];
|
|
676
|
+
for (const [p, nm] of pm) {
|
|
677
|
+
for (const [n, c] of nm)
|
|
678
|
+
candidates.push([p, n, c]);
|
|
679
|
+
}
|
|
680
|
+
candidates.sort((a, b) => a[2].lastAccessed - b[2].lastAccessed);
|
|
681
|
+
// Evict until we're at maxPerDomain - 1 (leaving room for new cookie)
|
|
682
|
+
let i = 0;
|
|
683
|
+
while (count >= this.maxPerDomain && i < candidates.length) {
|
|
684
|
+
const [p, n] = candidates[i++];
|
|
685
|
+
if (pm.get(p)?.delete(n)) {
|
|
686
|
+
this.total--;
|
|
687
|
+
count--;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
// Prune empty path entries
|
|
691
|
+
for (const [p, nm] of pm) {
|
|
692
|
+
if (nm.size === 0)
|
|
693
|
+
pm.delete(p);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Evict the least-recently-accessed cookie globally when
|
|
698
|
+
* the total cap is exceeded. Evicts down to below maxTotal.
|
|
699
|
+
*/
|
|
700
|
+
evictGlobal() {
|
|
701
|
+
if (this.total < this.maxTotal)
|
|
702
|
+
return;
|
|
703
|
+
// Collect all cookies globally, sort by lastAccessed ascending (LRU first)
|
|
704
|
+
const candidates = [];
|
|
705
|
+
for (const [d, pm] of this.dm) {
|
|
706
|
+
for (const [p, nm] of pm) {
|
|
707
|
+
for (const [n, c] of nm)
|
|
708
|
+
candidates.push([d, p, n, c]);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
candidates.sort((a, b) => a[3].lastAccessed - b[3].lastAccessed);
|
|
712
|
+
let i = 0;
|
|
713
|
+
while (this.total >= this.maxTotal && i < candidates.length) {
|
|
714
|
+
const [d, p, n] = candidates[i++];
|
|
715
|
+
const domainMap = this.dm.get(d);
|
|
716
|
+
const pathMap = domainMap?.get(p);
|
|
717
|
+
if (pathMap?.delete(n)) {
|
|
718
|
+
this.total--;
|
|
719
|
+
// Prune empty maps
|
|
720
|
+
if (pathMap.size === 0)
|
|
721
|
+
domainMap.delete(p);
|
|
722
|
+
if (domainMap.size === 0)
|
|
723
|
+
this.dm.delete(d);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Check whether a request host matches a cookie domain.
|
|
729
|
+
* Uses custom matcher if provided, otherwise does suffix matching.
|
|
730
|
+
*/
|
|
731
|
+
matchDomain(reqHost, cookieDomain) {
|
|
732
|
+
// Use custom domain matcher if provided
|
|
733
|
+
if (this.domainMatcherFn) {
|
|
734
|
+
return this.domainMatcherFn(reqHost, cookieDomain);
|
|
735
|
+
}
|
|
736
|
+
// For host-only cookies: exact match only.
|
|
737
|
+
// For domain cookies: domain-match (subdomains allowed).
|
|
738
|
+
// We can't know per-domain whether it's host-only without checking cookies,
|
|
739
|
+
// so we do the broader domain-match here and filter host-only per cookie.
|
|
740
|
+
if (reqHost === cookieDomain)
|
|
741
|
+
return true;
|
|
742
|
+
if (isIPAddress(reqHost))
|
|
743
|
+
return false;
|
|
744
|
+
return reqHost.endsWith("." + cookieDomain);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
// ============================================================================
|
|
748
|
+
// 8. UTILITY
|
|
749
|
+
// ============================================================================
|
|
750
|
+
/**
|
|
751
|
+
* Safely parse a URL string, returning null instead of throwing.
|
|
752
|
+
*
|
|
753
|
+
* @param url - URL string to parse
|
|
754
|
+
* @returns URL object or null if invalid
|
|
755
|
+
*/
|
|
756
|
+
function safeParseUrl(url) {
|
|
757
|
+
try {
|
|
758
|
+
return new URL(url);
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
// ============================================================================
|
|
765
|
+
// 9. FACTORY
|
|
766
|
+
// ============================================================================
|
|
767
|
+
/**
|
|
768
|
+
* Create a new empty CookieJar.
|
|
769
|
+
*
|
|
770
|
+
* @returns A new CookieJar instance
|
|
771
|
+
*/
|
|
772
|
+
export function createCookieJar() {
|
|
773
|
+
return new CookieJar();
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Load a CookieJar from previously serialized JSON data.
|
|
777
|
+
*
|
|
778
|
+
* @param data - CookieJSON array or JSON string
|
|
779
|
+
* @returns A new CookieJar populated with the deserialized cookies
|
|
780
|
+
*/
|
|
781
|
+
export function loadCookieJar(data) {
|
|
782
|
+
return CookieJar.fromJSON(data);
|
|
783
|
+
}
|
|
784
|
+
//# sourceMappingURL=cookie-store.js.map
|