create-request 0.0.1-beta.2 → 0.0.1-beta.5

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.
@@ -4,11 +4,11 @@
4
4
  var HttpMethod;
5
5
  (function (HttpMethod) {
6
6
  HttpMethod["GET"] = "GET";
7
- HttpMethod["POST"] = "POST";
8
7
  HttpMethod["PUT"] = "PUT";
9
- HttpMethod["DELETE"] = "DELETE";
10
- HttpMethod["PATCH"] = "PATCH";
8
+ HttpMethod["POST"] = "POST";
11
9
  HttpMethod["HEAD"] = "HEAD";
10
+ HttpMethod["PATCH"] = "PATCH";
11
+ HttpMethod["DELETE"] = "DELETE";
12
12
  HttpMethod["OPTIONS"] = "OPTIONS";
13
13
  })(HttpMethod || (HttpMethod = {}));
14
14
  /**
@@ -16,8 +16,8 @@ var HttpMethod;
16
16
  */
17
17
  var RequestPriority;
18
18
  (function (RequestPriority) {
19
- RequestPriority["HIGH"] = "high";
20
19
  RequestPriority["LOW"] = "low";
20
+ RequestPriority["HIGH"] = "high";
21
21
  RequestPriority["AUTO"] = "auto";
22
22
  })(RequestPriority || (RequestPriority = {}));
23
23
  /**
@@ -25,9 +25,9 @@ var RequestPriority;
25
25
  */
26
26
  var CredentialsPolicy;
27
27
  (function (CredentialsPolicy) {
28
+ CredentialsPolicy["OMIT"] = "omit";
28
29
  CredentialsPolicy["INCLUDE"] = "include";
29
30
  CredentialsPolicy["SAME_ORIGIN"] = "same-origin";
30
- CredentialsPolicy["OMIT"] = "omit";
31
31
  })(CredentialsPolicy || (CredentialsPolicy = {}));
32
32
  /**
33
33
  * Enum for request modes
@@ -44,8 +44,8 @@ var RequestMode;
44
44
  */
45
45
  var RedirectMode;
46
46
  (function (RedirectMode) {
47
- RedirectMode["FOLLOW"] = "follow";
48
47
  RedirectMode["ERROR"] = "error";
48
+ RedirectMode["FOLLOW"] = "follow";
49
49
  RedirectMode["MANUAL"] = "manual";
50
50
  })(RedirectMode || (RedirectMode = {}));
51
51
  /**
@@ -54,12 +54,30 @@ var RedirectMode;
54
54
  var CacheMode;
55
55
  (function (CacheMode) {
56
56
  CacheMode["DEFAULT"] = "default";
57
- CacheMode["NO_STORE"] = "no-store";
58
57
  CacheMode["RELOAD"] = "reload";
58
+ CacheMode["NO_STORE"] = "no-store";
59
59
  CacheMode["NO_CACHE"] = "no-cache";
60
60
  CacheMode["FORCE_CACHE"] = "force-cache";
61
61
  CacheMode["ONLY_IF_CACHED"] = "only-if-cached";
62
62
  })(CacheMode || (CacheMode = {}));
63
+ /**
64
+ * Enum for cookie SameSite policies
65
+ */
66
+ var SameSitePolicy;
67
+ (function (SameSitePolicy) {
68
+ SameSitePolicy["LAX"] = "Lax";
69
+ SameSitePolicy["NONE"] = "None";
70
+ SameSitePolicy["STRICT"] = "Strict";
71
+ })(SameSitePolicy || (SameSitePolicy = {}));
72
+ /**
73
+ * Enum for body types
74
+ */
75
+ var BodyType;
76
+ (function (BodyType) {
77
+ BodyType["JSON"] = "json";
78
+ BodyType["STRING"] = "string";
79
+ BodyType["BINARY"] = "binary";
80
+ })(BodyType || (BodyType = {}));
63
81
 
64
82
  class RequestError extends Error {
65
83
  constructor(message, url, method, options = {}) {
@@ -133,44 +151,241 @@ class ResponseWrapper {
133
151
  /**
134
152
  * Parse response as JSON
135
153
  */
136
- async json() {
154
+ async getJson() {
137
155
  return this.response.json();
138
156
  }
139
157
  /**
140
158
  * Get response as text
141
159
  */
142
- async text() {
160
+ async getText() {
143
161
  return this.response.text();
144
162
  }
145
163
  /**
146
164
  * Get response as Blob
147
165
  */
148
- async blob() {
166
+ async getBlob() {
149
167
  return this.response.blob();
150
168
  }
151
169
  /**
152
170
  * Get response as ArrayBuffer
153
171
  */
154
- async arrayBuffer() {
172
+ async getArrayBuffer() {
155
173
  return this.response.arrayBuffer();
156
174
  }
157
175
  /**
158
- * Get response as ReadableStream
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
+ * }
159
188
  */
160
- body() {
189
+ getBody() {
161
190
  return this.response.body;
162
191
  }
163
192
  }
164
193
 
194
+ /**
195
+ * Global configuration for create-request
196
+ */
197
+ class Config {
198
+ constructor() {
199
+ // CSRF configuration
200
+ this.csrfHeaderName = "X-CSRF-Token";
201
+ this.xsrfCookieName = "XSRF-TOKEN";
202
+ this.xsrfHeaderName = "X-XSRF-TOKEN";
203
+ this.csrfToken = null;
204
+ this.enableAutoXsrf = true;
205
+ this.enableAntiCsrf = true; // X-Requested-With header
206
+ }
207
+ /**
208
+ * Get the singleton instance
209
+ */
210
+ static getInstance() {
211
+ if (!Config.instance) {
212
+ Config.instance = new Config();
213
+ }
214
+ return Config.instance;
215
+ }
216
+ /**
217
+ * Set a global CSRF token to be used for all requests
218
+ */
219
+ setCsrfToken(token) {
220
+ this.csrfToken = token;
221
+ return this;
222
+ }
223
+ /**
224
+ * Get the global CSRF token
225
+ */
226
+ getCsrfToken() {
227
+ return this.csrfToken;
228
+ }
229
+ /**
230
+ * Set the CSRF header name (default is X-CSRF-Token)
231
+ */
232
+ setCsrfHeaderName(name) {
233
+ this.csrfHeaderName = name;
234
+ return this;
235
+ }
236
+ /**
237
+ * Get the CSRF header name
238
+ */
239
+ getCsrfHeaderName() {
240
+ return this.csrfHeaderName;
241
+ }
242
+ /**
243
+ * Set the XSRF cookie name to look for (default is XSRF-TOKEN)
244
+ */
245
+ setXsrfCookieName(name) {
246
+ this.xsrfCookieName = name;
247
+ return this;
248
+ }
249
+ /**
250
+ * Get the XSRF cookie name
251
+ */
252
+ getXsrfCookieName() {
253
+ return this.xsrfCookieName;
254
+ }
255
+ /**
256
+ * Set the XSRF header name to send the token in (default is X-XSRF-TOKEN)
257
+ */
258
+ setXsrfHeaderName(name) {
259
+ this.xsrfHeaderName = name;
260
+ return this;
261
+ }
262
+ /**
263
+ * Get the XSRF header name
264
+ */
265
+ getXsrfHeaderName() {
266
+ return this.xsrfHeaderName;
267
+ }
268
+ /**
269
+ * Enable or disable automatic XSRF token extraction from cookies
270
+ */
271
+ setEnableAutoXsrf(enable) {
272
+ this.enableAutoXsrf = enable;
273
+ return this;
274
+ }
275
+ /**
276
+ * Check if automatic XSRF is enabled
277
+ */
278
+ isAutoXsrfEnabled() {
279
+ return this.enableAutoXsrf;
280
+ }
281
+ /**
282
+ * Enable or disable automatic addition of anti-CSRF headers
283
+ */
284
+ setEnableAntiCsrf(enable) {
285
+ this.enableAntiCsrf = enable;
286
+ return this;
287
+ }
288
+ /**
289
+ * Check if anti-CSRF is enabled
290
+ */
291
+ isAntiCsrfEnabled() {
292
+ return this.enableAntiCsrf;
293
+ }
294
+ /**
295
+ * Reset configuration to defaults
296
+ */
297
+ reset() {
298
+ this.csrfToken = null;
299
+ this.csrfHeaderName = "X-CSRF-Token";
300
+ this.xsrfCookieName = "XSRF-TOKEN";
301
+ this.xsrfHeaderName = "X-XSRF-TOKEN";
302
+ this.enableAutoXsrf = true;
303
+ this.enableAntiCsrf = true;
304
+ return this;
305
+ }
306
+ }
307
+
308
+ class CookieUtils {
309
+ /**
310
+ * Formats cookies for a request
311
+ * @param cookies Object containing cookie name-value pairs or cookie options
312
+ * @returns Formatted cookie string for the Cookie header
313
+ */
314
+ static formatRequestCookies(cookies) {
315
+ const cookiePairs = [];
316
+ Object.entries(cookies).forEach(([name, valueOrOptions]) => {
317
+ let value;
318
+ if (typeof valueOrOptions === "string") {
319
+ value = valueOrOptions;
320
+ }
321
+ else {
322
+ // Extract value from options object without validation
323
+ value = valueOrOptions.value;
324
+ }
325
+ // Add the cookie to the request
326
+ cookiePairs.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`);
327
+ });
328
+ return cookiePairs.join("; ");
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Utility class for CSRF token management
334
+ */
335
+ class CsrfUtils {
336
+ /**
337
+ * Extracts CSRF token from a meta tag in the document head
338
+ * @param metaName The name attribute of the meta tag (default: "csrf-token")
339
+ * @returns The CSRF token or null if not found
340
+ */
341
+ static getTokenFromMeta(metaName = "csrf-token") {
342
+ if (typeof document === "undefined") {
343
+ return null;
344
+ }
345
+ const meta = document.querySelector(`meta[name="${metaName}"]`);
346
+ return (meta === null || meta === void 0 ? void 0 : meta.getAttribute("content")) || null;
347
+ }
348
+ /**
349
+ * Extracts CSRF token from a cookie
350
+ * @param cookieName The name of the cookie containing the CSRF token
351
+ * @returns The CSRF token or null if not found
352
+ */
353
+ static getTokenFromCookie(cookieName = "csrf-token") {
354
+ if (typeof document === "undefined") {
355
+ return null;
356
+ }
357
+ const cookies = document.cookie.split(";");
358
+ for (const cookie of cookies) {
359
+ const [name, value] = cookie.trim().split("=");
360
+ if (name === cookieName) {
361
+ return decodeURIComponent(value);
362
+ }
363
+ }
364
+ return null;
365
+ }
366
+ /**
367
+ * Validates if the provided string is a potential CSRF token
368
+ * Checks if the token meets minimum security requirements (non-empty, not too short)
369
+ * @param token The token to validate
370
+ * @returns Whether the token is potentially valid
371
+ */
372
+ static isValidToken(token) {
373
+ return typeof token === "string" && token.length >= 8;
374
+ }
375
+ }
376
+
165
377
  // Base class with common functionality for all request types
166
378
  class BaseRequest {
167
379
  constructor() {
168
- this.requestOptions = {};
380
+ this.requestOptions = {
381
+ headers: {},
382
+ };
169
383
  this.queryParams = new URLSearchParams();
384
+ this.autoApplyCsrfProtection = true;
170
385
  }
171
386
  withHeaders(headers) {
172
387
  this.requestOptions.headers = {
173
- ...(this.requestOptions.headers || {}),
388
+ ...this.requestOptions.headers,
174
389
  ...headers,
175
390
  };
176
391
  return this;
@@ -322,7 +537,7 @@ class BaseRequest {
322
537
  }
323
538
  /**
324
539
  * Sets cookies for the request
325
- * @param cookies - An object containing cookie name-value pairs
540
+ * @param cookies Object containing cookie name-value pairs or cookie options
326
541
  * @returns The instance for chaining
327
542
  */
328
543
  withCookies(cookies) {
@@ -330,18 +545,58 @@ class BaseRequest {
330
545
  if (cookieEntries.length === 0) {
331
546
  return this;
332
547
  }
333
- // Format cookies as name=value pairs
334
- const cookieString = cookieEntries
335
- .map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`)
336
- .join("; ");
337
548
  // Get current headers or initialize empty object
338
549
  const currentHeaders = this.requestOptions.headers || {};
339
- // If there's already a Cookie header, append to it; otherwise create a new one
340
- const existingCookies = currentHeaders["Cookie"] || "";
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];
555
+ const cookieString = CookieUtils.formatRequestCookies(cookies);
556
+ // Combine with existing cookies if present
341
557
  const newCookieValue = existingCookies ? `${existingCookies}; ${cookieString}` : cookieString;
342
- // Set the Cookie header
558
+ // Set the Cookie header, preserving original case if it exists
559
+ const headerName = cookieHeaderName || "Cookie";
343
560
  return this.withHeaders({
344
- Cookie: newCookieValue,
561
+ [headerName]: newCookieValue,
562
+ });
563
+ }
564
+ /**
565
+ * Set a single cookie
566
+ * @param name Cookie name
567
+ * @param value Cookie value or options object
568
+ * @returns The instance for chaining
569
+ */
570
+ withCookie(name, value) {
571
+ return this.withCookies({ [name]: value });
572
+ }
573
+ /**
574
+ * Sets a CSRF token in the request headers
575
+ * @param token The CSRF token
576
+ * @param headerName The name of the header to use (default: X-CSRF-Token)
577
+ * @returns The instance for chaining
578
+ */
579
+ withCsrfToken(token, headerName = "X-CSRF-Token") {
580
+ return this.withHeaders({
581
+ [headerName]: token,
582
+ });
583
+ }
584
+ /**
585
+ * Disables automatic anti-CSRF protection.
586
+ * By default, X-Requested-With: XMLHttpRequest header is sent with all requests.
587
+ * @returns The instance for chaining
588
+ */
589
+ withoutCsrfProtection() {
590
+ this.autoApplyCsrfProtection = false;
591
+ return this;
592
+ }
593
+ /**
594
+ * Sets common security headers to help prevent CSRF attacks
595
+ * @returns The instance for chaining
596
+ */
597
+ withAntiCsrfHeaders() {
598
+ return this.withHeaders({
599
+ "X-Requested-With": "XMLHttpRequest",
345
600
  });
346
601
  }
347
602
  /**
@@ -351,6 +606,41 @@ class BaseRequest {
351
606
  sendTo(url) {
352
607
  // Format the URL with query parameters
353
608
  url = this.formatUrlWithQueryParams(url);
609
+ // Apply automatic CSRF protection if enabled
610
+ if (this.autoApplyCsrfProtection) {
611
+ const config = Config.getInstance();
612
+ // Apply anti-CSRF headers if enabled
613
+ if (config.isAntiCsrfEnabled()) {
614
+ this.withHeaders({
615
+ "X-Requested-With": "XMLHttpRequest",
616
+ });
617
+ }
618
+ // Apply global CSRF token if set
619
+ const globalToken = config.getCsrfToken();
620
+ if (globalToken) {
621
+ // Check if local token exists
622
+ const headers = this.requestOptions.headers;
623
+ const hasLocalToken = Object.keys(headers).some(key => key === "X-CSRF-Token" || key === config.getCsrfHeaderName());
624
+ if (!hasLocalToken) {
625
+ this.withHeaders({
626
+ [config.getCsrfHeaderName()]: globalToken,
627
+ });
628
+ }
629
+ }
630
+ // check for XSRF token in cookies and send it as a header
631
+ if (config.isAutoXsrfEnabled() && typeof document !== "undefined") {
632
+ const xsrfToken = CsrfUtils.getTokenFromCookie(config.getXsrfCookieName());
633
+ if (xsrfToken && CsrfUtils.isValidToken(xsrfToken)) {
634
+ const headers = this.requestOptions.headers;
635
+ const hasLocalToken = Object.keys(headers).some(key => key === "X-XSRF-TOKEN" || key === config.getXsrfHeaderName());
636
+ if (!hasLocalToken) {
637
+ this.withHeaders({
638
+ [config.getXsrfHeaderName()]: xsrfToken,
639
+ });
640
+ }
641
+ }
642
+ }
643
+ }
354
644
  // Cast the request options to RequestInit to ensure compatibility
355
645
  const fetchOptions = {
356
646
  ...this.requestOptions,
@@ -360,28 +650,22 @@ class BaseRequest {
360
650
  const basePromise = !this.requestOptions.retries
361
651
  ? this.executeRequest(url, fetchOptions)
362
652
  : this.executeWithRetries(url, fetchOptions);
363
- // Enhance the promise with response methods
364
- const responsePromise = basePromise.then(response => response);
365
- // Attach convenience methods to the promise
366
- responsePromise.json = async () => {
367
- const response = await basePromise;
368
- return response.json();
653
+ const responsePromise = basePromise;
654
+ // Attach convenience methods that begin processing immediately
655
+ responsePromise.getJson = async () => {
656
+ return basePromise.then(response => response.getJson());
369
657
  };
370
- responsePromise.text = async () => {
371
- const response = await basePromise;
372
- return response.text();
658
+ responsePromise.getText = async () => {
659
+ return basePromise.then(response => response.getText());
373
660
  };
374
- responsePromise.blob = async () => {
375
- const response = await basePromise;
376
- return response.blob();
661
+ responsePromise.getBlob = async () => {
662
+ return basePromise.then(response => response.getBlob());
377
663
  };
378
- responsePromise.arrayBuffer = async () => {
379
- const response = await basePromise;
380
- return response.arrayBuffer();
664
+ responsePromise.getArrayBuffer = async () => {
665
+ return basePromise.then(response => response.getArrayBuffer());
381
666
  };
382
- responsePromise.body = async () => {
383
- const response = await basePromise;
384
- return response.body();
667
+ responsePromise.getBody = async () => {
668
+ return basePromise.then(response => response.getBody());
385
669
  };
386
670
  return responsePromise;
387
671
  }
@@ -408,7 +692,7 @@ class BaseRequest {
408
692
  * @returns A wrapped response object
409
693
  */
410
694
  async executeWithRetries(url, fetchOptions) {
411
- let retryCount = 0;
695
+ let attempt = 0;
412
696
  const maxRetries = this.requestOptions.retries || 0;
413
697
  // eslint-disable-next-line no-constant-condition
414
698
  while (true) {
@@ -419,12 +703,12 @@ class BaseRequest {
419
703
  const requestError = error instanceof RequestError
420
704
  ? error
421
705
  : RequestError.networkError(url, fetchOptions.method, error);
422
- if (retryCount >= maxRetries || requestError.timeoutError) {
706
+ if (attempt >= maxRetries || requestError.timeoutError) {
423
707
  throw requestError;
424
708
  }
425
- retryCount++;
709
+ attempt++;
426
710
  if (this.requestOptions.onRetry) {
427
- await this.requestOptions.onRetry({ retryCount, error: requestError });
711
+ await this.requestOptions.onRetry({ attempt, error: requestError });
428
712
  }
429
713
  }
430
714
  }
@@ -470,47 +754,72 @@ class BaseRequest {
470
754
  }
471
755
  }
472
756
 
473
- // An extension of BaseRequest for methods that support request bodies
757
+ /**
758
+ * Base class for requests that can have a body (POST, PUT, PATCH)
759
+ */
474
760
  class BodyRequest extends BaseRequest {
761
+ constructor() {
762
+ super(...arguments);
763
+ this.bodyProcessed = false;
764
+ }
475
765
  /**
476
- * Sets the request body with appropriate content type handling
766
+ * Sets the body of the request
767
+ * @param body - The request body
477
768
  */
478
769
  withBody(body) {
479
- if (body === undefined || body === null) {
480
- this.requestOptions.body = null;
481
- return this;
482
- }
483
- // Handle special body types that don't need transformation
484
- if (body instanceof Blob ||
485
- body instanceof FormData ||
486
- body instanceof URLSearchParams ||
487
- body instanceof ArrayBuffer ||
488
- body instanceof ReadableStream) {
489
- this.requestOptions.body = body;
490
- // For FormData, make sure to NOT explicitly set Content-Type
491
- // The browser will set it automatically with the correct boundary
492
- // If we set it manually, it would be missing the boundary parameter
493
- return this;
494
- }
495
- // Handle strings
770
+ this.body = body;
771
+ this.bodyProcessed = false;
772
+ // Set body type and validate
496
773
  if (typeof body === "string") {
497
- this.requestOptions.body = body;
498
- return this;
774
+ this.bodyType = BodyType.STRING;
775
+ this.setContentTypeIfNeeded("text/plain");
499
776
  }
500
- // For objects and other types, stringify and set JSON content-type
501
- try {
502
- this.requestOptions.body = JSON.stringify(body);
503
- this.requestOptions.headers = {
504
- "Content-Type": "application/json",
505
- ...(this.requestOptions.headers || {}),
506
- };
777
+ else if (body !== null &&
778
+ typeof body === "object" &&
779
+ !(body instanceof FormData ||
780
+ body instanceof Blob ||
781
+ body instanceof ArrayBuffer ||
782
+ body instanceof URLSearchParams ||
783
+ body instanceof ReadableStream)) {
784
+ this.bodyType = BodyType.JSON;
785
+ this.setContentTypeIfNeeded("application/json");
786
+ // Validate JSON is stringifiable early
787
+ try {
788
+ JSON.stringify(body);
789
+ }
790
+ catch (error) {
791
+ throw new Error(`Failed to stringify request body: ${String(error)}`);
792
+ }
507
793
  }
508
- catch (error) {
509
- const errorMessage = error instanceof Error ? error.message : String(error);
510
- throw new Error(`Failed to stringify request body: ${errorMessage}`);
794
+ else {
795
+ this.bodyType = BodyType.BINARY;
511
796
  }
512
797
  return this;
513
798
  }
799
+ setContentTypeIfNeeded(contentType) {
800
+ // Only set if not already set
801
+ const headers = this.requestOptions.headers;
802
+ if (!Object.keys(headers).some(header => header.toLowerCase() === "content-type")) {
803
+ this.withContentType(contentType);
804
+ }
805
+ }
806
+ /**
807
+ * Send the request to the specified URL
808
+ * Overrides the base implementation to add body handling
809
+ */
810
+ sendTo(url) {
811
+ // Process body only once
812
+ if (!this.bodyProcessed && this.body !== undefined && !this.requestOptions.body) {
813
+ if (this.bodyType === BodyType.JSON) {
814
+ this.requestOptions.body = JSON.stringify(this.body);
815
+ }
816
+ else {
817
+ this.requestOptions.body = this.body;
818
+ }
819
+ this.bodyProcessed = true;
820
+ }
821
+ return super.sendTo(url);
822
+ }
514
823
  }
515
824
 
516
825
  // Methods without body
@@ -602,7 +911,7 @@ function options() {
602
911
  return new OptionsRequest();
603
912
  }
604
913
 
605
- // Create the main API object
914
+ // Create the main API object with configuration
606
915
  const create = {
607
916
  get,
608
917
  post,
@@ -611,6 +920,8 @@ const create = {
611
920
  patch,
612
921
  head,
613
922
  options,
923
+ // Add configuration access
924
+ config: Config.getInstance(),
614
925
  };
615
926
 
616
- export { CacheMode, CredentialsPolicy, DeleteRequest, GetRequest, HeadRequest, HttpMethod, OptionsRequest, PatchRequest, PostRequest, PutRequest, RedirectMode, RequestError, RequestMode, RequestPriority, ResponseWrapper, create as default };
927
+ export { CacheMode, CookieUtils, CredentialsPolicy, DeleteRequest, GetRequest, HeadRequest, HttpMethod, OptionsRequest, PatchRequest, PostRequest, PutRequest, RedirectMode, RequestError, RequestMode, RequestPriority, ResponseWrapper, SameSitePolicy, create as default };