create-request 0.0.1-beta.9 → 1.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.
@@ -0,0 +1,2201 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Enum for HTTP methods
5
+ */
6
+ exports.HttpMethod = void 0;
7
+ (function (HttpMethod) {
8
+ HttpMethod["GET"] = "GET";
9
+ HttpMethod["PUT"] = "PUT";
10
+ HttpMethod["POST"] = "POST";
11
+ HttpMethod["HEAD"] = "HEAD";
12
+ HttpMethod["PATCH"] = "PATCH";
13
+ HttpMethod["DELETE"] = "DELETE";
14
+ HttpMethod["OPTIONS"] = "OPTIONS";
15
+ })(exports.HttpMethod || (exports.HttpMethod = {}));
16
+ /**
17
+ * Enum for request priorities
18
+ */
19
+ exports.RequestPriority = void 0;
20
+ (function (RequestPriority) {
21
+ RequestPriority["LOW"] = "low";
22
+ RequestPriority["HIGH"] = "high";
23
+ RequestPriority["AUTO"] = "auto";
24
+ })(exports.RequestPriority || (exports.RequestPriority = {}));
25
+ /**
26
+ * Enum for credentials policies
27
+ */
28
+ exports.CredentialsPolicy = void 0;
29
+ (function (CredentialsPolicy) {
30
+ CredentialsPolicy["OMIT"] = "omit";
31
+ CredentialsPolicy["INCLUDE"] = "include";
32
+ CredentialsPolicy["SAME_ORIGIN"] = "same-origin";
33
+ })(exports.CredentialsPolicy || (exports.CredentialsPolicy = {}));
34
+ /**
35
+ * Enum for request modes
36
+ */
37
+ exports.RequestMode = void 0;
38
+ (function (RequestMode) {
39
+ RequestMode["CORS"] = "cors";
40
+ RequestMode["NO_CORS"] = "no-cors";
41
+ RequestMode["SAME_ORIGIN"] = "same-origin";
42
+ RequestMode["NAVIGATE"] = "navigate";
43
+ })(exports.RequestMode || (exports.RequestMode = {}));
44
+ /**
45
+ * Enum for redirect modes
46
+ */
47
+ exports.RedirectMode = void 0;
48
+ (function (RedirectMode) {
49
+ RedirectMode["ERROR"] = "error";
50
+ RedirectMode["FOLLOW"] = "follow";
51
+ RedirectMode["MANUAL"] = "manual";
52
+ })(exports.RedirectMode || (exports.RedirectMode = {}));
53
+ /**
54
+ * Enum for cookie SameSite policies
55
+ */
56
+ exports.SameSitePolicy = void 0;
57
+ (function (SameSitePolicy) {
58
+ SameSitePolicy["LAX"] = "Lax";
59
+ SameSitePolicy["NONE"] = "None";
60
+ SameSitePolicy["STRICT"] = "Strict";
61
+ })(exports.SameSitePolicy || (exports.SameSitePolicy = {}));
62
+ /**
63
+ * Enum for body types
64
+ */
65
+ var BodyType;
66
+ (function (BodyType) {
67
+ BodyType["JSON"] = "json";
68
+ BodyType["STRING"] = "string";
69
+ BodyType["BINARY"] = "binary";
70
+ })(BodyType || (BodyType = {}));
71
+ /**
72
+ * Referrer policies for fetch requests
73
+ */
74
+ exports.ReferrerPolicy = void 0;
75
+ (function (ReferrerPolicy) {
76
+ ReferrerPolicy["ORIGIN"] = "origin";
77
+ ReferrerPolicy["UNSAFE_URL"] = "unsafe-url";
78
+ ReferrerPolicy["SAME_ORIGIN"] = "same-origin";
79
+ ReferrerPolicy["NO_REFERRER"] = "no-referrer";
80
+ ReferrerPolicy["STRICT_ORIGIN"] = "strict-origin";
81
+ ReferrerPolicy["ORIGIN_WHEN_CROSS_ORIGIN"] = "origin-when-cross-origin";
82
+ ReferrerPolicy["NO_REFERRER_WHEN_DOWNGRADE"] = "no-referrer-when-downgrade";
83
+ ReferrerPolicy["STRICT_ORIGIN_WHEN_CROSS_ORIGIN"] = "strict-origin-when-cross-origin";
84
+ })(exports.ReferrerPolicy || (exports.ReferrerPolicy = {}));
85
+
86
+ class RequestError extends Error {
87
+ constructor(message, url, method, options = {}) {
88
+ super(message);
89
+ this.name = "RequestError";
90
+ this.url = url;
91
+ this.method = method;
92
+ this.status = options.status;
93
+ this.response = options.response;
94
+ this.isTimeout = options.isTimeout;
95
+ this.isAborted = options.isAborted;
96
+ // For better stack traces in modern environments
97
+ if (Error.captureStackTrace) {
98
+ Error.captureStackTrace(this, RequestError);
99
+ }
100
+ // Maintains proper prototype chain for instanceof checks
101
+ Object.setPrototypeOf(this, RequestError.prototype);
102
+ }
103
+ /**
104
+ * Static methods for creating specific types of RequestError
105
+ */
106
+ static timeout(url, method, timeoutMs) {
107
+ return new RequestError(`Timeout: ${timeoutMs}ms`, url, method, {
108
+ isTimeout: true,
109
+ });
110
+ }
111
+ static fromResponse(response, url, method) {
112
+ return new RequestError(`HTTP ${response.status}`, url, method, {
113
+ status: response.status,
114
+ response,
115
+ });
116
+ }
117
+ static networkError(url, method, originalError) {
118
+ // Provide more descriptive error messages for common network errors
119
+ let message = originalError.message;
120
+ // Check for Node.js error codes (e.g., from undici/dns errors)
121
+ const errorCode = originalError.code;
122
+ const errorName = originalError.name;
123
+ const stack = originalError.stack || "";
124
+ const errorMessageLower = message.toLowerCase();
125
+ // Check for timeout errors (Node.js/undici TimeoutError)
126
+ const isTimeoutError = errorName === "TimeoutError" ||
127
+ errorMessageLower.includes("timeout") ||
128
+ errorMessageLower.includes("aborted due to timeout") ||
129
+ errorCode === "ETIMEDOUT" ||
130
+ stack.includes("TimeoutError") ||
131
+ stack.includes("timeout");
132
+ // If the error message is generic "fetch failed", provide more context
133
+ if (message === "fetch failed" || message === "Failed to fetch") {
134
+ // Check for DNS resolution errors
135
+ const isDnsError = errorCode === "ENOTFOUND" ||
136
+ errorCode === "EAI_AGAIN" ||
137
+ errorCode === "EAI_NODATA" ||
138
+ stack.includes("getaddrinfo") ||
139
+ stack.includes("ENOTFOUND") ||
140
+ stack.includes("EAI_AGAIN");
141
+ // Check for connection errors (but not timeout errors)
142
+ const isConnectionError = !isTimeoutError && (errorCode === "ECONNREFUSED" || errorCode === "ECONNRESET" || stack.includes("ECONNREFUSED") || stack.includes("connect"));
143
+ if (isTimeoutError) {
144
+ message = `Network error: Request timeout for ${url}`;
145
+ }
146
+ else if (isDnsError) {
147
+ message = `Network error: Unable to resolve hostname for ${url}`;
148
+ }
149
+ else if (isConnectionError) {
150
+ message = `Network error: Connection refused for ${url}`;
151
+ }
152
+ else {
153
+ message = `Network error: Failed to fetch ${url}`;
154
+ }
155
+ }
156
+ const error = new RequestError(message, url, method, {
157
+ ...(isTimeoutError ? { isTimeout: true } : {}),
158
+ });
159
+ // Create a proper RequestError stack trace, but append the original stack for debugging
160
+ // This way Node.js will show "RequestError: ..." instead of "TypeError: ..."
161
+ if (originalError.stack) {
162
+ // Get the current stack (which will start with RequestError)
163
+ const currentStack = error.stack || "";
164
+ // Append the original error's stack as "Caused by:" for debugging context
165
+ error.stack = `${currentStack}\n\nCaused by: ${originalError.stack}`;
166
+ }
167
+ return error;
168
+ }
169
+ static abortError(url, method) {
170
+ return new RequestError("Aborted", url, method, {
171
+ isAborted: true,
172
+ });
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Wrapper for HTTP responses with methods to transform the response data
178
+ * Provides caching and conversion between different response formats.
179
+ */
180
+ class ResponseWrapper {
181
+ constructor(response, url, method, graphQLOptions) {
182
+ this.bodyUsed = false;
183
+ this.response = response;
184
+ this.url = url;
185
+ this.method = method;
186
+ if (graphQLOptions) {
187
+ this.graphQLOptions = {
188
+ throwOnError: graphQLOptions.throwOnError,
189
+ };
190
+ }
191
+ }
192
+ // Wrapper properties
193
+ get status() {
194
+ return this.response.status;
195
+ }
196
+ get statusText() {
197
+ return this.response.statusText;
198
+ }
199
+ get headers() {
200
+ return this.response.headers;
201
+ }
202
+ get ok() {
203
+ return this.response.ok;
204
+ }
205
+ get raw() {
206
+ return this.response;
207
+ }
208
+ /**
209
+ * Check for GraphQL errors and throw if throwOnError is enabled
210
+ * @param data - The parsed JSON data
211
+ * @throws RequestError if GraphQL response contains errors and throwOnError is enabled
212
+ */
213
+ checkGraphQLErrors(data) {
214
+ var _a;
215
+ if (!((_a = this.graphQLOptions) === null || _a === void 0 ? void 0 : _a.throwOnError) || typeof data !== "object" || data === null)
216
+ return;
217
+ const responseData = data;
218
+ if (!Array.isArray(responseData.errors) || responseData.errors.length === 0)
219
+ return;
220
+ const errors = responseData.errors;
221
+ const errorMessages = errors.map(x => typeof x === "string" ? x : x && typeof x === "object" && "message" in x ? String(x.message || "Unknown error") : String(x));
222
+ const errorMessage = errorMessages.join(", ");
223
+ throw new RequestError(`GraphQL request failed with errors: ${errorMessage}`, this.url || "", this.method || "", {
224
+ status: this.response.status,
225
+ response: this.response,
226
+ });
227
+ }
228
+ /**
229
+ * Parse the response body as JSON
230
+ * Caches the result for subsequent calls.
231
+ * If GraphQL options are set with throwOnError=true, will check for GraphQL errors and throw.
232
+ *
233
+ * @returns The parsed JSON data
234
+ * @throws {RequestError} When the request fails, JSON parsing fails, GraphQL errors occur (if throwOnError enabled), or body is already consumed
235
+ *
236
+ * @example
237
+ * const data = await response.getJson();
238
+ * console.log(data.items);
239
+ *
240
+ * @example
241
+ * // Error handling - errors are always RequestError
242
+ * try {
243
+ * const data = await response.getJson();
244
+ * } catch (error) {
245
+ * if (error instanceof RequestError) {
246
+ * console.log(error.status, error.url, error.method);
247
+ * }
248
+ * }
249
+ */
250
+ async getJson() {
251
+ if (this.jsonCache !== undefined) {
252
+ this.checkGraphQLErrors(this.jsonCache);
253
+ return this.jsonCache;
254
+ }
255
+ if (this.bodyUsed) {
256
+ // If we already have text, try to parse it as JSON
257
+ if (this.textCache !== undefined) {
258
+ try {
259
+ const parsed = JSON.parse(this.textCache);
260
+ this.jsonCache = parsed;
261
+ this.checkGraphQLErrors(parsed);
262
+ return parsed;
263
+ }
264
+ catch (error) {
265
+ // Re-throw RequestErrors from checkGraphQLErrors without wrapping
266
+ if (error instanceof RequestError) {
267
+ throw error;
268
+ }
269
+ const errorMessage = error instanceof Error ? error.message : String(error);
270
+ throw new RequestError(`Invalid JSON: ${errorMessage}`, this.url || "", this.method || "", {
271
+ status: this.response.status,
272
+ response: this.response,
273
+ });
274
+ }
275
+ }
276
+ throw new RequestError("Body already consumed", this.url || "", this.method || "", {
277
+ status: this.response.status,
278
+ response: this.response,
279
+ });
280
+ }
281
+ this.bodyUsed = true;
282
+ try {
283
+ const parsed = await this.response.json();
284
+ this.jsonCache = parsed;
285
+ this.checkGraphQLErrors(parsed);
286
+ return parsed;
287
+ }
288
+ catch (error) {
289
+ // Re-throw RequestErrors from checkGraphQLErrors without wrapping
290
+ if (error instanceof RequestError) {
291
+ throw error;
292
+ }
293
+ const errorMessage = error instanceof Error ? error.message : String(error);
294
+ throw new RequestError(`Invalid JSON: ${errorMessage}`, this.url || "", this.method || "", {
295
+ status: this.response.status,
296
+ response: this.response,
297
+ });
298
+ }
299
+ }
300
+ /**
301
+ * Get the response body as text
302
+ * Caches the result for subsequent calls. Attempts to convert from JSON or Blob
303
+ * if the body has already been consumed in those formats.
304
+ *
305
+ * @returns The response text
306
+ * @throws {RequestError} When reading fails or the response has already been consumed in an incompatible format
307
+ *
308
+ * @example
309
+ * const text = await response.getText();
310
+ */
311
+ async getText() {
312
+ if (this.textCache !== undefined) {
313
+ return this.textCache;
314
+ }
315
+ if (this.bodyUsed) {
316
+ // If we already have JSON, convert it to text
317
+ if (this.jsonCache !== undefined) {
318
+ this.textCache = JSON.stringify(this.jsonCache);
319
+ return this.textCache;
320
+ }
321
+ // If we have blob, convert to text
322
+ if (this.blobCache !== undefined) {
323
+ try {
324
+ this.textCache = await this.blobCache.text();
325
+ return this.textCache;
326
+ }
327
+ catch (e) {
328
+ const errorMessage = e instanceof Error ? e.message : String(e);
329
+ throw new RequestError(`Cannot convert to text: ${errorMessage}`, this.url || "", this.method || "", {
330
+ status: this.response.status,
331
+ response: this.response,
332
+ });
333
+ }
334
+ }
335
+ throw new RequestError("Cannot convert to text", this.url || "", this.method || "", {
336
+ status: this.response.status,
337
+ response: this.response,
338
+ });
339
+ }
340
+ this.bodyUsed = true;
341
+ try {
342
+ this.textCache = await this.response.text();
343
+ return this.textCache;
344
+ }
345
+ catch (e) {
346
+ const errorMessage = e instanceof Error ? e.message : String(e);
347
+ throw new RequestError(`Read failed: ${errorMessage}`, this.url || "", this.method || "", {
348
+ status: this.response.status,
349
+ response: this.response,
350
+ });
351
+ }
352
+ }
353
+ /**
354
+ * Get the response body as a Blob
355
+ * Caches the result for subsequent calls. Attempts to convert from text
356
+ * if the body has already been consumed as text.
357
+ *
358
+ * @returns The response as a Blob
359
+ * @throws {RequestError} When reading fails or the response has already been consumed in an incompatible format
360
+ *
361
+ * @example
362
+ * const blob = await response.getBlob();
363
+ * const url = URL.createObjectURL(blob);
364
+ */
365
+ async getBlob() {
366
+ if (this.blobCache !== undefined) {
367
+ return this.blobCache;
368
+ }
369
+ if (this.bodyUsed) {
370
+ // If we have text, convert to Blob
371
+ if (this.textCache !== undefined) {
372
+ this.blobCache = new Blob([this.textCache], { type: "text/plain" });
373
+ return this.blobCache;
374
+ }
375
+ throw new RequestError("Cannot convert to Blob", this.url || "", this.method || "", {
376
+ status: this.response.status,
377
+ response: this.response,
378
+ });
379
+ }
380
+ this.bodyUsed = true;
381
+ try {
382
+ this.blobCache = await this.response.blob();
383
+ return this.blobCache;
384
+ }
385
+ catch (e) {
386
+ const errorMessage = e instanceof Error ? e.message : String(e);
387
+ throw new RequestError(`Read failed: ${errorMessage}`, this.url || "", this.method || "", {
388
+ status: this.response.status,
389
+ response: this.response,
390
+ });
391
+ }
392
+ }
393
+ /**
394
+ * Get the raw response body as a ReadableStream
395
+ * This method consumes the body and should only be called once.
396
+ *
397
+ * @returns The response body as a ReadableStream or null
398
+ * @throws {RequestError} When the response body has already been consumed
399
+ *
400
+ * @example
401
+ * const stream = response.getBody();
402
+ * if (stream) {
403
+ * const reader = stream.getReader();
404
+ * // Process the stream
405
+ * }
406
+ */
407
+ getBody() {
408
+ if (this.bodyUsed) {
409
+ throw new RequestError("Body already consumed", this.url || "", this.method || "", {
410
+ status: this.response.status,
411
+ response: this.response,
412
+ });
413
+ }
414
+ this.bodyUsed = true;
415
+ return this.response.body;
416
+ }
417
+ /**
418
+ * Extract specific data using a selector function
419
+ * If no selector is provided, returns the full JSON response.
420
+ *
421
+ * @param selector - Optional function to extract and transform data
422
+ * @returns A promise that resolves to the selected data
423
+ * @throws {RequestError} When the request fails, JSON parsing fails, or the selector throws an error
424
+ *
425
+ * @example
426
+ * // Get full response
427
+ * const data = await response.getData();
428
+ *
429
+ * // Extract specific data
430
+ * const users = await response.getData(data => data.results.users);
431
+ *
432
+ * @example
433
+ * // Error handling - errors are always RequestError
434
+ * try {
435
+ * const data = await response.getData();
436
+ * } catch (error) {
437
+ * if (error instanceof RequestError) {
438
+ * console.log(error.status, error.url, error.method);
439
+ * }
440
+ * }
441
+ */
442
+ async getData(selector) {
443
+ try {
444
+ const data = await this.getJson();
445
+ // If no selector is provided, return the raw JSON data
446
+ if (!selector)
447
+ return data;
448
+ // Apply the selector if provided
449
+ return selector(data);
450
+ }
451
+ catch (error) {
452
+ // If it's already a RequestError, re-throw it
453
+ if (error instanceof RequestError) {
454
+ throw error;
455
+ }
456
+ // Enhance selector errors with context
457
+ if (selector) {
458
+ const errorMessage = error instanceof Error ? error.message : String(error);
459
+ throw new RequestError(`Data selector failed: ${errorMessage}`, this.url || "", this.method || "", {
460
+ status: this.response.status,
461
+ response: this.response,
462
+ });
463
+ }
464
+ // If we get here and it's not a RequestError, wrap it
465
+ // This should rarely happen as getJson() should throw RequestError
466
+ const errorObj = error instanceof Error ? error : new Error(String(error));
467
+ throw RequestError.networkError(this.url || "", this.method || "", errorObj);
468
+ }
469
+ }
470
+ }
471
+
472
+ class CookieUtils {
473
+ /**
474
+ * Formats cookies for a request
475
+ * @param cookies Object containing cookie name-value pairs or cookie options
476
+ * @returns Formatted cookie string for the Cookie header
477
+ */
478
+ static formatRequestCookies(cookies) {
479
+ const cookiePairs = [];
480
+ Object.entries(cookies).forEach(([name, valueOrOptions]) => {
481
+ let value;
482
+ if (typeof valueOrOptions === "string") {
483
+ value = valueOrOptions;
484
+ }
485
+ else {
486
+ // Extract value from options object without validation
487
+ value = valueOrOptions.value;
488
+ }
489
+ // Add the cookie to the request
490
+ cookiePairs.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`);
491
+ });
492
+ return cookiePairs.join("; ");
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Utility class for CSRF token management
498
+ */
499
+ class CsrfUtils {
500
+ /**
501
+ * Extracts CSRF token from a meta tag in the document head
502
+ * @param metaName The name attribute of the meta tag (default: "csrf-token")
503
+ * @returns The CSRF token or null if not found
504
+ */
505
+ static getTokenFromMeta(metaName = "csrf-token") {
506
+ if (typeof document === "undefined") {
507
+ return null;
508
+ }
509
+ const meta = document.querySelector(`meta[name="${metaName}"]`);
510
+ return (meta === null || meta === void 0 ? void 0 : meta.getAttribute("content")) || null;
511
+ }
512
+ /**
513
+ * Extracts CSRF token from a cookie
514
+ * @param cookieName The name of the cookie containing the CSRF token
515
+ * @returns The CSRF token or null if not found
516
+ */
517
+ static getTokenFromCookie(cookieName = "csrf-token") {
518
+ if (typeof document === "undefined") {
519
+ return null;
520
+ }
521
+ const cookies = document.cookie.split(";");
522
+ for (const cookie of cookies) {
523
+ const [name, value] = cookie.trim().split("=");
524
+ if (name === cookieName) {
525
+ return decodeURIComponent(value);
526
+ }
527
+ }
528
+ return null;
529
+ }
530
+ /**
531
+ * Validates if the provided string is a potential CSRF token
532
+ * Checks if the token meets security requirements
533
+ * @param token The token to validate
534
+ * @returns Whether the token is valid
535
+ */
536
+ static isValidToken(token) {
537
+ // Basic checks for null/undefined and type
538
+ if (typeof token !== "string") {
539
+ return false;
540
+ }
541
+ if (token.length < 8)
542
+ return false;
543
+ // If token is longer than 10 chars, perform additional security checks
544
+ if (token.length > 10) {
545
+ // Check for valid character set (alphanumeric & common token symbols)
546
+ const validTokenRegex = /^[A-Za-z0-9\-_=+/.]+$/;
547
+ if (!validTokenRegex.test(token)) {
548
+ return false;
549
+ }
550
+ // For longer tokens, check for sufficient entropy (at least 2 character types)
551
+ const hasUpperCase = /[A-Z]/.test(token);
552
+ const hasLowerCase = /[a-z]/.test(token);
553
+ const hasNumbers = /[0-9]/.test(token);
554
+ const hasSpecials = /[-_=+/.]/.test(token);
555
+ const characterTypesCount = [hasUpperCase, hasLowerCase, hasNumbers, hasSpecials].filter(Boolean).length;
556
+ return characterTypesCount >= 2;
557
+ }
558
+ // For shorter tokens (8-10 chars), just return true if we reached here
559
+ return true;
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Global configuration for create-request
565
+ */
566
+ class Config {
567
+ constructor() {
568
+ // CSRF configuration
569
+ this.csrfHeaderName = "X-CSRF-Token";
570
+ this.xsrfCookieName = "XSRF-TOKEN";
571
+ this.xsrfHeaderName = "X-XSRF-TOKEN";
572
+ this.csrfToken = null;
573
+ this.enableAutoXsrf = true;
574
+ this.enableAntiCsrf = true; // X-Requested-With header
575
+ // Interceptor configuration
576
+ this.requestInterceptors = [];
577
+ this.responseInterceptors = [];
578
+ this.errorInterceptors = [];
579
+ this.nextInterceptorId = 1;
580
+ }
581
+ /**
582
+ * Get the singleton instance of the Config class
583
+ *
584
+ * @returns The global configuration instance
585
+ *
586
+ * @example
587
+ * const config = Config.getInstance();
588
+ * config.setCsrfToken('token123');
589
+ */
590
+ static getInstance() {
591
+ if (!Config.instance) {
592
+ Config.instance = new Config();
593
+ }
594
+ return Config.instance;
595
+ }
596
+ /**
597
+ * Set a global CSRF token to be used for all requests
598
+ *
599
+ * @param token - The CSRF token value
600
+ * @returns The config instance for chaining
601
+ *
602
+ * @example
603
+ * Config.getInstance().setCsrfToken('myToken123');
604
+ */
605
+ setCsrfToken(token) {
606
+ this.csrfToken = token;
607
+ return this;
608
+ }
609
+ /**
610
+ * Get the global CSRF token that will be automatically applied to requests
611
+ *
612
+ * @returns The current CSRF token or null if not set
613
+ */
614
+ getCsrfToken() {
615
+ return this.csrfToken;
616
+ }
617
+ /**
618
+ * Set the CSRF header name used when sending the token
619
+ *
620
+ * @param name - The header name to use
621
+ * @returns The config instance for chaining
622
+ *
623
+ * @example
624
+ * Config.getInstance().setCsrfHeaderName('X-My-CSRF-Token');
625
+ */
626
+ setCsrfHeaderName(name) {
627
+ this.csrfHeaderName = name;
628
+ return this;
629
+ }
630
+ /**
631
+ * Get the configured CSRF header name
632
+ *
633
+ * @returns The current CSRF header name
634
+ */
635
+ getCsrfHeaderName() {
636
+ return this.csrfHeaderName;
637
+ }
638
+ /**
639
+ * Set the XSRF cookie name to look for when extracting tokens from cookies
640
+ *
641
+ * @param name - The cookie name to look for
642
+ * @returns The config instance for chaining
643
+ *
644
+ * @example
645
+ * Config.getInstance().setXsrfCookieName('MY-XSRF-COOKIE');
646
+ */
647
+ setXsrfCookieName(name) {
648
+ this.xsrfCookieName = name;
649
+ return this;
650
+ }
651
+ /**
652
+ * Get the configured XSRF cookie name
653
+ *
654
+ * @returns The current XSRF cookie name
655
+ */
656
+ getXsrfCookieName() {
657
+ return this.xsrfCookieName;
658
+ }
659
+ /**
660
+ * Set the XSRF header name for sending tokens extracted from cookies
661
+ *
662
+ * @param name - The header name to use
663
+ * @returns The config instance for chaining
664
+ */
665
+ setXsrfHeaderName(name) {
666
+ this.xsrfHeaderName = name;
667
+ return this;
668
+ }
669
+ /**
670
+ * Get the configured XSRF header name
671
+ *
672
+ * @returns The current XSRF header name
673
+ */
674
+ getXsrfHeaderName() {
675
+ return this.xsrfHeaderName;
676
+ }
677
+ /**
678
+ * Enable or disable automatic extraction of XSRF tokens from cookies
679
+ * When enabled, the library will look for XSRF tokens in cookies and
680
+ * automatically add them to request headers.
681
+ *
682
+ * @param enable - Whether to enable this feature
683
+ * @returns The config instance for chaining
684
+ *
685
+ * @example
686
+ * Config.getInstance().setEnableAutoXsrf(false); // Disable XSRF extraction
687
+ */
688
+ setEnableAutoXsrf(enable) {
689
+ this.enableAutoXsrf = enable;
690
+ return this;
691
+ }
692
+ /**
693
+ * Check if automatic XSRF token extraction is enabled
694
+ *
695
+ * @returns True if automatic XSRF is enabled
696
+ */
697
+ isAutoXsrfEnabled() {
698
+ return this.enableAutoXsrf;
699
+ }
700
+ /**
701
+ * Enable or disable automatic addition of anti-CSRF headers
702
+ * When enabled, X-Requested-With: XMLHttpRequest will be added to all requests.
703
+ *
704
+ * @param enable - Whether to enable this feature
705
+ * @returns The config instance for chaining
706
+ */
707
+ setEnableAntiCsrf(enable) {
708
+ this.enableAntiCsrf = enable;
709
+ return this;
710
+ }
711
+ /**
712
+ * Check if anti-CSRF protection is enabled
713
+ *
714
+ * @returns True if anti-CSRF protection is enabled
715
+ */
716
+ isAntiCsrfEnabled() {
717
+ return this.enableAntiCsrf;
718
+ }
719
+ /**
720
+ * Add a global request interceptor
721
+ * Request interceptors can modify the request configuration or return an early response
722
+ *
723
+ * @param interceptor - The request interceptor function
724
+ * @returns The interceptor ID for later removal
725
+ *
726
+ * @example
727
+ * const id = Config.getInstance().addRequestInterceptor((config) => {
728
+ * config.headers['X-Custom'] = 'value';
729
+ * return config;
730
+ * });
731
+ */
732
+ addRequestInterceptor(interceptor) {
733
+ const id = this.nextInterceptorId++;
734
+ this.requestInterceptors.push({ id, interceptor });
735
+ return id;
736
+ }
737
+ /**
738
+ * Add a global response interceptor
739
+ * Response interceptors can transform the response
740
+ *
741
+ * @param interceptor - The response interceptor function
742
+ * @returns The interceptor ID for later removal
743
+ *
744
+ * @example
745
+ * const id = Config.getInstance().addResponseInterceptor((response) => {
746
+ * console.log('Response received:', response.status);
747
+ * return response;
748
+ * });
749
+ */
750
+ addResponseInterceptor(interceptor) {
751
+ const id = this.nextInterceptorId++;
752
+ this.responseInterceptors.push({ id, interceptor });
753
+ return id;
754
+ }
755
+ /**
756
+ * Add a global error interceptor
757
+ * Error interceptors can handle or transform errors
758
+ *
759
+ * @param interceptor - The error interceptor function
760
+ * @returns The interceptor ID for later removal
761
+ *
762
+ * @example
763
+ * const id = Config.getInstance().addErrorInterceptor((error) => {
764
+ * console.error('Request failed:', error);
765
+ * throw error;
766
+ * });
767
+ */
768
+ addErrorInterceptor(interceptor) {
769
+ const id = this.nextInterceptorId++;
770
+ this.errorInterceptors.push({ id, interceptor });
771
+ return id;
772
+ }
773
+ /**
774
+ * Remove a request interceptor by its ID
775
+ *
776
+ * @param id - The interceptor ID returned from addRequestInterceptor
777
+ *
778
+ * @example
779
+ * Config.getInstance().removeRequestInterceptor(id);
780
+ */
781
+ removeRequestInterceptor(id) {
782
+ this.requestInterceptors = this.requestInterceptors.filter(item => item.id !== id);
783
+ }
784
+ /**
785
+ * Remove a response interceptor by its ID
786
+ *
787
+ * @param id - The interceptor ID returned from addResponseInterceptor
788
+ *
789
+ * @example
790
+ * Config.getInstance().removeResponseInterceptor(id);
791
+ */
792
+ removeResponseInterceptor(id) {
793
+ this.responseInterceptors = this.responseInterceptors.filter(item => item.id !== id);
794
+ }
795
+ /**
796
+ * Remove an error interceptor by its ID
797
+ *
798
+ * @param id - The interceptor ID returned from addErrorInterceptor
799
+ *
800
+ * @example
801
+ * Config.getInstance().removeErrorInterceptor(id);
802
+ */
803
+ removeErrorInterceptor(id) {
804
+ this.errorInterceptors = this.errorInterceptors.filter(item => item.id !== id);
805
+ }
806
+ /**
807
+ * Clear all interceptors (request, response, and error)
808
+ *
809
+ * @example
810
+ * Config.getInstance().clearInterceptors();
811
+ */
812
+ clearInterceptors() {
813
+ this.requestInterceptors = [];
814
+ this.responseInterceptors = [];
815
+ this.errorInterceptors = [];
816
+ }
817
+ /**
818
+ * Get all global request interceptors (in registration order)
819
+ * @internal
820
+ */
821
+ getRequestInterceptors() {
822
+ return this.requestInterceptors.map(item => item.interceptor);
823
+ }
824
+ /**
825
+ * Get all global response interceptors (in registration order)
826
+ * @internal
827
+ */
828
+ getResponseInterceptors() {
829
+ return this.responseInterceptors.map(item => item.interceptor);
830
+ }
831
+ /**
832
+ * Get all global error interceptors (in registration order)
833
+ * @internal
834
+ */
835
+ getErrorInterceptors() {
836
+ return this.errorInterceptors.map(item => item.interceptor);
837
+ }
838
+ /**
839
+ * Reset all configuration options to their default values
840
+ *
841
+ * @returns The config instance for chaining
842
+ *
843
+ * @example
844
+ * Config.getInstance().reset();
845
+ */
846
+ reset() {
847
+ this.csrfToken = null;
848
+ this.csrfHeaderName = "X-CSRF-Token";
849
+ this.enableAntiCsrf = true;
850
+ this.xsrfCookieName = "XSRF-TOKEN";
851
+ this.xsrfHeaderName = "X-XSRF-TOKEN";
852
+ this.enableAutoXsrf = true;
853
+ this.clearInterceptors();
854
+ return this;
855
+ }
856
+ }
857
+
858
+ /**
859
+ * Base class with common functionality for all request types
860
+ * Provides the core request building and execution capabilities.
861
+ */
862
+ class BaseRequest {
863
+ constructor(url) {
864
+ this.requestOptions = {
865
+ headers: {},
866
+ };
867
+ this.queryParams = new URLSearchParams();
868
+ this.autoApplyCsrfProtection = true;
869
+ // Per-request interceptors
870
+ this.requestInterceptors = [];
871
+ this.responseInterceptors = [];
872
+ this.errorInterceptors = [];
873
+ this.url = url;
874
+ }
875
+ /**
876
+ * Get GraphQL options if set (only for BodyRequest subclasses)
877
+ * @returns GraphQL options or undefined
878
+ */
879
+ getGraphQLOptions() {
880
+ return undefined;
881
+ }
882
+ /**
883
+ * Creates a fluent API for setting enum-based options
884
+ * Combines direct setter with convenience methods
885
+ */
886
+ createFluentSetter(optionName, options) {
887
+ const fluent = {};
888
+ // Create convenience methods for each enum value
889
+ Object.entries(options).forEach(([key, value]) => {
890
+ fluent[key] = () => {
891
+ this.requestOptions[optionName] = value;
892
+ return this;
893
+ };
894
+ });
895
+ // Create the callable setter
896
+ const callable = (value) => {
897
+ this.requestOptions[optionName] = value;
898
+ return this;
899
+ };
900
+ return Object.assign(callable, fluent);
901
+ }
902
+ validateUrl(url) {
903
+ if (!(url === null || url === void 0 ? void 0 : url.trim()))
904
+ throw new RequestError("URL cannot be empty", url, this.method);
905
+ if (url.includes("\0") || url.includes("\r") || url.includes("\n")) {
906
+ throw new RequestError("Invalid URL (control chars)", url, this.method);
907
+ }
908
+ const trimmed = url.trim();
909
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
910
+ try {
911
+ new URL(trimmed);
912
+ }
913
+ catch (_a) {
914
+ throw new RequestError(`Invalid URL: ${trimmed}`, trimmed, this.method);
915
+ }
916
+ }
917
+ }
918
+ /**
919
+ * Add multiple HTTP headers to the request
920
+ *
921
+ * @param headers - Key-value pairs of header names and values
922
+ * @returns The request instance for chaining
923
+ *
924
+ * @example
925
+ * request.withHeaders({
926
+ * 'Accept': 'application/json',
927
+ * 'X-Custom-Header': 'value'
928
+ * });
929
+ */
930
+ withHeaders(headers) {
931
+ // Filter out null and undefined values
932
+ const filteredHeaders = {};
933
+ Object.entries(headers).forEach(([key, value]) => {
934
+ if (value !== null && value !== undefined) {
935
+ filteredHeaders[key] = value;
936
+ }
937
+ });
938
+ this.requestOptions.headers = {
939
+ ...this.getHeadersRecord(),
940
+ ...filteredHeaders,
941
+ };
942
+ return this;
943
+ }
944
+ /**
945
+ * Add a single HTTP header to the request
946
+ *
947
+ * @param key - The header name
948
+ * @param value - The header value
949
+ * @returns The request instance for chaining
950
+ *
951
+ * @example
952
+ * request.withHeader('Accept', 'application/json');
953
+ */
954
+ withHeader(key, value) {
955
+ // Ignore null and undefined values
956
+ if (value !== null && value !== undefined) {
957
+ return this.withHeaders({ [key]: value });
958
+ }
959
+ return this;
960
+ }
961
+ /**
962
+ * Set a timeout for the request
963
+ * If the request takes longer than the specified timeout, it will be aborted.
964
+ *
965
+ * @param timeout - The timeout in milliseconds
966
+ * @returns The request instance for chaining
967
+ * @throws RequestError if timeout is not a positive number
968
+ *
969
+ * @example
970
+ * request.withTimeout(5000); // 5 seconds timeout
971
+ */
972
+ withTimeout(timeout) {
973
+ if (!Number.isFinite(timeout) || timeout <= 0)
974
+ throw new RequestError("Timeout must be a positive number", this.url, this.method);
975
+ this.requestOptions.timeout = timeout;
976
+ return this;
977
+ }
978
+ /**
979
+ * Configure automatic retry behavior for failed requests
980
+ *
981
+ * @param retries - Number of retry attempts before failing
982
+ * @returns The request instance for chaining
983
+ * @throws RequestError if retries is not a non-negative integer
984
+ *
985
+ * @example
986
+ * request.withRetries(3); // Retry up to 3 times
987
+ */
988
+ withRetries(retries) {
989
+ if (!Number.isInteger(retries) || retries < 0)
990
+ throw new RequestError("Retry count must be a non-negative integer", this.url, this.method);
991
+ this.requestOptions.retries = retries;
992
+ return this;
993
+ }
994
+ /**
995
+ * Set a callback to be invoked before each retry attempt
996
+ * Useful for implementing backoff strategies or logging retry attempts.
997
+ *
998
+ * @param callback - Function to call before retrying
999
+ * @returns The request instance for chaining
1000
+ *
1001
+ * @example
1002
+ * request.onRetry(({ attempt, error }) => {
1003
+ * console.log(`Retry attempt ${attempt} after error: ${error.message}`);
1004
+ * return new Promise(resolve => setTimeout(resolve, attempt * 1000));
1005
+ * });
1006
+ */
1007
+ onRetry(callback) {
1008
+ this.requestOptions.onRetry = callback;
1009
+ return this;
1010
+ }
1011
+ /**
1012
+ * Sets credentials policy - supports both direct call and fluent API
1013
+ * @example
1014
+ * request.withCredentials("include")
1015
+ * request.withCredentials(CredentialsPolicy.INCLUDE)
1016
+ * request.withCredentials.INCLUDE()
1017
+ */
1018
+ get withCredentials() {
1019
+ return this.createFluentSetter("credentials", {
1020
+ INCLUDE: exports.CredentialsPolicy.INCLUDE,
1021
+ OMIT: exports.CredentialsPolicy.OMIT,
1022
+ SAME_ORIGIN: exports.CredentialsPolicy.SAME_ORIGIN,
1023
+ });
1024
+ }
1025
+ /**
1026
+ * Allows providing an external AbortController to cancel the request
1027
+ * @param controller - The AbortController to use for this request
1028
+ * @returns The instance for chaining
1029
+ */
1030
+ withAbortController(controller) {
1031
+ this.abortController = controller;
1032
+ return this;
1033
+ }
1034
+ /**
1035
+ * Sets the referrer for the request
1036
+ * @param referrer - The referrer URL or policy
1037
+ * @returns The instance for chaining
1038
+ */
1039
+ withReferrer(referrer) {
1040
+ this.requestOptions.referrer = referrer;
1041
+ return this;
1042
+ }
1043
+ /**
1044
+ * Sets referrer policy - supports both direct call and fluent API
1045
+ * @example
1046
+ * request.withReferrerPolicy("no-referrer")
1047
+ * request.withReferrerPolicy(ReferrerPolicy.NO_REFERRER)
1048
+ * request.withReferrerPolicy.NO_REFERRER()
1049
+ */
1050
+ get withReferrerPolicy() {
1051
+ return this.createFluentSetter("referrerPolicy", {
1052
+ ORIGIN: exports.ReferrerPolicy.ORIGIN,
1053
+ UNSAFE_URL: exports.ReferrerPolicy.UNSAFE_URL,
1054
+ SAME_ORIGIN: exports.ReferrerPolicy.SAME_ORIGIN,
1055
+ NO_REFERRER: exports.ReferrerPolicy.NO_REFERRER,
1056
+ STRICT_ORIGIN: exports.ReferrerPolicy.STRICT_ORIGIN,
1057
+ ORIGIN_WHEN_CROSS_ORIGIN: exports.ReferrerPolicy.ORIGIN_WHEN_CROSS_ORIGIN,
1058
+ NO_REFERRER_WHEN_DOWNGRADE: exports.ReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE,
1059
+ STRICT_ORIGIN_WHEN_CROSS_ORIGIN: exports.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
1060
+ });
1061
+ }
1062
+ /**
1063
+ * Sets redirect mode - supports both direct call and fluent API
1064
+ * @example
1065
+ * request.withRedirect("follow")
1066
+ * request.withRedirect(RedirectMode.FOLLOW)
1067
+ * request.withRedirect.FOLLOW()
1068
+ */
1069
+ get withRedirect() {
1070
+ return this.createFluentSetter("redirect", {
1071
+ FOLLOW: exports.RedirectMode.FOLLOW,
1072
+ ERROR: exports.RedirectMode.ERROR,
1073
+ MANUAL: exports.RedirectMode.MANUAL,
1074
+ });
1075
+ }
1076
+ /**
1077
+ * Sets the keepalive flag for the request
1078
+ * @param keepalive - Whether to allow the request to outlive the page
1079
+ * @returns The instance for chaining
1080
+ */
1081
+ withKeepAlive(keepalive) {
1082
+ this.requestOptions.keepalive = keepalive;
1083
+ return this;
1084
+ }
1085
+ /**
1086
+ * Sets request priority - supports both direct call and fluent API
1087
+ * @example
1088
+ * request.withPriority("high")
1089
+ * request.withPriority(RequestPriority.HIGH)
1090
+ * request.withPriority.HIGH()
1091
+ */
1092
+ get withPriority() {
1093
+ return this.createFluentSetter("priority", {
1094
+ HIGH: exports.RequestPriority.HIGH,
1095
+ LOW: exports.RequestPriority.LOW,
1096
+ AUTO: exports.RequestPriority.AUTO,
1097
+ });
1098
+ }
1099
+ /**
1100
+ * Adds query parameters to the request URL
1101
+ * @param params - An object containing the query parameters
1102
+ * @returns The instance for chaining
1103
+ */
1104
+ withQueryParams(params) {
1105
+ Object.entries(params).forEach(([key, value]) => {
1106
+ if (value === null || value === undefined) {
1107
+ return;
1108
+ }
1109
+ if (Array.isArray(value)) {
1110
+ // Handle array values - add multiple entries with the same key
1111
+ value.forEach(v => this.queryParams.append(key, String(v)));
1112
+ }
1113
+ else {
1114
+ this.queryParams.append(key, String(value));
1115
+ }
1116
+ });
1117
+ return this;
1118
+ }
1119
+ /**
1120
+ * Adds a single query parameter
1121
+ * @param key - The parameter name
1122
+ * @param value - The parameter value, can be a single value or array of values
1123
+ * @returns The instance for chaining
1124
+ */
1125
+ withQueryParam(key, value) {
1126
+ if (value === null || value === undefined)
1127
+ return this;
1128
+ if (Array.isArray(value))
1129
+ value.forEach(v => this.queryParams.append(key, String(v)));
1130
+ else
1131
+ this.queryParams.append(key, String(value));
1132
+ return this;
1133
+ }
1134
+ /**
1135
+ * Sets request mode - supports both direct call and fluent API
1136
+ * @example
1137
+ * request.withMode("cors")
1138
+ * request.withMode(RequestMode.CORS)
1139
+ * request.withMode.CORS()
1140
+ */
1141
+ get withMode() {
1142
+ return this.createFluentSetter("mode", {
1143
+ CORS: exports.RequestMode.CORS,
1144
+ NO_CORS: exports.RequestMode.NO_CORS,
1145
+ SAME_ORIGIN: exports.RequestMode.SAME_ORIGIN,
1146
+ NAVIGATE: exports.RequestMode.NAVIGATE,
1147
+ });
1148
+ }
1149
+ /**
1150
+ * Shorthand for setting the Content-Type header
1151
+ * @param contentType - The content type
1152
+ * @returns The instance for chaining
1153
+ */
1154
+ withContentType(contentType) {
1155
+ return this.withHeaders({
1156
+ "Content-Type": contentType,
1157
+ });
1158
+ }
1159
+ /**
1160
+ * Shorthand for setting the Authorization header
1161
+ * @param authValue - The authorization value
1162
+ * @returns The instance for chaining
1163
+ */
1164
+ withAuthorization(authValue) {
1165
+ return this.withHeaders({
1166
+ Authorization: authValue,
1167
+ });
1168
+ }
1169
+ /**
1170
+ * Shorthand for setting up Basic authentication
1171
+ * @param username - The username
1172
+ * @param password - The password
1173
+ * @returns The instance for chaining
1174
+ */
1175
+ withBasicAuth(username, password) {
1176
+ const credentials = this.encodeBase64(`${username}:${password}`);
1177
+ return this.withAuthorization(`Basic ${credentials}`);
1178
+ }
1179
+ /**
1180
+ * Cross-environment base64 encoding
1181
+ * Works in both browser and Node.js environments
1182
+ */
1183
+ encodeBase64(str) {
1184
+ // Modern approach using TextEncoder (available in both modern browsers and Node.js)
1185
+ if (typeof TextEncoder !== "undefined" && typeof btoa === "function") {
1186
+ const encoder = new TextEncoder();
1187
+ const bytes = encoder.encode(str);
1188
+ return btoa(String.fromCharCode.apply(null, [...new Uint8Array(bytes)]));
1189
+ }
1190
+ // Browser environment
1191
+ if (typeof btoa === "function")
1192
+ return btoa(str);
1193
+ // Node.js environment
1194
+ if (typeof Buffer !== "undefined")
1195
+ return Buffer.from(str).toString("base64");
1196
+ // Fallback (should never happen in modern environments)
1197
+ throw new RequestError("Base64 encoding is not supported", this.url, this.method);
1198
+ }
1199
+ /**
1200
+ * Sets the bearer token for authentication
1201
+ * @param token - The bearer token
1202
+ * @returns The instance for chaining
1203
+ */
1204
+ withBearerToken(token) {
1205
+ return this.withAuthorization(`Bearer ${token}`);
1206
+ }
1207
+ /**
1208
+ * Safely get headers as a Record<string, string>
1209
+ * @returns The headers object
1210
+ */
1211
+ getHeadersRecord() {
1212
+ if (typeof this.requestOptions.headers === "object" && this.requestOptions.headers !== null) {
1213
+ return this.requestOptions.headers;
1214
+ }
1215
+ return {};
1216
+ }
1217
+ /**
1218
+ * Helper function to check for header presence in a case-insensitive way
1219
+ * @param headerName Header name to check
1220
+ * @returns Boolean indicating if the header exists (case-insensitive)
1221
+ */
1222
+ hasHeader(headerName) {
1223
+ const headers = this.getHeadersRecord();
1224
+ return Object.keys(headers).some(key => key.toLowerCase() === headerName.toLowerCase());
1225
+ }
1226
+ /**
1227
+ * Sets cookies for the request
1228
+ * @param cookies Object containing cookie name-value pairs or cookie options
1229
+ * @returns The instance for chaining
1230
+ */
1231
+ withCookies(cookies) {
1232
+ const cookieEntries = Object.entries(cookies || {});
1233
+ if (cookieEntries.length === 0) {
1234
+ return this;
1235
+ }
1236
+ const currentHeaders = this.getHeadersRecord();
1237
+ // Get all existing cookie headers with different cases
1238
+ const cookieValues = [];
1239
+ const cookieHeaderKeys = [];
1240
+ Object.keys(currentHeaders).forEach(key => {
1241
+ if (key.toLowerCase() === "cookie") {
1242
+ // Don't add empty cookie values
1243
+ if (currentHeaders[key])
1244
+ cookieValues.push(currentHeaders[key]);
1245
+ cookieHeaderKeys.push(key);
1246
+ }
1247
+ });
1248
+ // Format the new cookies
1249
+ const cookieString = CookieUtils.formatRequestCookies(cookies);
1250
+ // Choose which header name to use - preserve existing case if possible
1251
+ const headerName = cookieHeaderKeys.length > 0 ? cookieHeaderKeys[0] : "Cookie";
1252
+ // Combine all existing cookie values with the new ones
1253
+ const combinedCookieValue = [...cookieValues, cookieString].filter(Boolean).join("; ");
1254
+ // Create a new headers object without any cookie headers
1255
+ const newHeaders = { ...currentHeaders };
1256
+ cookieHeaderKeys.forEach(key => {
1257
+ delete newHeaders[key];
1258
+ });
1259
+ // Set the new combined cookie header
1260
+ this.requestOptions.headers = {
1261
+ ...newHeaders,
1262
+ [headerName]: combinedCookieValue,
1263
+ };
1264
+ return this;
1265
+ }
1266
+ /**
1267
+ * Set a single cookie
1268
+ * @param name Cookie name
1269
+ * @param value Cookie value or options object
1270
+ * @returns The instance for chaining
1271
+ */
1272
+ withCookie(name, value) {
1273
+ return this.withCookies({ [name]: value });
1274
+ }
1275
+ /**
1276
+ * Sets a CSRF token in the request headers
1277
+ * @param token The CSRF token
1278
+ * @param headerName The name of the header to use (default: X-CSRF-Token)
1279
+ * @returns The instance for chaining
1280
+ */
1281
+ withCsrfToken(token, headerName = "X-CSRF-Token") {
1282
+ return this.withHeaders({
1283
+ [headerName]: token,
1284
+ });
1285
+ }
1286
+ /**
1287
+ * Disables automatic anti-CSRF protection.
1288
+ * By default, X-Requested-With: XMLHttpRequest header is sent with all requests.
1289
+ * @returns The instance for chaining
1290
+ */
1291
+ withoutCsrfProtection() {
1292
+ this.autoApplyCsrfProtection = false;
1293
+ return this;
1294
+ }
1295
+ /**
1296
+ * Sets common security headers to help prevent CSRF attacks
1297
+ * @returns The instance for chaining
1298
+ */
1299
+ withAntiCsrfHeaders() {
1300
+ return this.withHeaders({
1301
+ "X-Requested-With": "XMLHttpRequest",
1302
+ });
1303
+ }
1304
+ /**
1305
+ * Add a request interceptor for this specific request
1306
+ * Request interceptors can modify the request configuration or return an early response
1307
+ *
1308
+ * @param interceptor - The request interceptor function
1309
+ * @returns The instance for chaining
1310
+ *
1311
+ * @example
1312
+ * request.withRequestInterceptor((config) => {
1313
+ * config.headers['X-Custom'] = 'value';
1314
+ * return config;
1315
+ * });
1316
+ */
1317
+ withRequestInterceptor(interceptor) {
1318
+ this.requestInterceptors.push(interceptor);
1319
+ return this;
1320
+ }
1321
+ /**
1322
+ * Add a response interceptor for this specific request
1323
+ * Response interceptors can transform the response
1324
+ *
1325
+ * @param interceptor - The response interceptor function
1326
+ * @returns The instance for chaining
1327
+ *
1328
+ * @example
1329
+ * request.withResponseInterceptor((response) => {
1330
+ * console.log('Status:', response.status);
1331
+ * return response;
1332
+ * });
1333
+ */
1334
+ withResponseInterceptor(interceptor) {
1335
+ this.responseInterceptors.push(interceptor);
1336
+ return this;
1337
+ }
1338
+ /**
1339
+ * Add an error interceptor for this specific request
1340
+ * Error interceptors can handle or transform errors
1341
+ *
1342
+ * @param interceptor - The error interceptor function
1343
+ * @returns The instance for chaining
1344
+ *
1345
+ * @example
1346
+ * request.withErrorInterceptor((error) => {
1347
+ * console.error('Request failed:', error);
1348
+ * throw error;
1349
+ * });
1350
+ */
1351
+ withErrorInterceptor(interceptor) {
1352
+ this.errorInterceptors.push(interceptor);
1353
+ return this;
1354
+ }
1355
+ /**
1356
+ * Execute the request and return the ResponseWrapper
1357
+ * This is the base method for getting the full response.
1358
+ *
1359
+ * @returns A promise that resolves to the ResponseWrapper
1360
+ *
1361
+ * @example
1362
+ * const response = await request.getResponse();
1363
+ * console.log(response.status);
1364
+ */
1365
+ async getResponse() {
1366
+ const url = this.formatUrlWithQueryParams(this.url);
1367
+ this.applyCsrfProtection();
1368
+ const fetchOptions = {
1369
+ ...this.requestOptions,
1370
+ method: this.method,
1371
+ };
1372
+ return !this.requestOptions.retries ? this.executeRequest(url, fetchOptions) : this.executeWithRetries(url, fetchOptions);
1373
+ }
1374
+ /**
1375
+ * Execute the request and parse the response as JSON
1376
+ *
1377
+ * @returns A promise that resolves to the parsed JSON data
1378
+ * @throws {RequestError} When the request fails, JSON parsing fails, GraphQL errors occur (if throwOnError enabled), or body is already consumed
1379
+ *
1380
+ * @example
1381
+ * const users = await request.getJson<User[]>();
1382
+ *
1383
+ * @example
1384
+ * // Error handling - errors are always RequestError
1385
+ * try {
1386
+ * const data = await request.getJson();
1387
+ * } catch (error) {
1388
+ * if (error instanceof RequestError) {
1389
+ * console.log(error.status, error.url, error.method);
1390
+ * }
1391
+ * }
1392
+ */
1393
+ async getJson() {
1394
+ const response = await this.getResponse();
1395
+ return response.getJson();
1396
+ }
1397
+ /**
1398
+ * Execute the request and get the response as text
1399
+ *
1400
+ * @returns A promise that resolves to the response text
1401
+ *
1402
+ * @example
1403
+ * const text = await request.getText();
1404
+ */
1405
+ async getText() {
1406
+ const response = await this.getResponse();
1407
+ return response.getText();
1408
+ }
1409
+ /**
1410
+ * Execute the request and get the response as a Blob
1411
+ *
1412
+ * @returns A promise that resolves to the response Blob
1413
+ *
1414
+ * @example
1415
+ * const blob = await request.getBlob();
1416
+ */
1417
+ async getBlob() {
1418
+ const response = await this.getResponse();
1419
+ return response.getBlob();
1420
+ }
1421
+ /**
1422
+ * Execute the request and get the response body as a ReadableStream
1423
+ *
1424
+ * @returns A promise that resolves to the response body stream
1425
+ *
1426
+ * @example
1427
+ * const stream = await request.getBody();
1428
+ */
1429
+ async getBody() {
1430
+ const response = await this.getResponse();
1431
+ return response.getBody();
1432
+ }
1433
+ /**
1434
+ * Execute the request and extract specific data using a selector function
1435
+ * If no selector is provided, returns the full JSON response.
1436
+ *
1437
+ * @param selector - Optional function to extract and transform data
1438
+ * @returns A promise that resolves to the selected data
1439
+ * @throws {RequestError} When the request fails, JSON parsing fails, or the selector throws an error
1440
+ *
1441
+ * @example
1442
+ * // Get full response
1443
+ * const data = await request.getData();
1444
+ *
1445
+ * // Extract specific data
1446
+ * const users = await request.getData(data => data.results.users);
1447
+ *
1448
+ * @example
1449
+ * // Error handling - errors are always RequestError
1450
+ * try {
1451
+ * const data = await request.getData();
1452
+ * } catch (error) {
1453
+ * if (error instanceof RequestError) {
1454
+ * console.log(error.status, error.url, error.method);
1455
+ * }
1456
+ * }
1457
+ */
1458
+ async getData(selector) {
1459
+ const response = await this.getResponse();
1460
+ return response.getData(selector);
1461
+ }
1462
+ /**
1463
+ * Apply CSRF protection headers based on configuration
1464
+ */
1465
+ applyCsrfProtection() {
1466
+ if (!this.autoApplyCsrfProtection)
1467
+ return;
1468
+ const config = Config.getInstance();
1469
+ // Apply anti-CSRF headers if enabled
1470
+ if (config.isAntiCsrfEnabled()) {
1471
+ this.withHeaders({
1472
+ "X-Requested-With": "XMLHttpRequest",
1473
+ });
1474
+ }
1475
+ // Apply global CSRF token if set
1476
+ const globalToken = config.getCsrfToken();
1477
+ if (globalToken) {
1478
+ const csrfHeaderName = config.getCsrfHeaderName();
1479
+ const hasLocalToken = this.hasHeader("X-CSRF-Token") || this.hasHeader(csrfHeaderName);
1480
+ if (!hasLocalToken) {
1481
+ this.withHeaders({
1482
+ [csrfHeaderName]: globalToken,
1483
+ });
1484
+ }
1485
+ }
1486
+ // Check for XSRF token in cookies and send it as a header
1487
+ if (config.isAutoXsrfEnabled() && typeof document !== "undefined") {
1488
+ const xsrfToken = CsrfUtils.getTokenFromCookie(config.getXsrfCookieName());
1489
+ if (xsrfToken && CsrfUtils.isValidToken(xsrfToken)) {
1490
+ const xsrfHeaderName = config.getXsrfHeaderName();
1491
+ const hasLocalToken = this.hasHeader("X-XSRF-TOKEN") || this.hasHeader(xsrfHeaderName);
1492
+ if (!hasLocalToken) {
1493
+ this.withHeaders({
1494
+ [xsrfHeaderName]: xsrfToken,
1495
+ });
1496
+ }
1497
+ }
1498
+ }
1499
+ }
1500
+ /**
1501
+ * Formats the URL with any query parameters
1502
+ * @param url The base URL
1503
+ * @returns The URL with query parameters appended
1504
+ */
1505
+ formatUrlWithQueryParams(url) {
1506
+ const queryString = this.queryParams.toString();
1507
+ if (!queryString) {
1508
+ return url;
1509
+ }
1510
+ try {
1511
+ // Try to use the URL constructor (works for absolute URLs)
1512
+ const urlObj = new URL(url);
1513
+ // Merge our query params with any that might be in the URL already
1514
+ this.queryParams.forEach((value, key) => {
1515
+ urlObj.searchParams.append(key, value);
1516
+ });
1517
+ return urlObj.toString();
1518
+ }
1519
+ catch (error) {
1520
+ // Handle relative URLs
1521
+ const hasExistingParams = url.includes("?");
1522
+ const separator = hasExistingParams ? "&" : "?";
1523
+ return `${url}${separator}${queryString}`;
1524
+ }
1525
+ }
1526
+ /**
1527
+ * Executes a request with configured retry logic
1528
+ * @param url The formatted URL to send the request to
1529
+ * @param fetchOptions The fetch options to use
1530
+ * @returns A wrapped response object
1531
+ * @throws RequestError if the request fails after all retries
1532
+ */
1533
+ async executeWithRetries(url, fetchOptions) {
1534
+ const maxRetries = this.requestOptions.retries || 0;
1535
+ const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
1536
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1537
+ try {
1538
+ return await this.executeRequest(url, fetchOptions);
1539
+ }
1540
+ catch (error) {
1541
+ const requestError = error instanceof RequestError ? error : RequestError.networkError(url, method, error instanceof Error ? error : new Error(String(error)));
1542
+ if (attempt >= maxRetries)
1543
+ throw requestError;
1544
+ if (this.requestOptions.onRetry) {
1545
+ await this.requestOptions.onRetry({ attempt: attempt + 1, error: requestError });
1546
+ }
1547
+ }
1548
+ }
1549
+ // This should never happen but is needed for type safety
1550
+ throw new RequestError(`Max retries reached`, url, method);
1551
+ }
1552
+ /**
1553
+ * Run request interceptors in order: global interceptors first, then per-request
1554
+ * @param configParam - The request configuration
1555
+ * @returns Modified config or a Response to short-circuit
1556
+ */
1557
+ async runRequestInterceptors(configParam) {
1558
+ const globalConfig = Config.getInstance();
1559
+ const allInterceptors = [...globalConfig.getRequestInterceptors(), ...this.requestInterceptors];
1560
+ let currentConfig = configParam;
1561
+ for (let i = 0; i < allInterceptors.length; i++) {
1562
+ try {
1563
+ const result = await allInterceptors[i](currentConfig);
1564
+ // If interceptor returns a Response, short-circuit
1565
+ if (result instanceof Response) {
1566
+ return result;
1567
+ }
1568
+ currentConfig = result;
1569
+ }
1570
+ catch (error) {
1571
+ const errorMessage = error instanceof Error ? error.message : String(error);
1572
+ throw new RequestError(`Request interceptor failed: ${errorMessage}`, currentConfig.url, currentConfig.method);
1573
+ }
1574
+ }
1575
+ return currentConfig;
1576
+ }
1577
+ /**
1578
+ * Run response interceptors in reverse order: per-request interceptors first, then global in reverse
1579
+ * @param response - The response wrapper
1580
+ * @returns Modified response wrapper
1581
+ */
1582
+ async runResponseInterceptors(response) {
1583
+ const globalConfig = Config.getInstance();
1584
+ const globalInterceptors = globalConfig.getResponseInterceptors();
1585
+ // Per-request in order, then global in reverse
1586
+ const allInterceptors = [...this.responseInterceptors, ...globalInterceptors.reverse()];
1587
+ let currentResponse = response;
1588
+ for (let i = 0; i < allInterceptors.length; i++) {
1589
+ try {
1590
+ currentResponse = await allInterceptors[i](currentResponse);
1591
+ }
1592
+ catch (error) {
1593
+ const errorMessage = error instanceof Error ? error.message : String(error);
1594
+ const url = currentResponse.url || "";
1595
+ const method = currentResponse.method || "";
1596
+ throw new RequestError(`Response interceptor failed: ${errorMessage}`, url, method);
1597
+ }
1598
+ }
1599
+ return currentResponse;
1600
+ }
1601
+ /**
1602
+ * Run error interceptors in reverse order: per-request interceptors first, then global in reverse
1603
+ * @param error - The error that occurred
1604
+ * @returns Modified error or a ResponseWrapper to recover
1605
+ */
1606
+ async runErrorInterceptors(error) {
1607
+ const globalConfig = Config.getInstance();
1608
+ const globalInterceptors = globalConfig.getErrorInterceptors();
1609
+ // Per-request in order, then global in reverse
1610
+ const allInterceptors = [...this.errorInterceptors, ...globalInterceptors.reverse()];
1611
+ let currentError = error;
1612
+ for (let i = 0; i < allInterceptors.length; i++) {
1613
+ try {
1614
+ // If previous interceptor recovered with a response, stop processing
1615
+ if (currentError instanceof ResponseWrapper) {
1616
+ return currentError;
1617
+ }
1618
+ const result = await allInterceptors[i](currentError);
1619
+ currentError = result;
1620
+ }
1621
+ catch (interceptorError) {
1622
+ // If an error interceptor throws, that becomes the new error
1623
+ if (interceptorError instanceof RequestError) {
1624
+ currentError = interceptorError;
1625
+ }
1626
+ else {
1627
+ const errorMessage = interceptorError instanceof Error ? interceptorError.message : String(interceptorError);
1628
+ // Always wrap in RequestError when we have context
1629
+ if (currentError instanceof RequestError) {
1630
+ currentError = new RequestError(`Error interceptor ${i + 1} failed: ${errorMessage}`, currentError.url, currentError.method, {
1631
+ status: currentError.status,
1632
+ response: currentError.response,
1633
+ });
1634
+ }
1635
+ else if (currentError instanceof ResponseWrapper && currentError.url && currentError.method) {
1636
+ // If it's a ResponseWrapper, we can still create a proper RequestError from its context
1637
+ currentError = new RequestError(`Error interceptor ${i + 1} failed: ${errorMessage}`, currentError.url, currentError.method, {
1638
+ status: currentError.status,
1639
+ });
1640
+ }
1641
+ else {
1642
+ // Last resort: if we have no context at all, use the original error's context
1643
+ // This shouldn't happen in practice, but handle it gracefully
1644
+ const errorObj = interceptorError instanceof Error ? interceptorError : new Error(String(interceptorError));
1645
+ currentError = RequestError.networkError(error.url, error.method, errorObj);
1646
+ }
1647
+ }
1648
+ }
1649
+ }
1650
+ return currentError;
1651
+ }
1652
+ /**
1653
+ * Helper to create an abort signal with timeout support
1654
+ * Handles various AbortSignal API levels gracefully
1655
+ */
1656
+ createAbortSignal(timeoutMs, externalController) {
1657
+ let timeoutId;
1658
+ let timeoutController;
1659
+ let isTimeout = false;
1660
+ // No timeout - just use external controller
1661
+ if (!timeoutMs) {
1662
+ return {
1663
+ signal: externalController === null || externalController === void 0 ? void 0 : externalController.signal,
1664
+ cleanup: () => { },
1665
+ wasTimeout: () => false,
1666
+ };
1667
+ }
1668
+ // Check if AbortSignal.any() is available (modern browsers)
1669
+ const hasAbortSignalAny = typeof AbortSignal.any === "function";
1670
+ const hasAbortSignalTimeout = typeof AbortSignal.timeout === "function";
1671
+ // Create timeout signal
1672
+ const createTimeoutSignal = () => {
1673
+ if (hasAbortSignalTimeout) {
1674
+ const signal = AbortSignal.timeout(timeoutMs);
1675
+ signal.addEventListener("abort", () => (isTimeout = true), { once: true });
1676
+ return signal;
1677
+ }
1678
+ else {
1679
+ timeoutController = new AbortController();
1680
+ timeoutId = setTimeout(() => {
1681
+ isTimeout = true;
1682
+ timeoutController.abort();
1683
+ }, timeoutMs);
1684
+ return timeoutController.signal;
1685
+ }
1686
+ };
1687
+ const timeoutSignal = createTimeoutSignal();
1688
+ // Combine signals if we have both timeout and external controller
1689
+ const finalSignal = externalController && hasAbortSignalAny
1690
+ ? AbortSignal.any([externalController.signal, timeoutSignal])
1691
+ : externalController
1692
+ ? this.combineSignalsManually(externalController.signal, timeoutSignal)
1693
+ : timeoutSignal;
1694
+ return {
1695
+ signal: finalSignal,
1696
+ cleanup: () => {
1697
+ if (timeoutId !== undefined)
1698
+ clearTimeout(timeoutId);
1699
+ },
1700
+ wasTimeout: () => isTimeout,
1701
+ };
1702
+ }
1703
+ /**
1704
+ * Manually combine two abort signals for older environments
1705
+ * Returns the first signal and listens to the second
1706
+ */
1707
+ combineSignalsManually(signal1, signal2) {
1708
+ // If either is already aborted, use that one
1709
+ if (signal1.aborted)
1710
+ return signal1;
1711
+ if (signal2.aborted)
1712
+ return signal2;
1713
+ // Use a controller to create a combined signal
1714
+ const controller = new AbortController();
1715
+ const abort = () => controller.abort();
1716
+ signal1.addEventListener("abort", abort, { once: true });
1717
+ signal2.addEventListener("abort", abort, { once: true });
1718
+ return controller.signal;
1719
+ }
1720
+ /**
1721
+ * Convert fetchOptions to RequestConfig with proper typing
1722
+ */
1723
+ createRequestConfig(url, fetchOptions) {
1724
+ const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
1725
+ const headers = this.getHeadersRecord();
1726
+ const extendedOptions = fetchOptions;
1727
+ return {
1728
+ url,
1729
+ method,
1730
+ headers,
1731
+ body: fetchOptions.body,
1732
+ signal: fetchOptions.signal || undefined,
1733
+ credentials: fetchOptions.credentials,
1734
+ mode: fetchOptions.mode,
1735
+ redirect: fetchOptions.redirect,
1736
+ referrer: fetchOptions.referrer,
1737
+ referrerPolicy: fetchOptions.referrerPolicy,
1738
+ keepalive: fetchOptions.keepalive,
1739
+ priority: extendedOptions.priority,
1740
+ };
1741
+ }
1742
+ /**
1743
+ * Apply interceptor results back to fetchOptions
1744
+ */
1745
+ applyRequestConfig(config, fetchOptions) {
1746
+ fetchOptions.headers = config.headers;
1747
+ if (config.body !== undefined) {
1748
+ fetchOptions.body = config.body;
1749
+ }
1750
+ }
1751
+ async executeRequest(url, fetchOptions) {
1752
+ const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
1753
+ // Setup abort signal with timeout
1754
+ const abortSignal = this.createAbortSignal(this.requestOptions.timeout, this.abortController);
1755
+ try {
1756
+ // Run request interceptors before making the request
1757
+ const requestConfig = this.createRequestConfig(url, fetchOptions);
1758
+ const interceptorResult = await this.runRequestInterceptors(requestConfig);
1759
+ // If interceptor returned a Response, short-circuit and wrap it
1760
+ if (interceptorResult instanceof Response) {
1761
+ const graphQLOptions = this.getGraphQLOptions();
1762
+ const wrappedResponse = new ResponseWrapper(interceptorResult, this.url, this.method, graphQLOptions);
1763
+ return await this.runResponseInterceptors(wrappedResponse);
1764
+ }
1765
+ // Update fetchOptions with interceptor modifications
1766
+ url = interceptorResult.url;
1767
+ this.validateUrl(url);
1768
+ this.applyRequestConfig(interceptorResult, fetchOptions);
1769
+ // Set the combined abort signal
1770
+ fetchOptions.signal = abortSignal.signal;
1771
+ // Execute fetch
1772
+ let response;
1773
+ try {
1774
+ response = await fetch(url, fetchOptions);
1775
+ }
1776
+ catch (error) {
1777
+ // Check if it's an abort error (DOMException in browsers, or AbortSignal abort)
1778
+ if (error instanceof DOMException && error.name === "AbortError") {
1779
+ if (abortSignal.wasTimeout()) {
1780
+ throw RequestError.timeout(url, method, this.requestOptions.timeout);
1781
+ }
1782
+ throw RequestError.abortError(url, method);
1783
+ }
1784
+ // Check for Node.js/undici TimeoutError
1785
+ const errorObj = error instanceof Error ? error : new Error(String(error));
1786
+ const errorName = errorObj.name;
1787
+ const errorMessage = errorObj.message.toLowerCase();
1788
+ // Detect timeout errors from Node.js/undici (TimeoutError)
1789
+ const isTimeoutError = errorName === "TimeoutError" || errorMessage.includes("timeout") || errorMessage.includes("aborted due to timeout") || abortSignal.wasTimeout();
1790
+ if (isTimeoutError && this.requestOptions.timeout) {
1791
+ throw RequestError.timeout(url, method, this.requestOptions.timeout);
1792
+ }
1793
+ throw RequestError.networkError(url, method, errorObj);
1794
+ }
1795
+ if (!response.ok) {
1796
+ throw RequestError.fromResponse(response, url, method);
1797
+ }
1798
+ const graphQLOptions = this.getGraphQLOptions();
1799
+ const wrappedResponse = new ResponseWrapper(response, url, method, graphQLOptions);
1800
+ return await this.runResponseInterceptors(wrappedResponse);
1801
+ }
1802
+ catch (error) {
1803
+ // Convert to RequestError if needed
1804
+ let requestError;
1805
+ if (error instanceof RequestError) {
1806
+ requestError = error;
1807
+ }
1808
+ else {
1809
+ const errorObj = error instanceof Error ? error : new Error(String(error));
1810
+ requestError = RequestError.networkError(url, method, errorObj);
1811
+ }
1812
+ // Run error interceptors
1813
+ const interceptorResult = await this.runErrorInterceptors(requestError);
1814
+ // If error interceptor returned a ResponseWrapper, recover from error
1815
+ if (interceptorResult instanceof ResponseWrapper) {
1816
+ return interceptorResult;
1817
+ }
1818
+ throw interceptorResult;
1819
+ }
1820
+ finally {
1821
+ abortSignal.cleanup();
1822
+ }
1823
+ }
1824
+ }
1825
+
1826
+ /**
1827
+ * Base class for requests that can have a body (POST, PUT, PATCH)
1828
+ */
1829
+ class BodyRequest extends BaseRequest {
1830
+ constructor(url) {
1831
+ super(url);
1832
+ this.graphQLOptions = undefined;
1833
+ }
1834
+ /**
1835
+ * Sets the body of the request
1836
+ * @param body - The request body
1837
+ */
1838
+ withBody(body) {
1839
+ this.body = body;
1840
+ // Set body type and validate
1841
+ if (typeof body === "string") {
1842
+ this.bodyType = BodyType.STRING;
1843
+ this.setContentTypeIfNeeded("text/plain");
1844
+ }
1845
+ else if (body !== null &&
1846
+ typeof body === "object" &&
1847
+ !(body instanceof FormData || body instanceof Blob || body instanceof ArrayBuffer || body instanceof URLSearchParams || body instanceof ReadableStream)) {
1848
+ this.bodyType = BodyType.JSON;
1849
+ this.setContentTypeIfNeeded("application/json");
1850
+ // Validate JSON is stringifiable early
1851
+ try {
1852
+ JSON.stringify(body);
1853
+ }
1854
+ catch (error) {
1855
+ const errorMessage = error instanceof Error ? error.message : String(error);
1856
+ throw new RequestError(`JSON stringify failed: ${errorMessage}`, this.url, this.method);
1857
+ }
1858
+ }
1859
+ else {
1860
+ this.bodyType = BodyType.BINARY;
1861
+ }
1862
+ return this;
1863
+ }
1864
+ /**
1865
+ * Sets a GraphQL query or mutation as the request body
1866
+ * Automatically formats the body as JSON and sets Content-Type to application/json
1867
+ *
1868
+ * @param query - The GraphQL query or mutation string
1869
+ * @param variables - Optional variables object to pass with the query
1870
+ * @param options - Optional GraphQL-specific options
1871
+ * @returns The request instance for chaining
1872
+ *
1873
+ * @example
1874
+ * const request = new PostRequest('/graphql')
1875
+ * .withGraphQL('query { user(id: $id) { name email } }', { id: '123' });
1876
+ *
1877
+ * @example
1878
+ * const request = new PostRequest('/graphql')
1879
+ * .withGraphQL('mutation { createUser(name: $name) { id } }', { name: 'John' });
1880
+ *
1881
+ * @example
1882
+ * // Throw an error if the GraphQL response contains errors
1883
+ * const request = new PostRequest('/graphql')
1884
+ * .withGraphQL('query { user { id } }', undefined, { throwOnError: true });
1885
+ */
1886
+ withGraphQL(query, variables, options) {
1887
+ if (typeof query !== "string" || query.length === 0) {
1888
+ throw new RequestError("Invalid GraphQL query", this.url, this.method);
1889
+ }
1890
+ const graphQLBody = {
1891
+ query: query,
1892
+ };
1893
+ if (variables !== undefined) {
1894
+ if (typeof variables !== "object" || variables === null || Array.isArray(variables)) {
1895
+ throw new RequestError("Invalid GraphQL variables", this.url, this.method);
1896
+ }
1897
+ graphQLBody.variables = variables;
1898
+ }
1899
+ // Store GraphQL options if provided
1900
+ if (options !== undefined) {
1901
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
1902
+ throw new RequestError("Invalid GraphQL options", this.url, this.method);
1903
+ }
1904
+ // Store only the known GraphQL options properties
1905
+ const opts = options;
1906
+ this.graphQLOptions = {
1907
+ throwOnError: typeof opts.throwOnError === "boolean" ? opts.throwOnError : undefined,
1908
+ };
1909
+ }
1910
+ // Validate JSON is stringifiable early
1911
+ try {
1912
+ JSON.stringify(graphQLBody);
1913
+ }
1914
+ catch (error) {
1915
+ const errorMessage = error instanceof Error ? error.message : String(error);
1916
+ throw new RequestError(`JSON stringify failed: ${errorMessage}`, this.url, this.method);
1917
+ }
1918
+ this.body = graphQLBody;
1919
+ this.bodyType = BodyType.JSON;
1920
+ this.setContentTypeIfNeeded("application/json");
1921
+ return this;
1922
+ }
1923
+ /**
1924
+ * Check if Content-Type header is already set (case-insensitive)
1925
+ */
1926
+ hasContentType() {
1927
+ const headers = this.requestOptions.headers;
1928
+ if (typeof headers === "object" && headers !== null) {
1929
+ const headersObj = headers;
1930
+ return Object.keys(headersObj).some(header => header.toLowerCase() === "content-type");
1931
+ }
1932
+ return false;
1933
+ }
1934
+ setContentTypeIfNeeded(contentType) {
1935
+ if (!this.hasContentType()) {
1936
+ this.withContentType(contentType);
1937
+ }
1938
+ }
1939
+ /**
1940
+ * Get the GraphQL options if set
1941
+ * @returns The GraphQL options or undefined
1942
+ */
1943
+ getGraphQLOptions() {
1944
+ return this.graphQLOptions;
1945
+ }
1946
+ /**
1947
+ * Execute the request and return the ResponseWrapper
1948
+ * Overrides the base implementation to add body handling
1949
+ */
1950
+ async getResponse() {
1951
+ if (this.body !== undefined) {
1952
+ // Remove previous body if it exists
1953
+ if (this.requestOptions.body)
1954
+ delete this.requestOptions.body;
1955
+ // Process the body based on its type
1956
+ if (this.bodyType === BodyType.JSON) {
1957
+ this.requestOptions.body = JSON.stringify(this.body);
1958
+ }
1959
+ else {
1960
+ this.requestOptions.body = this.body;
1961
+ }
1962
+ }
1963
+ return super.getResponse();
1964
+ }
1965
+ }
1966
+
1967
+ /**
1968
+ * HTTP GET request implementation
1969
+ * Used for retrieving data from an API endpoint.
1970
+ *
1971
+ * @example
1972
+ * const request = new GetRequest('/api/users')
1973
+ * .withQueryParams({ id: '123' });
1974
+ * const data = await request.getData();
1975
+ */
1976
+ class GetRequest extends BaseRequest {
1977
+ constructor(url) {
1978
+ super(url);
1979
+ this.method = exports.HttpMethod.GET;
1980
+ }
1981
+ }
1982
+ /**
1983
+ * HTTP HEAD request implementation
1984
+ * Similar to GET but returns only headers without a response body.
1985
+ *
1986
+ * @example
1987
+ * const request = new HeadRequest('/api/users/123');
1988
+ * const response = await request.getResponse();
1989
+ */
1990
+ class HeadRequest extends BaseRequest {
1991
+ constructor(url) {
1992
+ super(url);
1993
+ this.method = exports.HttpMethod.HEAD;
1994
+ }
1995
+ }
1996
+ /**
1997
+ * HTTP OPTIONS request implementation
1998
+ * Used to describe the communication options for the target resource.
1999
+ *
2000
+ * @example
2001
+ * const request = new OptionsRequest('/api/users');
2002
+ * const response = await request.getResponse();
2003
+ */
2004
+ class OptionsRequest extends BaseRequest {
2005
+ constructor(url) {
2006
+ super(url);
2007
+ this.method = exports.HttpMethod.OPTIONS;
2008
+ }
2009
+ }
2010
+ /**
2011
+ * HTTP DELETE request implementation
2012
+ * Used to delete a resource on the server.
2013
+ *
2014
+ * @example
2015
+ * const request = new DeleteRequest('/api/users/123');
2016
+ * await request.getData();
2017
+ */
2018
+ class DeleteRequest extends BaseRequest {
2019
+ constructor(url) {
2020
+ super(url);
2021
+ this.method = exports.HttpMethod.DELETE;
2022
+ }
2023
+ }
2024
+ /**
2025
+ * HTTP POST request implementation
2026
+ * Used to create a new resource or submit data for processing.
2027
+ *
2028
+ * @example
2029
+ * const request = new PostRequest('/api/users')
2030
+ * .withJson({ name: 'John', email: 'john@example.com' });
2031
+ * const data = await request.getData();
2032
+ */
2033
+ class PostRequest extends BodyRequest {
2034
+ constructor(url) {
2035
+ super(url);
2036
+ this.method = exports.HttpMethod.POST;
2037
+ }
2038
+ }
2039
+ /**
2040
+ * HTTP PUT request implementation
2041
+ * Used to replace or update an existing resource.
2042
+ *
2043
+ * @example
2044
+ * const request = new PutRequest('/api/users/123')
2045
+ * .withJson({ id: '123', name: 'John', email: 'john@example.com' });
2046
+ * const data = await request.getData();
2047
+ */
2048
+ class PutRequest extends BodyRequest {
2049
+ constructor(url) {
2050
+ super(url);
2051
+ this.method = exports.HttpMethod.PUT;
2052
+ }
2053
+ }
2054
+ /**
2055
+ * HTTP PATCH request implementation
2056
+ * Used to apply partial modifications to a resource.
2057
+ *
2058
+ * @example
2059
+ * const request = new PatchRequest('/api/users/123')
2060
+ * .withJson({ email: 'new.email@example.com' });
2061
+ * const data = await request.getData();
2062
+ */
2063
+ class PatchRequest extends BodyRequest {
2064
+ constructor(url) {
2065
+ super(url);
2066
+ this.method = exports.HttpMethod.PATCH;
2067
+ }
2068
+ }
2069
+
2070
+ /**
2071
+ * Create a GET request
2072
+ * Used for retrieving resources from the server.
2073
+ *
2074
+ * @param url - The URL to send the request to
2075
+ * @returns A new GET request instance
2076
+ *
2077
+ * @example
2078
+ * const request = get('/api/users')
2079
+ * .withQueryParams({ limit: 10 });
2080
+ * const users = await request.getData();
2081
+ */
2082
+ function get(url) {
2083
+ return new GetRequest(url);
2084
+ }
2085
+ /**
2086
+ * Create a POST request
2087
+ * Used for creating new resources or submitting data.
2088
+ *
2089
+ * @param url - The URL to send the request to
2090
+ * @returns A new POST request instance
2091
+ *
2092
+ * @example
2093
+ * const request = post('/api/users')
2094
+ * .withJson({ name: 'John', email: 'john@example.com' });
2095
+ * const newUser = await request.getData();
2096
+ */
2097
+ function post(url) {
2098
+ return new PostRequest(url);
2099
+ }
2100
+ /**
2101
+ * Create a PUT request
2102
+ * Used for replacing or updating an existing resource.
2103
+ *
2104
+ * @param url - The URL to send the request to
2105
+ * @returns A new PUT request instance
2106
+ *
2107
+ * @example
2108
+ * const request = put('/api/users/123')
2109
+ * .withJson({ name: 'John Updated', email: 'john@example.com' });
2110
+ * const updatedUser = await request.getData();
2111
+ */
2112
+ function put(url) {
2113
+ return new PutRequest(url);
2114
+ }
2115
+ /**
2116
+ * Create a DELETE request
2117
+ * Used for removing resources from the server.
2118
+ *
2119
+ * @param url - The URL to send the request to
2120
+ * @returns A new DELETE request instance
2121
+ *
2122
+ * @example
2123
+ * const request = del('/api/users/123');
2124
+ * await request.getData();
2125
+ */
2126
+ function del(url) {
2127
+ return new DeleteRequest(url);
2128
+ }
2129
+ /**
2130
+ * Create a PATCH request
2131
+ * Used for applying partial modifications to a resource.
2132
+ *
2133
+ * @param url - The URL to send the request to
2134
+ * @returns A new PATCH request instance
2135
+ *
2136
+ * @example
2137
+ * const request = patch('/api/users/123')
2138
+ * .withJson({ status: 'active' });
2139
+ * const patchedUser = await request.getData();
2140
+ */
2141
+ function patch(url) {
2142
+ return new PatchRequest(url);
2143
+ }
2144
+ /**
2145
+ * Create a HEAD request
2146
+ * Similar to GET but returns only headers without a body.
2147
+ *
2148
+ * @param url - The URL to send the request to
2149
+ * @returns A new HEAD request instance
2150
+ *
2151
+ * @example
2152
+ * const request = head('/api/users/123');
2153
+ * const response = await request.getResponse();
2154
+ * console.log(response.headers.get('Last-Modified'));
2155
+ */
2156
+ function head(url) {
2157
+ return new HeadRequest(url);
2158
+ }
2159
+ /**
2160
+ * Create an OPTIONS request
2161
+ * Used to describe the communication options for a resource.
2162
+ *
2163
+ * @param url - The URL to send the request to
2164
+ * @returns A new OPTIONS request instance
2165
+ *
2166
+ * @example
2167
+ * const request = options('/api/users');
2168
+ * const response = await request.getResponse();
2169
+ * console.log(response.headers.get('Allow'));
2170
+ */
2171
+ function options(url) {
2172
+ return new OptionsRequest(url);
2173
+ }
2174
+
2175
+ /**
2176
+ * Main API object for creating HTTP requests
2177
+ * Provides factory methods for all HTTP methods and access to global configuration.
2178
+ */
2179
+ const create = {
2180
+ get,
2181
+ post,
2182
+ put,
2183
+ del,
2184
+ patch,
2185
+ head,
2186
+ options,
2187
+ config: Config.getInstance(),
2188
+ };
2189
+
2190
+ exports.CookieUtils = CookieUtils;
2191
+ exports.DeleteRequest = DeleteRequest;
2192
+ exports.GetRequest = GetRequest;
2193
+ exports.HeadRequest = HeadRequest;
2194
+ exports.OptionsRequest = OptionsRequest;
2195
+ exports.PatchRequest = PatchRequest;
2196
+ exports.PostRequest = PostRequest;
2197
+ exports.PutRequest = PutRequest;
2198
+ exports.RequestError = RequestError;
2199
+ exports.ResponseWrapper = ResponseWrapper;
2200
+ exports["default"] = create;
2201
+ //# sourceMappingURL=index.cjs.map