@revenexx/integrations-node-sdk 1.13.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.
@@ -92,6 +92,53 @@ function parsePackageMeta(raw) {
92
92
  icon: icon !== "" ? icon : void 0
93
93
  };
94
94
  }
95
+ var SLUG_PATTERN = "^[a-z][a-z0-9-]{0,62}$";
96
+ var NAMESPACED_SLUG_PATTERN = "^[a-z][a-z0-9-]{0,62}:[a-z][a-z0-9-]{0,62}$";
97
+ var SEMVER_PATTERN = "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$";
98
+ var CONFIG_KEY_PATTERN = "^[a-zA-Z][a-zA-Z0-9-]*$";
99
+ var PATTERN_HINT = {
100
+ [SLUG_PATTERN]: 'lowercase kebab-case, such as "not-found"',
101
+ [NAMESPACED_SLUG_PATTERN]: '"<namespace>:<name>" in lowercase kebab-case, such as "core:http-request"',
102
+ [SEMVER_PATTERN]: 'a semantic version, such as "1.0.0"',
103
+ [CONFIG_KEY_PATTERN]: 'a letter, then letters, digits or "-", such as "postalCode"'
104
+ };
105
+ function nodeManifestProblems(nodes) {
106
+ const problems = [];
107
+ for (const [index, node] of nodes.entries()) {
108
+ const who = `node ${index + 1} (${typeof node.slug === "string" ? node.slug : "no slug"})`;
109
+ const check = (path, value, pattern) => {
110
+ if (value === void 0 || typeof value === "string" && new RegExp(pattern).test(value)) return;
111
+ problems.push(
112
+ `${who}: ${path} ${JSON.stringify(value)} is refused by the registry \u2014 it must match ${pattern} (${PATTERN_HINT[pattern]}).`
113
+ );
114
+ };
115
+ check("slug", node.slug, NAMESPACED_SLUG_PATTERN);
116
+ check("version", node.version, SEMVER_PATTERN);
117
+ for (const [i, port] of (node.outputs ?? []).entries()) {
118
+ check(`outputs[${i}].name`, port.name, SLUG_PATTERN);
119
+ check(`outputs[${i}].sourceFromConfig`, port.sourceFromConfig, SLUG_PATTERN);
120
+ check(`outputs[${i}].fallback.name`, port.fallback?.name, SLUG_PATTERN);
121
+ }
122
+ const checkField = (field, at) => {
123
+ check(`${at}.key`, field.key, CONFIG_KEY_PATTERN);
124
+ for (const [j, key] of (field.dependsOn ?? []).entries()) check(`${at}.dependsOn[${j}]`, key, CONFIG_KEY_PATTERN);
125
+ check(`${at}.showIf.key`, field.showIf?.key, CONFIG_KEY_PATTERN);
126
+ const types = field.credentialType;
127
+ if (Array.isArray(types)) {
128
+ for (const [j, type] of types.entries()) check(`${at}.credentialType[${j}]`, type, NAMESPACED_SLUG_PATTERN);
129
+ } else {
130
+ check(`${at}.credentialType`, types, NAMESPACED_SLUG_PATTERN);
131
+ }
132
+ walk(field.properties, `${at}.properties`);
133
+ if (field.items) checkField(field.items, `${at}.items`);
134
+ };
135
+ const walk = (fields, path) => {
136
+ for (const [i, field] of (fields ?? []).entries()) checkField(field, `${path}[${i}]`);
137
+ };
138
+ walk(node.config, "config");
139
+ }
140
+ return problems;
141
+ }
95
142
  function buildManifest(nodes, credentials = [], templates = []) {
96
143
  const manifest = {
97
144
  manifestVersion: MANIFEST_VERSION,
@@ -118,5 +165,10 @@ export {
118
165
  MANIFEST_VERSION,
119
166
  packageIconProblem,
120
167
  parsePackageMeta,
168
+ SLUG_PATTERN,
169
+ NAMESPACED_SLUG_PATTERN,
170
+ SEMVER_PATTERN,
171
+ CONFIG_KEY_PATTERN,
172
+ nodeManifestProblems,
121
173
  buildManifest
122
174
  };
package/dist/cli.cjs CHANGED
@@ -147,6 +147,53 @@ function parsePackageMeta(raw) {
147
147
  icon: icon !== "" ? icon : void 0
148
148
  };
149
149
  }
150
+ var SLUG_PATTERN = "^[a-z][a-z0-9-]{0,62}$";
151
+ var NAMESPACED_SLUG_PATTERN = "^[a-z][a-z0-9-]{0,62}:[a-z][a-z0-9-]{0,62}$";
152
+ var SEMVER_PATTERN = "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$";
153
+ var CONFIG_KEY_PATTERN = "^[a-zA-Z][a-zA-Z0-9-]*$";
154
+ var PATTERN_HINT = {
155
+ [SLUG_PATTERN]: 'lowercase kebab-case, such as "not-found"',
156
+ [NAMESPACED_SLUG_PATTERN]: '"<namespace>:<name>" in lowercase kebab-case, such as "core:http-request"',
157
+ [SEMVER_PATTERN]: 'a semantic version, such as "1.0.0"',
158
+ [CONFIG_KEY_PATTERN]: 'a letter, then letters, digits or "-", such as "postalCode"'
159
+ };
160
+ function nodeManifestProblems(nodes) {
161
+ const problems = [];
162
+ for (const [index, node] of nodes.entries()) {
163
+ const who = `node ${index + 1} (${typeof node.slug === "string" ? node.slug : "no slug"})`;
164
+ const check = (path, value, pattern) => {
165
+ if (value === void 0 || typeof value === "string" && new RegExp(pattern).test(value)) return;
166
+ problems.push(
167
+ `${who}: ${path} ${JSON.stringify(value)} is refused by the registry \u2014 it must match ${pattern} (${PATTERN_HINT[pattern]}).`
168
+ );
169
+ };
170
+ check("slug", node.slug, NAMESPACED_SLUG_PATTERN);
171
+ check("version", node.version, SEMVER_PATTERN);
172
+ for (const [i, port] of (node.outputs ?? []).entries()) {
173
+ check(`outputs[${i}].name`, port.name, SLUG_PATTERN);
174
+ check(`outputs[${i}].sourceFromConfig`, port.sourceFromConfig, SLUG_PATTERN);
175
+ check(`outputs[${i}].fallback.name`, port.fallback?.name, SLUG_PATTERN);
176
+ }
177
+ const checkField = (field, at) => {
178
+ check(`${at}.key`, field.key, CONFIG_KEY_PATTERN);
179
+ for (const [j, key] of (field.dependsOn ?? []).entries()) check(`${at}.dependsOn[${j}]`, key, CONFIG_KEY_PATTERN);
180
+ check(`${at}.showIf.key`, field.showIf?.key, CONFIG_KEY_PATTERN);
181
+ const types = field.credentialType;
182
+ if (Array.isArray(types)) {
183
+ for (const [j, type] of types.entries()) check(`${at}.credentialType[${j}]`, type, NAMESPACED_SLUG_PATTERN);
184
+ } else {
185
+ check(`${at}.credentialType`, types, NAMESPACED_SLUG_PATTERN);
186
+ }
187
+ walk(field.properties, `${at}.properties`);
188
+ if (field.items) checkField(field.items, `${at}.items`);
189
+ };
190
+ const walk = (fields, path) => {
191
+ for (const [i, field] of (fields ?? []).entries()) checkField(field, `${path}[${i}]`);
192
+ };
193
+ walk(node.config, "config");
194
+ }
195
+ return problems;
196
+ }
150
197
  function buildManifest(nodes, credentials = [], templates = []) {
151
198
  const manifest = {
152
199
  manifestVersion: MANIFEST_VERSION,
@@ -213,6 +260,8 @@ async function runManifest() {
213
260
  console.warn(`\u26A0 ${w} This will be an error in the next major version.`);
214
261
  }
215
262
  const manifest = buildManifest(mod.NODES, credentials, templates);
263
+ const nameProblems = nodeManifestProblems(manifest.nodes);
264
+ if (nameProblems.length > 0) fail(nameProblems.join("\n"));
216
265
  const outDir = (0, import_node_path2.resolve)(projectRoot, "dist");
217
266
  const outFile = (0, import_node_path2.resolve)(outDir, "manifest.json");
218
267
  fs2.mkdirSync(outDir, { recursive: true });
package/dist/cli.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  buildManifest,
4
+ nodeManifestProblems,
4
5
  outputWarnings,
5
6
  packageIconProblem,
6
7
  parsePackageMeta
7
- } from "./chunk-XO73F62E.js";
8
+ } from "./chunk-CA6JVLLF.js";
8
9
 
9
10
  // src/cli.ts
10
11
  import * as fs2 from "fs";
@@ -112,6 +113,8 @@ async function runManifest() {
112
113
  console.warn(`\u26A0 ${w} This will be an error in the next major version.`);
113
114
  }
114
115
  const manifest = buildManifest(mod.NODES, credentials, templates);
116
+ const nameProblems = nodeManifestProblems(manifest.nodes);
117
+ if (nameProblems.length > 0) fail(nameProblems.join("\n"));
115
118
  const outDir = resolve2(projectRoot, "dist");
116
119
  const outFile = resolve2(outDir, "manifest.json");
117
120
  fs2.mkdirSync(outDir, { recursive: true });
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,11 +44,14 @@ __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,
53
57
  VPN_ERROR_CODES: () => VPN_ERROR_CODES,
@@ -66,6 +70,8 @@ __export(index_exports, {
66
70
  extractCredentialManifests: () => extractCredentialManifests,
67
71
  extractManifest: () => extractManifest,
68
72
  extractManifests: () => extractManifests,
73
+ guardRelaxedForLocalDev: () => guardRelaxedForLocalDev,
74
+ guardedLookup: () => guardedLookup,
69
75
  headerCrossesOrigin: () => headerCrossesOrigin,
70
76
  httpErrorResult: () => httpErrorResult,
71
77
  installAllowedTargets: () => installAllowedTargets,
@@ -77,6 +83,7 @@ __export(index_exports, {
77
83
  isOperator: () => isOperator,
78
84
  isVpnErrorCode: () => isVpnErrorCode,
79
85
  maxBytesConfigField: () => maxBytesConfigField,
86
+ nodeManifestProblems: () => nodeManifestProblems,
80
87
  normalizeCredentialType: () => normalizeCredentialType,
81
88
  normalizeLocalized: () => normalizeLocalized,
82
89
  outputProblems: () => outputProblems,
@@ -525,19 +532,25 @@ function guardRelaxedForLocalDev() {
525
532
  if (relaxed && !bypassNoticeLogged) {
526
533
  bypassNoticeLogged = true;
527
534
  console.warn(
528
- "[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."
529
536
  );
530
537
  }
531
538
  return relaxed;
532
539
  }
540
+ function logBlocked(noun, host, address) {
541
+ console.warn(`[ssrf] blocked ${noun} to ${host}: resolves to private/reserved address ${address}`);
542
+ }
533
543
  function blockedError(host, address) {
534
544
  if (host === address) {
535
545
  return new NodeError("BLOCKED_ADDRESS", `Blocked request to private or reserved address ${address}`, {
536
546
  status: 0
537
547
  });
538
548
  }
539
- console.warn(`[ssrf] blocked request to ${host}: resolves to private/reserved address ${address}`);
540
- 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`, {
541
554
  status: 0
542
555
  });
543
556
  }
@@ -619,6 +632,16 @@ function unreadablePortError(host) {
619
632
  );
620
633
  }
621
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) {
622
645
  return (hostname, options, callback) => {
623
646
  import_node_dns.default.lookup(hostname, { ...options, all: true }, (err, addresses) => {
624
647
  if (err) {
@@ -626,15 +649,18 @@ function judgingLookup(port) {
626
649
  return;
627
650
  }
628
651
  if (addresses.length === 0) {
629
- callback(new NodeError("BLOCKED_ADDRESS", `Could not resolve host: ${hostname}`, { status: 0 }), "");
652
+ callback(emptyAnswer(hostname), "");
630
653
  return;
631
654
  }
632
655
  if (!guardRelaxedForLocalDev()) {
656
+ const host = hostname;
633
657
  for (const a of addresses) {
634
- if (isBlockedAddress(a.address) && !isAllowedTarget(a.address, port)) {
635
- callback(blockedError(normalizeHost(hostname), a.address), "");
636
- return;
637
- }
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;
638
664
  }
639
665
  }
640
666
  if (options.all) callback(null, addresses);
@@ -820,34 +846,84 @@ function credentialOriginRefusal(target, bound, hop) {
820
846
  { status: 0 }
821
847
  );
822
848
  }
823
- async function timedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
849
+ function attemptDeadline(ctxSignal, effectiveMs) {
824
850
  const ac = new AbortController();
825
- const timer = setTimeout(() => ac.abort(), effectiveMs);
826
- 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;
827
898
  try {
828
- return await fetch(url, { ...fetchOptions, signal, dispatcher: guardedDispatcher() });
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) {
912
+ try {
913
+ return await fetch(url, { ...fetchOptions, signal: deadline.signal, dispatcher: guardedDispatcher() });
829
914
  } catch (err) {
830
915
  const blocked = connectionRefusal(err);
831
916
  if (blocked) throw blocked;
832
- if (ctxSignal?.aborted) throw ctxSignal.reason;
833
- if (ac.signal.aborted) throw new NodeError("TIMEOUT", `Request timed out after ${effectiveMs}ms`);
834
- throw err;
835
- } finally {
836
- clearTimeout(timer);
917
+ throw attemptFailure(err, ctxSignal, deadline);
837
918
  }
838
919
  }
839
- async function guardUrl(url, ctxSignal, effectiveMs) {
840
- const ac = new AbortController();
841
- const timer = setTimeout(() => ac.abort(), effectiveMs);
842
- const signal = ctxSignal ? AbortSignal.any([ctxSignal, ac.signal]) : ac.signal;
920
+ async function guardUrl(url, ctxSignal, deadline, effectiveMs) {
843
921
  try {
844
- await assertPublicUrl(url, { signal });
922
+ await assertPublicUrl(url, { signal: deadline.signal });
845
923
  } catch (err) {
846
924
  if (ctxSignal?.aborted) throw ctxSignal.reason;
847
- 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`);
848
926
  throw err;
849
- } finally {
850
- clearTimeout(timer);
851
927
  }
852
928
  }
853
929
  function hasContent(body) {
@@ -869,16 +945,27 @@ function refuseUserinfo(url, status = 0) {
869
945
  }
870
946
  }
871
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) {
872
959
  let currentUrl = url;
873
960
  let currentInit = { ...fetchOptions, redirect: "manual" };
874
- await guardUrl(currentUrl, ctxSignal, effectiveMs);
961
+ await guardUrl(currentUrl, ctxSignal, deadline, effectiveMs);
875
962
  for (let hop = 0; ; hop++) {
876
963
  const release = guardConnectionsTo(
877
964
  currentUrl instanceof URL ? currentUrl : new URL(currentUrl)
878
965
  );
879
966
  let res;
880
967
  try {
881
- res = await timedFetch(currentUrl, currentInit, ctxSignal, effectiveMs);
968
+ res = await timedFetch(currentUrl, currentInit, ctxSignal, deadline);
882
969
  } finally {
883
970
  release();
884
971
  }
@@ -920,7 +1007,7 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding)
920
1007
  { status: 0 }
921
1008
  );
922
1009
  }
923
- await guardUrl(nextUrl, ctxSignal, effectiveMs);
1010
+ await guardUrl(nextUrl, ctxSignal, deadline, effectiveMs);
924
1011
  if (binding && nextUrl.origin !== binding.origin) {
925
1012
  for (const name of [...headers.keys()]) headers.delete(name);
926
1013
  } else if (nextUrl.origin !== base.origin) {
@@ -1068,8 +1155,8 @@ function timeoutConfigField(opts) {
1068
1155
  key: "timeoutMs",
1069
1156
  label: { en: "Timeout (ms)", de: "Zeitlimit (ms)" },
1070
1157
  description: {
1071
- 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.",
1072
- 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."
1073
1160
  },
1074
1161
  type: "number",
1075
1162
  default: opts?.default ?? DEFAULT_TIMEOUT_MS,
@@ -1192,6 +1279,53 @@ function parsePackageMeta(raw) {
1192
1279
  icon: icon !== "" ? icon : void 0
1193
1280
  };
1194
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
+ }
1195
1329
  function buildManifest(nodes, credentials = [], templates = []) {
1196
1330
  const manifest = {
1197
1331
  manifestVersion: MANIFEST_VERSION,
@@ -1476,6 +1610,7 @@ function base64Url(buf) {
1476
1610
  ApiKeyCredential,
1477
1611
  BaseCredential,
1478
1612
  BasicAuthCredential,
1613
+ CONFIG_KEY_PATTERN,
1479
1614
  DEFAULT_MAX_RESPONSE_BYTES,
1480
1615
  DEFAULT_RETRY_ATTEMPTS,
1481
1616
  DEFAULT_RETRY_DELAY_MS,
@@ -1486,11 +1621,14 @@ function base64Url(buf) {
1486
1621
  MAX_RESPONSE_BYTES,
1487
1622
  MAX_RETRY_ATTEMPTS,
1488
1623
  MAX_TIMEOUT_MS,
1624
+ NAMESPACED_SLUG_PATTERN,
1489
1625
  NodeError,
1490
1626
  OAuth2AuthCodeCredential,
1491
1627
  OAuth2ClientCredentialsCredential,
1492
1628
  OPERATORS,
1493
1629
  RetryableError,
1630
+ SEMVER_PATTERN,
1631
+ SLUG_PATTERN,
1494
1632
  SimpleValueCredential,
1495
1633
  VALUELESS_OPERATORS,
1496
1634
  VPN_ERROR_CODES,
@@ -1509,6 +1647,8 @@ function base64Url(buf) {
1509
1647
  extractCredentialManifests,
1510
1648
  extractManifest,
1511
1649
  extractManifests,
1650
+ guardRelaxedForLocalDev,
1651
+ guardedLookup,
1512
1652
  headerCrossesOrigin,
1513
1653
  httpErrorResult,
1514
1654
  installAllowedTargets,
@@ -1520,6 +1660,7 @@ function base64Url(buf) {
1520
1660
  isOperator,
1521
1661
  isVpnErrorCode,
1522
1662
  maxBytesConfigField,
1663
+ nodeManifestProblems,
1523
1664
  normalizeCredentialType,
1524
1665
  normalizeLocalized,
1525
1666
  outputProblems,
package/dist/index.d.ts CHANGED
@@ -8,8 +8,8 @@ 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
13
  export { allowedTargets, installAllowedTargets, isAllowedTarget } from './allowedTargets.js';
14
14
  export * from './retry.js';
15
15
  export * from './manifest.js';
package/dist/index.js CHANGED
@@ -1,5 +1,9 @@
1
1
  import {
2
+ CONFIG_KEY_PATTERN,
2
3
  MANIFEST_VERSION,
4
+ NAMESPACED_SLUG_PATTERN,
5
+ SEMVER_PATTERN,
6
+ SLUG_PATTERN,
3
7
  assertBranchNamed,
4
8
  buildManifest,
5
9
  extractCredentialManifest,
@@ -7,11 +11,12 @@ import {
7
11
  extractManifest,
8
12
  extractManifests,
9
13
  isMultiPort,
14
+ nodeManifestProblems,
10
15
  outputProblems,
11
16
  outputWarnings,
12
17
  packageIconProblem,
13
18
  parsePackageMeta
14
- } from "./chunk-XO73F62E.js";
19
+ } from "./chunk-CA6JVLLF.js";
15
20
 
16
21
  // src/types.ts
17
22
  function isNodeWithIteration(node) {
@@ -367,19 +372,25 @@ function guardRelaxedForLocalDev() {
367
372
  if (relaxed && !bypassNoticeLogged) {
368
373
  bypassNoticeLogged = true;
369
374
  console.warn(
370
- "[ssrf] RVNXX_SSRF_ALLOW_PRIVATE is set: allowing private/loopback fetch targets. Intended for local development only."
375
+ "[ssrf] RVNXX_SSRF_ALLOW_PRIVATE is set: allowing private/loopback targets for web requests and connections. Intended for local development only."
371
376
  );
372
377
  }
373
378
  return relaxed;
374
379
  }
380
+ function logBlocked(noun, host, address) {
381
+ console.warn(`[ssrf] blocked ${noun} to ${host}: resolves to private/reserved address ${address}`);
382
+ }
375
383
  function blockedError(host, address) {
376
384
  if (host === address) {
377
385
  return new NodeError("BLOCKED_ADDRESS", `Blocked request to private or reserved address ${address}`, {
378
386
  status: 0
379
387
  });
380
388
  }
381
- console.warn(`[ssrf] blocked request to ${host}: resolves to private/reserved address ${address}`);
382
- return new NodeError("BLOCKED_ADDRESS", `Blocked request to ${host}: resolves to a private or reserved address`, {
389
+ logBlocked("request", host, address);
390
+ return refusedName(host);
391
+ }
392
+ function refusedName(host, noun = "request") {
393
+ return new NodeError("BLOCKED_ADDRESS", `Blocked ${noun} to ${host}: resolves to a private or reserved address`, {
383
394
  status: 0
384
395
  });
385
396
  }
@@ -461,6 +472,16 @@ function unreadablePortError(host) {
461
472
  );
462
473
  }
463
474
  function judgingLookup(port) {
475
+ return lookupJudgedFor(port, { noun: "request" }, (hostname) => new NodeError("BLOCKED_ADDRESS", `Could not resolve host: ${hostname}`, { status: 0 }));
476
+ }
477
+ function guardedLookup(port, options = {}) {
478
+ return lookupJudgedFor(
479
+ port,
480
+ options,
481
+ (hostname) => Object.assign(new Error(`getaddrinfo ENOTFOUND ${hostname}`), { code: "ENOTFOUND", hostname })
482
+ );
483
+ }
484
+ function lookupJudgedFor(port, { refusal, unroutable, noun = "connection" }, emptyAnswer) {
464
485
  return (hostname, options, callback) => {
465
486
  dns.lookup(hostname, { ...options, all: true }, (err, addresses) => {
466
487
  if (err) {
@@ -468,15 +489,18 @@ function judgingLookup(port) {
468
489
  return;
469
490
  }
470
491
  if (addresses.length === 0) {
471
- callback(new NodeError("BLOCKED_ADDRESS", `Could not resolve host: ${hostname}`, { status: 0 }), "");
492
+ callback(emptyAnswer(hostname), "");
472
493
  return;
473
494
  }
474
495
  if (!guardRelaxedForLocalDev()) {
496
+ const host = hostname;
475
497
  for (const a of addresses) {
476
- if (isBlockedAddress(a.address) && !isAllowedTarget(a.address, port)) {
477
- callback(blockedError(normalizeHost(hostname), a.address), "");
478
- return;
479
- }
498
+ if (!isBlockedAddress(a.address)) continue;
499
+ const allowed = isAllowedTarget(a.address, port);
500
+ if (allowed && !unroutable) continue;
501
+ logBlocked(noun, host, a.address);
502
+ callback(allowed && unroutable ? unroutable(host) : refusal ? refusal(host) : refusedName(host, noun), "");
503
+ return;
480
504
  }
481
505
  }
482
506
  if (options.all) callback(null, addresses);
@@ -662,34 +686,84 @@ function credentialOriginRefusal(target, bound, hop) {
662
686
  { status: 0 }
663
687
  );
664
688
  }
665
- async function timedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
689
+ function attemptDeadline(ctxSignal, effectiveMs) {
666
690
  const ac = new AbortController();
667
- const timer = setTimeout(() => ac.abort(), effectiveMs);
668
- const signal = ctxSignal ? AbortSignal.any([ctxSignal, ac.signal]) : ac.signal;
691
+ let timer;
692
+ const arm = (message) => {
693
+ clearTimeout(timer);
694
+ timer = setTimeout(() => ac.abort(new NodeError("TIMEOUT", message)), effectiveMs);
695
+ };
696
+ arm(`Request timed out after ${effectiveMs}ms`);
697
+ return {
698
+ signal: ctxSignal ? AbortSignal.any([ctxSignal, ac.signal]) : ac.signal,
699
+ expired: () => ac.signal.aborted,
700
+ reason: () => ac.signal.reason,
701
+ clear: () => clearTimeout(timer),
702
+ awaitChunk: () => arm(`The answer sent nothing for ${effectiveMs}ms`)
703
+ };
704
+ }
705
+ function attemptFailure(err, ctxSignal, deadline) {
706
+ if (ctxSignal?.aborted) return ctxSignal.reason;
707
+ if (deadline.expired()) return deadline.reason();
708
+ return err;
709
+ }
710
+ function answerUnderDeadline(res, ctxSignal, deadline) {
711
+ if (!res.body) return res;
712
+ let reader;
713
+ const body = new ReadableStream(
714
+ {
715
+ async pull(controller) {
716
+ reader ??= res.body.getReader();
717
+ deadline.awaitChunk();
718
+ try {
719
+ const { done, value } = await reader.read();
720
+ if (done) controller.close();
721
+ else controller.enqueue(value);
722
+ } catch (err) {
723
+ controller.error(attemptFailure(err, ctxSignal, deadline));
724
+ } finally {
725
+ deadline.clear();
726
+ }
727
+ },
728
+ cancel(reason) {
729
+ deadline.clear();
730
+ return reader ? reader.cancel(reason) : res.body?.cancel(reason);
731
+ }
732
+ },
733
+ // Read nothing ahead of the caller: the limit times the caller's waits, not
734
+ // a buffer filling while nobody reads.
735
+ { highWaterMark: 0 }
736
+ );
737
+ let answer;
669
738
  try {
670
- return await fetch(url, { ...fetchOptions, signal, dispatcher: guardedDispatcher() });
739
+ answer = new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers });
740
+ } catch {
741
+ return res;
742
+ }
743
+ return withUrl(answer, res.url);
744
+ }
745
+ function withUrl(answer, url) {
746
+ const clone = answer.clone.bind(answer);
747
+ Object.defineProperty(answer, "url", { value: url });
748
+ Object.defineProperty(answer, "clone", { value: () => withUrl(clone(), url) });
749
+ return answer;
750
+ }
751
+ async function timedFetch(url, fetchOptions, ctxSignal, deadline) {
752
+ try {
753
+ return await fetch(url, { ...fetchOptions, signal: deadline.signal, dispatcher: guardedDispatcher() });
671
754
  } catch (err) {
672
755
  const blocked = connectionRefusal(err);
673
756
  if (blocked) throw blocked;
674
- if (ctxSignal?.aborted) throw ctxSignal.reason;
675
- if (ac.signal.aborted) throw new NodeError("TIMEOUT", `Request timed out after ${effectiveMs}ms`);
676
- throw err;
677
- } finally {
678
- clearTimeout(timer);
757
+ throw attemptFailure(err, ctxSignal, deadline);
679
758
  }
680
759
  }
681
- async function guardUrl(url, ctxSignal, effectiveMs) {
682
- const ac = new AbortController();
683
- const timer = setTimeout(() => ac.abort(), effectiveMs);
684
- const signal = ctxSignal ? AbortSignal.any([ctxSignal, ac.signal]) : ac.signal;
760
+ async function guardUrl(url, ctxSignal, deadline, effectiveMs) {
685
761
  try {
686
- await assertPublicUrl(url, { signal });
762
+ await assertPublicUrl(url, { signal: deadline.signal });
687
763
  } catch (err) {
688
764
  if (ctxSignal?.aborted) throw ctxSignal.reason;
689
- if (ac.signal.aborted) throw new NodeError("TIMEOUT", `DNS resolution timed out after ${effectiveMs}ms`);
765
+ if (deadline.expired()) throw new NodeError("TIMEOUT", `DNS resolution timed out after ${effectiveMs}ms`);
690
766
  throw err;
691
- } finally {
692
- clearTimeout(timer);
693
767
  }
694
768
  }
695
769
  function hasContent(body) {
@@ -711,16 +785,27 @@ function refuseUserinfo(url, status = 0) {
711
785
  }
712
786
  }
713
787
  async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding) {
788
+ const deadline = attemptDeadline(ctxSignal, effectiveMs);
789
+ try {
790
+ const res = await followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, binding);
791
+ deadline.clear();
792
+ return answerUnderDeadline(res, ctxSignal, deadline);
793
+ } catch (err) {
794
+ deadline.clear();
795
+ throw err;
796
+ }
797
+ }
798
+ async function followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, binding) {
714
799
  let currentUrl = url;
715
800
  let currentInit = { ...fetchOptions, redirect: "manual" };
716
- await guardUrl(currentUrl, ctxSignal, effectiveMs);
801
+ await guardUrl(currentUrl, ctxSignal, deadline, effectiveMs);
717
802
  for (let hop = 0; ; hop++) {
718
803
  const release = guardConnectionsTo(
719
804
  currentUrl instanceof URL ? currentUrl : new URL(currentUrl)
720
805
  );
721
806
  let res;
722
807
  try {
723
- res = await timedFetch(currentUrl, currentInit, ctxSignal, effectiveMs);
808
+ res = await timedFetch(currentUrl, currentInit, ctxSignal, deadline);
724
809
  } finally {
725
810
  release();
726
811
  }
@@ -762,7 +847,7 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding)
762
847
  { status: 0 }
763
848
  );
764
849
  }
765
- await guardUrl(nextUrl, ctxSignal, effectiveMs);
850
+ await guardUrl(nextUrl, ctxSignal, deadline, effectiveMs);
766
851
  if (binding && nextUrl.origin !== binding.origin) {
767
852
  for (const name of [...headers.keys()]) headers.delete(name);
768
853
  } else if (nextUrl.origin !== base.origin) {
@@ -910,8 +995,8 @@ function timeoutConfigField(opts) {
910
995
  key: "timeoutMs",
911
996
  label: { en: "Timeout (ms)", de: "Zeitlimit (ms)" },
912
997
  description: {
913
- 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.",
914
- 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."
998
+ 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.",
999
+ 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."
915
1000
  },
916
1001
  type: "number",
917
1002
  default: opts?.default ?? DEFAULT_TIMEOUT_MS,
@@ -1282,6 +1367,7 @@ export {
1282
1367
  ApiKeyCredential,
1283
1368
  BaseCredential,
1284
1369
  BasicAuthCredential,
1370
+ CONFIG_KEY_PATTERN,
1285
1371
  DEFAULT_MAX_RESPONSE_BYTES,
1286
1372
  DEFAULT_RETRY_ATTEMPTS,
1287
1373
  DEFAULT_RETRY_DELAY_MS,
@@ -1292,11 +1378,14 @@ export {
1292
1378
  MAX_RESPONSE_BYTES,
1293
1379
  MAX_RETRY_ATTEMPTS,
1294
1380
  MAX_TIMEOUT_MS,
1381
+ NAMESPACED_SLUG_PATTERN,
1295
1382
  NodeError,
1296
1383
  OAuth2AuthCodeCredential,
1297
1384
  OAuth2ClientCredentialsCredential,
1298
1385
  OPERATORS,
1299
1386
  RetryableError,
1387
+ SEMVER_PATTERN,
1388
+ SLUG_PATTERN,
1300
1389
  SimpleValueCredential,
1301
1390
  VALUELESS_OPERATORS,
1302
1391
  VPN_ERROR_CODES,
@@ -1315,6 +1404,8 @@ export {
1315
1404
  extractCredentialManifests,
1316
1405
  extractManifest,
1317
1406
  extractManifests,
1407
+ guardRelaxedForLocalDev,
1408
+ guardedLookup,
1318
1409
  headerCrossesOrigin,
1319
1410
  httpErrorResult,
1320
1411
  installAllowedTargets,
@@ -1326,6 +1417,7 @@ export {
1326
1417
  isOperator,
1327
1418
  isVpnErrorCode,
1328
1419
  maxBytesConfigField,
1420
+ nodeManifestProblems,
1329
1421
  normalizeCredentialType,
1330
1422
  normalizeLocalized,
1331
1423
  outputProblems,
@@ -73,6 +73,31 @@ export declare function packageIconProblem(icon: string | undefined): string | u
73
73
  * enforces that on upload; this is a typed, lenient read for tooling.
74
74
  */
75
75
  export declare function parsePackageMeta(raw: unknown): NodePackageMeta;
76
+ /**
77
+ * The name patterns the registry enforces on a node's manifest when a package is
78
+ * uploaded (PO-744), as written in the integrations service's node schema
79
+ * (`app/Support/Schema/Versions/Node/V0DraftSchema.php`, read at `973b28c2`, 27
80
+ * October 2026), under the same names. A copy, not a shared source: a change on
81
+ * either side has to be made on both. They are exported as the schema's text, so
82
+ * a test on the other side can compare them byte for byte.
83
+ */
84
+ /** A port name, `sourceFromConfig` and `fallback.name`: lowercase kebab-case. */
85
+ export declare const SLUG_PATTERN = "^[a-z][a-z0-9-]{0,62}$";
86
+ /** A node's `slug`, and a credential type a setting names: `<namespace>:<slug>`. */
87
+ export declare const NAMESPACED_SLUG_PATTERN = "^[a-z][a-z0-9-]{0,62}:[a-z][a-z0-9-]{0,62}$";
88
+ /** A node's `version`. */
89
+ export declare const SEMVER_PATTERN = "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$";
90
+ /** A setting's `key`, `dependsOn[]` and `showIf.key`: camelCase is allowed here, unlike in a port name. */
91
+ export declare const CONFIG_KEY_PATTERN = "^[a-zA-Z][a-zA-Z0-9-]*$";
92
+ /**
93
+ * Why the registry would refuse these nodes' names on upload, one line per value,
94
+ * or `[]` when it would accept them (PO-744). Each line names the node, the path
95
+ * of the field, the value and the pattern. Only values that are present are
96
+ * judged: a missing slug or version is the schema's other rule, and a port may
97
+ * leave its name out. Settings are walked into `properties` and `items` at any
98
+ * depth; the schema allows less, so none is missed.
99
+ */
100
+ export declare function nodeManifestProblems(nodes: readonly INodeDescription[]): string[];
76
101
  /**
77
102
  * Builds the manifest envelope from a package's `NODES` (and optional
78
103
  * `CREDENTIALS` / `TEMPLATES`) exports. The result is what gets written to
package/dist/ssrf.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type LookupFunction } from 'node:net';
1
2
  import { Agent } from 'undici';
2
3
  import { NodeError } from './errors.js';
3
4
  /**
@@ -40,6 +41,44 @@ export declare const ssrfResolver: {
40
41
  * Promised behaviour: specs/ssrf-guard.md (AC-9, AC-11, AC-19).
41
42
  */
42
43
  export declare function isBlockedAddress(ip: string): boolean;
44
+ export declare function guardRelaxedForLocalDev(): boolean;
45
+ /** How a connection that `safeFetch` does not make reports what {@link guardedLookup} decides. */
46
+ export interface GuardedLookupOptions {
47
+ /**
48
+ * The error a refused name fails with. It is handed to the caller and may reach
49
+ * a workflow author, so it names the host and never an address it resolved to.
50
+ * Default: `Blocked <noun> to <host>: resolves to a private or reserved
51
+ * address`, code `BLOCKED_ADDRESS` — `safeFetch`'s words with this lookup's noun.
52
+ */
53
+ refusal?: (host: string) => Error;
54
+ /**
55
+ * For a client a VPN route cannot carry: a private address on the run's
56
+ * allow-list is refused with this error instead of passing. Absent, the
57
+ * allow-list applies as it does to `safeFetch`.
58
+ */
59
+ unroutable?: (host: string) => Error;
60
+ /** The noun of the operator's log line, `[ssrf] blocked <noun> to …`, and of the default refusal. Default `'connection'`. */
61
+ noun?: 'request' | 'connection';
62
+ }
63
+ /**
64
+ * A socket lookup, shaped like `net.LookupFunction`, that judges every address a
65
+ * name resolves to by the rule `safeFetch` connects under (PO-698) — for a client
66
+ * that opens its own socket (FTP, SFTP, a plain `node:http` stream) and so never
67
+ * meets `safeFetch`. Pass it as the socket's `lookup` with the port it connects to.
68
+ *
69
+ * The check runs inside the socket's own name resolution, so the socket can only
70
+ * reach addresses that were judged, with no second lookup a changed DNS answer
71
+ * could slip through, and Node still tries every approved address in turn. A
72
+ * name is refused when any address is private or reserved and not on the run's
73
+ * allow-list for `port` (PO-828); the refusal is logged for the operator; the
74
+ * local-development switch ({@link guardRelaxedForLocalDev}) relaxes it. A name
75
+ * that does not resolve fails as the resolver said, not as a refusal.
76
+ *
77
+ * **Node calls no lookup for a host written as an address**, so a literal never
78
+ * reaches this. A caller judges a literal itself, with `isBlockedAddress` and
79
+ * `isAllowedTarget`, before it opens the socket.
80
+ */
81
+ export declare function guardedLookup(port: number, options?: GuardedLookupOptions): LookupFunction;
43
82
  /**
44
83
  * The dispatcher `safeFetch` hands every request to. One per process, so
45
84
  * keep-alive survives across calls; its pool holds only connections a `safeFetch`
package/dist/types.d.ts CHANGED
@@ -72,6 +72,12 @@ export interface IOutputField {
72
72
  export interface IOutputPort {
73
73
  kind: OutputKind;
74
74
  dataType: DataType;
75
+ /**
76
+ * The port's name, as a branch and an edge refer to it. Lowercase kebab-case —
77
+ * `not-found`, never `notFound` — because the registry refuses anything else
78
+ * on upload and `rvnxx-nodes manifest` stops on it (`SLUG_PATTERN`). A setting's
79
+ * `key` may be camelCase; a port name may not.
80
+ */
75
81
  name?: string;
76
82
  label?: LocalizedString;
77
83
  description?: LocalizedString;
@@ -95,6 +101,7 @@ export interface IOutputPort {
95
101
  */
96
102
  resolveOutputs?: boolean;
97
103
  fallback?: {
104
+ /** Lowercase kebab-case, as {@link IOutputPort.name}. */
98
105
  name: string;
99
106
  label?: LocalizedString;
100
107
  description?: LocalizedString;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/integrations-node-sdk",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "TypeScript interfaces and utilities for Revenexx integration nodes",
5
5
  "license": "MIT",
6
6
  "author": "revenexx GmbH",