create-request 0.0.1-beta.4 → 0.0.1-beta.6

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.
@@ -57,10 +57,10 @@ export declare abstract class BaseRequest {
57
57
  /**
58
58
  * Adds a single query parameter
59
59
  * @param key - The parameter name
60
- * @param value - The parameter value
60
+ * @param value - The parameter value, can be a single value or array of values
61
61
  * @returns The instance for chaining
62
62
  */
63
- withQueryParam(key: string, value: string | number | boolean | null | undefined): this;
63
+ withQueryParam(key: string, value: string | string[] | number | boolean | null | undefined): this;
64
64
  /**
65
65
  * Sets the mode of the request
66
66
  * @param mode - The mode for the request (cors, no-cors, same-origin, navigate)
@@ -86,12 +86,24 @@ export declare abstract class BaseRequest {
86
86
  * @returns The instance for chaining
87
87
  */
88
88
  withBasicAuth(username: string, password: string): this;
89
+ /**
90
+ * Cross-environment base64 encoding
91
+ * Works in both browser and Node.js environments
92
+ */
93
+ private encodeBase64;
89
94
  /**
90
95
  * Sets the bearer token for authentication
91
96
  * @param token - The bearer token
92
97
  * @returns The instance for chaining
93
98
  */
94
99
  withBearerToken(token: string): this;
100
+ /**
101
+ * Helper function to check for header presence in a case-insensitive way
102
+ * @param headers Headers object
103
+ * @param headerName Header name to check
104
+ * @returns Boolean indicating if the header exists (case-insensitive)
105
+ */
106
+ private hasHeader;
95
107
  /**
96
108
  * Sets cookies for the request
97
109
  * @param cookies Object containing cookie name-value pairs or cookie options
@@ -5,7 +5,6 @@ import type { Body } from "./types";
5
5
  */
6
6
  export declare abstract class BodyRequest extends BaseRequest {
7
7
  protected body?: Body;
8
- private bodyProcessed;
9
8
  private bodyType?;
10
9
  /**
11
10
  * Sets the body of the request
@@ -2,55 +2,22 @@
2
2
  * Wrapper for HTTP responses with methods to transform the response data
3
3
  */
4
4
  export declare class ResponseWrapper {
5
- readonly ok: boolean;
6
- private response;
5
+ private readonly response;
6
+ private textCache;
7
+ private jsonCache;
8
+ private blobCache;
9
+ private arrayBufferCache;
10
+ private bodyUsed;
7
11
  constructor(response: Response);
8
- /**
9
- * Get the raw Response object
10
- */
11
- get raw(): Response;
12
- /**
13
- * Get the HTTP status code
14
- */
15
12
  get status(): number;
16
- /**
17
- * Get the HTTP status text
18
- */
19
13
  get statusText(): string;
20
- /**
21
- * Get response headers
22
- */
23
14
  get headers(): Headers;
24
- /**
25
- * Parse response as JSON
26
- */
15
+ get ok(): boolean;
16
+ get raw(): Response;
27
17
  getJson<T = unknown>(): Promise<T>;
28
- /**
29
- * Get response as text
30
- */
31
18
  getText(): Promise<string>;
32
- /**
33
- * Get response as Blob
34
- */
35
19
  getBlob(): Promise<Blob>;
36
- /**
37
- * Get response as ArrayBuffer
38
- */
39
20
  getArrayBuffer(): Promise<ArrayBuffer>;
40
- /**
41
- * Get response as ReadableStream for true streaming consumption.
42
- * This allows you to process data incrementally as it arrives.
43
- *
44
- * @example
45
- * const stream = await request.sendTo('https://example.com/large-file').getBody();
46
- * const reader = stream.getReader();
47
- *
48
- * while (true) {
49
- * const { done, value } = await reader.read();
50
- * if (done) break;
51
- * // Process chunk of data in 'value'
52
- * }
53
- */
54
21
  getBody(): ReadableStream<Uint8Array> | null;
55
22
  }
56
23
  /**
@@ -1,11 +1,11 @@
1
1
  import { get, put, head, post, patch, del, options } from "./requestFactories";
2
2
  import { Config } from "./utils/Config";
3
- export { HttpMethod, RequestPriority, CredentialsPolicy, RequestMode, RedirectMode, CacheMode, SameSitePolicy, } from "./enums";
3
+ export { HttpMethod, RequestPriority, CredentialsPolicy, RequestMode, RedirectMode, CacheMode, SameSitePolicy } from "./enums";
4
4
  export type { RetryCallback, RequestOptions, CookieOptions, CookiesRecord } from "./types";
5
5
  export { RequestError } from "./RequestError";
6
6
  export { ResponseWrapper } from "./ResponseWrapper";
7
7
  export { CookieUtils } from "./utils/CookieUtils";
8
- export { GetRequest, PostRequest, PutRequest, DeleteRequest, PatchRequest, HeadRequest, OptionsRequest, } from "./requestMethods";
8
+ export { GetRequest, PostRequest, PutRequest, DeleteRequest, PatchRequest, HeadRequest, OptionsRequest } from "./requestMethods";
9
9
  declare const create: {
10
10
  readonly get: typeof get;
11
11
  readonly post: typeof post;
@@ -121,72 +121,114 @@ class RequestError extends Error {
121
121
  */
122
122
  class ResponseWrapper {
123
123
  constructor(response) {
124
+ this.bodyUsed = false;
124
125
  this.response = response;
125
- this.ok = response.status >= 200 && response.status < 300; // Standard Fetch API behavior
126
126
  }
127
- /**
128
- * Get the raw Response object
129
- */
130
- get raw() {
131
- return this.response;
132
- }
133
- /**
134
- * Get the HTTP status code
135
- */
127
+ // Wrapper properties
136
128
  get status() {
137
129
  return this.response.status;
138
130
  }
139
- /**
140
- * Get the HTTP status text
141
- */
142
131
  get statusText() {
143
132
  return this.response.statusText;
144
133
  }
145
- /**
146
- * Get response headers
147
- */
148
134
  get headers() {
149
135
  return this.response.headers;
150
136
  }
151
- /**
152
- * Parse response as JSON
153
- */
137
+ get ok() {
138
+ return this.response.ok;
139
+ }
140
+ get raw() {
141
+ return this.response;
142
+ }
143
+ // Response body methods with caching
154
144
  async getJson() {
155
- return this.response.json();
145
+ if (this.jsonCache !== undefined) {
146
+ return this.jsonCache;
147
+ }
148
+ if (this.bodyUsed) {
149
+ // If we already have text, try to parse it as JSON
150
+ if (this.textCache !== undefined) {
151
+ try {
152
+ // Fix unsafe assignment by explicitly casting to unknown first
153
+ this.jsonCache = JSON.parse(this.textCache);
154
+ return this.jsonCache;
155
+ }
156
+ catch (e) {
157
+ throw new Error("Cannot parse cached text as JSON");
158
+ }
159
+ }
160
+ throw new Error("Response body has already been consumed in a different format");
161
+ }
162
+ this.bodyUsed = true;
163
+ this.jsonCache = (await this.response.json());
164
+ return this.jsonCache;
156
165
  }
157
- /**
158
- * Get response as text
159
- */
160
166
  async getText() {
161
- return this.response.text();
167
+ if (this.textCache !== undefined) {
168
+ return this.textCache;
169
+ }
170
+ if (this.bodyUsed) {
171
+ // If we already have JSON, convert it to text
172
+ if (this.jsonCache !== undefined) {
173
+ this.textCache = JSON.stringify(this.jsonCache);
174
+ return this.textCache;
175
+ }
176
+ // If we have blob, convert to text
177
+ if (this.blobCache !== undefined) {
178
+ this.textCache = await this.blobCache.text();
179
+ return this.textCache;
180
+ }
181
+ throw new Error("Response body has already been consumed in a format that cannot be converted to text");
182
+ }
183
+ this.bodyUsed = true;
184
+ this.textCache = await this.response.text();
185
+ return this.textCache;
162
186
  }
163
- /**
164
- * Get response as Blob
165
- */
166
187
  async getBlob() {
167
- return this.response.blob();
188
+ if (this.blobCache !== undefined) {
189
+ return this.blobCache;
190
+ }
191
+ if (this.bodyUsed) {
192
+ // If we have text, convert to Blob
193
+ if (this.textCache !== undefined) {
194
+ this.blobCache = new Blob([this.textCache], { type: "text/plain" });
195
+ return this.blobCache;
196
+ }
197
+ throw new Error("Response body has already been consumed in a format that cannot be converted to Blob");
198
+ }
199
+ this.bodyUsed = true;
200
+ this.blobCache = await this.response.blob();
201
+ return this.blobCache;
168
202
  }
169
- /**
170
- * Get response as ArrayBuffer
171
- */
172
203
  async getArrayBuffer() {
173
- return this.response.arrayBuffer();
174
- }
175
- /**
176
- * Get response as ReadableStream for true streaming consumption.
177
- * This allows you to process data incrementally as it arrives.
178
- *
179
- * @example
180
- * const stream = await request.sendTo('https://example.com/large-file').getBody();
181
- * const reader = stream.getReader();
182
- *
183
- * while (true) {
184
- * const { done, value } = await reader.read();
185
- * if (done) break;
186
- * // Process chunk of data in 'value'
187
- * }
188
- */
204
+ if (this.arrayBufferCache !== undefined) {
205
+ return this.arrayBufferCache;
206
+ }
207
+ if (this.bodyUsed) {
208
+ // If we have text, convert to ArrayBuffer
209
+ if (this.textCache !== undefined) {
210
+ const encoder = new TextEncoder();
211
+ // Fix ArrayBufferLike by explicitly casting to ArrayBuffer
212
+ const uint8Array = encoder.encode(this.textCache);
213
+ this.arrayBufferCache = uint8Array.buffer;
214
+ return this.arrayBufferCache;
215
+ }
216
+ // If we have blob, convert to ArrayBuffer
217
+ if (this.blobCache !== undefined) {
218
+ this.arrayBufferCache = await this.blobCache.arrayBuffer();
219
+ return this.arrayBufferCache;
220
+ }
221
+ throw new Error("Response body has already been consumed in a format that cannot be converted to ArrayBuffer");
222
+ }
223
+ this.bodyUsed = true;
224
+ this.arrayBufferCache = await this.response.arrayBuffer();
225
+ return this.arrayBufferCache;
226
+ }
189
227
  getBody() {
228
+ if (this.bodyUsed) {
229
+ throw new Error("Response body has already been consumed");
230
+ }
231
+ this.bodyUsed = true;
190
232
  return this.response.body;
191
233
  }
192
234
  }
@@ -292,15 +334,15 @@ class Config {
292
334
  return this.enableAntiCsrf;
293
335
  }
294
336
  /**
295
- * Reset configuration to defaults
337
+ * Reset all configuration options to their defaults
296
338
  */
297
339
  reset() {
298
340
  this.csrfToken = null;
299
341
  this.csrfHeaderName = "X-CSRF-Token";
342
+ this.enableAntiCsrf = true;
300
343
  this.xsrfCookieName = "XSRF-TOKEN";
301
344
  this.xsrfHeaderName = "X-XSRF-TOKEN";
302
345
  this.enableAutoXsrf = true;
303
- this.enableAntiCsrf = true;
304
346
  return this;
305
347
  }
306
348
  }
@@ -365,12 +407,34 @@ class CsrfUtils {
365
407
  }
366
408
  /**
367
409
  * Validates if the provided string is a potential CSRF token
368
- * Checks if the token meets minimum security requirements (non-empty, not too short)
410
+ * Checks if the token meets security requirements
369
411
  * @param token The token to validate
370
- * @returns Whether the token is potentially valid
412
+ * @returns Whether the token is valid
371
413
  */
372
414
  static isValidToken(token) {
373
- return typeof token === "string" && token.length >= 8;
415
+ // Basic checks for null/undefined and type
416
+ if (typeof token !== "string") {
417
+ return false;
418
+ }
419
+ if (token.length < 8)
420
+ return false;
421
+ // If token is longer than 10 chars, perform additional security checks
422
+ if (token.length > 10) {
423
+ // Check for valid character set (alphanumeric & common token symbols)
424
+ const validTokenRegex = /^[A-Za-z0-9\-_=+/.]+$/;
425
+ if (!validTokenRegex.test(token)) {
426
+ return false;
427
+ }
428
+ // For longer tokens, check for sufficient entropy (at least 2 character types)
429
+ const hasUpperCase = /[A-Z]/.test(token);
430
+ const hasLowerCase = /[a-z]/.test(token);
431
+ const hasNumbers = /[0-9]/.test(token);
432
+ const hasSpecials = /[-_=+/.]/.test(token);
433
+ const characterTypesCount = [hasUpperCase, hasLowerCase, hasNumbers, hasSpecials].filter(Boolean).length;
434
+ return characterTypesCount >= 2;
435
+ }
436
+ // For shorter tokens (8-10 chars), just return true if we reached here
437
+ return true;
374
438
  }
375
439
  }
376
440
 
@@ -391,10 +455,14 @@ class BaseRequest {
391
455
  return this;
392
456
  }
393
457
  withTimeout(timeout) {
458
+ if (!Number.isFinite(timeout) || timeout <= 0)
459
+ throw new Error("Timeout must be a positive number");
394
460
  this.requestOptions.timeout = timeout;
395
461
  return this;
396
462
  }
397
463
  withRetries(retries) {
464
+ if (!Number.isInteger(retries) || retries < 0)
465
+ throw new Error("Retry count must be a non-negative integer");
398
466
  this.requestOptions.retries = retries;
399
467
  return this;
400
468
  }
@@ -479,13 +547,16 @@ class BaseRequest {
479
547
  /**
480
548
  * Adds a single query parameter
481
549
  * @param key - The parameter name
482
- * @param value - The parameter value
550
+ * @param value - The parameter value, can be a single value or array of values
483
551
  * @returns The instance for chaining
484
552
  */
485
553
  withQueryParam(key, value) {
486
- if (value !== null && value !== undefined) {
554
+ if (value === null || value === undefined)
555
+ return this;
556
+ if (Array.isArray(value))
557
+ value.forEach(v => this.queryParams.append(key, String(v)));
558
+ else
487
559
  this.queryParams.append(key, String(value));
488
- }
489
560
  return this;
490
561
  }
491
562
  /**
@@ -524,9 +595,23 @@ class BaseRequest {
524
595
  * @returns The instance for chaining
525
596
  */
526
597
  withBasicAuth(username, password) {
527
- const credentials = btoa(`${username}:${password}`);
598
+ const credentials = this.encodeBase64(`${username}:${password}`);
528
599
  return this.withAuthorization(`Basic ${credentials}`);
529
600
  }
601
+ /**
602
+ * Cross-environment base64 encoding
603
+ * Works in both browser and Node.js environments
604
+ */
605
+ encodeBase64(str) {
606
+ // Browser environment
607
+ if (typeof btoa === "function")
608
+ return btoa(str);
609
+ // Node.js environment
610
+ if (typeof Buffer !== "undefined")
611
+ return Buffer.from(str).toString("base64");
612
+ // Fallback (should never happen in modern environments)
613
+ throw new Error("Base64 encoding is not supported in this environment");
614
+ }
530
615
  /**
531
616
  * Sets the bearer token for authentication
532
617
  * @param token - The bearer token
@@ -535,6 +620,15 @@ class BaseRequest {
535
620
  withBearerToken(token) {
536
621
  return this.withAuthorization(`Bearer ${token}`);
537
622
  }
623
+ /**
624
+ * Helper function to check for header presence in a case-insensitive way
625
+ * @param headers Headers object
626
+ * @param headerName Header name to check
627
+ * @returns Boolean indicating if the header exists (case-insensitive)
628
+ */
629
+ hasHeader(headers, headerName) {
630
+ return Object.keys(headers).some(key => key.toLowerCase() === headerName.toLowerCase());
631
+ }
538
632
  /**
539
633
  * Sets cookies for the request
540
634
  * @param cookies Object containing cookie name-value pairs or cookie options
@@ -547,19 +641,34 @@ class BaseRequest {
547
641
  }
548
642
  // Get current headers or initialize empty object
549
643
  const currentHeaders = this.requestOptions.headers || {};
550
- // Get the existing cookie header in a case-insensitive way
551
- let existingCookies = "";
552
- const cookieHeaderName = Object.keys(currentHeaders).find(header => header.toLowerCase() === "cookie");
553
- if (cookieHeaderName)
554
- existingCookies = currentHeaders[cookieHeaderName];
644
+ // Get all existing cookie headers with different cases
645
+ const cookieValues = [];
646
+ const cookieHeaderKeys = [];
647
+ Object.keys(currentHeaders).forEach(key => {
648
+ if (key.toLowerCase() === "cookie") {
649
+ // Don't add empty cookie values
650
+ if (currentHeaders[key])
651
+ cookieValues.push(currentHeaders[key]);
652
+ cookieHeaderKeys.push(key);
653
+ }
654
+ });
655
+ // Format the new cookies
555
656
  const cookieString = CookieUtils.formatRequestCookies(cookies);
556
- // Combine with existing cookies if present
557
- const newCookieValue = existingCookies ? `${existingCookies}; ${cookieString}` : cookieString;
558
- // Set the Cookie header, preserving original case if it exists
559
- const headerName = cookieHeaderName || "Cookie";
560
- return this.withHeaders({
561
- [headerName]: newCookieValue,
657
+ // Choose which header name to use - preserve existing case if possible
658
+ const headerName = cookieHeaderKeys.length > 0 ? cookieHeaderKeys[0] : "Cookie";
659
+ // Combine all existing cookie values with the new ones
660
+ const combinedCookieValue = [...cookieValues, cookieString].filter(Boolean).join("; ");
661
+ // Create a new headers object without any cookie headers
662
+ const newHeaders = { ...currentHeaders };
663
+ cookieHeaderKeys.forEach(key => {
664
+ delete newHeaders[key];
562
665
  });
666
+ // Set the new combined cookie header
667
+ this.requestOptions.headers = {
668
+ ...newHeaders,
669
+ [headerName]: combinedCookieValue,
670
+ };
671
+ return this;
563
672
  }
564
673
  /**
565
674
  * Set a single cookie
@@ -618,12 +727,13 @@ class BaseRequest {
618
727
  // Apply global CSRF token if set
619
728
  const globalToken = config.getCsrfToken();
620
729
  if (globalToken) {
621
- // Check if local token exists
730
+ // Check if local token exists using case-insensitive comparison
622
731
  const headers = this.requestOptions.headers;
623
- const hasLocalToken = Object.keys(headers).some(key => key === "X-CSRF-Token" || key === config.getCsrfHeaderName());
732
+ const csrfHeaderName = config.getCsrfHeaderName();
733
+ const hasLocalToken = this.hasHeader(headers, "X-CSRF-Token") || this.hasHeader(headers, csrfHeaderName);
624
734
  if (!hasLocalToken) {
625
735
  this.withHeaders({
626
- [config.getCsrfHeaderName()]: globalToken,
736
+ [csrfHeaderName]: globalToken,
627
737
  });
628
738
  }
629
739
  }
@@ -632,10 +742,11 @@ class BaseRequest {
632
742
  const xsrfToken = CsrfUtils.getTokenFromCookie(config.getXsrfCookieName());
633
743
  if (xsrfToken && CsrfUtils.isValidToken(xsrfToken)) {
634
744
  const headers = this.requestOptions.headers;
635
- const hasLocalToken = Object.keys(headers).some(key => key === "X-XSRF-TOKEN" || key === config.getXsrfHeaderName());
745
+ const xsrfHeaderName = config.getXsrfHeaderName();
746
+ const hasLocalToken = this.hasHeader(headers, "X-XSRF-TOKEN") || this.hasHeader(headers, xsrfHeaderName);
636
747
  if (!hasLocalToken) {
637
748
  this.withHeaders({
638
- [config.getXsrfHeaderName()]: xsrfToken,
749
+ [xsrfHeaderName]: xsrfToken,
639
750
  });
640
751
  }
641
752
  }
@@ -647,9 +758,7 @@ class BaseRequest {
647
758
  method: this.method,
648
759
  };
649
760
  // Create the base promise
650
- const basePromise = !this.requestOptions.retries
651
- ? this.executeRequest(url, fetchOptions)
652
- : this.executeWithRetries(url, fetchOptions);
761
+ const basePromise = !this.requestOptions.retries ? this.executeRequest(url, fetchOptions) : this.executeWithRetries(url, fetchOptions);
653
762
  const responsePromise = basePromise;
654
763
  // Attach convenience methods that begin processing immediately
655
764
  responsePromise.getJson = async () => {
@@ -678,12 +787,21 @@ class BaseRequest {
678
787
  if (!this.queryParams.toString()) {
679
788
  return url;
680
789
  }
681
- const urlObj = new URL(url);
682
- // Merge our query params with any that might be in the URL already
683
- this.queryParams.forEach((value, key) => {
684
- urlObj.searchParams.append(key, value);
685
- });
686
- return urlObj.toString();
790
+ try {
791
+ // Try to use the URL constructor (works for absolute URLs)
792
+ const urlObj = new URL(url);
793
+ // Merge our query params with any that might be in the URL already
794
+ this.queryParams.forEach((value, key) => {
795
+ urlObj.searchParams.append(key, value);
796
+ });
797
+ return urlObj.toString();
798
+ }
799
+ catch (error) {
800
+ // Handle relative URLs
801
+ const hasExistingParams = url.includes("?");
802
+ const separator = hasExistingParams ? "&" : "?";
803
+ return `${url}${separator}${this.queryParams.toString()}`;
804
+ }
687
805
  }
688
806
  /**
689
807
  * Executes a request with configured retry logic
@@ -694,38 +812,65 @@ class BaseRequest {
694
812
  async executeWithRetries(url, fetchOptions) {
695
813
  let attempt = 0;
696
814
  const maxRetries = this.requestOptions.retries || 0;
697
- // eslint-disable-next-line no-constant-condition
698
- while (true) {
815
+ do {
699
816
  try {
700
817
  return await this.executeRequest(url, fetchOptions);
701
818
  }
702
819
  catch (error) {
703
- const requestError = error instanceof RequestError
704
- ? error
705
- : RequestError.networkError(url, fetchOptions.method, error);
706
- if (attempt >= maxRetries || requestError.timeoutError) {
820
+ const requestError = error instanceof RequestError ? error : RequestError.networkError(url, fetchOptions.method, error);
821
+ // Don't retry on timeout errors or if we've reached the max retries
822
+ if (attempt >= maxRetries || requestError.timeoutError)
707
823
  throw requestError;
708
- }
709
824
  attempt++;
710
825
  if (this.requestOptions.onRetry) {
711
826
  await this.requestOptions.onRetry({ attempt, error: requestError });
712
827
  }
713
828
  }
714
- }
829
+ } while (attempt <= maxRetries);
830
+ // If we somehow get here, we throw an error to avoid undefined behavior
831
+ throw new RequestError(`Max retries reached (${maxRetries}) but request still failed`, url, fetchOptions.method);
715
832
  }
716
833
  async executeRequest(url, fetchOptions) {
717
834
  // Create a request controller for timeout if needed, or use the provided one
718
835
  const controller = this.abortController || new AbortController();
719
836
  const { signal } = controller;
720
837
  // Use the controller signal for the fetch request, but don't overwrite an existing signal
721
- // that might have been set in requestOptions if it exists
722
838
  fetchOptions.signal = fetchOptions.signal || signal;
723
839
  let timeoutId;
724
- // If we have a timeout and we're using our own controller (not provided externally)
725
- if (this.requestOptions.timeout && !this.abortController) {
726
- timeoutId = setTimeout(() => {
727
- controller.abort();
728
- }, this.requestOptions.timeout);
840
+ // Flag to track if abort was caused by our timeout
841
+ let abortedByTimeout = false;
842
+ // Handle timeout regardless of whether an external controller is provided
843
+ if (this.requestOptions.timeout) {
844
+ if (this.abortController) {
845
+ // If we have an external controller, create a separate timeout controller
846
+ // that won't interfere with the external one
847
+ // Use AbortSignal.timeout if available (modern browsers)
848
+ if (typeof AbortSignal.timeout === "function") {
849
+ const timeoutSignal = AbortSignal.timeout(this.requestOptions.timeout);
850
+ // Add event listener to propagate the abort to the main controller
851
+ timeoutSignal.addEventListener("abort", () => {
852
+ if (!controller.signal.aborted) {
853
+ abortedByTimeout = true;
854
+ controller.abort();
855
+ }
856
+ });
857
+ }
858
+ else {
859
+ // Fallback for browsers without AbortSignal.timeout
860
+ timeoutId = setTimeout(() => {
861
+ if (!controller.signal.aborted) {
862
+ abortedByTimeout = true;
863
+ controller.abort();
864
+ }
865
+ }, this.requestOptions.timeout);
866
+ }
867
+ }
868
+ else {
869
+ timeoutId = setTimeout(() => {
870
+ abortedByTimeout = true;
871
+ controller.abort();
872
+ }, this.requestOptions.timeout);
873
+ }
729
874
  }
730
875
  try {
731
876
  let response;
@@ -733,11 +878,11 @@ class BaseRequest {
733
878
  response = await fetch(url, fetchOptions);
734
879
  }
735
880
  catch (error) {
736
- // Check if this is an abort error from our timeout
737
- if (error instanceof DOMException && error.name === "AbortError" && timeoutId) {
881
+ // Check if this is an abort error that was specifically caused by our timeout
882
+ if (error instanceof DOMException && error.name === "AbortError" && abortedByTimeout) {
738
883
  throw RequestError.timeout(url, fetchOptions.method, this.requestOptions.timeout);
739
884
  }
740
- // Otherwise it's a network error
885
+ // Otherwise it's either a user-triggered abort or another network error
741
886
  throw RequestError.networkError(url, fetchOptions.method, error);
742
887
  }
743
888
  if (!response.ok) {
@@ -758,17 +903,12 @@ class BaseRequest {
758
903
  * Base class for requests that can have a body (POST, PUT, PATCH)
759
904
  */
760
905
  class BodyRequest extends BaseRequest {
761
- constructor() {
762
- super(...arguments);
763
- this.bodyProcessed = false;
764
- }
765
906
  /**
766
907
  * Sets the body of the request
767
908
  * @param body - The request body
768
909
  */
769
910
  withBody(body) {
770
911
  this.body = body;
771
- this.bodyProcessed = false;
772
912
  // Set body type and validate
773
913
  if (typeof body === "string") {
774
914
  this.bodyType = BodyType.STRING;
@@ -776,11 +916,7 @@ class BodyRequest extends BaseRequest {
776
916
  }
777
917
  else if (body !== null &&
778
918
  typeof body === "object" &&
779
- !(body instanceof FormData ||
780
- body instanceof Blob ||
781
- body instanceof ArrayBuffer ||
782
- body instanceof URLSearchParams ||
783
- body instanceof ReadableStream)) {
919
+ !(body instanceof FormData || body instanceof Blob || body instanceof ArrayBuffer || body instanceof URLSearchParams || body instanceof ReadableStream)) {
784
920
  this.bodyType = BodyType.JSON;
785
921
  this.setContentTypeIfNeeded("application/json");
786
922
  // Validate JSON is stringifiable early
@@ -808,15 +944,17 @@ class BodyRequest extends BaseRequest {
808
944
  * Overrides the base implementation to add body handling
809
945
  */
810
946
  sendTo(url) {
811
- // Process body only once
812
- if (!this.bodyProcessed && this.body !== undefined && !this.requestOptions.body) {
947
+ if (this.body !== undefined) {
948
+ // Remove previous body if it exists
949
+ if (this.requestOptions.body)
950
+ delete this.requestOptions.body;
951
+ // Process the body based on its type
813
952
  if (this.bodyType === BodyType.JSON) {
814
953
  this.requestOptions.body = JSON.stringify(this.body);
815
954
  }
816
955
  else {
817
956
  this.requestOptions.body = this.body;
818
957
  }
819
- this.bodyProcessed = true;
820
958
  }
821
959
  return super.sendTo(url);
822
960
  }
@@ -127,72 +127,114 @@
127
127
  */
128
128
  class ResponseWrapper {
129
129
  constructor(response) {
130
+ this.bodyUsed = false;
130
131
  this.response = response;
131
- this.ok = response.status >= 200 && response.status < 300; // Standard Fetch API behavior
132
132
  }
133
- /**
134
- * Get the raw Response object
135
- */
136
- get raw() {
137
- return this.response;
138
- }
139
- /**
140
- * Get the HTTP status code
141
- */
133
+ // Wrapper properties
142
134
  get status() {
143
135
  return this.response.status;
144
136
  }
145
- /**
146
- * Get the HTTP status text
147
- */
148
137
  get statusText() {
149
138
  return this.response.statusText;
150
139
  }
151
- /**
152
- * Get response headers
153
- */
154
140
  get headers() {
155
141
  return this.response.headers;
156
142
  }
157
- /**
158
- * Parse response as JSON
159
- */
143
+ get ok() {
144
+ return this.response.ok;
145
+ }
146
+ get raw() {
147
+ return this.response;
148
+ }
149
+ // Response body methods with caching
160
150
  async getJson() {
161
- return this.response.json();
151
+ if (this.jsonCache !== undefined) {
152
+ return this.jsonCache;
153
+ }
154
+ if (this.bodyUsed) {
155
+ // If we already have text, try to parse it as JSON
156
+ if (this.textCache !== undefined) {
157
+ try {
158
+ // Fix unsafe assignment by explicitly casting to unknown first
159
+ this.jsonCache = JSON.parse(this.textCache);
160
+ return this.jsonCache;
161
+ }
162
+ catch (e) {
163
+ throw new Error("Cannot parse cached text as JSON");
164
+ }
165
+ }
166
+ throw new Error("Response body has already been consumed in a different format");
167
+ }
168
+ this.bodyUsed = true;
169
+ this.jsonCache = (await this.response.json());
170
+ return this.jsonCache;
162
171
  }
163
- /**
164
- * Get response as text
165
- */
166
172
  async getText() {
167
- return this.response.text();
173
+ if (this.textCache !== undefined) {
174
+ return this.textCache;
175
+ }
176
+ if (this.bodyUsed) {
177
+ // If we already have JSON, convert it to text
178
+ if (this.jsonCache !== undefined) {
179
+ this.textCache = JSON.stringify(this.jsonCache);
180
+ return this.textCache;
181
+ }
182
+ // If we have blob, convert to text
183
+ if (this.blobCache !== undefined) {
184
+ this.textCache = await this.blobCache.text();
185
+ return this.textCache;
186
+ }
187
+ throw new Error("Response body has already been consumed in a format that cannot be converted to text");
188
+ }
189
+ this.bodyUsed = true;
190
+ this.textCache = await this.response.text();
191
+ return this.textCache;
168
192
  }
169
- /**
170
- * Get response as Blob
171
- */
172
193
  async getBlob() {
173
- return this.response.blob();
194
+ if (this.blobCache !== undefined) {
195
+ return this.blobCache;
196
+ }
197
+ if (this.bodyUsed) {
198
+ // If we have text, convert to Blob
199
+ if (this.textCache !== undefined) {
200
+ this.blobCache = new Blob([this.textCache], { type: "text/plain" });
201
+ return this.blobCache;
202
+ }
203
+ throw new Error("Response body has already been consumed in a format that cannot be converted to Blob");
204
+ }
205
+ this.bodyUsed = true;
206
+ this.blobCache = await this.response.blob();
207
+ return this.blobCache;
174
208
  }
175
- /**
176
- * Get response as ArrayBuffer
177
- */
178
209
  async getArrayBuffer() {
179
- return this.response.arrayBuffer();
180
- }
181
- /**
182
- * Get response as ReadableStream for true streaming consumption.
183
- * This allows you to process data incrementally as it arrives.
184
- *
185
- * @example
186
- * const stream = await request.sendTo('https://example.com/large-file').getBody();
187
- * const reader = stream.getReader();
188
- *
189
- * while (true) {
190
- * const { done, value } = await reader.read();
191
- * if (done) break;
192
- * // Process chunk of data in 'value'
193
- * }
194
- */
210
+ if (this.arrayBufferCache !== undefined) {
211
+ return this.arrayBufferCache;
212
+ }
213
+ if (this.bodyUsed) {
214
+ // If we have text, convert to ArrayBuffer
215
+ if (this.textCache !== undefined) {
216
+ const encoder = new TextEncoder();
217
+ // Fix ArrayBufferLike by explicitly casting to ArrayBuffer
218
+ const uint8Array = encoder.encode(this.textCache);
219
+ this.arrayBufferCache = uint8Array.buffer;
220
+ return this.arrayBufferCache;
221
+ }
222
+ // If we have blob, convert to ArrayBuffer
223
+ if (this.blobCache !== undefined) {
224
+ this.arrayBufferCache = await this.blobCache.arrayBuffer();
225
+ return this.arrayBufferCache;
226
+ }
227
+ throw new Error("Response body has already been consumed in a format that cannot be converted to ArrayBuffer");
228
+ }
229
+ this.bodyUsed = true;
230
+ this.arrayBufferCache = await this.response.arrayBuffer();
231
+ return this.arrayBufferCache;
232
+ }
195
233
  getBody() {
234
+ if (this.bodyUsed) {
235
+ throw new Error("Response body has already been consumed");
236
+ }
237
+ this.bodyUsed = true;
196
238
  return this.response.body;
197
239
  }
198
240
  }
@@ -298,15 +340,15 @@
298
340
  return this.enableAntiCsrf;
299
341
  }
300
342
  /**
301
- * Reset configuration to defaults
343
+ * Reset all configuration options to their defaults
302
344
  */
303
345
  reset() {
304
346
  this.csrfToken = null;
305
347
  this.csrfHeaderName = "X-CSRF-Token";
348
+ this.enableAntiCsrf = true;
306
349
  this.xsrfCookieName = "XSRF-TOKEN";
307
350
  this.xsrfHeaderName = "X-XSRF-TOKEN";
308
351
  this.enableAutoXsrf = true;
309
- this.enableAntiCsrf = true;
310
352
  return this;
311
353
  }
312
354
  }
@@ -371,12 +413,34 @@
371
413
  }
372
414
  /**
373
415
  * Validates if the provided string is a potential CSRF token
374
- * Checks if the token meets minimum security requirements (non-empty, not too short)
416
+ * Checks if the token meets security requirements
375
417
  * @param token The token to validate
376
- * @returns Whether the token is potentially valid
418
+ * @returns Whether the token is valid
377
419
  */
378
420
  static isValidToken(token) {
379
- return typeof token === "string" && token.length >= 8;
421
+ // Basic checks for null/undefined and type
422
+ if (typeof token !== "string") {
423
+ return false;
424
+ }
425
+ if (token.length < 8)
426
+ return false;
427
+ // If token is longer than 10 chars, perform additional security checks
428
+ if (token.length > 10) {
429
+ // Check for valid character set (alphanumeric & common token symbols)
430
+ const validTokenRegex = /^[A-Za-z0-9\-_=+/.]+$/;
431
+ if (!validTokenRegex.test(token)) {
432
+ return false;
433
+ }
434
+ // For longer tokens, check for sufficient entropy (at least 2 character types)
435
+ const hasUpperCase = /[A-Z]/.test(token);
436
+ const hasLowerCase = /[a-z]/.test(token);
437
+ const hasNumbers = /[0-9]/.test(token);
438
+ const hasSpecials = /[-_=+/.]/.test(token);
439
+ const characterTypesCount = [hasUpperCase, hasLowerCase, hasNumbers, hasSpecials].filter(Boolean).length;
440
+ return characterTypesCount >= 2;
441
+ }
442
+ // For shorter tokens (8-10 chars), just return true if we reached here
443
+ return true;
380
444
  }
381
445
  }
382
446
 
@@ -397,10 +461,14 @@
397
461
  return this;
398
462
  }
399
463
  withTimeout(timeout) {
464
+ if (!Number.isFinite(timeout) || timeout <= 0)
465
+ throw new Error("Timeout must be a positive number");
400
466
  this.requestOptions.timeout = timeout;
401
467
  return this;
402
468
  }
403
469
  withRetries(retries) {
470
+ if (!Number.isInteger(retries) || retries < 0)
471
+ throw new Error("Retry count must be a non-negative integer");
404
472
  this.requestOptions.retries = retries;
405
473
  return this;
406
474
  }
@@ -485,13 +553,16 @@
485
553
  /**
486
554
  * Adds a single query parameter
487
555
  * @param key - The parameter name
488
- * @param value - The parameter value
556
+ * @param value - The parameter value, can be a single value or array of values
489
557
  * @returns The instance for chaining
490
558
  */
491
559
  withQueryParam(key, value) {
492
- if (value !== null && value !== undefined) {
560
+ if (value === null || value === undefined)
561
+ return this;
562
+ if (Array.isArray(value))
563
+ value.forEach(v => this.queryParams.append(key, String(v)));
564
+ else
493
565
  this.queryParams.append(key, String(value));
494
- }
495
566
  return this;
496
567
  }
497
568
  /**
@@ -530,9 +601,23 @@
530
601
  * @returns The instance for chaining
531
602
  */
532
603
  withBasicAuth(username, password) {
533
- const credentials = btoa(`${username}:${password}`);
604
+ const credentials = this.encodeBase64(`${username}:${password}`);
534
605
  return this.withAuthorization(`Basic ${credentials}`);
535
606
  }
607
+ /**
608
+ * Cross-environment base64 encoding
609
+ * Works in both browser and Node.js environments
610
+ */
611
+ encodeBase64(str) {
612
+ // Browser environment
613
+ if (typeof btoa === "function")
614
+ return btoa(str);
615
+ // Node.js environment
616
+ if (typeof Buffer !== "undefined")
617
+ return Buffer.from(str).toString("base64");
618
+ // Fallback (should never happen in modern environments)
619
+ throw new Error("Base64 encoding is not supported in this environment");
620
+ }
536
621
  /**
537
622
  * Sets the bearer token for authentication
538
623
  * @param token - The bearer token
@@ -541,6 +626,15 @@
541
626
  withBearerToken(token) {
542
627
  return this.withAuthorization(`Bearer ${token}`);
543
628
  }
629
+ /**
630
+ * Helper function to check for header presence in a case-insensitive way
631
+ * @param headers Headers object
632
+ * @param headerName Header name to check
633
+ * @returns Boolean indicating if the header exists (case-insensitive)
634
+ */
635
+ hasHeader(headers, headerName) {
636
+ return Object.keys(headers).some(key => key.toLowerCase() === headerName.toLowerCase());
637
+ }
544
638
  /**
545
639
  * Sets cookies for the request
546
640
  * @param cookies Object containing cookie name-value pairs or cookie options
@@ -553,19 +647,34 @@
553
647
  }
554
648
  // Get current headers or initialize empty object
555
649
  const currentHeaders = this.requestOptions.headers || {};
556
- // Get the existing cookie header in a case-insensitive way
557
- let existingCookies = "";
558
- const cookieHeaderName = Object.keys(currentHeaders).find(header => header.toLowerCase() === "cookie");
559
- if (cookieHeaderName)
560
- existingCookies = currentHeaders[cookieHeaderName];
650
+ // Get all existing cookie headers with different cases
651
+ const cookieValues = [];
652
+ const cookieHeaderKeys = [];
653
+ Object.keys(currentHeaders).forEach(key => {
654
+ if (key.toLowerCase() === "cookie") {
655
+ // Don't add empty cookie values
656
+ if (currentHeaders[key])
657
+ cookieValues.push(currentHeaders[key]);
658
+ cookieHeaderKeys.push(key);
659
+ }
660
+ });
661
+ // Format the new cookies
561
662
  const cookieString = CookieUtils.formatRequestCookies(cookies);
562
- // Combine with existing cookies if present
563
- const newCookieValue = existingCookies ? `${existingCookies}; ${cookieString}` : cookieString;
564
- // Set the Cookie header, preserving original case if it exists
565
- const headerName = cookieHeaderName || "Cookie";
566
- return this.withHeaders({
567
- [headerName]: newCookieValue,
663
+ // Choose which header name to use - preserve existing case if possible
664
+ const headerName = cookieHeaderKeys.length > 0 ? cookieHeaderKeys[0] : "Cookie";
665
+ // Combine all existing cookie values with the new ones
666
+ const combinedCookieValue = [...cookieValues, cookieString].filter(Boolean).join("; ");
667
+ // Create a new headers object without any cookie headers
668
+ const newHeaders = { ...currentHeaders };
669
+ cookieHeaderKeys.forEach(key => {
670
+ delete newHeaders[key];
568
671
  });
672
+ // Set the new combined cookie header
673
+ this.requestOptions.headers = {
674
+ ...newHeaders,
675
+ [headerName]: combinedCookieValue,
676
+ };
677
+ return this;
569
678
  }
570
679
  /**
571
680
  * Set a single cookie
@@ -624,12 +733,13 @@
624
733
  // Apply global CSRF token if set
625
734
  const globalToken = config.getCsrfToken();
626
735
  if (globalToken) {
627
- // Check if local token exists
736
+ // Check if local token exists using case-insensitive comparison
628
737
  const headers = this.requestOptions.headers;
629
- const hasLocalToken = Object.keys(headers).some(key => key === "X-CSRF-Token" || key === config.getCsrfHeaderName());
738
+ const csrfHeaderName = config.getCsrfHeaderName();
739
+ const hasLocalToken = this.hasHeader(headers, "X-CSRF-Token") || this.hasHeader(headers, csrfHeaderName);
630
740
  if (!hasLocalToken) {
631
741
  this.withHeaders({
632
- [config.getCsrfHeaderName()]: globalToken,
742
+ [csrfHeaderName]: globalToken,
633
743
  });
634
744
  }
635
745
  }
@@ -638,10 +748,11 @@
638
748
  const xsrfToken = CsrfUtils.getTokenFromCookie(config.getXsrfCookieName());
639
749
  if (xsrfToken && CsrfUtils.isValidToken(xsrfToken)) {
640
750
  const headers = this.requestOptions.headers;
641
- const hasLocalToken = Object.keys(headers).some(key => key === "X-XSRF-TOKEN" || key === config.getXsrfHeaderName());
751
+ const xsrfHeaderName = config.getXsrfHeaderName();
752
+ const hasLocalToken = this.hasHeader(headers, "X-XSRF-TOKEN") || this.hasHeader(headers, xsrfHeaderName);
642
753
  if (!hasLocalToken) {
643
754
  this.withHeaders({
644
- [config.getXsrfHeaderName()]: xsrfToken,
755
+ [xsrfHeaderName]: xsrfToken,
645
756
  });
646
757
  }
647
758
  }
@@ -653,9 +764,7 @@
653
764
  method: this.method,
654
765
  };
655
766
  // Create the base promise
656
- const basePromise = !this.requestOptions.retries
657
- ? this.executeRequest(url, fetchOptions)
658
- : this.executeWithRetries(url, fetchOptions);
767
+ const basePromise = !this.requestOptions.retries ? this.executeRequest(url, fetchOptions) : this.executeWithRetries(url, fetchOptions);
659
768
  const responsePromise = basePromise;
660
769
  // Attach convenience methods that begin processing immediately
661
770
  responsePromise.getJson = async () => {
@@ -684,12 +793,21 @@
684
793
  if (!this.queryParams.toString()) {
685
794
  return url;
686
795
  }
687
- const urlObj = new URL(url);
688
- // Merge our query params with any that might be in the URL already
689
- this.queryParams.forEach((value, key) => {
690
- urlObj.searchParams.append(key, value);
691
- });
692
- return urlObj.toString();
796
+ try {
797
+ // Try to use the URL constructor (works for absolute URLs)
798
+ const urlObj = new URL(url);
799
+ // Merge our query params with any that might be in the URL already
800
+ this.queryParams.forEach((value, key) => {
801
+ urlObj.searchParams.append(key, value);
802
+ });
803
+ return urlObj.toString();
804
+ }
805
+ catch (error) {
806
+ // Handle relative URLs
807
+ const hasExistingParams = url.includes("?");
808
+ const separator = hasExistingParams ? "&" : "?";
809
+ return `${url}${separator}${this.queryParams.toString()}`;
810
+ }
693
811
  }
694
812
  /**
695
813
  * Executes a request with configured retry logic
@@ -700,38 +818,65 @@
700
818
  async executeWithRetries(url, fetchOptions) {
701
819
  let attempt = 0;
702
820
  const maxRetries = this.requestOptions.retries || 0;
703
- // eslint-disable-next-line no-constant-condition
704
- while (true) {
821
+ do {
705
822
  try {
706
823
  return await this.executeRequest(url, fetchOptions);
707
824
  }
708
825
  catch (error) {
709
- const requestError = error instanceof RequestError
710
- ? error
711
- : RequestError.networkError(url, fetchOptions.method, error);
712
- if (attempt >= maxRetries || requestError.timeoutError) {
826
+ const requestError = error instanceof RequestError ? error : RequestError.networkError(url, fetchOptions.method, error);
827
+ // Don't retry on timeout errors or if we've reached the max retries
828
+ if (attempt >= maxRetries || requestError.timeoutError)
713
829
  throw requestError;
714
- }
715
830
  attempt++;
716
831
  if (this.requestOptions.onRetry) {
717
832
  await this.requestOptions.onRetry({ attempt, error: requestError });
718
833
  }
719
834
  }
720
- }
835
+ } while (attempt <= maxRetries);
836
+ // If we somehow get here, we throw an error to avoid undefined behavior
837
+ throw new RequestError(`Max retries reached (${maxRetries}) but request still failed`, url, fetchOptions.method);
721
838
  }
722
839
  async executeRequest(url, fetchOptions) {
723
840
  // Create a request controller for timeout if needed, or use the provided one
724
841
  const controller = this.abortController || new AbortController();
725
842
  const { signal } = controller;
726
843
  // Use the controller signal for the fetch request, but don't overwrite an existing signal
727
- // that might have been set in requestOptions if it exists
728
844
  fetchOptions.signal = fetchOptions.signal || signal;
729
845
  let timeoutId;
730
- // If we have a timeout and we're using our own controller (not provided externally)
731
- if (this.requestOptions.timeout && !this.abortController) {
732
- timeoutId = setTimeout(() => {
733
- controller.abort();
734
- }, this.requestOptions.timeout);
846
+ // Flag to track if abort was caused by our timeout
847
+ let abortedByTimeout = false;
848
+ // Handle timeout regardless of whether an external controller is provided
849
+ if (this.requestOptions.timeout) {
850
+ if (this.abortController) {
851
+ // If we have an external controller, create a separate timeout controller
852
+ // that won't interfere with the external one
853
+ // Use AbortSignal.timeout if available (modern browsers)
854
+ if (typeof AbortSignal.timeout === "function") {
855
+ const timeoutSignal = AbortSignal.timeout(this.requestOptions.timeout);
856
+ // Add event listener to propagate the abort to the main controller
857
+ timeoutSignal.addEventListener("abort", () => {
858
+ if (!controller.signal.aborted) {
859
+ abortedByTimeout = true;
860
+ controller.abort();
861
+ }
862
+ });
863
+ }
864
+ else {
865
+ // Fallback for browsers without AbortSignal.timeout
866
+ timeoutId = setTimeout(() => {
867
+ if (!controller.signal.aborted) {
868
+ abortedByTimeout = true;
869
+ controller.abort();
870
+ }
871
+ }, this.requestOptions.timeout);
872
+ }
873
+ }
874
+ else {
875
+ timeoutId = setTimeout(() => {
876
+ abortedByTimeout = true;
877
+ controller.abort();
878
+ }, this.requestOptions.timeout);
879
+ }
735
880
  }
736
881
  try {
737
882
  let response;
@@ -739,11 +884,11 @@
739
884
  response = await fetch(url, fetchOptions);
740
885
  }
741
886
  catch (error) {
742
- // Check if this is an abort error from our timeout
743
- if (error instanceof DOMException && error.name === "AbortError" && timeoutId) {
887
+ // Check if this is an abort error that was specifically caused by our timeout
888
+ if (error instanceof DOMException && error.name === "AbortError" && abortedByTimeout) {
744
889
  throw RequestError.timeout(url, fetchOptions.method, this.requestOptions.timeout);
745
890
  }
746
- // Otherwise it's a network error
891
+ // Otherwise it's either a user-triggered abort or another network error
747
892
  throw RequestError.networkError(url, fetchOptions.method, error);
748
893
  }
749
894
  if (!response.ok) {
@@ -764,17 +909,12 @@
764
909
  * Base class for requests that can have a body (POST, PUT, PATCH)
765
910
  */
766
911
  class BodyRequest extends BaseRequest {
767
- constructor() {
768
- super(...arguments);
769
- this.bodyProcessed = false;
770
- }
771
912
  /**
772
913
  * Sets the body of the request
773
914
  * @param body - The request body
774
915
  */
775
916
  withBody(body) {
776
917
  this.body = body;
777
- this.bodyProcessed = false;
778
918
  // Set body type and validate
779
919
  if (typeof body === "string") {
780
920
  this.bodyType = BodyType.STRING;
@@ -782,11 +922,7 @@
782
922
  }
783
923
  else if (body !== null &&
784
924
  typeof body === "object" &&
785
- !(body instanceof FormData ||
786
- body instanceof Blob ||
787
- body instanceof ArrayBuffer ||
788
- body instanceof URLSearchParams ||
789
- body instanceof ReadableStream)) {
925
+ !(body instanceof FormData || body instanceof Blob || body instanceof ArrayBuffer || body instanceof URLSearchParams || body instanceof ReadableStream)) {
790
926
  this.bodyType = BodyType.JSON;
791
927
  this.setContentTypeIfNeeded("application/json");
792
928
  // Validate JSON is stringifiable early
@@ -814,15 +950,17 @@
814
950
  * Overrides the base implementation to add body handling
815
951
  */
816
952
  sendTo(url) {
817
- // Process body only once
818
- if (!this.bodyProcessed && this.body !== undefined && !this.requestOptions.body) {
953
+ if (this.body !== undefined) {
954
+ // Remove previous body if it exists
955
+ if (this.requestOptions.body)
956
+ delete this.requestOptions.body;
957
+ // Process the body based on its type
819
958
  if (this.bodyType === BodyType.JSON) {
820
959
  this.requestOptions.body = JSON.stringify(this.body);
821
960
  }
822
961
  else {
823
962
  this.requestOptions.body = this.body;
824
963
  }
825
- this.bodyProcessed = true;
826
964
  }
827
965
  return super.sendTo(url);
828
966
  }
@@ -16,7 +16,7 @@ export interface CookieOptions {
16
16
  maxAge?: number;
17
17
  }
18
18
  export type CookiesRecord = Record<string, string | CookieOptions>;
19
- export { CacheMode, HttpMethod, RequestMode, RedirectMode, SameSitePolicy, RequestPriority, CredentialsPolicy, };
19
+ export { CacheMode, HttpMethod, RequestMode, RedirectMode, SameSitePolicy, RequestPriority, CredentialsPolicy };
20
20
  export interface RequestOptions extends Omit<RequestInit, "signal" | "body" | "method" | "credentials" | "mode" | "redirect" | "cache" | "priority"> {
21
21
  timeout?: number;
22
22
  retries?: number;
@@ -63,7 +63,7 @@ export declare class Config {
63
63
  */
64
64
  isAntiCsrfEnabled(): boolean;
65
65
  /**
66
- * Reset configuration to defaults
66
+ * Reset all configuration options to their defaults
67
67
  */
68
68
  reset(): Config;
69
69
  }
@@ -16,9 +16,9 @@ export declare class CsrfUtils {
16
16
  static getTokenFromCookie(cookieName?: string): string | null;
17
17
  /**
18
18
  * Validates if the provided string is a potential CSRF token
19
- * Checks if the token meets minimum security requirements (non-empty, not too short)
19
+ * Checks if the token meets security requirements
20
20
  * @param token The token to validate
21
- * @returns Whether the token is potentially valid
21
+ * @returns Whether the token is valid
22
22
  */
23
23
  static isValidToken(token: string | null | undefined): boolean;
24
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-request",
3
- "version": "0.0.1-beta.4",
3
+ "version": "0.0.1-beta.6",
4
4
  "description": "A modern, chainable wrapper for fetch with automatic retries, timeouts, comprehensive error handling, and first-class TypeScript support",
5
5
  "main": "dist/library/index.js",
6
6
  "module": "dist/library/index.esm.js",
@@ -15,10 +15,10 @@
15
15
  "test:coverage": "tsc -p tsconfig.test.json && node --test --experimental-test-coverage dist/tests/test/",
16
16
  "pretest": "tsc -p tsconfig.test.json",
17
17
  "prepublishOnly": "npm run check && npm run build",
18
- "publish:beta": "npm version prerelease --preid=beta && git push origin --tags && npm publish --tag beta",
19
- "publish:patch": "npm version patch && git push origin --tags && npm publish",
20
- "publish:minor": "npm version minor && git push origin --tags && npm publish",
21
- "publish:major": "npm version major && git push origin --tags && npm publish",
18
+ "publish:beta": "npm run check && npm version prerelease --preid=beta && git push origin --tags && npm publish --tag latest",
19
+ "disabled:publish:patch": "npm version patch && git push origin --tags && npm publish",
20
+ "disabled:publish:minor": "npm version minor && git push origin --tags && npm publish",
21
+ "disabled:publish:major": "npm version major && git push origin --tags && npm publish",
22
22
  "lint": "eslint 'src/**/*.ts' 'test/**/*.ts'",
23
23
  "lint:fix": "eslint 'src/**/*.ts' 'test/**/*.ts' --fix",
24
24
  "format": "prettier --write 'src/**/*.ts' 'test/**/*.ts'",
@@ -36,7 +36,7 @@
36
36
  "license": "MIT",
37
37
  "repository": {
38
38
  "type": "git",
39
- "url": "https://github.com/DanielAmenou/create-request"
39
+ "url": "https://github.com/DanielAmenou/create-request.git"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@rollup/plugin-commonjs": "^22.0.0",