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.
Files changed (231) hide show
  1. package/LICENSE +16 -4
  2. package/README.md +2434 -701
  3. package/dist/browser/kinetex.esm.js +86 -0
  4. package/dist/browser/kinetex.js +19056 -1999
  5. package/dist/browser/kinetex.min.js +85 -8
  6. package/dist/cjs/aws-sigv4.js +903 -0
  7. package/dist/cjs/cache.js +1099 -0
  8. package/dist/cjs/circuit-breaker.js +362 -0
  9. package/dist/cjs/client.js +2890 -0
  10. package/dist/cjs/cookie-parser.js +2182 -0
  11. package/dist/cjs/cookie-store.js +783 -0
  12. package/dist/cjs/cookiejar.js +175 -0
  13. package/dist/cjs/core.js +1267 -0
  14. package/dist/cjs/dedup.js +267 -0
  15. package/dist/cjs/digest.js +552 -0
  16. package/dist/cjs/graphql.js +1086 -0
  17. package/dist/cjs/headers.js +2264 -0
  18. package/dist/cjs/interceptors.js +1115 -0
  19. package/dist/cjs/lifecycle.js +886 -0
  20. package/dist/cjs/logging.js +887 -0
  21. package/dist/cjs/mod.js +98 -0
  22. package/dist/cjs/package.json +1 -3
  23. package/dist/cjs/pagination.js +535 -0
  24. package/dist/cjs/progress.js +694 -0
  25. package/dist/cjs/response.js +1212 -0
  26. package/dist/cjs/socks5.js +681 -0
  27. package/dist/cjs/sse.js +827 -0
  28. package/dist/cjs/types.js +217 -0
  29. package/dist/cjs/url.js +1269 -0
  30. package/dist/cjs/utils.js +901 -0
  31. package/dist/cjs/worker.js +56 -0
  32. package/dist/cjs/ws.js +1099 -0
  33. package/dist/esm/aws-sigv4.js +904 -0
  34. package/dist/esm/aws-sigv4.js.map +1 -0
  35. package/dist/esm/cache.js +1100 -0
  36. package/dist/esm/cache.js.map +1 -0
  37. package/dist/esm/circuit-breaker.js +363 -0
  38. package/dist/esm/circuit-breaker.js.map +1 -0
  39. package/dist/esm/client.js +2891 -0
  40. package/dist/esm/client.js.map +1 -0
  41. package/dist/esm/cookie-parser.js +2183 -0
  42. package/dist/esm/cookie-parser.js.map +1 -0
  43. package/dist/esm/cookie-store.js +784 -0
  44. package/dist/esm/cookie-store.js.map +1 -0
  45. package/dist/esm/cookiejar.js +176 -0
  46. package/dist/esm/cookiejar.js.map +1 -0
  47. package/dist/esm/core.js +1268 -0
  48. package/dist/esm/core.js.map +1 -0
  49. package/dist/esm/dedup.js +268 -0
  50. package/dist/esm/dedup.js.map +1 -0
  51. package/dist/esm/digest.js +553 -0
  52. package/dist/esm/digest.js.map +1 -0
  53. package/dist/esm/graphql.js +1087 -0
  54. package/dist/esm/graphql.js.map +1 -0
  55. package/dist/esm/headers.js +2265 -0
  56. package/dist/esm/headers.js.map +1 -0
  57. package/dist/esm/interceptors.js +1116 -0
  58. package/dist/esm/interceptors.js.map +1 -0
  59. package/dist/esm/lifecycle.js +887 -0
  60. package/dist/esm/lifecycle.js.map +1 -0
  61. package/dist/esm/logging.js +888 -0
  62. package/dist/esm/logging.js.map +1 -0
  63. package/dist/esm/mod.js +99 -0
  64. package/dist/esm/mod.js.map +1 -0
  65. package/dist/esm/package.json +1 -0
  66. package/dist/esm/pagination.js +536 -0
  67. package/dist/esm/pagination.js.map +1 -0
  68. package/dist/esm/progress.js +695 -0
  69. package/dist/esm/progress.js.map +1 -0
  70. package/dist/esm/response.js +1213 -0
  71. package/dist/esm/response.js.map +1 -0
  72. package/dist/esm/socks5.js +682 -0
  73. package/dist/esm/socks5.js.map +1 -0
  74. package/dist/esm/sse.js +828 -0
  75. package/dist/esm/sse.js.map +1 -0
  76. package/dist/esm/types.js +218 -0
  77. package/dist/esm/types.js.map +1 -0
  78. package/dist/esm/url.js +1270 -0
  79. package/dist/esm/url.js.map +1 -0
  80. package/dist/esm/utils.js +902 -0
  81. package/dist/esm/utils.js.map +1 -0
  82. package/dist/esm/worker.js +57 -0
  83. package/dist/esm/worker.js.map +1 -0
  84. package/dist/esm/ws.js +1100 -0
  85. package/dist/esm/ws.js.map +1 -0
  86. package/dist/types/aws-sigv4.d.ts +382 -0
  87. package/dist/types/aws-sigv4.d.ts.map +1 -0
  88. package/dist/types/cache.d.ts +397 -0
  89. package/dist/types/cache.d.ts.map +1 -0
  90. package/dist/types/circuit-breaker.d.ts +241 -0
  91. package/dist/types/circuit-breaker.d.ts.map +1 -0
  92. package/dist/types/client.d.ts +616 -0
  93. package/dist/types/client.d.ts.map +1 -0
  94. package/dist/types/cookie-parser.d.ts +203 -0
  95. package/dist/types/cookie-parser.d.ts.map +1 -0
  96. package/dist/types/cookie-store.d.ts +316 -0
  97. package/dist/types/cookie-store.d.ts.map +1 -0
  98. package/dist/types/cookiejar.d.ts +165 -0
  99. package/dist/types/cookiejar.d.ts.map +1 -0
  100. package/dist/types/core.d.ts +297 -0
  101. package/dist/types/core.d.ts.map +1 -0
  102. package/dist/types/dedup.d.ts +164 -0
  103. package/dist/types/dedup.d.ts.map +1 -0
  104. package/dist/types/digest.d.ts +136 -0
  105. package/dist/types/digest.d.ts.map +1 -0
  106. package/dist/types/graphql.d.ts +530 -0
  107. package/dist/types/graphql.d.ts.map +1 -0
  108. package/dist/types/headers.d.ts +1119 -0
  109. package/dist/types/headers.d.ts.map +1 -0
  110. package/dist/types/interceptors.d.ts +681 -0
  111. package/dist/types/interceptors.d.ts.map +1 -0
  112. package/dist/types/lifecycle.d.ts +553 -0
  113. package/dist/types/lifecycle.d.ts.map +1 -0
  114. package/dist/types/logging.d.ts +518 -0
  115. package/dist/types/logging.d.ts.map +1 -0
  116. package/dist/types/mod.d.ts +92 -0
  117. package/dist/types/mod.d.ts.map +1 -0
  118. package/dist/types/pagination.d.ts +401 -0
  119. package/dist/types/pagination.d.ts.map +1 -0
  120. package/dist/types/progress.d.ts +344 -0
  121. package/dist/types/progress.d.ts.map +1 -0
  122. package/dist/types/response.d.ts +484 -0
  123. package/dist/types/response.d.ts.map +1 -0
  124. package/dist/types/socks5.d.ts +218 -0
  125. package/dist/types/socks5.d.ts.map +1 -0
  126. package/dist/types/sse.d.ts +399 -0
  127. package/dist/types/sse.d.ts.map +1 -0
  128. package/dist/types/types.d.ts +967 -0
  129. package/dist/types/types.d.ts.map +1 -0
  130. package/dist/types/url.d.ts +530 -0
  131. package/dist/types/url.d.ts.map +1 -0
  132. package/dist/types/utils.d.ts +300 -0
  133. package/dist/types/utils.d.ts.map +1 -0
  134. package/dist/types/worker.d.ts +55 -0
  135. package/dist/types/worker.d.ts.map +1 -0
  136. package/dist/types/ws.d.ts +480 -0
  137. package/dist/types/ws.d.ts.map +1 -0
  138. package/package.json +194 -127
  139. package/CHANGELOG.md +0 -50
  140. package/SECURITY.md +0 -31
  141. package/dist/browser/kinetex.cjs.js +0 -9
  142. package/dist/browser/kinetex.esm.min.js +0 -9
  143. package/dist/browser/kinetex.js.map +0 -7
  144. package/dist/cjs/auth/index.cjs +0 -333
  145. package/dist/cjs/auth/index.d.cts +0 -41
  146. package/dist/cjs/auth/index.d.ts.map +0 -1
  147. package/dist/cjs/auth/index.js.map +0 -1
  148. package/dist/cjs/core/chain.cjs +0 -127
  149. package/dist/cjs/core/chain.d.cts +0 -44
  150. package/dist/cjs/core/chain.d.ts.map +0 -1
  151. package/dist/cjs/core/chain.js.map +0 -1
  152. package/dist/cjs/core/transport.cjs +0 -544
  153. package/dist/cjs/core/transport.d.cts +0 -11
  154. package/dist/cjs/core/transport.d.ts.map +0 -1
  155. package/dist/cjs/core/transport.js.map +0 -1
  156. package/dist/cjs/core/types.cjs +0 -54
  157. package/dist/cjs/core/types.d.cts +0 -322
  158. package/dist/cjs/core/types.d.ts.map +0 -1
  159. package/dist/cjs/core/types.js.map +0 -1
  160. package/dist/cjs/index.cjs +0 -272
  161. package/dist/cjs/index.d.cts +0 -12
  162. package/dist/cjs/index.d.ts.map +0 -1
  163. package/dist/cjs/index.js.map +0 -1
  164. package/dist/cjs/middleware/pipeline.cjs +0 -451
  165. package/dist/cjs/middleware/pipeline.d.cts +0 -32
  166. package/dist/cjs/middleware/pipeline.d.ts.map +0 -1
  167. package/dist/cjs/middleware/pipeline.js.map +0 -1
  168. package/dist/cjs/plugins/cookies.cjs +0 -192
  169. package/dist/cjs/plugins/cookies.d.cts +0 -57
  170. package/dist/cjs/plugins/cookies.d.ts.map +0 -1
  171. package/dist/cjs/plugins/cookies.js.map +0 -1
  172. package/dist/cjs/plugins/extras.cjs +0 -316
  173. package/dist/cjs/plugins/extras.d.cts +0 -87
  174. package/dist/cjs/plugins/extras.d.ts.map +0 -1
  175. package/dist/cjs/plugins/extras.js.map +0 -1
  176. package/dist/cjs/plugins/index.cjs +0 -18
  177. package/dist/cjs/plugins/index.d.cts +0 -7
  178. package/dist/cjs/plugins/index.d.ts.map +0 -1
  179. package/dist/cjs/plugins/index.js.map +0 -1
  180. package/dist/cjs/plugins/proxy.cjs +0 -161
  181. package/dist/cjs/plugins/proxy.d.cts +0 -25
  182. package/dist/cjs/plugins/proxy.d.ts.map +0 -1
  183. package/dist/cjs/plugins/proxy.js.map +0 -1
  184. package/dist/cjs/utils/index.cjs +0 -12
  185. package/dist/cjs/utils/index.d.cts +0 -6
  186. package/dist/cjs/utils/index.d.ts.map +0 -1
  187. package/dist/cjs/utils/index.js.map +0 -1
  188. package/dist/esm/auth/index.js +0 -324
  189. package/dist/esm/auth/index.js.map +0 -1
  190. package/dist/esm/core/chain.js +0 -123
  191. package/dist/esm/core/chain.js.map +0 -1
  192. package/dist/esm/core/transport.js +0 -536
  193. package/dist/esm/core/transport.js.map +0 -1
  194. package/dist/esm/core/types.js +0 -46
  195. package/dist/esm/core/types.js.map +0 -1
  196. package/dist/esm/index.js +0 -241
  197. package/dist/esm/index.js.map +0 -1
  198. package/dist/esm/middleware/pipeline.js +0 -438
  199. package/dist/esm/middleware/pipeline.js.map +0 -1
  200. package/dist/esm/plugins/cookies.js +0 -187
  201. package/dist/esm/plugins/cookies.js.map +0 -1
  202. package/dist/esm/plugins/extras.js +0 -306
  203. package/dist/esm/plugins/extras.js.map +0 -1
  204. package/dist/esm/plugins/index.js +0 -4
  205. package/dist/esm/plugins/index.js.map +0 -1
  206. package/dist/esm/plugins/proxy.js +0 -124
  207. package/dist/esm/plugins/proxy.js.map +0 -1
  208. package/dist/esm/utils/index.js +0 -6
  209. package/dist/esm/utils/index.js.map +0 -1
  210. package/dist/types/auth/index.d.ts +0 -41
  211. package/dist/types/auth/index.d.ts.map +0 -1
  212. package/dist/types/core/chain.d.ts +0 -44
  213. package/dist/types/core/chain.d.ts.map +0 -1
  214. package/dist/types/core/transport.d.ts +0 -11
  215. package/dist/types/core/transport.d.ts.map +0 -1
  216. package/dist/types/core/types.d.ts +0 -322
  217. package/dist/types/core/types.d.ts.map +0 -1
  218. package/dist/types/index.d.ts +0 -12
  219. package/dist/types/index.d.ts.map +0 -1
  220. package/dist/types/middleware/pipeline.d.ts +0 -32
  221. package/dist/types/middleware/pipeline.d.ts.map +0 -1
  222. package/dist/types/plugins/cookies.d.ts +0 -57
  223. package/dist/types/plugins/cookies.d.ts.map +0 -1
  224. package/dist/types/plugins/extras.d.ts +0 -87
  225. package/dist/types/plugins/extras.d.ts.map +0 -1
  226. package/dist/types/plugins/index.d.ts +0 -7
  227. package/dist/types/plugins/index.d.ts.map +0 -1
  228. package/dist/types/plugins/proxy.d.ts +0 -25
  229. package/dist/types/plugins/proxy.d.ts.map +0 -1
  230. package/dist/types/utils/index.d.ts +0 -6
  231. 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