@revenexx/integrations-node-sdk 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -496,6 +496,43 @@ var CROSS_ORIGIN_SAFE_HEADERS = /* @__PURE__ */ new Set([
496
496
  function headerCrossesOrigin(name) {
497
497
  return CROSS_ORIGIN_SAFE_HEADERS.has(name.trim().toLowerCase());
498
498
  }
499
+ var UNSENDABLE_IN_HEADER_VALUE = /[\0-\x08\x0a-\x1f\x7f\u0100-\uffff]/;
500
+ var HEADER_NAME_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
501
+ function isHttpWhitespace(code) {
502
+ return code === 9 || code === 10 || code === 13 || code === 32;
503
+ }
504
+ function trimHttpWhitespace(value) {
505
+ let start = 0;
506
+ let end = value.length;
507
+ while (start < end && isHttpWhitespace(value.charCodeAt(start))) start++;
508
+ while (end > start && isHttpWhitespace(value.charCodeAt(end - 1))) end--;
509
+ return value.slice(start, end);
510
+ }
511
+ function invalidHeader(detail) {
512
+ return new NodeError("INVALID_HEADER", detail, { status: 0 });
513
+ }
514
+ function sendableHeaders(headers) {
515
+ if (typeof headers !== "object" || headers === null) {
516
+ throw invalidHeader("Headers must be an object or a list of pairs");
517
+ }
518
+ if (!(Symbol.iterator in headers) && Object.getOwnPropertySymbols(headers).length > 0) {
519
+ throw invalidHeader("A header name must be a string, not a symbol");
520
+ }
521
+ const entries = Symbol.iterator in headers ? Array.from(headers) : Object.entries(headers);
522
+ return entries.map((entry, i) => {
523
+ const pair = typeof entry === "object" && entry !== null && Symbol.iterator in entry ? Array.from(entry) : [];
524
+ if (pair.length !== 2) throw invalidHeader(`Header entry ${i} is not a name/value pair`);
525
+ const name = String(pair[0]);
526
+ const value = String(pair[1]);
527
+ if (!HEADER_NAME_TOKEN.test(name)) {
528
+ throw invalidHeader(`Header name ${JSON.stringify(name)} cannot be sent`);
529
+ }
530
+ if (UNSENDABLE_IN_HEADER_VALUE.test(trimHttpWhitespace(value))) {
531
+ throw invalidHeader(`Header ${JSON.stringify(name)} has a character in its value that cannot be sent`);
532
+ }
533
+ return [name, value];
534
+ });
535
+ }
499
536
  var DEFAULT_RETRY_ATTEMPTS = 0;
500
537
  var MAX_RETRY_ATTEMPTS = 5;
501
538
  var DEFAULT_RETRY_DELAY_MS = 1e3;
@@ -535,6 +572,15 @@ async function guardUrl(url, ctxSignal, effectiveMs) {
535
572
  clearTimeout(timer);
536
573
  }
537
574
  }
575
+ function hasContent(body) {
576
+ if (body == null) return false;
577
+ if (typeof body === "string") return body.length > 0;
578
+ if (body instanceof ArrayBuffer) return body.byteLength > 0;
579
+ if (ArrayBuffer.isView(body)) return body.byteLength > 0;
580
+ if (body instanceof Blob) return body.size > 0;
581
+ if (body instanceof URLSearchParams) return body.toString().length > 0;
582
+ return true;
583
+ }
538
584
  async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
539
585
  let currentUrl = url;
540
586
  let currentInit = { ...fetchOptions, redirect: "manual" };
@@ -567,8 +613,7 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
567
613
  if (base.protocol === "https:" && nextUrl.protocol !== "https:") {
568
614
  throw new NodeError("BLOCKED_ADDRESS", "Blocked https\u2192http downgrade on redirect", { status: 0 });
569
615
  }
570
- await guardUrl(nextUrl, ctxSignal, effectiveMs);
571
- const headers = new Headers(currentInit.headers ?? void 0);
616
+ const headers = new Headers(currentInit.headers);
572
617
  let method = (currentInit.method ?? "GET").toUpperCase();
573
618
  let body = currentInit.body;
574
619
  if (redirectStatus === 303 || (redirectStatus === 301 || redirectStatus === 302) && method === "POST") {
@@ -577,6 +622,14 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
577
622
  headers.delete("content-type");
578
623
  headers.delete("content-length");
579
624
  }
625
+ if (hasContent(body) && nextUrl.origin !== base.origin) {
626
+ throw new NodeError(
627
+ "BLOCKED_ADDRESS",
628
+ `Blocked ${redirectStatus} redirect to another origin: it would resend the request body to ${nextUrl.origin}`,
629
+ { status: 0 }
630
+ );
631
+ }
632
+ await guardUrl(nextUrl, ctxSignal, effectiveMs);
580
633
  if (nextUrl.origin !== base.origin) {
581
634
  for (const name of [...headers.keys()]) {
582
635
  if (!headerCrossesOrigin(name)) headers.delete(name);
@@ -593,6 +646,8 @@ async function safeFetch(url, options = {}) {
593
646
  const safeAttempts = Number.isFinite(rawAttempts) ? Math.min(Math.max(0, rawAttempts), MAX_RETRY_ATTEMPTS) : DEFAULT_RETRY_ATTEMPTS;
594
647
  const maxAttempts = safeAttempts + 1;
595
648
  const retryDelayMs = retry?.delayMs ?? DEFAULT_RETRY_DELAY_MS;
649
+ if (ctxSignal?.aborted) throw ctxSignal.reason;
650
+ if (fetchOptions.headers !== void 0) fetchOptions.headers = sendableHeaders(fetchOptions.headers);
596
651
  let lastError;
597
652
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
598
653
  if (ctxSignal?.aborted) throw ctxSignal.reason;
package/dist/index.js CHANGED
@@ -404,6 +404,43 @@ var CROSS_ORIGIN_SAFE_HEADERS = /* @__PURE__ */ new Set([
404
404
  function headerCrossesOrigin(name) {
405
405
  return CROSS_ORIGIN_SAFE_HEADERS.has(name.trim().toLowerCase());
406
406
  }
407
+ var UNSENDABLE_IN_HEADER_VALUE = /[\0-\x08\x0a-\x1f\x7f\u0100-\uffff]/;
408
+ var HEADER_NAME_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
409
+ function isHttpWhitespace(code) {
410
+ return code === 9 || code === 10 || code === 13 || code === 32;
411
+ }
412
+ function trimHttpWhitespace(value) {
413
+ let start = 0;
414
+ let end = value.length;
415
+ while (start < end && isHttpWhitespace(value.charCodeAt(start))) start++;
416
+ while (end > start && isHttpWhitespace(value.charCodeAt(end - 1))) end--;
417
+ return value.slice(start, end);
418
+ }
419
+ function invalidHeader(detail) {
420
+ return new NodeError("INVALID_HEADER", detail, { status: 0 });
421
+ }
422
+ function sendableHeaders(headers) {
423
+ if (typeof headers !== "object" || headers === null) {
424
+ throw invalidHeader("Headers must be an object or a list of pairs");
425
+ }
426
+ if (!(Symbol.iterator in headers) && Object.getOwnPropertySymbols(headers).length > 0) {
427
+ throw invalidHeader("A header name must be a string, not a symbol");
428
+ }
429
+ const entries = Symbol.iterator in headers ? Array.from(headers) : Object.entries(headers);
430
+ return entries.map((entry, i) => {
431
+ const pair = typeof entry === "object" && entry !== null && Symbol.iterator in entry ? Array.from(entry) : [];
432
+ if (pair.length !== 2) throw invalidHeader(`Header entry ${i} is not a name/value pair`);
433
+ const name = String(pair[0]);
434
+ const value = String(pair[1]);
435
+ if (!HEADER_NAME_TOKEN.test(name)) {
436
+ throw invalidHeader(`Header name ${JSON.stringify(name)} cannot be sent`);
437
+ }
438
+ if (UNSENDABLE_IN_HEADER_VALUE.test(trimHttpWhitespace(value))) {
439
+ throw invalidHeader(`Header ${JSON.stringify(name)} has a character in its value that cannot be sent`);
440
+ }
441
+ return [name, value];
442
+ });
443
+ }
407
444
  var DEFAULT_RETRY_ATTEMPTS = 0;
408
445
  var MAX_RETRY_ATTEMPTS = 5;
409
446
  var DEFAULT_RETRY_DELAY_MS = 1e3;
@@ -443,6 +480,15 @@ async function guardUrl(url, ctxSignal, effectiveMs) {
443
480
  clearTimeout(timer);
444
481
  }
445
482
  }
483
+ function hasContent(body) {
484
+ if (body == null) return false;
485
+ if (typeof body === "string") return body.length > 0;
486
+ if (body instanceof ArrayBuffer) return body.byteLength > 0;
487
+ if (ArrayBuffer.isView(body)) return body.byteLength > 0;
488
+ if (body instanceof Blob) return body.size > 0;
489
+ if (body instanceof URLSearchParams) return body.toString().length > 0;
490
+ return true;
491
+ }
446
492
  async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
447
493
  let currentUrl = url;
448
494
  let currentInit = { ...fetchOptions, redirect: "manual" };
@@ -475,8 +521,7 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
475
521
  if (base.protocol === "https:" && nextUrl.protocol !== "https:") {
476
522
  throw new NodeError("BLOCKED_ADDRESS", "Blocked https\u2192http downgrade on redirect", { status: 0 });
477
523
  }
478
- await guardUrl(nextUrl, ctxSignal, effectiveMs);
479
- const headers = new Headers(currentInit.headers ?? void 0);
524
+ const headers = new Headers(currentInit.headers);
480
525
  let method = (currentInit.method ?? "GET").toUpperCase();
481
526
  let body = currentInit.body;
482
527
  if (redirectStatus === 303 || (redirectStatus === 301 || redirectStatus === 302) && method === "POST") {
@@ -485,6 +530,14 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
485
530
  headers.delete("content-type");
486
531
  headers.delete("content-length");
487
532
  }
533
+ if (hasContent(body) && nextUrl.origin !== base.origin) {
534
+ throw new NodeError(
535
+ "BLOCKED_ADDRESS",
536
+ `Blocked ${redirectStatus} redirect to another origin: it would resend the request body to ${nextUrl.origin}`,
537
+ { status: 0 }
538
+ );
539
+ }
540
+ await guardUrl(nextUrl, ctxSignal, effectiveMs);
488
541
  if (nextUrl.origin !== base.origin) {
489
542
  for (const name of [...headers.keys()]) {
490
543
  if (!headerCrossesOrigin(name)) headers.delete(name);
@@ -501,6 +554,8 @@ async function safeFetch(url, options = {}) {
501
554
  const safeAttempts = Number.isFinite(rawAttempts) ? Math.min(Math.max(0, rawAttempts), MAX_RETRY_ATTEMPTS) : DEFAULT_RETRY_ATTEMPTS;
502
555
  const maxAttempts = safeAttempts + 1;
503
556
  const retryDelayMs = retry?.delayMs ?? DEFAULT_RETRY_DELAY_MS;
557
+ if (ctxSignal?.aborted) throw ctxSignal.reason;
558
+ if (fetchOptions.headers !== void 0) fetchOptions.headers = sendableHeaders(fetchOptions.headers);
504
559
  let lastError;
505
560
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
506
561
  if (ctxSignal?.aborted) throw ctxSignal.reason;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/integrations-node-sdk",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "TypeScript interfaces and utilities for Revenexx integration nodes",
5
5
  "license": "MIT",
6
6
  "author": "revenexx GmbH",