@revenexx/integrations-node-sdk 1.11.0 → 1.14.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
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  ApiKeyCredential: () => ApiKeyCredential,
34
34
  BaseCredential: () => BaseCredential,
35
35
  BasicAuthCredential: () => BasicAuthCredential,
36
+ CONFIG_KEY_PATTERN: () => CONFIG_KEY_PATTERN,
36
37
  DEFAULT_MAX_RESPONSE_BYTES: () => DEFAULT_MAX_RESPONSE_BYTES,
37
38
  DEFAULT_RETRY_ATTEMPTS: () => DEFAULT_RETRY_ATTEMPTS,
38
39
  DEFAULT_RETRY_DELAY_MS: () => DEFAULT_RETRY_DELAY_MS,
@@ -43,13 +44,20 @@ __export(index_exports, {
43
44
  MAX_RESPONSE_BYTES: () => MAX_RESPONSE_BYTES,
44
45
  MAX_RETRY_ATTEMPTS: () => MAX_RETRY_ATTEMPTS,
45
46
  MAX_TIMEOUT_MS: () => MAX_TIMEOUT_MS,
47
+ NAMESPACED_SLUG_PATTERN: () => NAMESPACED_SLUG_PATTERN,
46
48
  NodeError: () => NodeError,
47
49
  OAuth2AuthCodeCredential: () => OAuth2AuthCodeCredential,
48
50
  OAuth2ClientCredentialsCredential: () => OAuth2ClientCredentialsCredential,
49
51
  OPERATORS: () => OPERATORS,
50
52
  RetryableError: () => RetryableError,
53
+ SEMVER_PATTERN: () => SEMVER_PATTERN,
54
+ SLUG_PATTERN: () => SLUG_PATTERN,
51
55
  SimpleValueCredential: () => SimpleValueCredential,
52
56
  VALUELESS_OPERATORS: () => VALUELESS_OPERATORS,
57
+ VPN_ERROR_CODES: () => VPN_ERROR_CODES,
58
+ VPN_ROUTE_MISSING: () => VPN_ROUTE_MISSING,
59
+ VPN_UNAVAILABLE: () => VPN_UNAVAILABLE,
60
+ allowedTargets: () => allowedTargets,
53
61
  assertBranchNamed: () => assertBranchNamed,
54
62
  assertPublicUrl: () => assertPublicUrl,
55
63
  backoffDelay: () => backoffDelay,
@@ -62,14 +70,20 @@ __export(index_exports, {
62
70
  extractCredentialManifests: () => extractCredentialManifests,
63
71
  extractManifest: () => extractManifest,
64
72
  extractManifests: () => extractManifests,
73
+ guardRelaxedForLocalDev: () => guardRelaxedForLocalDev,
74
+ guardedLookup: () => guardedLookup,
65
75
  headerCrossesOrigin: () => headerCrossesOrigin,
66
76
  httpErrorResult: () => httpErrorResult,
77
+ installAllowedTargets: () => installAllowedTargets,
78
+ isAllowedTarget: () => isAllowedTarget,
67
79
  isBlockedAddress: () => isBlockedAddress,
68
80
  isMultiPort: () => isMultiPort,
69
81
  isNodeWithIteration: () => isNodeWithIteration,
70
82
  isOAuthAuthorizeCredential: () => isOAuthAuthorizeCredential,
71
83
  isOperator: () => isOperator,
84
+ isVpnErrorCode: () => isVpnErrorCode,
72
85
  maxBytesConfigField: () => maxBytesConfigField,
86
+ nodeManifestProblems: () => nodeManifestProblems,
73
87
  normalizeCredentialType: () => normalizeCredentialType,
74
88
  normalizeLocalized: () => normalizeLocalized,
75
89
  outputProblems: () => outputProblems,
@@ -213,6 +227,12 @@ var NodeError = class extends Error {
213
227
  this.meta = meta;
214
228
  }
215
229
  };
230
+ var VPN_UNAVAILABLE = "VPN_UNAVAILABLE";
231
+ var VPN_ROUTE_MISSING = "VPN_ROUTE_MISSING";
232
+ var VPN_ERROR_CODES = Object.freeze([VPN_UNAVAILABLE, VPN_ROUTE_MISSING]);
233
+ function isVpnErrorCode(code) {
234
+ return VPN_ERROR_CODES.includes(code);
235
+ }
216
236
 
217
237
  // src/nodeErrors.ts
218
238
  function toErrorOutput(err) {
@@ -359,8 +379,123 @@ function assertBranchNamed(node, result, config) {
359
379
 
360
380
  // src/ssrf.ts
361
381
  var import_node_diagnostics_channel = require("diagnostics_channel");
382
+ var import_node_dns = __toESM(require("dns"), 1);
383
+ var import_node_net2 = require("net");
384
+ var import_ipaddr2 = __toESM(require("ipaddr.js"), 1);
385
+ var import_undici = require("undici");
386
+
387
+ // src/allowedTargets.ts
362
388
  var import_node_net = require("net");
363
389
  var import_ipaddr = __toESM(require("ipaddr.js"), 1);
390
+ var ALLOWED_TARGETS_SYMBOL = /* @__PURE__ */ Symbol.for("revenexx.ssrf.allowedTargets");
391
+ var EMPTY = Object.freeze([]);
392
+ var V4_ENTRY = /^(\d{1,3}(?:\.\d{1,3}){3}):(\d{1,5})$/;
393
+ var V6_ENTRY = /^\[([0-9A-Fa-f:.]+)\]:(\d{1,5})$/;
394
+ function canonicalAddress(ip) {
395
+ const bare = ip.startsWith("[") && ip.endsWith("]") ? ip.slice(1, -1) : ip;
396
+ if ((0, import_node_net.isIP)(bare) === 0) return void 0;
397
+ try {
398
+ return import_ipaddr.default.process(bare).toString();
399
+ } catch {
400
+ return void 0;
401
+ }
402
+ }
403
+ function validPort(port) {
404
+ return Number.isInteger(port) && port >= 1 && port <= 65535;
405
+ }
406
+ var TARGET_RANGES = /* @__PURE__ */ new Set(["private", "uniqueLocal"]);
407
+ function loopbackTargetsAllowed() {
408
+ const raw = process.env["RVNXX_SSRF_TARGETS_ALLOW_LOOPBACK"];
409
+ return raw != null && ["1", "true", "yes"].includes(raw.trim().toLowerCase());
410
+ }
411
+ function parseTarget(entry, allowLoopback) {
412
+ if (typeof entry !== "string") return void 0;
413
+ const v4 = V4_ENTRY.exec(entry);
414
+ const m = v4 ?? V6_ENTRY.exec(entry);
415
+ if (!m) return void 0;
416
+ const host = m[1];
417
+ const port = Number(m[2]);
418
+ if (v4 && !import_ipaddr.default.IPv4.isValidFourPartDecimal(host)) return void 0;
419
+ if (!validPort(port)) return void 0;
420
+ const address = canonicalAddress(host);
421
+ if (address === void 0) return void 0;
422
+ const range = import_ipaddr.default.parse(address).range();
423
+ if (!TARGET_RANGES.has(range) && !(allowLoopback && range === "loopback")) return void 0;
424
+ return `${address}:${port}`;
425
+ }
426
+ var allowListStats = { validations: 0 };
427
+ var keysByList = /* @__PURE__ */ new WeakMap();
428
+ function keysOf(list) {
429
+ const allowLoopback = loopbackTargetsAllowed();
430
+ let byMode = keysByList.get(list);
431
+ if (byMode === void 0) {
432
+ byMode = /* @__PURE__ */ new Map();
433
+ keysByList.set(list, byMode);
434
+ }
435
+ const cached = byMode.get(allowLoopback);
436
+ if (cached !== void 0) return cached;
437
+ allowListStats.validations++;
438
+ const keys = /* @__PURE__ */ new Set();
439
+ let result = keys;
440
+ for (const entry of list) {
441
+ const key = parseTarget(entry, allowLoopback);
442
+ if (key === void 0) {
443
+ result = null;
444
+ break;
445
+ }
446
+ keys.add(key);
447
+ }
448
+ byMode.set(allowLoopback, result);
449
+ return result;
450
+ }
451
+ function installedList(holder) {
452
+ const descriptor = Object.getOwnPropertyDescriptor(holder, ALLOWED_TARGETS_SYMBOL);
453
+ if (descriptor === void 0 || !("value" in descriptor)) return null;
454
+ if (descriptor.writable || descriptor.configurable) return null;
455
+ const value = descriptor.value;
456
+ if (!Array.isArray(value) || !Object.isFrozen(value)) return null;
457
+ const keys = keysOf(value);
458
+ return keys === null ? null : { list: value, keys };
459
+ }
460
+ function readAllowedTargets(holder) {
461
+ return installedList(holder)?.list ?? EMPTY;
462
+ }
463
+ function isAllowedTargetOn(holder, ip, port) {
464
+ const installed = installedList(holder);
465
+ if (installed === null || !validPort(port)) return false;
466
+ const address = canonicalAddress(ip);
467
+ return address !== void 0 && installed.keys.has(`${address}:${port}`);
468
+ }
469
+ function installAllowedTargetsOn(holder, targets) {
470
+ if (Object.getOwnPropertyDescriptor(holder, ALLOWED_TARGETS_SYMBOL) !== void 0) {
471
+ throw new Error("The SSRF allow-list is already installed, and is installed once per process");
472
+ }
473
+ if (!Array.isArray(targets)) throw new TypeError('The SSRF allow-list must be an array of "<ip>:<port>" strings');
474
+ for (const entry of targets) {
475
+ if (parseTarget(entry, loopbackTargetsAllowed()) === void 0) {
476
+ throw new TypeError(
477
+ `Not an allowed target: ${JSON.stringify(entry)} (expected "<ip>:<port>" or "[v6]:<port>" at an RFC 1918 or unique-local address)`
478
+ );
479
+ }
480
+ }
481
+ Object.defineProperty(holder, ALLOWED_TARGETS_SYMBOL, {
482
+ value: Object.freeze([...targets]),
483
+ writable: false,
484
+ configurable: false,
485
+ enumerable: false
486
+ });
487
+ }
488
+ function installAllowedTargets(targets) {
489
+ installAllowedTargetsOn(globalThis, targets);
490
+ }
491
+ function allowedTargets() {
492
+ return readAllowedTargets(globalThis);
493
+ }
494
+ function isAllowedTarget(ip, port) {
495
+ return isAllowedTargetOn(globalThis, ip, port);
496
+ }
497
+
498
+ // src/ssrf.ts
364
499
  var ssrfResolver = {
365
500
  lookup: async (hostname) => {
366
501
  const { lookup } = await import("dns/promises");
@@ -372,15 +507,15 @@ function isIPv6(addr) {
372
507
  return addr.kind() === "ipv6";
373
508
  }
374
509
  var PUBLIC_RANGE = "unicast";
375
- var GLOBAL_UNICAST_V6 = import_ipaddr.default.parseCIDR("2000::/3");
510
+ var GLOBAL_UNICAST_V6 = import_ipaddr2.default.parseCIDR("2000::/3");
376
511
  var V4_EMBEDDING_RANGES = /* @__PURE__ */ new Set(["ipv4Mapped", "rfc6052"]);
377
512
  function embeddedIpv4(addr) {
378
- return import_ipaddr.default.fromByteArray(addr.toByteArray().slice(12));
513
+ return import_ipaddr2.default.fromByteArray(addr.toByteArray().slice(12));
379
514
  }
380
515
  function isBlockedAddress(ip) {
381
516
  let addr;
382
517
  try {
383
- addr = import_ipaddr.default.parse(ip);
518
+ addr = import_ipaddr2.default.parse(ip);
384
519
  } catch {
385
520
  return true;
386
521
  }
@@ -397,19 +532,25 @@ function guardRelaxedForLocalDev() {
397
532
  if (relaxed && !bypassNoticeLogged) {
398
533
  bypassNoticeLogged = true;
399
534
  console.warn(
400
- "[ssrf] RVNXX_SSRF_ALLOW_PRIVATE is set: allowing private/loopback fetch targets. Intended for local development only."
535
+ "[ssrf] RVNXX_SSRF_ALLOW_PRIVATE is set: allowing private/loopback targets for web requests and connections. Intended for local development only."
401
536
  );
402
537
  }
403
538
  return relaxed;
404
539
  }
540
+ function logBlocked(noun, host, address) {
541
+ console.warn(`[ssrf] blocked ${noun} to ${host}: resolves to private/reserved address ${address}`);
542
+ }
405
543
  function blockedError(host, address) {
406
544
  if (host === address) {
407
545
  return new NodeError("BLOCKED_ADDRESS", `Blocked request to private or reserved address ${address}`, {
408
546
  status: 0
409
547
  });
410
548
  }
411
- console.warn(`[ssrf] blocked request to ${host}: resolves to private/reserved address ${address}`);
412
- return new NodeError("BLOCKED_ADDRESS", `Blocked request to ${host}: resolves to a private or reserved address`, {
549
+ logBlocked("request", host, address);
550
+ return refusedName(host);
551
+ }
552
+ function refusedName(host, noun = "request") {
553
+ return new NodeError("BLOCKED_ADDRESS", `Blocked ${noun} to ${host}: resolves to a private or reserved address`, {
413
554
  status: 0
414
555
  });
415
556
  }
@@ -466,8 +607,86 @@ function judgeConnection(message) {
466
607
  if (guardRelaxedForLocalDev()) return;
467
608
  const peer = socket.remoteAddress;
468
609
  if (peer != null && !isBlockedAddress(peer)) return;
610
+ const remotePort = socket.remotePort;
611
+ if (peer != null && allowedTargets().length > 0 && !Number.isInteger(remotePort)) {
612
+ reportUnreadablePeerPort();
613
+ socket.destroy(unreadablePortError(normalizeHost(hostname)));
614
+ return;
615
+ }
616
+ if (peer != null && isAllowedTarget(peer, remotePort)) return;
469
617
  socket.destroy(connectBlockedError(normalizeHost(hostname), peer));
470
618
  }
619
+ var unreadablePeerPortLogged = false;
620
+ function reportUnreadablePeerPort() {
621
+ if (unreadablePeerPortLogged) return;
622
+ unreadablePeerPortLogged = true;
623
+ console.warn(
624
+ "[ssrf] a connection reported no remote port: the SSRF allow-list cannot be consulted, so private peers are refused."
625
+ );
626
+ }
627
+ function unreadablePortError(host) {
628
+ return new NodeError(
629
+ "BLOCKED_ADDRESS",
630
+ `Blocked request to ${host}: the port the connection landed on could not be read, so the allow-list could not be consulted`,
631
+ { status: 0 }
632
+ );
633
+ }
634
+ function judgingLookup(port) {
635
+ return lookupJudgedFor(port, { noun: "request" }, (hostname) => new NodeError("BLOCKED_ADDRESS", `Could not resolve host: ${hostname}`, { status: 0 }));
636
+ }
637
+ function guardedLookup(port, options = {}) {
638
+ return lookupJudgedFor(
639
+ port,
640
+ options,
641
+ (hostname) => Object.assign(new Error(`getaddrinfo ENOTFOUND ${hostname}`), { code: "ENOTFOUND", hostname })
642
+ );
643
+ }
644
+ function lookupJudgedFor(port, { refusal, unroutable, noun = "connection" }, emptyAnswer) {
645
+ return (hostname, options, callback) => {
646
+ import_node_dns.default.lookup(hostname, { ...options, all: true }, (err, addresses) => {
647
+ if (err) {
648
+ callback(err, "");
649
+ return;
650
+ }
651
+ if (addresses.length === 0) {
652
+ callback(emptyAnswer(hostname), "");
653
+ return;
654
+ }
655
+ if (!guardRelaxedForLocalDev()) {
656
+ const host = hostname;
657
+ for (const a of addresses) {
658
+ if (!isBlockedAddress(a.address)) continue;
659
+ const allowed = isAllowedTarget(a.address, port);
660
+ if (allowed && !unroutable) continue;
661
+ logBlocked(noun, host, a.address);
662
+ callback(allowed && unroutable ? unroutable(host) : refusal ? refusal(host) : refusedName(host, noun), "");
663
+ return;
664
+ }
665
+ }
666
+ if (options.all) callback(null, addresses);
667
+ else callback(null, addresses[0].address, addresses[0].family);
668
+ });
669
+ };
670
+ }
671
+ var connectors = /* @__PURE__ */ new Map();
672
+ function connectorFor(port) {
673
+ let connector = connectors.get(port);
674
+ if (connector === void 0) {
675
+ connector = (0, import_undici.buildConnector)({ lookup: judgingLookup(port) });
676
+ connectors.set(port, connector);
677
+ }
678
+ return connector;
679
+ }
680
+ var dispatcher;
681
+ function guardedDispatcher() {
682
+ dispatcher ??= new import_undici.Agent({
683
+ connect: (opts, callback) => {
684
+ const port = opts.port === "" ? opts.protocol === "https:" ? 443 : 80 : Number(opts.port);
685
+ return connectorFor(port)(opts, callback);
686
+ }
687
+ });
688
+ return dispatcher;
689
+ }
471
690
  function guardConnectionsTo(url) {
472
691
  if (!connectGuardInstalled) {
473
692
  connectGuardInstalled = true;
@@ -520,8 +739,10 @@ async function assertPublicUrl(url, opts = {}) {
520
739
  if (lower === "localhost" || lower.endsWith(".localhost")) {
521
740
  throw new NodeError("BLOCKED_ADDRESS", `Blocked loopback host: ${host}`, { status: 0 });
522
741
  }
523
- if ((0, import_node_net.isIP)(host) !== 0) {
524
- if (isBlockedAddress(host)) throw blockedError(host, host);
742
+ const port = u.port === "" ? u.protocol === "https:" ? 443 : 80 : Number(u.port);
743
+ const refused = (address) => isBlockedAddress(address) && !isAllowedTarget(address, port);
744
+ if ((0, import_node_net2.isIP)(host) !== 0) {
745
+ if (refused(host)) throw blockedError(host, host);
525
746
  return;
526
747
  }
527
748
  const lookup = opts.lookup ?? ssrfResolver.lookup;
@@ -530,7 +751,7 @@ async function assertPublicUrl(url, opts = {}) {
530
751
  throw new NodeError("BLOCKED_ADDRESS", `Could not resolve host: ${host}`, { status: 0 });
531
752
  }
532
753
  for (const a of addresses) {
533
- if (isBlockedAddress(a.address)) throw blockedError(host, a.address);
754
+ if (refused(a.address)) throw blockedError(host, a.address);
534
755
  }
535
756
  }
536
757
 
@@ -625,34 +846,84 @@ function credentialOriginRefusal(target, bound, hop) {
625
846
  { status: 0 }
626
847
  );
627
848
  }
628
- async function timedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
849
+ function attemptDeadline(ctxSignal, effectiveMs) {
629
850
  const ac = new AbortController();
630
- const timer = setTimeout(() => ac.abort(), effectiveMs);
631
- const signal = ctxSignal ? AbortSignal.any([ctxSignal, ac.signal]) : ac.signal;
851
+ let timer;
852
+ const arm = (message) => {
853
+ clearTimeout(timer);
854
+ timer = setTimeout(() => ac.abort(new NodeError("TIMEOUT", message)), effectiveMs);
855
+ };
856
+ arm(`Request timed out after ${effectiveMs}ms`);
857
+ return {
858
+ signal: ctxSignal ? AbortSignal.any([ctxSignal, ac.signal]) : ac.signal,
859
+ expired: () => ac.signal.aborted,
860
+ reason: () => ac.signal.reason,
861
+ clear: () => clearTimeout(timer),
862
+ awaitChunk: () => arm(`The answer sent nothing for ${effectiveMs}ms`)
863
+ };
864
+ }
865
+ function attemptFailure(err, ctxSignal, deadline) {
866
+ if (ctxSignal?.aborted) return ctxSignal.reason;
867
+ if (deadline.expired()) return deadline.reason();
868
+ return err;
869
+ }
870
+ function answerUnderDeadline(res, ctxSignal, deadline) {
871
+ if (!res.body) return res;
872
+ let reader;
873
+ const body = new ReadableStream(
874
+ {
875
+ async pull(controller) {
876
+ reader ??= res.body.getReader();
877
+ deadline.awaitChunk();
878
+ try {
879
+ const { done, value } = await reader.read();
880
+ if (done) controller.close();
881
+ else controller.enqueue(value);
882
+ } catch (err) {
883
+ controller.error(attemptFailure(err, ctxSignal, deadline));
884
+ } finally {
885
+ deadline.clear();
886
+ }
887
+ },
888
+ cancel(reason) {
889
+ deadline.clear();
890
+ return reader ? reader.cancel(reason) : res.body?.cancel(reason);
891
+ }
892
+ },
893
+ // Read nothing ahead of the caller: the limit times the caller's waits, not
894
+ // a buffer filling while nobody reads.
895
+ { highWaterMark: 0 }
896
+ );
897
+ let answer;
898
+ try {
899
+ answer = new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers });
900
+ } catch {
901
+ return res;
902
+ }
903
+ return withUrl(answer, res.url);
904
+ }
905
+ function withUrl(answer, url) {
906
+ const clone = answer.clone.bind(answer);
907
+ Object.defineProperty(answer, "url", { value: url });
908
+ Object.defineProperty(answer, "clone", { value: () => withUrl(clone(), url) });
909
+ return answer;
910
+ }
911
+ async function timedFetch(url, fetchOptions, ctxSignal, deadline) {
632
912
  try {
633
- return await fetch(url, { ...fetchOptions, signal });
913
+ return await fetch(url, { ...fetchOptions, signal: deadline.signal, dispatcher: guardedDispatcher() });
634
914
  } catch (err) {
635
915
  const blocked = connectionRefusal(err);
636
916
  if (blocked) throw blocked;
637
- if (ctxSignal?.aborted) throw ctxSignal.reason;
638
- if (ac.signal.aborted) throw new NodeError("TIMEOUT", `Request timed out after ${effectiveMs}ms`);
639
- throw err;
640
- } finally {
641
- clearTimeout(timer);
917
+ throw attemptFailure(err, ctxSignal, deadline);
642
918
  }
643
919
  }
644
- async function guardUrl(url, ctxSignal, effectiveMs) {
645
- const ac = new AbortController();
646
- const timer = setTimeout(() => ac.abort(), effectiveMs);
647
- const signal = ctxSignal ? AbortSignal.any([ctxSignal, ac.signal]) : ac.signal;
920
+ async function guardUrl(url, ctxSignal, deadline, effectiveMs) {
648
921
  try {
649
- await assertPublicUrl(url, { signal });
922
+ await assertPublicUrl(url, { signal: deadline.signal });
650
923
  } catch (err) {
651
924
  if (ctxSignal?.aborted) throw ctxSignal.reason;
652
- if (ac.signal.aborted) throw new NodeError("TIMEOUT", `DNS resolution timed out after ${effectiveMs}ms`);
925
+ if (deadline.expired()) throw new NodeError("TIMEOUT", `DNS resolution timed out after ${effectiveMs}ms`);
653
926
  throw err;
654
- } finally {
655
- clearTimeout(timer);
656
927
  }
657
928
  }
658
929
  function hasContent(body) {
@@ -674,16 +945,27 @@ function refuseUserinfo(url, status = 0) {
674
945
  }
675
946
  }
676
947
  async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding) {
948
+ const deadline = attemptDeadline(ctxSignal, effectiveMs);
949
+ try {
950
+ const res = await followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, binding);
951
+ deadline.clear();
952
+ return answerUnderDeadline(res, ctxSignal, deadline);
953
+ } catch (err) {
954
+ deadline.clear();
955
+ throw err;
956
+ }
957
+ }
958
+ async function followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, binding) {
677
959
  let currentUrl = url;
678
960
  let currentInit = { ...fetchOptions, redirect: "manual" };
679
- await guardUrl(currentUrl, ctxSignal, effectiveMs);
961
+ await guardUrl(currentUrl, ctxSignal, deadline, effectiveMs);
680
962
  for (let hop = 0; ; hop++) {
681
963
  const release = guardConnectionsTo(
682
964
  currentUrl instanceof URL ? currentUrl : new URL(currentUrl)
683
965
  );
684
966
  let res;
685
967
  try {
686
- res = await timedFetch(currentUrl, currentInit, ctxSignal, effectiveMs);
968
+ res = await timedFetch(currentUrl, currentInit, ctxSignal, deadline);
687
969
  } finally {
688
970
  release();
689
971
  }
@@ -725,7 +1007,7 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding)
725
1007
  { status: 0 }
726
1008
  );
727
1009
  }
728
- await guardUrl(nextUrl, ctxSignal, effectiveMs);
1010
+ await guardUrl(nextUrl, ctxSignal, deadline, effectiveMs);
729
1011
  if (binding && nextUrl.origin !== binding.origin) {
730
1012
  for (const name of [...headers.keys()]) headers.delete(name);
731
1013
  } else if (nextUrl.origin !== base.origin) {
@@ -873,8 +1155,8 @@ function timeoutConfigField(opts) {
873
1155
  key: "timeoutMs",
874
1156
  label: { en: "Timeout (ms)", de: "Zeitlimit (ms)" },
875
1157
  description: {
876
- en: "How long one attempt may take before it fails as a timeout. Counted per attempt and per redirect hop, not across the whole call.",
877
- de: "Wie lange ein einzelner Versuch dauern darf, bevor er als Zeit\xFCberschreitung fehlschl\xE4gt. Gilt je Versuch und je Weiterleitung, nicht f\xFCr den gesamten Aufruf."
1158
+ en: "How long one attempt may wait for an answer to start, redirects included, and how long the answer may then go silent before it fails as a timeout. An answer that keeps arriving is not cut off. Each retry gets the time again.",
1159
+ de: "Wie lange ein einzelner Versuch auf den Beginn einer Antwort warten darf, Weiterleitungen eingeschlossen, und wie lange die Antwort danach stocken darf, bevor sie als Zeit\xFCberschreitung fehlschl\xE4gt. Eine Antwort, die weiter eintrifft, wird nicht abgebrochen. Jede Wiederholung bekommt die Zeit erneut."
878
1160
  },
879
1161
  type: "number",
880
1162
  default: opts?.default ?? DEFAULT_TIMEOUT_MS,
@@ -997,6 +1279,53 @@ function parsePackageMeta(raw) {
997
1279
  icon: icon !== "" ? icon : void 0
998
1280
  };
999
1281
  }
1282
+ var SLUG_PATTERN = "^[a-z][a-z0-9-]{0,62}$";
1283
+ var NAMESPACED_SLUG_PATTERN = "^[a-z][a-z0-9-]{0,62}:[a-z][a-z0-9-]{0,62}$";
1284
+ var SEMVER_PATTERN = "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$";
1285
+ var CONFIG_KEY_PATTERN = "^[a-zA-Z][a-zA-Z0-9-]*$";
1286
+ var PATTERN_HINT = {
1287
+ [SLUG_PATTERN]: 'lowercase kebab-case, such as "not-found"',
1288
+ [NAMESPACED_SLUG_PATTERN]: '"<namespace>:<name>" in lowercase kebab-case, such as "core:http-request"',
1289
+ [SEMVER_PATTERN]: 'a semantic version, such as "1.0.0"',
1290
+ [CONFIG_KEY_PATTERN]: 'a letter, then letters, digits or "-", such as "postalCode"'
1291
+ };
1292
+ function nodeManifestProblems(nodes) {
1293
+ const problems = [];
1294
+ for (const [index, node] of nodes.entries()) {
1295
+ const who = `node ${index + 1} (${typeof node.slug === "string" ? node.slug : "no slug"})`;
1296
+ const check = (path, value, pattern) => {
1297
+ if (value === void 0 || typeof value === "string" && new RegExp(pattern).test(value)) return;
1298
+ problems.push(
1299
+ `${who}: ${path} ${JSON.stringify(value)} is refused by the registry \u2014 it must match ${pattern} (${PATTERN_HINT[pattern]}).`
1300
+ );
1301
+ };
1302
+ check("slug", node.slug, NAMESPACED_SLUG_PATTERN);
1303
+ check("version", node.version, SEMVER_PATTERN);
1304
+ for (const [i, port] of (node.outputs ?? []).entries()) {
1305
+ check(`outputs[${i}].name`, port.name, SLUG_PATTERN);
1306
+ check(`outputs[${i}].sourceFromConfig`, port.sourceFromConfig, SLUG_PATTERN);
1307
+ check(`outputs[${i}].fallback.name`, port.fallback?.name, SLUG_PATTERN);
1308
+ }
1309
+ const checkField = (field, at) => {
1310
+ check(`${at}.key`, field.key, CONFIG_KEY_PATTERN);
1311
+ for (const [j, key] of (field.dependsOn ?? []).entries()) check(`${at}.dependsOn[${j}]`, key, CONFIG_KEY_PATTERN);
1312
+ check(`${at}.showIf.key`, field.showIf?.key, CONFIG_KEY_PATTERN);
1313
+ const types = field.credentialType;
1314
+ if (Array.isArray(types)) {
1315
+ for (const [j, type] of types.entries()) check(`${at}.credentialType[${j}]`, type, NAMESPACED_SLUG_PATTERN);
1316
+ } else {
1317
+ check(`${at}.credentialType`, types, NAMESPACED_SLUG_PATTERN);
1318
+ }
1319
+ walk(field.properties, `${at}.properties`);
1320
+ if (field.items) checkField(field.items, `${at}.items`);
1321
+ };
1322
+ const walk = (fields, path) => {
1323
+ for (const [i, field] of (fields ?? []).entries()) checkField(field, `${path}[${i}]`);
1324
+ };
1325
+ walk(node.config, "config");
1326
+ }
1327
+ return problems;
1328
+ }
1000
1329
  function buildManifest(nodes, credentials = [], templates = []) {
1001
1330
  const manifest = {
1002
1331
  manifestVersion: MANIFEST_VERSION,
@@ -1281,6 +1610,7 @@ function base64Url(buf) {
1281
1610
  ApiKeyCredential,
1282
1611
  BaseCredential,
1283
1612
  BasicAuthCredential,
1613
+ CONFIG_KEY_PATTERN,
1284
1614
  DEFAULT_MAX_RESPONSE_BYTES,
1285
1615
  DEFAULT_RETRY_ATTEMPTS,
1286
1616
  DEFAULT_RETRY_DELAY_MS,
@@ -1291,13 +1621,20 @@ function base64Url(buf) {
1291
1621
  MAX_RESPONSE_BYTES,
1292
1622
  MAX_RETRY_ATTEMPTS,
1293
1623
  MAX_TIMEOUT_MS,
1624
+ NAMESPACED_SLUG_PATTERN,
1294
1625
  NodeError,
1295
1626
  OAuth2AuthCodeCredential,
1296
1627
  OAuth2ClientCredentialsCredential,
1297
1628
  OPERATORS,
1298
1629
  RetryableError,
1630
+ SEMVER_PATTERN,
1631
+ SLUG_PATTERN,
1299
1632
  SimpleValueCredential,
1300
1633
  VALUELESS_OPERATORS,
1634
+ VPN_ERROR_CODES,
1635
+ VPN_ROUTE_MISSING,
1636
+ VPN_UNAVAILABLE,
1637
+ allowedTargets,
1301
1638
  assertBranchNamed,
1302
1639
  assertPublicUrl,
1303
1640
  backoffDelay,
@@ -1310,14 +1647,20 @@ function base64Url(buf) {
1310
1647
  extractCredentialManifests,
1311
1648
  extractManifest,
1312
1649
  extractManifests,
1650
+ guardRelaxedForLocalDev,
1651
+ guardedLookup,
1313
1652
  headerCrossesOrigin,
1314
1653
  httpErrorResult,
1654
+ installAllowedTargets,
1655
+ isAllowedTarget,
1315
1656
  isBlockedAddress,
1316
1657
  isMultiPort,
1317
1658
  isNodeWithIteration,
1318
1659
  isOAuthAuthorizeCredential,
1319
1660
  isOperator,
1661
+ isVpnErrorCode,
1320
1662
  maxBytesConfigField,
1663
+ nodeManifestProblems,
1321
1664
  normalizeCredentialType,
1322
1665
  normalizeLocalized,
1323
1666
  outputProblems,
package/dist/index.d.ts CHANGED
@@ -8,8 +8,9 @@ export * from './nodeErrors.js';
8
8
  export * from './extract.js';
9
9
  export * from './branches.js';
10
10
  export * from './fetch.js';
11
- export { assertPublicUrl, isBlockedAddress } from './ssrf.js';
12
- export type { LookupAddress, LookupFn } from './ssrf.js';
11
+ export { assertPublicUrl, guardedLookup, guardRelaxedForLocalDev, isBlockedAddress } from './ssrf.js';
12
+ export type { GuardedLookupOptions, LookupAddress, LookupFn } from './ssrf.js';
13
+ export { allowedTargets, installAllowedTargets, isAllowedTarget } from './allowedTargets.js';
13
14
  export * from './retry.js';
14
15
  export * from './manifest.js';
15
16
  export * from './credentials.js';