@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.
- package/dist/{chunk-XO73F62E.js → chunk-CA6JVLLF.js} +52 -0
- package/dist/cli.cjs +49 -0
- package/dist/cli.js +4 -1
- package/dist/index.cjs +171 -30
- package/dist/index.d.ts +2 -2
- package/dist/index.js +123 -31
- 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/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
|
|
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
|
-
|
|
540
|
-
return
|
|
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(
|
|
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)
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
849
|
+
function attemptDeadline(ctxSignal, effectiveMs) {
|
|
824
850
|
const ac = new AbortController();
|
|
825
|
-
|
|
826
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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,
|
|
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
|
|
1072
|
-
de: "Wie lange ein einzelner Versuch
|
|
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-
|
|
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
|
|
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
|
-
|
|
382
|
-
return
|
|
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(
|
|
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)
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
689
|
+
function attemptDeadline(ctxSignal, effectiveMs) {
|
|
666
690
|
const ac = new AbortController();
|
|
667
|
-
|
|
668
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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,
|
|
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
|
|
914
|
-
de: "Wie lange ein einzelner Versuch
|
|
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,
|
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;
|