@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.
- package/package.json +63 -0
- package/src/abort/AbortController.ts +23 -0
- package/src/abort/AbortSignal.ts +152 -0
- package/src/abort/index.ts +2 -0
- package/src/accessibility.ts +12 -0
- package/src/arraybuffer-detach.ts +109 -0
- package/src/base64/base64.ts +168 -0
- package/src/base64/index.ts +1 -0
- package/src/blob/Blob.ts +259 -0
- package/src/blob/File.ts +59 -0
- package/src/blob/FormData.ts +323 -0
- package/src/blob/index.ts +3 -0
- package/src/bootstrap.ts +1946 -0
- package/src/broadcast/BroadcastChannel.ts +280 -0
- package/src/broadcast/index.ts +5 -0
- package/src/cache/Cache.ts +349 -0
- package/src/cache/CacheStorage.ts +89 -0
- package/src/cache/index.ts +27 -0
- package/src/camera/index.ts +6202 -0
- package/src/camera/processor.worker.ts +194 -0
- package/src/camera/scene.ts +195 -0
- package/src/clipboard/Clipboard.ts +129 -0
- package/src/clipboard/ClipboardItem.ts +97 -0
- package/src/clipboard/index.ts +6 -0
- package/src/clone/index.ts +1 -0
- package/src/clone/structuredClone.ts +389 -0
- package/src/clone/transferableSymbols.ts +2 -0
- package/src/compression/CompressionStream.ts +146 -0
- package/src/compression/DecompressionStream.ts +342 -0
- package/src/compression/index.ts +4 -0
- package/src/console/Console.ts +341 -0
- package/src/console/index.ts +2 -0
- package/src/core/accessibility-state.ts +263 -0
- package/src/core/accessibility.ts +184 -0
- package/src/core/agent-state.ts +37 -0
- package/src/core/diagnostics-logs.ts +144 -0
- package/src/core/host-call-bridge.ts +16 -0
- package/src/core/i18n-helpers.ts +189 -0
- package/src/core/locale-state.ts +253 -0
- package/src/core/locale.ts +95 -0
- package/src/crypto/Crypto.ts +2743 -0
- package/src/crypto/index.ts +1 -0
- package/src/diagnostics/logs.ts +7 -0
- package/src/encoding/TextDecoder.ts +1181 -0
- package/src/encoding/TextDecoderStream.ts +58 -0
- package/src/encoding/TextEncoder.ts +180 -0
- package/src/encoding/TextEncoderStream.ts +39 -0
- package/src/encoding/index.ts +8 -0
- package/src/events/CloseEvent.ts +91 -0
- package/src/events/DOMException.ts +409 -0
- package/src/events/ErrorEvent.ts +39 -0
- package/src/events/Event.ts +151 -0
- package/src/events/EventTarget.ts +280 -0
- package/src/events/FocusEvent.ts +27 -0
- package/src/events/KeyboardEvent.ts +46 -0
- package/src/events/MessageEvent.ts +61 -0
- package/src/events/ProgressEvent.ts +33 -0
- package/src/events/PromiseRejectionEvent.ts +31 -0
- package/src/events/index.ts +52 -0
- package/src/eventsource/EventSource.ts +371 -0
- package/src/eventsource/index.ts +2 -0
- package/src/fetch/Headers.ts +642 -0
- package/src/fetch/Request.ts +760 -0
- package/src/fetch/Response.ts +543 -0
- package/src/fetch/body.ts +1256 -0
- package/src/fetch/cookie-jar.ts +566 -0
- package/src/fetch/demo.ts +207 -0
- package/src/fetch/errors.ts +101 -0
- package/src/fetch/fetch.ts +2610 -0
- package/src/fetch/index.ts +101 -0
- package/src/fetch/native-bridge.ts +65 -0
- package/src/fetch/types.ts +258 -0
- package/src/filereader/FileReader.ts +236 -0
- package/src/filereader/index.ts +1 -0
- package/src/fs/Dirent.ts +39 -0
- package/src/fs/ExactFile.ts +450 -0
- package/src/fs/Stats.ts +80 -0
- package/src/fs/index.ts +944 -0
- package/src/fs/promises.ts +386 -0
- package/src/fs/shared.ts +328 -0
- package/src/http-server/index.js +697 -0
- package/src/http-server/index.ts +27 -0
- package/src/identity.generated.ts +14 -0
- package/src/index.ts +283 -0
- package/src/indexeddb/IDBCursor.ts +188 -0
- package/src/indexeddb/IDBDatabase.ts +343 -0
- package/src/indexeddb/IDBFactory.ts +269 -0
- package/src/indexeddb/IDBIndex.ts +194 -0
- package/src/indexeddb/IDBKeyRange.ts +109 -0
- package/src/indexeddb/IDBObjectStore.ts +468 -0
- package/src/indexeddb/IDBRequest.ts +163 -0
- package/src/indexeddb/IDBTransaction.ts +207 -0
- package/src/indexeddb/index.ts +34 -0
- package/src/indexeddb/utils.ts +52 -0
- package/src/inspect/index.ts +1 -0
- package/src/inspect/inspect.ts +465 -0
- package/src/internal/detect.ts +104 -0
- package/src/locale.ts +10 -0
- package/src/location/index.ts +1059 -0
- package/src/locks/LockManager.ts +460 -0
- package/src/locks/index.ts +12 -0
- package/src/media/VideoFrame.ts +58 -0
- package/src/messaging/MessageChannel.ts +31 -0
- package/src/messaging/MessagePort.ts +180 -0
- package/src/messaging/index.ts +2 -0
- package/src/messaging.ts +247 -0
- package/src/native/NativeModules.ts +354 -0
- package/src/native/index.ts +1 -0
- package/src/navigator/Navigator.ts +351 -0
- package/src/navigator/index.ts +1 -0
- package/src/node/Buffer.ts +1786 -0
- package/src/node/index.ts +4 -0
- package/src/node/path.ts +495 -0
- package/src/node/process.ts +2528 -0
- package/src/performance/Performance.ts +532 -0
- package/src/performance/index.ts +21 -0
- package/src/polyfills/array.ts +236 -0
- package/src/polyfills/arraybuffer.ts +172 -0
- package/src/polyfills/groupby.ts +85 -0
- package/src/polyfills/index.ts +85 -0
- package/src/polyfills/intl.ts +1956 -0
- package/src/polyfills/iterator.ts +479 -0
- package/src/polyfills/promise.ts +37 -0
- package/src/polyfills/set.ts +245 -0
- package/src/polyfills/string.ts +85 -0
- package/src/polyfills/typedarray.ts +110 -0
- package/src/promise-rejection-tracking.ts +464 -0
- package/src/react-native/index.ts +388 -0
- package/src/runtime-entry.ts +55 -0
- package/src/scheduling/AnimationFrame.ts +105 -0
- package/src/scheduling/IdleCallback.ts +167 -0
- package/src/scheduling/index.ts +13 -0
- package/src/security/Capabilities.ts +1146 -0
- package/src/security/Permissions.ts +392 -0
- package/src/security/capability-bits.generated.ts +63 -0
- package/src/security/index.ts +16 -0
- package/src/sqlite/Database.ts +456 -0
- package/src/sqlite/Statement.ts +206 -0
- package/src/sqlite/constants.ts +79 -0
- package/src/sqlite/errors.ts +25 -0
- package/src/sqlite/index.ts +34 -0
- package/src/sqlite/module.js +438 -0
- package/src/storage/Storage.ts +291 -0
- package/src/storage/StorageManager.ts +91 -0
- package/src/storage/index.ts +3 -0
- package/src/stream-compat.ts +47 -0
- package/src/streams/ReadableStream.ts +4131 -0
- package/src/streams/TransformStream.ts +375 -0
- package/src/streams/WritableStream.ts +866 -0
- package/src/streams/index.ts +41 -0
- package/src/timers/Timers.ts +296 -0
- package/src/timers/index.ts +11 -0
- package/src/url/URL.ts +656 -0
- package/src/url/URLPattern.ts +850 -0
- package/src/url/URLSearchParams.ts +244 -0
- package/src/url/index.ts +9 -0
- package/src/websocket/WebSocket.ts +770 -0
- package/src/websocket/WebSocketError.ts +52 -0
- package/src/websocket/WebSocketStream.ts +628 -0
- package/src/websocket/index.ts +7 -0
- 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();
|