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