@revenexx/integrations-node-sdk 1.13.0 → 1.15.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/{chunk-XO73F62E.js → chunk-CA6JVLLF.js} +52 -0
- package/dist/cli.cjs +49 -0
- package/dist/cli.js +4 -1
- package/dist/fetch.d.ts +22 -0
- package/dist/index.cjs +235 -32
- package/dist/index.d.ts +2 -2
- package/dist/index.js +186 -33
- package/dist/manifest.d.ts +25 -0
- package/dist/ssrf.d.ts +39 -0
- package/dist/types.d.ts +7 -0
- package/package.json +1 -1
|
@@ -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-
|
|
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/fetch.d.ts
CHANGED
|
@@ -65,6 +65,28 @@ export interface SafeFetchOptions extends RequestInit {
|
|
|
65
65
|
/** Bind the request to the origin of the credential it carries. */
|
|
66
66
|
credential?: SafeFetchCredential;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether the request behind a failed {@link safeFetch} call may have reached
|
|
70
|
+
* the host — `false` only when the call is known to have put nothing on the wire
|
|
71
|
+
* on any hop of any attempt, and `true` for every other error, an unknown one
|
|
72
|
+
* included. A node that writes reads it to decide whether a create may be
|
|
73
|
+
* repeated: when this is `false` nothing was sent, so nothing can have been
|
|
74
|
+
* created.
|
|
75
|
+
*
|
|
76
|
+
* It reads `meta.sent === false`, which `safeFetch` puts on what it throws in
|
|
77
|
+
* that case: a refused address — before the lookup or at connect time — a lookup
|
|
78
|
+
* that ran out of time, a header or URL that cannot be sent, a credential bound
|
|
79
|
+
* to another origin, and a connection that was refused, could not be resolved or
|
|
80
|
+
* reached, or timed out while connecting. The last group arrives as the runtime's
|
|
81
|
+
* own `TypeError('fetch failed')`, unwrapped as before, with the marker added. A
|
|
82
|
+
* cancellation is the caller's own reason and is never marked.
|
|
83
|
+
*/
|
|
84
|
+
export declare function mayHaveBeenSent(err: unknown): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Make a request through the SSRF guard, under a time budget and an optional
|
|
87
|
+
* retry budget. A failure that left nothing on the wire says so with
|
|
88
|
+
* `meta.sent = false` — read it through {@link mayHaveBeenSent}.
|
|
89
|
+
*/
|
|
68
90
|
export declare function safeFetch(url: string | URL, options?: SafeFetchOptions): Promise<Response>;
|
|
69
91
|
/**
|
|
70
92
|
* Read a response body into an ArrayBuffer while enforcing a hard byte cap.
|
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,8 @@ __export(index_exports, {
|
|
|
77
83
|
isOperator: () => isOperator,
|
|
78
84
|
isVpnErrorCode: () => isVpnErrorCode,
|
|
79
85
|
maxBytesConfigField: () => maxBytesConfigField,
|
|
86
|
+
mayHaveBeenSent: () => mayHaveBeenSent,
|
|
87
|
+
nodeManifestProblems: () => nodeManifestProblems,
|
|
80
88
|
normalizeCredentialType: () => normalizeCredentialType,
|
|
81
89
|
normalizeLocalized: () => normalizeLocalized,
|
|
82
90
|
outputProblems: () => outputProblems,
|
|
@@ -370,6 +378,9 @@ function assertBranchNamed(node, result, config) {
|
|
|
370
378
|
}
|
|
371
379
|
}
|
|
372
380
|
|
|
381
|
+
// src/fetch.ts
|
|
382
|
+
var import_undici2 = require("undici");
|
|
383
|
+
|
|
373
384
|
// src/ssrf.ts
|
|
374
385
|
var import_node_diagnostics_channel = require("diagnostics_channel");
|
|
375
386
|
var import_node_dns = __toESM(require("dns"), 1);
|
|
@@ -525,19 +536,25 @@ function guardRelaxedForLocalDev() {
|
|
|
525
536
|
if (relaxed && !bypassNoticeLogged) {
|
|
526
537
|
bypassNoticeLogged = true;
|
|
527
538
|
console.warn(
|
|
528
|
-
"[ssrf] RVNXX_SSRF_ALLOW_PRIVATE is set: allowing private/loopback
|
|
539
|
+
"[ssrf] RVNXX_SSRF_ALLOW_PRIVATE is set: allowing private/loopback targets for web requests and connections. Intended for local development only."
|
|
529
540
|
);
|
|
530
541
|
}
|
|
531
542
|
return relaxed;
|
|
532
543
|
}
|
|
544
|
+
function logBlocked(noun, host, address) {
|
|
545
|
+
console.warn(`[ssrf] blocked ${noun} to ${host}: resolves to private/reserved address ${address}`);
|
|
546
|
+
}
|
|
533
547
|
function blockedError(host, address) {
|
|
534
548
|
if (host === address) {
|
|
535
549
|
return new NodeError("BLOCKED_ADDRESS", `Blocked request to private or reserved address ${address}`, {
|
|
536
550
|
status: 0
|
|
537
551
|
});
|
|
538
552
|
}
|
|
539
|
-
|
|
540
|
-
return
|
|
553
|
+
logBlocked("request", host, address);
|
|
554
|
+
return refusedName(host);
|
|
555
|
+
}
|
|
556
|
+
function refusedName(host, noun = "request") {
|
|
557
|
+
return new NodeError("BLOCKED_ADDRESS", `Blocked ${noun} to ${host}: resolves to a private or reserved address`, {
|
|
541
558
|
status: 0
|
|
542
559
|
});
|
|
543
560
|
}
|
|
@@ -619,6 +636,16 @@ function unreadablePortError(host) {
|
|
|
619
636
|
);
|
|
620
637
|
}
|
|
621
638
|
function judgingLookup(port) {
|
|
639
|
+
return lookupJudgedFor(port, { noun: "request" }, (hostname) => new NodeError("BLOCKED_ADDRESS", `Could not resolve host: ${hostname}`, { status: 0 }));
|
|
640
|
+
}
|
|
641
|
+
function guardedLookup(port, options = {}) {
|
|
642
|
+
return lookupJudgedFor(
|
|
643
|
+
port,
|
|
644
|
+
options,
|
|
645
|
+
(hostname) => Object.assign(new Error(`getaddrinfo ENOTFOUND ${hostname}`), { code: "ENOTFOUND", hostname })
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
function lookupJudgedFor(port, { refusal, unroutable, noun = "connection" }, emptyAnswer) {
|
|
622
649
|
return (hostname, options, callback) => {
|
|
623
650
|
import_node_dns.default.lookup(hostname, { ...options, all: true }, (err, addresses) => {
|
|
624
651
|
if (err) {
|
|
@@ -626,15 +653,18 @@ function judgingLookup(port) {
|
|
|
626
653
|
return;
|
|
627
654
|
}
|
|
628
655
|
if (addresses.length === 0) {
|
|
629
|
-
callback(
|
|
656
|
+
callback(emptyAnswer(hostname), "");
|
|
630
657
|
return;
|
|
631
658
|
}
|
|
632
659
|
if (!guardRelaxedForLocalDev()) {
|
|
660
|
+
const host = hostname;
|
|
633
661
|
for (const a of addresses) {
|
|
634
|
-
if (isBlockedAddress(a.address)
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
662
|
+
if (!isBlockedAddress(a.address)) continue;
|
|
663
|
+
const allowed = isAllowedTarget(a.address, port);
|
|
664
|
+
if (allowed && !unroutable) continue;
|
|
665
|
+
logBlocked(noun, host, a.address);
|
|
666
|
+
callback(allowed && unroutable ? unroutable(host) : refusal ? refusal(host) : refusedName(host, noun), "");
|
|
667
|
+
return;
|
|
638
668
|
}
|
|
639
669
|
}
|
|
640
670
|
if (options.all) callback(null, addresses);
|
|
@@ -820,34 +850,132 @@ function credentialOriginRefusal(target, bound, hop) {
|
|
|
820
850
|
{ status: 0 }
|
|
821
851
|
);
|
|
822
852
|
}
|
|
823
|
-
|
|
853
|
+
function attemptDeadline(ctxSignal, effectiveMs) {
|
|
824
854
|
const ac = new AbortController();
|
|
825
|
-
|
|
826
|
-
const
|
|
855
|
+
let timer;
|
|
856
|
+
const arm = (message) => {
|
|
857
|
+
clearTimeout(timer);
|
|
858
|
+
timer = setTimeout(() => ac.abort(new NodeError("TIMEOUT", message)), effectiveMs);
|
|
859
|
+
};
|
|
860
|
+
arm(`Request timed out after ${effectiveMs}ms`);
|
|
861
|
+
return {
|
|
862
|
+
signal: ctxSignal ? AbortSignal.any([ctxSignal, ac.signal]) : ac.signal,
|
|
863
|
+
expired: () => ac.signal.aborted,
|
|
864
|
+
reason: () => ac.signal.reason,
|
|
865
|
+
clear: () => clearTimeout(timer),
|
|
866
|
+
awaitChunk: () => arm(`The answer sent nothing for ${effectiveMs}ms`)
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
function attemptFailure(err, ctxSignal, deadline) {
|
|
870
|
+
if (ctxSignal?.aborted) return ctxSignal.reason;
|
|
871
|
+
if (deadline.expired()) return deadline.reason();
|
|
872
|
+
return err;
|
|
873
|
+
}
|
|
874
|
+
function answerUnderDeadline(res, ctxSignal, deadline) {
|
|
875
|
+
if (!res.body) return rebuilt(res, null) ?? asRuntimeResponse(res);
|
|
876
|
+
let reader;
|
|
877
|
+
const body = new ReadableStream(
|
|
878
|
+
{
|
|
879
|
+
async pull(controller) {
|
|
880
|
+
reader ??= res.body.getReader();
|
|
881
|
+
deadline.awaitChunk();
|
|
882
|
+
try {
|
|
883
|
+
const { done, value } = await reader.read();
|
|
884
|
+
if (done) controller.close();
|
|
885
|
+
else controller.enqueue(value);
|
|
886
|
+
} catch (err) {
|
|
887
|
+
controller.error(attemptFailure(err, ctxSignal, deadline));
|
|
888
|
+
} finally {
|
|
889
|
+
deadline.clear();
|
|
890
|
+
}
|
|
891
|
+
},
|
|
892
|
+
cancel(reason) {
|
|
893
|
+
deadline.clear();
|
|
894
|
+
return reader ? reader.cancel(reason) : res.body?.cancel(reason);
|
|
895
|
+
}
|
|
896
|
+
},
|
|
897
|
+
// Read nothing ahead of the caller: the limit times the caller's waits, not
|
|
898
|
+
// a buffer filling while nobody reads.
|
|
899
|
+
{ highWaterMark: 0 }
|
|
900
|
+
);
|
|
901
|
+
return rebuilt(res, body) ?? asRuntimeResponse(res);
|
|
902
|
+
}
|
|
903
|
+
function rebuilt(res, body) {
|
|
904
|
+
try {
|
|
905
|
+
return withUrl(new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers }), res.url);
|
|
906
|
+
} catch {
|
|
907
|
+
return void 0;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function asRuntimeResponse(res) {
|
|
911
|
+
return withUrl(withStatusLine(new Response(res.body, { headers: res.headers }), res), res.url);
|
|
912
|
+
}
|
|
913
|
+
function withStatusLine(answer, from) {
|
|
914
|
+
const clone = answer.clone.bind(answer);
|
|
915
|
+
Object.defineProperties(answer, {
|
|
916
|
+
status: { value: from.status },
|
|
917
|
+
statusText: { value: from.statusText },
|
|
918
|
+
ok: { value: from.ok },
|
|
919
|
+
clone: { value: () => withStatusLine(clone(), from), configurable: true }
|
|
920
|
+
});
|
|
921
|
+
return answer;
|
|
922
|
+
}
|
|
923
|
+
function withUrl(answer, url) {
|
|
924
|
+
const clone = answer.clone.bind(answer);
|
|
925
|
+
Object.defineProperty(answer, "url", { value: url });
|
|
926
|
+
Object.defineProperty(answer, "clone", { value: () => withUrl(clone(), url) });
|
|
927
|
+
return answer;
|
|
928
|
+
}
|
|
929
|
+
var runtimeFetch = globalThis.fetch;
|
|
930
|
+
function transport() {
|
|
931
|
+
const current = globalThis.fetch;
|
|
932
|
+
return current === runtimeFetch ? import_undici2.fetch : current;
|
|
933
|
+
}
|
|
934
|
+
var NEVER_CONNECTED = /* @__PURE__ */ new Set(["ENOTFOUND", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT"]);
|
|
935
|
+
var REFUSED_WHILE_CONNECTING = /* @__PURE__ */ new Set(["ECONNREFUSED", "EHOSTUNREACH", "ENETUNREACH"]);
|
|
936
|
+
function connectFailed(e) {
|
|
937
|
+
if (typeof e?.code !== "string") return false;
|
|
938
|
+
return NEVER_CONNECTED.has(e.code) || REFUSED_WHILE_CONNECTING.has(e.code) && e.syscall === "connect";
|
|
939
|
+
}
|
|
940
|
+
function neverConnected(err) {
|
|
941
|
+
const cause = err?.cause;
|
|
942
|
+
if (!cause) return false;
|
|
943
|
+
if (Array.isArray(cause.errors) && cause.errors.length > 0) return cause.errors.every(connectFailed);
|
|
944
|
+
return connectFailed(cause);
|
|
945
|
+
}
|
|
946
|
+
function markUnsent(err) {
|
|
947
|
+
if (typeof err !== "object" || err === null) return;
|
|
948
|
+
const meta = err.meta;
|
|
949
|
+
const sent = { ...typeof meta === "object" && meta !== null ? meta : {}, sent: false };
|
|
827
950
|
try {
|
|
828
|
-
|
|
951
|
+
Object.defineProperty(err, "meta", { value: sent, writable: true, configurable: true, enumerable: true });
|
|
952
|
+
} catch {
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function mayHaveBeenSent(err) {
|
|
956
|
+
const meta = err?.meta;
|
|
957
|
+
return meta?.sent !== false;
|
|
958
|
+
}
|
|
959
|
+
async function timedFetch(url, fetchOptions, ctxSignal, deadline, wire) {
|
|
960
|
+
try {
|
|
961
|
+
const send = transport();
|
|
962
|
+
const res = await send(url, { ...fetchOptions, signal: deadline.signal, dispatcher: guardedDispatcher() });
|
|
963
|
+
wire.maybeSent = true;
|
|
964
|
+
return res;
|
|
829
965
|
} catch (err) {
|
|
830
966
|
const blocked = connectionRefusal(err);
|
|
831
967
|
if (blocked) throw blocked;
|
|
832
|
-
if (
|
|
833
|
-
|
|
834
|
-
throw err;
|
|
835
|
-
} finally {
|
|
836
|
-
clearTimeout(timer);
|
|
968
|
+
if (!neverConnected(err)) wire.maybeSent = true;
|
|
969
|
+
throw attemptFailure(err, ctxSignal, deadline);
|
|
837
970
|
}
|
|
838
971
|
}
|
|
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;
|
|
972
|
+
async function guardUrl(url, ctxSignal, deadline, effectiveMs) {
|
|
843
973
|
try {
|
|
844
|
-
await assertPublicUrl(url, { signal });
|
|
974
|
+
await assertPublicUrl(url, { signal: deadline.signal });
|
|
845
975
|
} catch (err) {
|
|
846
976
|
if (ctxSignal?.aborted) throw ctxSignal.reason;
|
|
847
|
-
if (
|
|
977
|
+
if (deadline.expired()) throw new NodeError("TIMEOUT", `DNS resolution timed out after ${effectiveMs}ms`);
|
|
848
978
|
throw err;
|
|
849
|
-
} finally {
|
|
850
|
-
clearTimeout(timer);
|
|
851
979
|
}
|
|
852
980
|
}
|
|
853
981
|
function hasContent(body) {
|
|
@@ -868,17 +996,28 @@ function refuseUserinfo(url, status = 0) {
|
|
|
868
996
|
);
|
|
869
997
|
}
|
|
870
998
|
}
|
|
871
|
-
async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding) {
|
|
999
|
+
async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, wire, binding) {
|
|
1000
|
+
const deadline = attemptDeadline(ctxSignal, effectiveMs);
|
|
1001
|
+
try {
|
|
1002
|
+
const res = await followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, wire, binding);
|
|
1003
|
+
deadline.clear();
|
|
1004
|
+
return answerUnderDeadline(res, ctxSignal, deadline);
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
deadline.clear();
|
|
1007
|
+
throw err;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
async function followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, wire, binding) {
|
|
872
1011
|
let currentUrl = url;
|
|
873
1012
|
let currentInit = { ...fetchOptions, redirect: "manual" };
|
|
874
|
-
await guardUrl(currentUrl, ctxSignal, effectiveMs);
|
|
1013
|
+
await guardUrl(currentUrl, ctxSignal, deadline, effectiveMs);
|
|
875
1014
|
for (let hop = 0; ; hop++) {
|
|
876
1015
|
const release = guardConnectionsTo(
|
|
877
1016
|
currentUrl instanceof URL ? currentUrl : new URL(currentUrl)
|
|
878
1017
|
);
|
|
879
1018
|
let res;
|
|
880
1019
|
try {
|
|
881
|
-
res = await timedFetch(currentUrl, currentInit, ctxSignal,
|
|
1020
|
+
res = await timedFetch(currentUrl, currentInit, ctxSignal, deadline, wire);
|
|
882
1021
|
} finally {
|
|
883
1022
|
release();
|
|
884
1023
|
}
|
|
@@ -920,7 +1059,7 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding)
|
|
|
920
1059
|
{ status: 0 }
|
|
921
1060
|
);
|
|
922
1061
|
}
|
|
923
|
-
await guardUrl(nextUrl, ctxSignal, effectiveMs);
|
|
1062
|
+
await guardUrl(nextUrl, ctxSignal, deadline, effectiveMs);
|
|
924
1063
|
if (binding && nextUrl.origin !== binding.origin) {
|
|
925
1064
|
for (const name of [...headers.keys()]) headers.delete(name);
|
|
926
1065
|
} else if (nextUrl.origin !== base.origin) {
|
|
@@ -933,6 +1072,15 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding)
|
|
|
933
1072
|
}
|
|
934
1073
|
}
|
|
935
1074
|
async function safeFetch(url, options = {}) {
|
|
1075
|
+
const wire = { maybeSent: false };
|
|
1076
|
+
try {
|
|
1077
|
+
return await sendWithin(url, options, wire);
|
|
1078
|
+
} catch (err) {
|
|
1079
|
+
if (!wire.maybeSent && err !== options.signal?.reason) markUnsent(err);
|
|
1080
|
+
throw err;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
async function sendWithin(url, options, wire) {
|
|
936
1084
|
const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: ctxSignal, retry, credential, ...fetchOptions } = options;
|
|
937
1085
|
const effectiveMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.min(timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
|
|
938
1086
|
const rawAttempts = retry?.attempts ?? DEFAULT_RETRY_ATTEMPTS;
|
|
@@ -957,7 +1105,7 @@ async function safeFetch(url, options = {}) {
|
|
|
957
1105
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
958
1106
|
if (ctxSignal?.aborted) throw ctxSignal.reason;
|
|
959
1107
|
try {
|
|
960
|
-
return await guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding);
|
|
1108
|
+
return await guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, wire, binding);
|
|
961
1109
|
} catch (err) {
|
|
962
1110
|
if (ctxSignal?.aborted) throw ctxSignal.reason;
|
|
963
1111
|
if (err instanceof NodeError && (err.code === "BLOCKED_ADDRESS" || err.code === "TOO_MANY_REDIRECTS" || err.code === "CREDENTIAL_ORIGIN" || err.code === "INVALID_URL")) {
|
|
@@ -1068,8 +1216,8 @@ function timeoutConfigField(opts) {
|
|
|
1068
1216
|
key: "timeoutMs",
|
|
1069
1217
|
label: { en: "Timeout (ms)", de: "Zeitlimit (ms)" },
|
|
1070
1218
|
description: {
|
|
1071
|
-
en: "How long one attempt may
|
|
1072
|
-
de: "Wie lange ein einzelner Versuch
|
|
1219
|
+
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.",
|
|
1220
|
+
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
1221
|
},
|
|
1074
1222
|
type: "number",
|
|
1075
1223
|
default: opts?.default ?? DEFAULT_TIMEOUT_MS,
|
|
@@ -1192,6 +1340,53 @@ function parsePackageMeta(raw) {
|
|
|
1192
1340
|
icon: icon !== "" ? icon : void 0
|
|
1193
1341
|
};
|
|
1194
1342
|
}
|
|
1343
|
+
var SLUG_PATTERN = "^[a-z][a-z0-9-]{0,62}$";
|
|
1344
|
+
var NAMESPACED_SLUG_PATTERN = "^[a-z][a-z0-9-]{0,62}:[a-z][a-z0-9-]{0,62}$";
|
|
1345
|
+
var SEMVER_PATTERN = "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$";
|
|
1346
|
+
var CONFIG_KEY_PATTERN = "^[a-zA-Z][a-zA-Z0-9-]*$";
|
|
1347
|
+
var PATTERN_HINT = {
|
|
1348
|
+
[SLUG_PATTERN]: 'lowercase kebab-case, such as "not-found"',
|
|
1349
|
+
[NAMESPACED_SLUG_PATTERN]: '"<namespace>:<name>" in lowercase kebab-case, such as "core:http-request"',
|
|
1350
|
+
[SEMVER_PATTERN]: 'a semantic version, such as "1.0.0"',
|
|
1351
|
+
[CONFIG_KEY_PATTERN]: 'a letter, then letters, digits or "-", such as "postalCode"'
|
|
1352
|
+
};
|
|
1353
|
+
function nodeManifestProblems(nodes) {
|
|
1354
|
+
const problems = [];
|
|
1355
|
+
for (const [index, node] of nodes.entries()) {
|
|
1356
|
+
const who = `node ${index + 1} (${typeof node.slug === "string" ? node.slug : "no slug"})`;
|
|
1357
|
+
const check = (path, value, pattern) => {
|
|
1358
|
+
if (value === void 0 || typeof value === "string" && new RegExp(pattern).test(value)) return;
|
|
1359
|
+
problems.push(
|
|
1360
|
+
`${who}: ${path} ${JSON.stringify(value)} is refused by the registry \u2014 it must match ${pattern} (${PATTERN_HINT[pattern]}).`
|
|
1361
|
+
);
|
|
1362
|
+
};
|
|
1363
|
+
check("slug", node.slug, NAMESPACED_SLUG_PATTERN);
|
|
1364
|
+
check("version", node.version, SEMVER_PATTERN);
|
|
1365
|
+
for (const [i, port] of (node.outputs ?? []).entries()) {
|
|
1366
|
+
check(`outputs[${i}].name`, port.name, SLUG_PATTERN);
|
|
1367
|
+
check(`outputs[${i}].sourceFromConfig`, port.sourceFromConfig, SLUG_PATTERN);
|
|
1368
|
+
check(`outputs[${i}].fallback.name`, port.fallback?.name, SLUG_PATTERN);
|
|
1369
|
+
}
|
|
1370
|
+
const checkField = (field, at) => {
|
|
1371
|
+
check(`${at}.key`, field.key, CONFIG_KEY_PATTERN);
|
|
1372
|
+
for (const [j, key] of (field.dependsOn ?? []).entries()) check(`${at}.dependsOn[${j}]`, key, CONFIG_KEY_PATTERN);
|
|
1373
|
+
check(`${at}.showIf.key`, field.showIf?.key, CONFIG_KEY_PATTERN);
|
|
1374
|
+
const types = field.credentialType;
|
|
1375
|
+
if (Array.isArray(types)) {
|
|
1376
|
+
for (const [j, type] of types.entries()) check(`${at}.credentialType[${j}]`, type, NAMESPACED_SLUG_PATTERN);
|
|
1377
|
+
} else {
|
|
1378
|
+
check(`${at}.credentialType`, types, NAMESPACED_SLUG_PATTERN);
|
|
1379
|
+
}
|
|
1380
|
+
walk(field.properties, `${at}.properties`);
|
|
1381
|
+
if (field.items) checkField(field.items, `${at}.items`);
|
|
1382
|
+
};
|
|
1383
|
+
const walk = (fields, path) => {
|
|
1384
|
+
for (const [i, field] of (fields ?? []).entries()) checkField(field, `${path}[${i}]`);
|
|
1385
|
+
};
|
|
1386
|
+
walk(node.config, "config");
|
|
1387
|
+
}
|
|
1388
|
+
return problems;
|
|
1389
|
+
}
|
|
1195
1390
|
function buildManifest(nodes, credentials = [], templates = []) {
|
|
1196
1391
|
const manifest = {
|
|
1197
1392
|
manifestVersion: MANIFEST_VERSION,
|
|
@@ -1476,6 +1671,7 @@ function base64Url(buf) {
|
|
|
1476
1671
|
ApiKeyCredential,
|
|
1477
1672
|
BaseCredential,
|
|
1478
1673
|
BasicAuthCredential,
|
|
1674
|
+
CONFIG_KEY_PATTERN,
|
|
1479
1675
|
DEFAULT_MAX_RESPONSE_BYTES,
|
|
1480
1676
|
DEFAULT_RETRY_ATTEMPTS,
|
|
1481
1677
|
DEFAULT_RETRY_DELAY_MS,
|
|
@@ -1486,11 +1682,14 @@ function base64Url(buf) {
|
|
|
1486
1682
|
MAX_RESPONSE_BYTES,
|
|
1487
1683
|
MAX_RETRY_ATTEMPTS,
|
|
1488
1684
|
MAX_TIMEOUT_MS,
|
|
1685
|
+
NAMESPACED_SLUG_PATTERN,
|
|
1489
1686
|
NodeError,
|
|
1490
1687
|
OAuth2AuthCodeCredential,
|
|
1491
1688
|
OAuth2ClientCredentialsCredential,
|
|
1492
1689
|
OPERATORS,
|
|
1493
1690
|
RetryableError,
|
|
1691
|
+
SEMVER_PATTERN,
|
|
1692
|
+
SLUG_PATTERN,
|
|
1494
1693
|
SimpleValueCredential,
|
|
1495
1694
|
VALUELESS_OPERATORS,
|
|
1496
1695
|
VPN_ERROR_CODES,
|
|
@@ -1509,6 +1708,8 @@ function base64Url(buf) {
|
|
|
1509
1708
|
extractCredentialManifests,
|
|
1510
1709
|
extractManifest,
|
|
1511
1710
|
extractManifests,
|
|
1711
|
+
guardRelaxedForLocalDev,
|
|
1712
|
+
guardedLookup,
|
|
1512
1713
|
headerCrossesOrigin,
|
|
1513
1714
|
httpErrorResult,
|
|
1514
1715
|
installAllowedTargets,
|
|
@@ -1520,6 +1721,8 @@ function base64Url(buf) {
|
|
|
1520
1721
|
isOperator,
|
|
1521
1722
|
isVpnErrorCode,
|
|
1522
1723
|
maxBytesConfigField,
|
|
1724
|
+
mayHaveBeenSent,
|
|
1725
|
+
nodeManifestProblems,
|
|
1523
1726
|
normalizeCredentialType,
|
|
1524
1727
|
normalizeLocalized,
|
|
1525
1728
|
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-
|
|
19
|
+
} from "./chunk-CA6JVLLF.js";
|
|
15
20
|
|
|
16
21
|
// src/types.ts
|
|
17
22
|
function isNodeWithIteration(node) {
|
|
@@ -212,6 +217,9 @@ function errorPort(opts = {}) {
|
|
|
212
217
|
};
|
|
213
218
|
}
|
|
214
219
|
|
|
220
|
+
// src/fetch.ts
|
|
221
|
+
import { fetch as undiciFetch } from "undici";
|
|
222
|
+
|
|
215
223
|
// src/ssrf.ts
|
|
216
224
|
import { subscribe } from "diagnostics_channel";
|
|
217
225
|
import dns from "dns";
|
|
@@ -367,19 +375,25 @@ function guardRelaxedForLocalDev() {
|
|
|
367
375
|
if (relaxed && !bypassNoticeLogged) {
|
|
368
376
|
bypassNoticeLogged = true;
|
|
369
377
|
console.warn(
|
|
370
|
-
"[ssrf] RVNXX_SSRF_ALLOW_PRIVATE is set: allowing private/loopback
|
|
378
|
+
"[ssrf] RVNXX_SSRF_ALLOW_PRIVATE is set: allowing private/loopback targets for web requests and connections. Intended for local development only."
|
|
371
379
|
);
|
|
372
380
|
}
|
|
373
381
|
return relaxed;
|
|
374
382
|
}
|
|
383
|
+
function logBlocked(noun, host, address) {
|
|
384
|
+
console.warn(`[ssrf] blocked ${noun} to ${host}: resolves to private/reserved address ${address}`);
|
|
385
|
+
}
|
|
375
386
|
function blockedError(host, address) {
|
|
376
387
|
if (host === address) {
|
|
377
388
|
return new NodeError("BLOCKED_ADDRESS", `Blocked request to private or reserved address ${address}`, {
|
|
378
389
|
status: 0
|
|
379
390
|
});
|
|
380
391
|
}
|
|
381
|
-
|
|
382
|
-
return
|
|
392
|
+
logBlocked("request", host, address);
|
|
393
|
+
return refusedName(host);
|
|
394
|
+
}
|
|
395
|
+
function refusedName(host, noun = "request") {
|
|
396
|
+
return new NodeError("BLOCKED_ADDRESS", `Blocked ${noun} to ${host}: resolves to a private or reserved address`, {
|
|
383
397
|
status: 0
|
|
384
398
|
});
|
|
385
399
|
}
|
|
@@ -461,6 +475,16 @@ function unreadablePortError(host) {
|
|
|
461
475
|
);
|
|
462
476
|
}
|
|
463
477
|
function judgingLookup(port) {
|
|
478
|
+
return lookupJudgedFor(port, { noun: "request" }, (hostname) => new NodeError("BLOCKED_ADDRESS", `Could not resolve host: ${hostname}`, { status: 0 }));
|
|
479
|
+
}
|
|
480
|
+
function guardedLookup(port, options = {}) {
|
|
481
|
+
return lookupJudgedFor(
|
|
482
|
+
port,
|
|
483
|
+
options,
|
|
484
|
+
(hostname) => Object.assign(new Error(`getaddrinfo ENOTFOUND ${hostname}`), { code: "ENOTFOUND", hostname })
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
function lookupJudgedFor(port, { refusal, unroutable, noun = "connection" }, emptyAnswer) {
|
|
464
488
|
return (hostname, options, callback) => {
|
|
465
489
|
dns.lookup(hostname, { ...options, all: true }, (err, addresses) => {
|
|
466
490
|
if (err) {
|
|
@@ -468,15 +492,18 @@ function judgingLookup(port) {
|
|
|
468
492
|
return;
|
|
469
493
|
}
|
|
470
494
|
if (addresses.length === 0) {
|
|
471
|
-
callback(
|
|
495
|
+
callback(emptyAnswer(hostname), "");
|
|
472
496
|
return;
|
|
473
497
|
}
|
|
474
498
|
if (!guardRelaxedForLocalDev()) {
|
|
499
|
+
const host = hostname;
|
|
475
500
|
for (const a of addresses) {
|
|
476
|
-
if (isBlockedAddress(a.address)
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
501
|
+
if (!isBlockedAddress(a.address)) continue;
|
|
502
|
+
const allowed = isAllowedTarget(a.address, port);
|
|
503
|
+
if (allowed && !unroutable) continue;
|
|
504
|
+
logBlocked(noun, host, a.address);
|
|
505
|
+
callback(allowed && unroutable ? unroutable(host) : refusal ? refusal(host) : refusedName(host, noun), "");
|
|
506
|
+
return;
|
|
480
507
|
}
|
|
481
508
|
}
|
|
482
509
|
if (options.all) callback(null, addresses);
|
|
@@ -662,34 +689,132 @@ function credentialOriginRefusal(target, bound, hop) {
|
|
|
662
689
|
{ status: 0 }
|
|
663
690
|
);
|
|
664
691
|
}
|
|
665
|
-
|
|
692
|
+
function attemptDeadline(ctxSignal, effectiveMs) {
|
|
666
693
|
const ac = new AbortController();
|
|
667
|
-
|
|
668
|
-
const
|
|
694
|
+
let timer;
|
|
695
|
+
const arm = (message) => {
|
|
696
|
+
clearTimeout(timer);
|
|
697
|
+
timer = setTimeout(() => ac.abort(new NodeError("TIMEOUT", message)), effectiveMs);
|
|
698
|
+
};
|
|
699
|
+
arm(`Request timed out after ${effectiveMs}ms`);
|
|
700
|
+
return {
|
|
701
|
+
signal: ctxSignal ? AbortSignal.any([ctxSignal, ac.signal]) : ac.signal,
|
|
702
|
+
expired: () => ac.signal.aborted,
|
|
703
|
+
reason: () => ac.signal.reason,
|
|
704
|
+
clear: () => clearTimeout(timer),
|
|
705
|
+
awaitChunk: () => arm(`The answer sent nothing for ${effectiveMs}ms`)
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
function attemptFailure(err, ctxSignal, deadline) {
|
|
709
|
+
if (ctxSignal?.aborted) return ctxSignal.reason;
|
|
710
|
+
if (deadline.expired()) return deadline.reason();
|
|
711
|
+
return err;
|
|
712
|
+
}
|
|
713
|
+
function answerUnderDeadline(res, ctxSignal, deadline) {
|
|
714
|
+
if (!res.body) return rebuilt(res, null) ?? asRuntimeResponse(res);
|
|
715
|
+
let reader;
|
|
716
|
+
const body = new ReadableStream(
|
|
717
|
+
{
|
|
718
|
+
async pull(controller) {
|
|
719
|
+
reader ??= res.body.getReader();
|
|
720
|
+
deadline.awaitChunk();
|
|
721
|
+
try {
|
|
722
|
+
const { done, value } = await reader.read();
|
|
723
|
+
if (done) controller.close();
|
|
724
|
+
else controller.enqueue(value);
|
|
725
|
+
} catch (err) {
|
|
726
|
+
controller.error(attemptFailure(err, ctxSignal, deadline));
|
|
727
|
+
} finally {
|
|
728
|
+
deadline.clear();
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
cancel(reason) {
|
|
732
|
+
deadline.clear();
|
|
733
|
+
return reader ? reader.cancel(reason) : res.body?.cancel(reason);
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
// Read nothing ahead of the caller: the limit times the caller's waits, not
|
|
737
|
+
// a buffer filling while nobody reads.
|
|
738
|
+
{ highWaterMark: 0 }
|
|
739
|
+
);
|
|
740
|
+
return rebuilt(res, body) ?? asRuntimeResponse(res);
|
|
741
|
+
}
|
|
742
|
+
function rebuilt(res, body) {
|
|
669
743
|
try {
|
|
670
|
-
return
|
|
744
|
+
return withUrl(new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers }), res.url);
|
|
745
|
+
} catch {
|
|
746
|
+
return void 0;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
function asRuntimeResponse(res) {
|
|
750
|
+
return withUrl(withStatusLine(new Response(res.body, { headers: res.headers }), res), res.url);
|
|
751
|
+
}
|
|
752
|
+
function withStatusLine(answer, from) {
|
|
753
|
+
const clone = answer.clone.bind(answer);
|
|
754
|
+
Object.defineProperties(answer, {
|
|
755
|
+
status: { value: from.status },
|
|
756
|
+
statusText: { value: from.statusText },
|
|
757
|
+
ok: { value: from.ok },
|
|
758
|
+
clone: { value: () => withStatusLine(clone(), from), configurable: true }
|
|
759
|
+
});
|
|
760
|
+
return answer;
|
|
761
|
+
}
|
|
762
|
+
function withUrl(answer, url) {
|
|
763
|
+
const clone = answer.clone.bind(answer);
|
|
764
|
+
Object.defineProperty(answer, "url", { value: url });
|
|
765
|
+
Object.defineProperty(answer, "clone", { value: () => withUrl(clone(), url) });
|
|
766
|
+
return answer;
|
|
767
|
+
}
|
|
768
|
+
var runtimeFetch = globalThis.fetch;
|
|
769
|
+
function transport() {
|
|
770
|
+
const current = globalThis.fetch;
|
|
771
|
+
return current === runtimeFetch ? undiciFetch : current;
|
|
772
|
+
}
|
|
773
|
+
var NEVER_CONNECTED = /* @__PURE__ */ new Set(["ENOTFOUND", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT"]);
|
|
774
|
+
var REFUSED_WHILE_CONNECTING = /* @__PURE__ */ new Set(["ECONNREFUSED", "EHOSTUNREACH", "ENETUNREACH"]);
|
|
775
|
+
function connectFailed(e) {
|
|
776
|
+
if (typeof e?.code !== "string") return false;
|
|
777
|
+
return NEVER_CONNECTED.has(e.code) || REFUSED_WHILE_CONNECTING.has(e.code) && e.syscall === "connect";
|
|
778
|
+
}
|
|
779
|
+
function neverConnected(err) {
|
|
780
|
+
const cause = err?.cause;
|
|
781
|
+
if (!cause) return false;
|
|
782
|
+
if (Array.isArray(cause.errors) && cause.errors.length > 0) return cause.errors.every(connectFailed);
|
|
783
|
+
return connectFailed(cause);
|
|
784
|
+
}
|
|
785
|
+
function markUnsent(err) {
|
|
786
|
+
if (typeof err !== "object" || err === null) return;
|
|
787
|
+
const meta = err.meta;
|
|
788
|
+
const sent = { ...typeof meta === "object" && meta !== null ? meta : {}, sent: false };
|
|
789
|
+
try {
|
|
790
|
+
Object.defineProperty(err, "meta", { value: sent, writable: true, configurable: true, enumerable: true });
|
|
791
|
+
} catch {
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function mayHaveBeenSent(err) {
|
|
795
|
+
const meta = err?.meta;
|
|
796
|
+
return meta?.sent !== false;
|
|
797
|
+
}
|
|
798
|
+
async function timedFetch(url, fetchOptions, ctxSignal, deadline, wire) {
|
|
799
|
+
try {
|
|
800
|
+
const send = transport();
|
|
801
|
+
const res = await send(url, { ...fetchOptions, signal: deadline.signal, dispatcher: guardedDispatcher() });
|
|
802
|
+
wire.maybeSent = true;
|
|
803
|
+
return res;
|
|
671
804
|
} catch (err) {
|
|
672
805
|
const blocked = connectionRefusal(err);
|
|
673
806
|
if (blocked) throw blocked;
|
|
674
|
-
if (
|
|
675
|
-
|
|
676
|
-
throw err;
|
|
677
|
-
} finally {
|
|
678
|
-
clearTimeout(timer);
|
|
807
|
+
if (!neverConnected(err)) wire.maybeSent = true;
|
|
808
|
+
throw attemptFailure(err, ctxSignal, deadline);
|
|
679
809
|
}
|
|
680
810
|
}
|
|
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;
|
|
811
|
+
async function guardUrl(url, ctxSignal, deadline, effectiveMs) {
|
|
685
812
|
try {
|
|
686
|
-
await assertPublicUrl(url, { signal });
|
|
813
|
+
await assertPublicUrl(url, { signal: deadline.signal });
|
|
687
814
|
} catch (err) {
|
|
688
815
|
if (ctxSignal?.aborted) throw ctxSignal.reason;
|
|
689
|
-
if (
|
|
816
|
+
if (deadline.expired()) throw new NodeError("TIMEOUT", `DNS resolution timed out after ${effectiveMs}ms`);
|
|
690
817
|
throw err;
|
|
691
|
-
} finally {
|
|
692
|
-
clearTimeout(timer);
|
|
693
818
|
}
|
|
694
819
|
}
|
|
695
820
|
function hasContent(body) {
|
|
@@ -710,17 +835,28 @@ function refuseUserinfo(url, status = 0) {
|
|
|
710
835
|
);
|
|
711
836
|
}
|
|
712
837
|
}
|
|
713
|
-
async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding) {
|
|
838
|
+
async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, wire, binding) {
|
|
839
|
+
const deadline = attemptDeadline(ctxSignal, effectiveMs);
|
|
840
|
+
try {
|
|
841
|
+
const res = await followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, wire, binding);
|
|
842
|
+
deadline.clear();
|
|
843
|
+
return answerUnderDeadline(res, ctxSignal, deadline);
|
|
844
|
+
} catch (err) {
|
|
845
|
+
deadline.clear();
|
|
846
|
+
throw err;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
async function followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, wire, binding) {
|
|
714
850
|
let currentUrl = url;
|
|
715
851
|
let currentInit = { ...fetchOptions, redirect: "manual" };
|
|
716
|
-
await guardUrl(currentUrl, ctxSignal, effectiveMs);
|
|
852
|
+
await guardUrl(currentUrl, ctxSignal, deadline, effectiveMs);
|
|
717
853
|
for (let hop = 0; ; hop++) {
|
|
718
854
|
const release = guardConnectionsTo(
|
|
719
855
|
currentUrl instanceof URL ? currentUrl : new URL(currentUrl)
|
|
720
856
|
);
|
|
721
857
|
let res;
|
|
722
858
|
try {
|
|
723
|
-
res = await timedFetch(currentUrl, currentInit, ctxSignal,
|
|
859
|
+
res = await timedFetch(currentUrl, currentInit, ctxSignal, deadline, wire);
|
|
724
860
|
} finally {
|
|
725
861
|
release();
|
|
726
862
|
}
|
|
@@ -762,7 +898,7 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding)
|
|
|
762
898
|
{ status: 0 }
|
|
763
899
|
);
|
|
764
900
|
}
|
|
765
|
-
await guardUrl(nextUrl, ctxSignal, effectiveMs);
|
|
901
|
+
await guardUrl(nextUrl, ctxSignal, deadline, effectiveMs);
|
|
766
902
|
if (binding && nextUrl.origin !== binding.origin) {
|
|
767
903
|
for (const name of [...headers.keys()]) headers.delete(name);
|
|
768
904
|
} else if (nextUrl.origin !== base.origin) {
|
|
@@ -775,6 +911,15 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding)
|
|
|
775
911
|
}
|
|
776
912
|
}
|
|
777
913
|
async function safeFetch(url, options = {}) {
|
|
914
|
+
const wire = { maybeSent: false };
|
|
915
|
+
try {
|
|
916
|
+
return await sendWithin(url, options, wire);
|
|
917
|
+
} catch (err) {
|
|
918
|
+
if (!wire.maybeSent && err !== options.signal?.reason) markUnsent(err);
|
|
919
|
+
throw err;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
async function sendWithin(url, options, wire) {
|
|
778
923
|
const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: ctxSignal, retry, credential, ...fetchOptions } = options;
|
|
779
924
|
const effectiveMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.min(timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
|
|
780
925
|
const rawAttempts = retry?.attempts ?? DEFAULT_RETRY_ATTEMPTS;
|
|
@@ -799,7 +944,7 @@ async function safeFetch(url, options = {}) {
|
|
|
799
944
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
800
945
|
if (ctxSignal?.aborted) throw ctxSignal.reason;
|
|
801
946
|
try {
|
|
802
|
-
return await guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding);
|
|
947
|
+
return await guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, wire, binding);
|
|
803
948
|
} catch (err) {
|
|
804
949
|
if (ctxSignal?.aborted) throw ctxSignal.reason;
|
|
805
950
|
if (err instanceof NodeError && (err.code === "BLOCKED_ADDRESS" || err.code === "TOO_MANY_REDIRECTS" || err.code === "CREDENTIAL_ORIGIN" || err.code === "INVALID_URL")) {
|
|
@@ -910,8 +1055,8 @@ function timeoutConfigField(opts) {
|
|
|
910
1055
|
key: "timeoutMs",
|
|
911
1056
|
label: { en: "Timeout (ms)", de: "Zeitlimit (ms)" },
|
|
912
1057
|
description: {
|
|
913
|
-
en: "How long one attempt may
|
|
914
|
-
de: "Wie lange ein einzelner Versuch
|
|
1058
|
+
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.",
|
|
1059
|
+
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
1060
|
},
|
|
916
1061
|
type: "number",
|
|
917
1062
|
default: opts?.default ?? DEFAULT_TIMEOUT_MS,
|
|
@@ -1282,6 +1427,7 @@ export {
|
|
|
1282
1427
|
ApiKeyCredential,
|
|
1283
1428
|
BaseCredential,
|
|
1284
1429
|
BasicAuthCredential,
|
|
1430
|
+
CONFIG_KEY_PATTERN,
|
|
1285
1431
|
DEFAULT_MAX_RESPONSE_BYTES,
|
|
1286
1432
|
DEFAULT_RETRY_ATTEMPTS,
|
|
1287
1433
|
DEFAULT_RETRY_DELAY_MS,
|
|
@@ -1292,11 +1438,14 @@ export {
|
|
|
1292
1438
|
MAX_RESPONSE_BYTES,
|
|
1293
1439
|
MAX_RETRY_ATTEMPTS,
|
|
1294
1440
|
MAX_TIMEOUT_MS,
|
|
1441
|
+
NAMESPACED_SLUG_PATTERN,
|
|
1295
1442
|
NodeError,
|
|
1296
1443
|
OAuth2AuthCodeCredential,
|
|
1297
1444
|
OAuth2ClientCredentialsCredential,
|
|
1298
1445
|
OPERATORS,
|
|
1299
1446
|
RetryableError,
|
|
1447
|
+
SEMVER_PATTERN,
|
|
1448
|
+
SLUG_PATTERN,
|
|
1300
1449
|
SimpleValueCredential,
|
|
1301
1450
|
VALUELESS_OPERATORS,
|
|
1302
1451
|
VPN_ERROR_CODES,
|
|
@@ -1315,6 +1464,8 @@ export {
|
|
|
1315
1464
|
extractCredentialManifests,
|
|
1316
1465
|
extractManifest,
|
|
1317
1466
|
extractManifests,
|
|
1467
|
+
guardRelaxedForLocalDev,
|
|
1468
|
+
guardedLookup,
|
|
1318
1469
|
headerCrossesOrigin,
|
|
1319
1470
|
httpErrorResult,
|
|
1320
1471
|
installAllowedTargets,
|
|
@@ -1326,6 +1477,8 @@ export {
|
|
|
1326
1477
|
isOperator,
|
|
1327
1478
|
isVpnErrorCode,
|
|
1328
1479
|
maxBytesConfigField,
|
|
1480
|
+
mayHaveBeenSent,
|
|
1481
|
+
nodeManifestProblems,
|
|
1329
1482
|
normalizeCredentialType,
|
|
1330
1483
|
normalizeLocalized,
|
|
1331
1484
|
outputProblems,
|
package/dist/manifest.d.ts
CHANGED
|
@@ -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;
|