@ccheever/exact-ibex-runtime 0.1.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.
Files changed (161) hide show
  1. package/package.json +63 -0
  2. package/src/abort/AbortController.ts +23 -0
  3. package/src/abort/AbortSignal.ts +152 -0
  4. package/src/abort/index.ts +2 -0
  5. package/src/accessibility.ts +12 -0
  6. package/src/arraybuffer-detach.ts +109 -0
  7. package/src/base64/base64.ts +168 -0
  8. package/src/base64/index.ts +1 -0
  9. package/src/blob/Blob.ts +259 -0
  10. package/src/blob/File.ts +59 -0
  11. package/src/blob/FormData.ts +323 -0
  12. package/src/blob/index.ts +3 -0
  13. package/src/bootstrap.ts +1946 -0
  14. package/src/broadcast/BroadcastChannel.ts +280 -0
  15. package/src/broadcast/index.ts +5 -0
  16. package/src/cache/Cache.ts +349 -0
  17. package/src/cache/CacheStorage.ts +89 -0
  18. package/src/cache/index.ts +27 -0
  19. package/src/camera/index.ts +6202 -0
  20. package/src/camera/processor.worker.ts +194 -0
  21. package/src/camera/scene.ts +195 -0
  22. package/src/clipboard/Clipboard.ts +129 -0
  23. package/src/clipboard/ClipboardItem.ts +97 -0
  24. package/src/clipboard/index.ts +6 -0
  25. package/src/clone/index.ts +1 -0
  26. package/src/clone/structuredClone.ts +389 -0
  27. package/src/clone/transferableSymbols.ts +2 -0
  28. package/src/compression/CompressionStream.ts +146 -0
  29. package/src/compression/DecompressionStream.ts +342 -0
  30. package/src/compression/index.ts +4 -0
  31. package/src/console/Console.ts +341 -0
  32. package/src/console/index.ts +2 -0
  33. package/src/core/accessibility-state.ts +263 -0
  34. package/src/core/accessibility.ts +184 -0
  35. package/src/core/agent-state.ts +37 -0
  36. package/src/core/diagnostics-logs.ts +144 -0
  37. package/src/core/host-call-bridge.ts +16 -0
  38. package/src/core/i18n-helpers.ts +189 -0
  39. package/src/core/locale-state.ts +253 -0
  40. package/src/core/locale.ts +95 -0
  41. package/src/crypto/Crypto.ts +2743 -0
  42. package/src/crypto/index.ts +1 -0
  43. package/src/diagnostics/logs.ts +7 -0
  44. package/src/encoding/TextDecoder.ts +1181 -0
  45. package/src/encoding/TextDecoderStream.ts +58 -0
  46. package/src/encoding/TextEncoder.ts +180 -0
  47. package/src/encoding/TextEncoderStream.ts +39 -0
  48. package/src/encoding/index.ts +8 -0
  49. package/src/events/CloseEvent.ts +91 -0
  50. package/src/events/DOMException.ts +409 -0
  51. package/src/events/ErrorEvent.ts +39 -0
  52. package/src/events/Event.ts +151 -0
  53. package/src/events/EventTarget.ts +280 -0
  54. package/src/events/FocusEvent.ts +27 -0
  55. package/src/events/KeyboardEvent.ts +46 -0
  56. package/src/events/MessageEvent.ts +61 -0
  57. package/src/events/ProgressEvent.ts +33 -0
  58. package/src/events/PromiseRejectionEvent.ts +31 -0
  59. package/src/events/index.ts +52 -0
  60. package/src/eventsource/EventSource.ts +371 -0
  61. package/src/eventsource/index.ts +2 -0
  62. package/src/fetch/Headers.ts +642 -0
  63. package/src/fetch/Request.ts +760 -0
  64. package/src/fetch/Response.ts +543 -0
  65. package/src/fetch/body.ts +1256 -0
  66. package/src/fetch/cookie-jar.ts +566 -0
  67. package/src/fetch/demo.ts +207 -0
  68. package/src/fetch/errors.ts +101 -0
  69. package/src/fetch/fetch.ts +2610 -0
  70. package/src/fetch/index.ts +101 -0
  71. package/src/fetch/native-bridge.ts +65 -0
  72. package/src/fetch/types.ts +258 -0
  73. package/src/filereader/FileReader.ts +236 -0
  74. package/src/filereader/index.ts +1 -0
  75. package/src/fs/Dirent.ts +39 -0
  76. package/src/fs/ExactFile.ts +450 -0
  77. package/src/fs/Stats.ts +80 -0
  78. package/src/fs/index.ts +944 -0
  79. package/src/fs/promises.ts +386 -0
  80. package/src/fs/shared.ts +328 -0
  81. package/src/http-server/index.js +697 -0
  82. package/src/http-server/index.ts +27 -0
  83. package/src/identity.generated.ts +14 -0
  84. package/src/index.ts +283 -0
  85. package/src/indexeddb/IDBCursor.ts +188 -0
  86. package/src/indexeddb/IDBDatabase.ts +343 -0
  87. package/src/indexeddb/IDBFactory.ts +269 -0
  88. package/src/indexeddb/IDBIndex.ts +194 -0
  89. package/src/indexeddb/IDBKeyRange.ts +109 -0
  90. package/src/indexeddb/IDBObjectStore.ts +468 -0
  91. package/src/indexeddb/IDBRequest.ts +163 -0
  92. package/src/indexeddb/IDBTransaction.ts +207 -0
  93. package/src/indexeddb/index.ts +34 -0
  94. package/src/indexeddb/utils.ts +52 -0
  95. package/src/inspect/index.ts +1 -0
  96. package/src/inspect/inspect.ts +465 -0
  97. package/src/internal/detect.ts +104 -0
  98. package/src/locale.ts +10 -0
  99. package/src/location/index.ts +1059 -0
  100. package/src/locks/LockManager.ts +460 -0
  101. package/src/locks/index.ts +12 -0
  102. package/src/media/VideoFrame.ts +58 -0
  103. package/src/messaging/MessageChannel.ts +31 -0
  104. package/src/messaging/MessagePort.ts +180 -0
  105. package/src/messaging/index.ts +2 -0
  106. package/src/messaging.ts +247 -0
  107. package/src/native/NativeModules.ts +354 -0
  108. package/src/native/index.ts +1 -0
  109. package/src/navigator/Navigator.ts +351 -0
  110. package/src/navigator/index.ts +1 -0
  111. package/src/node/Buffer.ts +1786 -0
  112. package/src/node/index.ts +4 -0
  113. package/src/node/path.ts +495 -0
  114. package/src/node/process.ts +2528 -0
  115. package/src/performance/Performance.ts +532 -0
  116. package/src/performance/index.ts +21 -0
  117. package/src/polyfills/array.ts +236 -0
  118. package/src/polyfills/arraybuffer.ts +172 -0
  119. package/src/polyfills/groupby.ts +85 -0
  120. package/src/polyfills/index.ts +85 -0
  121. package/src/polyfills/intl.ts +1956 -0
  122. package/src/polyfills/iterator.ts +479 -0
  123. package/src/polyfills/promise.ts +37 -0
  124. package/src/polyfills/set.ts +245 -0
  125. package/src/polyfills/string.ts +85 -0
  126. package/src/polyfills/typedarray.ts +110 -0
  127. package/src/promise-rejection-tracking.ts +464 -0
  128. package/src/react-native/index.ts +388 -0
  129. package/src/runtime-entry.ts +55 -0
  130. package/src/scheduling/AnimationFrame.ts +105 -0
  131. package/src/scheduling/IdleCallback.ts +167 -0
  132. package/src/scheduling/index.ts +13 -0
  133. package/src/security/Capabilities.ts +1146 -0
  134. package/src/security/Permissions.ts +392 -0
  135. package/src/security/capability-bits.generated.ts +63 -0
  136. package/src/security/index.ts +16 -0
  137. package/src/sqlite/Database.ts +456 -0
  138. package/src/sqlite/Statement.ts +206 -0
  139. package/src/sqlite/constants.ts +79 -0
  140. package/src/sqlite/errors.ts +25 -0
  141. package/src/sqlite/index.ts +34 -0
  142. package/src/sqlite/module.js +438 -0
  143. package/src/storage/Storage.ts +291 -0
  144. package/src/storage/StorageManager.ts +91 -0
  145. package/src/storage/index.ts +3 -0
  146. package/src/stream-compat.ts +47 -0
  147. package/src/streams/ReadableStream.ts +4131 -0
  148. package/src/streams/TransformStream.ts +375 -0
  149. package/src/streams/WritableStream.ts +866 -0
  150. package/src/streams/index.ts +41 -0
  151. package/src/timers/Timers.ts +296 -0
  152. package/src/timers/index.ts +11 -0
  153. package/src/url/URL.ts +656 -0
  154. package/src/url/URLPattern.ts +850 -0
  155. package/src/url/URLSearchParams.ts +244 -0
  156. package/src/url/index.ts +9 -0
  157. package/src/websocket/WebSocket.ts +770 -0
  158. package/src/websocket/WebSocketError.ts +52 -0
  159. package/src/websocket/WebSocketStream.ts +628 -0
  160. package/src/websocket/index.ts +7 -0
  161. package/src/window/index.ts +872 -0
@@ -0,0 +1,566 @@
1
+ /**
2
+ * Cookie Jar Implementation (RFC 6265)
3
+ *
4
+ * A self-contained, spec-aligned cookie store for the Ibex runtime.
5
+ * Cookies are managed entirely in the JS fetch layer — the native HTTP
6
+ * backends are stateless pipes with no implicit cookie handling.
7
+ *
8
+ * @see https://www.rfc-editor.org/rfc/rfc6265
9
+ */
10
+
11
+ import type { RequestCredentials, RequestMode } from './types.js';
12
+
13
+ // =============================================================================
14
+ // Types
15
+ // =============================================================================
16
+
17
+ export interface StoredCookie {
18
+ name: string;
19
+ value: string;
20
+ /** Normalized domain: lowercase, with leading dot for domain cookies */
21
+ domain: string;
22
+ /** Defaults to request path up to last '/' */
23
+ path: string;
24
+ /** Unix ms timestamp, null = session cookie */
25
+ expires: number | null;
26
+ secure: boolean;
27
+ httpOnly: boolean;
28
+ sameSite: 'strict' | 'lax' | 'none';
29
+ /** true if no Domain attribute was set (exact host match required) */
30
+ hostOnly: boolean;
31
+ creationTime: number;
32
+ lastAccessTime: number;
33
+ }
34
+
35
+ // =============================================================================
36
+ // Constants
37
+ // =============================================================================
38
+
39
+ const MAX_COOKIES_PER_DOMAIN = 300;
40
+ const MAX_COOKIES_TOTAL = 3000;
41
+ const MAX_COOKIE_SIZE = 4096; // name + value bytes
42
+
43
+ // =============================================================================
44
+ // Runtime Origin
45
+ // =============================================================================
46
+
47
+ let _runtimeOrigin: URL | null = null;
48
+
49
+ /**
50
+ * Set the runtime origin for same-origin credential checks.
51
+ */
52
+ export function setRuntimeOrigin(origin: string): void {
53
+ _runtimeOrigin = new URL(origin);
54
+ }
55
+
56
+ /**
57
+ * Get the runtime origin. Falls back to location.href, then exact://app.
58
+ */
59
+ export function getRuntimeOrigin(): URL {
60
+ if (_runtimeOrigin) return _runtimeOrigin;
61
+
62
+ // Try globalThis.location
63
+ const location = (globalThis as any).location;
64
+ if (location && typeof location === 'object') {
65
+ const href = typeof location.href === 'string' ? location.href : null;
66
+ if (href) {
67
+ try {
68
+ _runtimeOrigin = new URL(href);
69
+ return _runtimeOrigin;
70
+ } catch {
71
+ // invalid, fall through
72
+ }
73
+ }
74
+ }
75
+
76
+ // Fallback
77
+ _runtimeOrigin = new URL('exact://app');
78
+ return _runtimeOrigin;
79
+ }
80
+
81
+ // =============================================================================
82
+ // Helpers
83
+ // =============================================================================
84
+
85
+ /**
86
+ * Get the default cookie path from a request URL.
87
+ * Per RFC 6265 Section 5.1.4.
88
+ */
89
+ function defaultCookiePath(url: URL): string {
90
+ const uriPath = url.pathname;
91
+ if (!uriPath || uriPath[0] !== '/' || uriPath === '/') {
92
+ return '/';
93
+ }
94
+ const lastSlash = uriPath.lastIndexOf('/');
95
+ if (lastSlash <= 0) return '/';
96
+ return uriPath.substring(0, lastSlash);
97
+ }
98
+
99
+ /**
100
+ * Check if a cookie domain matches a request host.
101
+ * Per RFC 6265 Section 5.1.3.
102
+ */
103
+ function domainMatch(cookieDomain: string, requestHost: string): boolean {
104
+ // Exact match
105
+ if (cookieDomain === requestHost) return true;
106
+
107
+ // requestHost must end with cookieDomain and the character before the suffix must be a dot
108
+ if (requestHost.endsWith(cookieDomain)) {
109
+ const prefixLen = requestHost.length - cookieDomain.length;
110
+ if (prefixLen > 0 && requestHost[prefixLen - 1] === '.') {
111
+ // Also, requestHost must not be an IP address
112
+ if (!isIPAddress(requestHost)) {
113
+ return true;
114
+ }
115
+ }
116
+ }
117
+
118
+ // Leading-dot domain match: .example.com matches sub.example.com
119
+ if (cookieDomain.startsWith('.')) {
120
+ const withoutDot = cookieDomain.substring(1);
121
+ if (requestHost === withoutDot || requestHost.endsWith('.' + withoutDot)) {
122
+ return true;
123
+ }
124
+ }
125
+
126
+ return false;
127
+ }
128
+
129
+ /**
130
+ * Check if a cookie path matches a request path.
131
+ * Per RFC 6265 Section 5.1.4.
132
+ */
133
+ function pathMatch(cookiePath: string, requestPath: string): boolean {
134
+ if (cookiePath === requestPath) return true;
135
+ if (requestPath.startsWith(cookiePath)) {
136
+ if (cookiePath.endsWith('/')) return true;
137
+ if (requestPath[cookiePath.length] === '/') return true;
138
+ }
139
+ return false;
140
+ }
141
+
142
+ /**
143
+ * Rough check for IP address (IPv4 or IPv6).
144
+ */
145
+ function isIPAddress(host: string): boolean {
146
+ // IPv6
147
+ if (host.includes(':')) return true;
148
+ // IPv4: all parts are digits
149
+ const parts = host.split('.');
150
+ return parts.length === 4 && parts.every(p => /^\d+$/.test(p));
151
+ }
152
+
153
+ /**
154
+ * Get the registrable domain (eTLD+1) from a hostname.
155
+ * Simplified: returns last two labels (e.g. "example.com" from "sub.example.com").
156
+ */
157
+ function getRegistrableDomain(host: string): string {
158
+ const parts = host.split('.');
159
+ if (parts.length <= 2) return host;
160
+ return parts.slice(-2).join('.');
161
+ }
162
+
163
+ /**
164
+ * Check if two URLs are same-site (same registrable domain).
165
+ */
166
+ function isSameSite(a: URL, b: URL): boolean {
167
+ const domainA = getRegistrableDomain(a.hostname.toLowerCase());
168
+ const domainB = getRegistrableDomain(b.hostname.toLowerCase());
169
+ return domainA === domainB;
170
+ }
171
+
172
+ /**
173
+ * Check if two URLs are same-origin.
174
+ */
175
+ function isSameOrigin(a: URL, b: URL): boolean {
176
+ return a.origin === b.origin;
177
+ }
178
+
179
+ /**
180
+ * Parse a date string from Expires attribute.
181
+ * Returns unix ms timestamp or NaN.
182
+ */
183
+ function parseExpiresDate(dateStr: string): number {
184
+ const ms = Date.parse(dateStr);
185
+ return ms;
186
+ }
187
+
188
+ // =============================================================================
189
+ // Set-Cookie Parser
190
+ // =============================================================================
191
+
192
+ interface ParsedSetCookie {
193
+ name: string;
194
+ value: string;
195
+ domain?: string;
196
+ path?: string;
197
+ expires?: number; // unix ms
198
+ maxAge?: number; // seconds
199
+ secure: boolean;
200
+ httpOnly: boolean;
201
+ sameSite: 'strict' | 'lax' | 'none';
202
+ }
203
+
204
+ /**
205
+ * Parse a single Set-Cookie header value.
206
+ * Per RFC 6265 Section 5.2.
207
+ */
208
+ function parseSetCookie(header: string): ParsedSetCookie | null {
209
+ // Split on first ';' to separate name-value from attributes
210
+ const semiIndex = header.indexOf(';');
211
+ const nameValuePart = semiIndex === -1 ? header : header.substring(0, semiIndex);
212
+ const attributesPart = semiIndex === -1 ? '' : header.substring(semiIndex + 1);
213
+
214
+ // Split name=value on first '='
215
+ const eqIndex = nameValuePart.indexOf('=');
216
+ if (eqIndex === -1) return null; // no name=value pair
217
+
218
+ const name = nameValuePart.substring(0, eqIndex).trim();
219
+ const value = nameValuePart.substring(eqIndex + 1).trim();
220
+
221
+ if (!name) return null; // empty name
222
+
223
+ const result: ParsedSetCookie = {
224
+ name,
225
+ value,
226
+ secure: false,
227
+ httpOnly: false,
228
+ sameSite: 'lax', // default per modern browser behavior
229
+ };
230
+
231
+ // Parse attributes
232
+ if (attributesPart) {
233
+ const attrs = attributesPart.split(';');
234
+ for (const attr of attrs) {
235
+ const trimmed = attr.trim();
236
+ if (!trimmed) continue;
237
+
238
+ const attrEq = trimmed.indexOf('=');
239
+ const attrName = (attrEq === -1 ? trimmed : trimmed.substring(0, attrEq)).trim().toLowerCase();
240
+ const attrValue = attrEq === -1 ? '' : trimmed.substring(attrEq + 1).trim();
241
+
242
+ switch (attrName) {
243
+ case 'domain': {
244
+ let d = attrValue.toLowerCase();
245
+ if (d.startsWith('.')) d = d.substring(1);
246
+ result.domain = d;
247
+ break;
248
+ }
249
+ case 'path':
250
+ result.path = attrValue;
251
+ break;
252
+ case 'expires': {
253
+ const ms = parseExpiresDate(attrValue);
254
+ if (!isNaN(ms)) {
255
+ result.expires = ms;
256
+ }
257
+ break;
258
+ }
259
+ case 'max-age': {
260
+ const seconds = parseInt(attrValue, 10);
261
+ if (!isNaN(seconds)) {
262
+ result.maxAge = seconds;
263
+ }
264
+ break;
265
+ }
266
+ case 'secure':
267
+ result.secure = true;
268
+ break;
269
+ case 'httponly':
270
+ result.httpOnly = true;
271
+ break;
272
+ case 'samesite': {
273
+ const sv = attrValue.toLowerCase();
274
+ if (sv === 'strict') result.sameSite = 'strict';
275
+ else if (sv === 'lax') result.sameSite = 'lax';
276
+ else if (sv === 'none') result.sameSite = 'none';
277
+ break;
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ return result;
284
+ }
285
+
286
+ // =============================================================================
287
+ // CookieJar
288
+ // =============================================================================
289
+
290
+ export class CookieJar {
291
+ /** Cookies stored by domain */
292
+ private _store: Map<string, StoredCookie[]> = new Map();
293
+ private _totalCount = 0;
294
+
295
+ /**
296
+ * Parse a Set-Cookie header and store the cookie.
297
+ * Per RFC 6265 Section 5.3.
298
+ */
299
+ setCookie(setCookieHeader: string, requestUrl: URL): void {
300
+ const parsed = parseSetCookie(setCookieHeader);
301
+ if (!parsed) return;
302
+
303
+ const now = Date.now();
304
+ const requestHost = requestUrl.hostname.toLowerCase();
305
+
306
+ // Determine domain
307
+ let domain: string;
308
+ let hostOnly: boolean;
309
+
310
+ if (parsed.domain) {
311
+ // Domain attribute was set — check domain-match
312
+ if (!domainMatch(parsed.domain, requestHost) &&
313
+ parsed.domain !== requestHost) {
314
+ return; // reject: domain doesn't match request
315
+ }
316
+ domain = parsed.domain;
317
+ hostOnly = false;
318
+ } else {
319
+ // No Domain attribute — host-only cookie
320
+ domain = requestHost;
321
+ hostOnly = true;
322
+ }
323
+
324
+ // Determine path
325
+ const path = parsed.path || defaultCookiePath(requestUrl);
326
+
327
+ // Determine expiry: Max-Age takes precedence over Expires
328
+ let expires: number | null = null;
329
+ if (parsed.maxAge !== undefined) {
330
+ if (parsed.maxAge <= 0) {
331
+ expires = 0; // delete cookie
332
+ } else {
333
+ expires = now + parsed.maxAge * 1000;
334
+ }
335
+ } else if (parsed.expires !== undefined) {
336
+ expires = parsed.expires;
337
+ }
338
+
339
+ // Check cookie size limit
340
+ if (parsed.name.length + parsed.value.length > MAX_COOKIE_SIZE) {
341
+ return; // too large
342
+ }
343
+
344
+ const cookie: StoredCookie = {
345
+ name: parsed.name,
346
+ value: parsed.value,
347
+ domain,
348
+ path,
349
+ expires,
350
+ secure: parsed.secure,
351
+ httpOnly: parsed.httpOnly,
352
+ sameSite: parsed.sameSite,
353
+ hostOnly,
354
+ creationTime: now,
355
+ lastAccessTime: now,
356
+ };
357
+
358
+ // If expires is in the past, this is a delete
359
+ if (expires !== null && expires <= now) {
360
+ this._removeCookie(domain, parsed.name, path);
361
+ return;
362
+ }
363
+
364
+ // Store: replace existing cookie with same name/domain/path
365
+ let domainCookies = this._store.get(domain);
366
+ if (!domainCookies) {
367
+ domainCookies = [];
368
+ this._store.set(domain, domainCookies);
369
+ }
370
+
371
+ const existingIdx = domainCookies.findIndex(
372
+ c => c.name === cookie.name && c.path === cookie.path
373
+ );
374
+
375
+ if (existingIdx !== -1) {
376
+ // Preserve creation time from existing cookie
377
+ cookie.creationTime = domainCookies[existingIdx].creationTime;
378
+ domainCookies[existingIdx] = cookie;
379
+ } else {
380
+ // Enforce per-domain limit
381
+ if (domainCookies.length >= MAX_COOKIES_PER_DOMAIN) {
382
+ this._evictFromDomain(domainCookies);
383
+ }
384
+ // Enforce total limit
385
+ if (this._totalCount >= MAX_COOKIES_TOTAL) {
386
+ this._evictGlobal();
387
+ }
388
+ domainCookies.push(cookie);
389
+ this._totalCount++;
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Get the Cookie header value for a request URL + credentials mode.
395
+ * Returns null if no cookies match.
396
+ */
397
+ getCookieHeader(
398
+ requestUrl: URL,
399
+ credentials: RequestCredentials,
400
+ requestOrigin: URL,
401
+ requestMode?: RequestMode,
402
+ requestMethod?: string
403
+ ): string | null {
404
+ if (credentials === 'omit') return null;
405
+
406
+ // Per Fetch spec: same-origin credentials in CORS mode should not send cookies
407
+ if (credentials === 'same-origin' && requestMode === 'cors') {
408
+ return null;
409
+ }
410
+
411
+ // same-origin check
412
+ if (credentials === 'same-origin') {
413
+ if (!isSameOrigin(requestUrl, requestOrigin)) {
414
+ return null;
415
+ }
416
+ }
417
+
418
+ const now = Date.now();
419
+ const requestHost = requestUrl.hostname.toLowerCase();
420
+ const requestPath = requestUrl.pathname || '/';
421
+ const isSecure = requestUrl.protocol === 'https:' || requestUrl.protocol === 'wss:';
422
+ const upperMethod = (requestMethod || 'GET').toUpperCase();
423
+ const isSafeMethod = upperMethod === 'GET' || upperMethod === 'HEAD';
424
+
425
+ const matches: StoredCookie[] = [];
426
+
427
+ for (const [domain, cookies] of this._store) {
428
+ for (const cookie of cookies) {
429
+ // Check expiry
430
+ if (cookie.expires !== null && cookie.expires <= now) continue;
431
+
432
+ // Domain match
433
+ if (cookie.hostOnly) {
434
+ if (domain !== requestHost) continue;
435
+ } else {
436
+ if (!domainMatch(domain, requestHost)) continue;
437
+ }
438
+
439
+ // Path match
440
+ if (!pathMatch(cookie.path, requestPath)) continue;
441
+
442
+ // Secure check
443
+ if (cookie.secure && !isSecure) continue;
444
+
445
+ // SameSite check
446
+ if (cookie.sameSite === 'strict') {
447
+ if (!isSameSite(requestUrl, requestOrigin)) continue;
448
+ } else if (cookie.sameSite === 'lax') {
449
+ // Lax: allow on same-site, or cross-site safe (GET/HEAD) top-level requests
450
+ if (!isSameSite(requestUrl, requestOrigin)) {
451
+ // Cross-site: only allow safe methods (GET/HEAD)
452
+ if (!isSafeMethod) continue;
453
+ }
454
+ }
455
+ // 'none' always passes (but requires Secure — enforced at set time)
456
+
457
+ cookie.lastAccessTime = now;
458
+ matches.push(cookie);
459
+ }
460
+ }
461
+
462
+ if (matches.length === 0) return null;
463
+
464
+ // Sort per RFC 6265 Section 5.4:
465
+ // 1. Longer paths first
466
+ // 2. Earlier creation time first (for same path length)
467
+ matches.sort((a, b) => {
468
+ if (b.path.length !== a.path.length) return b.path.length - a.path.length;
469
+ return a.creationTime - b.creationTime;
470
+ });
471
+
472
+ return matches.map(c => `${c.name}=${c.value}`).join('; ');
473
+ }
474
+
475
+ /**
476
+ * Clear all cookies.
477
+ */
478
+ clearAll(): void {
479
+ this._store.clear();
480
+ this._totalCount = 0;
481
+ }
482
+
483
+ /**
484
+ * Remove expired cookies from the store.
485
+ */
486
+ pruneExpired(): void {
487
+ const now = Date.now();
488
+ for (const [domain, cookies] of this._store) {
489
+ const filtered = cookies.filter(c => c.expires === null || c.expires > now);
490
+ const removed = cookies.length - filtered.length;
491
+ if (removed > 0) {
492
+ this._totalCount -= removed;
493
+ if (filtered.length === 0) {
494
+ this._store.delete(domain);
495
+ } else {
496
+ this._store.set(domain, filtered);
497
+ }
498
+ }
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Get the number of stored cookies (for testing/debugging).
504
+ */
505
+ get count(): number {
506
+ return this._totalCount;
507
+ }
508
+
509
+ // =========================================================================
510
+ // Private helpers
511
+ // =========================================================================
512
+
513
+ private _removeCookie(domain: string, name: string, path: string): void {
514
+ const cookies = this._store.get(domain);
515
+ if (!cookies) return;
516
+ const idx = cookies.findIndex(c => c.name === name && c.path === path);
517
+ if (idx !== -1) {
518
+ cookies.splice(idx, 1);
519
+ this._totalCount--;
520
+ if (cookies.length === 0) {
521
+ this._store.delete(domain);
522
+ }
523
+ }
524
+ }
525
+
526
+ private _evictFromDomain(domainCookies: StoredCookie[]): void {
527
+ // Evict the cookie with the oldest lastAccessTime
528
+ let oldestIdx = 0;
529
+ for (let i = 1; i < domainCookies.length; i++) {
530
+ if (domainCookies[i].lastAccessTime < domainCookies[oldestIdx].lastAccessTime) {
531
+ oldestIdx = i;
532
+ }
533
+ }
534
+ domainCookies.splice(oldestIdx, 1);
535
+ this._totalCount--;
536
+ }
537
+
538
+ private _evictGlobal(): void {
539
+ // Find the cookie with the oldest lastAccessTime across all domains
540
+ let oldestTime = Infinity;
541
+ let oldestDomain = '';
542
+ let oldestIdx = -1;
543
+
544
+ for (const [domain, cookies] of this._store) {
545
+ for (let i = 0; i < cookies.length; i++) {
546
+ if (cookies[i].lastAccessTime < oldestTime) {
547
+ oldestTime = cookies[i].lastAccessTime;
548
+ oldestDomain = domain;
549
+ oldestIdx = i;
550
+ }
551
+ }
552
+ }
553
+
554
+ if (oldestIdx !== -1) {
555
+ const cookies = this._store.get(oldestDomain)!;
556
+ cookies.splice(oldestIdx, 1);
557
+ this._totalCount--;
558
+ if (cookies.length === 0) {
559
+ this._store.delete(oldestDomain);
560
+ }
561
+ }
562
+ }
563
+ }
564
+
565
+ /** Global cookie jar instance shared by the fetch pipeline */
566
+ export const cookieJar = new CookieJar();