@revenexx/integrations-node-sdk 1.10.0 → 1.13.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/branches.d.ts +47 -0
- package/dist/chunk-XO73F62E.js +122 -0
- package/dist/cli.cjs +38 -0
- package/dist/cli.js +5 -1
- package/dist/errors.d.ts +21 -0
- package/dist/index.cjs +276 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.js +211 -8
- package/dist/ssrf.d.ts +15 -2
- package/dist/types.d.ts +65 -1
- package/package.json +3 -2
- package/dist/chunk-5BCAXRU3.js +0 -59
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-run allow-list of the SSRF guard (PO-828): the exact `ip:port` pairs a
|
|
3
|
+
* run may reach although the address is private — its own tenant's VPN routes,
|
|
4
|
+
* served by the relay at a private overlay address (an RFC 1918 or IPv6
|
|
5
|
+
* unique-local one — no other address may be listed).
|
|
6
|
+
*
|
|
7
|
+
* **The global symbol is the contract, not this module.** Every node bundle carries
|
|
8
|
+
* its own copy of this package, so a module-level setter would reach only the copy
|
|
9
|
+
* that happened to be imported by the host. The trusted host instead defines one
|
|
10
|
+
* property on `globalThis`, before any node bundle is imported, and every copy reads
|
|
11
|
+
* that same property:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* Object.defineProperty(globalThis, Symbol.for('revenexx.ssrf.allowedTargets'), {
|
|
15
|
+
* value: Object.freeze([...targets]),
|
|
16
|
+
* writable: false, configurable: false, enumerable: false,
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* The host may do that itself (the runtimes do, without depending on this package)
|
|
21
|
+
* or call {@link installAllowedTargets}, which does exactly that.
|
|
22
|
+
*
|
|
23
|
+
* A property that is not shaped that way is read as no list at all. Non-writable and
|
|
24
|
+
* non-configurable is what makes the list impossible to widen after the host set it;
|
|
25
|
+
* a property that lacks either was not set by a host following the contract, and a
|
|
26
|
+
* guard that honoured it would let whoever set it decide what is private.
|
|
27
|
+
*/
|
|
28
|
+
export declare const ALLOWED_TARGETS_SYMBOL: unique symbol;
|
|
29
|
+
/** Test seam: how many times a list has been validated. Not re-exported from the package. */
|
|
30
|
+
export declare const allowListStats: {
|
|
31
|
+
validations: number;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Read the list off `holder` — `globalThis` in production, a fresh object in the
|
|
35
|
+
* tests that need a shape the one-shot global cannot be given twice. Anything but a
|
|
36
|
+
* frozen array of valid entries on a non-writable, non-configurable data property
|
|
37
|
+
* reads as the empty list: fail closed, never partly.
|
|
38
|
+
*/
|
|
39
|
+
export declare function readAllowedTargets(holder: object): readonly string[];
|
|
40
|
+
/** {@link isAllowedTarget} against the list on `holder`. */
|
|
41
|
+
export declare function isAllowedTargetOn(holder: object, ip: string, port: number): boolean;
|
|
42
|
+
/** {@link installAllowedTargets} onto `holder`. */
|
|
43
|
+
export declare function installAllowedTargetsOn(holder: object, targets: readonly string[]): void;
|
|
44
|
+
/**
|
|
45
|
+
* Install the per-run allow-list on `globalThis`. For the trusted host, before any
|
|
46
|
+
* node bundle is imported; one-shot — a second call throws, as does an entry that is
|
|
47
|
+
* not `"<ip>:<port>"` (IPv4 dotted) or `"[v6]:<port>"`, or whose address is not an
|
|
48
|
+
* RFC 1918 or IPv6 unique-local one.
|
|
49
|
+
*
|
|
50
|
+
* Install it even when the list is empty. A process that never installs one leaves
|
|
51
|
+
* the property free for the first code that runs to define it — node code included.
|
|
52
|
+
*
|
|
53
|
+
* Promised behaviour: specs/ssrf-guard.md (AC-26 to AC-31).
|
|
54
|
+
*/
|
|
55
|
+
export declare function installAllowedTargets(targets: readonly string[]): void;
|
|
56
|
+
/**
|
|
57
|
+
* The per-run allow-list as the host installed it, or an empty list when none is
|
|
58
|
+
* installed or the property is malformed (see {@link ALLOWED_TARGETS_SYMBOL}).
|
|
59
|
+
*/
|
|
60
|
+
export declare function allowedTargets(): readonly string[];
|
|
61
|
+
/**
|
|
62
|
+
* Whether the run may reach `ip` on `port` although the address is private: `true`
|
|
63
|
+
* only when exactly that pair is on the allow-list. The port is part of the target —
|
|
64
|
+
* `10.31.0.7:8080` on the list does not allow `10.31.0.7:22`. For a caller that opens
|
|
65
|
+
* its own socket; `safeFetch` and `assertPublicUrl` already consult it.
|
|
66
|
+
*/
|
|
67
|
+
export declare function isAllowedTarget(ip: string, port: number): boolean;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { INode, INodeDescription, INodeResult, IOutputPort } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A node's output ports are alternatives: exactly one fires. A node with a
|
|
4
|
+
* single port has nothing to choose between, so its result may leave `branch`
|
|
5
|
+
* out; a node with several must say which one it took, because the worker
|
|
6
|
+
* otherwise activates every outgoing connection (PO-751).
|
|
7
|
+
*
|
|
8
|
+
* A node counts as multi-port when EITHER holds:
|
|
9
|
+
* - more than one port is declared (an `error` port counts: `out` + `error` is two);
|
|
10
|
+
* - a port is marked `resolveOutputs`, because that set is only known while the
|
|
11
|
+
* author edits and may hold several.
|
|
12
|
+
*
|
|
13
|
+
* A single port is one way out however it is declared — of kind `branch`, or named
|
|
14
|
+
* from a setting by `sourceFromConfig` (one name, with its `fallback`).
|
|
15
|
+
*/
|
|
16
|
+
export declare function isMultiPort(outputs: readonly IOutputPort[] | undefined): boolean;
|
|
17
|
+
/** A result that names its branch — what every multi-port node returns. */
|
|
18
|
+
export interface IBranchedNodeResult extends INodeResult {
|
|
19
|
+
branch: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Declaration problems of a node's output ports that the manifest build can see.
|
|
23
|
+
* Returns one sentence per problem, empty when there is none.
|
|
24
|
+
*
|
|
25
|
+
* A node with several ports must give each fixed port a unique `name`, since `branch`
|
|
26
|
+
* names one of them, and a generated port's `fallback` name must not repeat a fixed
|
|
27
|
+
* one. (Whether `execute` really returns a `branch` cannot be read from a declaration
|
|
28
|
+
* — see {@link assertBranchNamed}.)
|
|
29
|
+
*/
|
|
30
|
+
export declare function outputProblems(description: INodeDescription): string[];
|
|
31
|
+
/**
|
|
32
|
+
* The warnings `rvnxx-nodes manifest` prints for a package's nodes: every
|
|
33
|
+
* {@link outputProblems} finding, worded for the log. In 1.x they are warnings and the
|
|
34
|
+
* build continues; the next major turns them into errors.
|
|
35
|
+
*/
|
|
36
|
+
export declare function outputWarnings(nodes: readonly INode[]): string[];
|
|
37
|
+
/**
|
|
38
|
+
* Check a result against the node's declaration. For a node's own tests:
|
|
39
|
+
* `assertBranchNamed(node, await node.execute(ctx, inputs), config)`.
|
|
40
|
+
*
|
|
41
|
+
* Throws when a multi-port node returned no `branch`, and when the branch is none of
|
|
42
|
+
* the names the node can produce: a fixed name is always accepted; beyond that, a
|
|
43
|
+
* `resolveOutputs` port accepts any name (it is unknown here), and a `sourceFromConfig`
|
|
44
|
+
* port accepts its fallback name and — when `config` is passed — the value it holds
|
|
45
|
+
* (with no `config`, any name). A single-port node may leave `branch` out.
|
|
46
|
+
*/
|
|
47
|
+
export declare function assertBranchNamed(node: INode | INodeDescription, result: INodeResult, config?: Record<string, unknown>): void;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// src/branches.ts
|
|
2
|
+
function isMultiPort(outputs) {
|
|
3
|
+
if (!outputs) return false;
|
|
4
|
+
return outputs.length > 1 || outputs.some((p) => p.resolveOutputs === true);
|
|
5
|
+
}
|
|
6
|
+
var isGenerated = (p) => p.sourceFromConfig !== void 0 || p.resolveOutputs === true;
|
|
7
|
+
function outputProblems(description) {
|
|
8
|
+
const outputs = description.outputs ?? [];
|
|
9
|
+
if (!isMultiPort(outputs)) return [];
|
|
10
|
+
const problems = [];
|
|
11
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12
|
+
for (const [i, port] of outputs.entries()) {
|
|
13
|
+
if (isGenerated(port)) continue;
|
|
14
|
+
if (port.name === void 0 || port.name === "") {
|
|
15
|
+
problems.push(
|
|
16
|
+
`${description.slug}: output ${i + 1} has no name, so a branch cannot choose it \u2014 a node with several outputs must name each one.`
|
|
17
|
+
);
|
|
18
|
+
} else if (seen.has(port.name)) {
|
|
19
|
+
problems.push(`${description.slug}: output name "${port.name}" is declared twice.`);
|
|
20
|
+
} else {
|
|
21
|
+
seen.add(port.name);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
for (const port of outputs) {
|
|
25
|
+
const fb = port.fallback?.name;
|
|
26
|
+
if (isGenerated(port) && fb !== void 0 && seen.has(fb)) {
|
|
27
|
+
problems.push(`${description.slug}: the fallback name "${fb}" is also the name of a fixed output, so a branch "${fb}" is ambiguous.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return problems;
|
|
31
|
+
}
|
|
32
|
+
function outputWarnings(nodes) {
|
|
33
|
+
return nodes.flatMap((n) => outputProblems(n.description));
|
|
34
|
+
}
|
|
35
|
+
function assertBranchNamed(node, result, config) {
|
|
36
|
+
const description = "execute" in node ? node.description : node;
|
|
37
|
+
const outputs = description.outputs ?? [];
|
|
38
|
+
if (!isMultiPort(outputs)) return;
|
|
39
|
+
const branch = result.branch;
|
|
40
|
+
if (typeof branch !== "string" || branch === "") {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`${description.slug} has several outputs, so its result must name the branch it took; without it every output fires.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (outputs.some((p) => !isGenerated(p) && p.name === branch)) return;
|
|
46
|
+
const producible = outputs.some((p) => {
|
|
47
|
+
if (p.resolveOutputs === true) return true;
|
|
48
|
+
if (p.sourceFromConfig === void 0) return false;
|
|
49
|
+
if (p.fallback?.name === branch) return true;
|
|
50
|
+
return config === void 0 || config[p.sourceFromConfig] === branch;
|
|
51
|
+
});
|
|
52
|
+
if (!producible) {
|
|
53
|
+
const names = outputs.map((p) => p.name ?? p.fallback?.name).filter((n) => n);
|
|
54
|
+
throw new Error(
|
|
55
|
+
`${description.slug} returned branch "${branch}", which none of its outputs can produce (${names.join(", ")}).`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/extract.ts
|
|
61
|
+
function extractManifest(node) {
|
|
62
|
+
return node.description;
|
|
63
|
+
}
|
|
64
|
+
function extractManifests(nodes) {
|
|
65
|
+
return nodes.map(extractManifest);
|
|
66
|
+
}
|
|
67
|
+
function extractCredentialManifest(credential) {
|
|
68
|
+
return credential.description;
|
|
69
|
+
}
|
|
70
|
+
function extractCredentialManifests(credentials) {
|
|
71
|
+
return credentials.map(extractCredentialManifest);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/manifest.ts
|
|
75
|
+
var MANIFEST_VERSION = "v0-draft";
|
|
76
|
+
var PACKAGE_ICON = /^lucide:[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
77
|
+
var PHP_TRIM = /^[ \t\n\r\0\v]+|[ \t\n\r\0\v]+$/g;
|
|
78
|
+
function packageIconProblem(icon) {
|
|
79
|
+
if (icon === void 0 || PACKAGE_ICON.test(icon)) return void 0;
|
|
80
|
+
return `package.json "revenexx.icon" must be a Lucide icon name such as "lucide:bell", got "${icon}".`;
|
|
81
|
+
}
|
|
82
|
+
function parsePackageMeta(raw) {
|
|
83
|
+
const obj = raw && typeof raw === "object" ? raw : {};
|
|
84
|
+
const str = (v) => typeof v === "string" ? v.trim() : "";
|
|
85
|
+
const revenexx = obj.revenexx && typeof obj.revenexx === "object" ? obj.revenexx : {};
|
|
86
|
+
const displayName = str(revenexx.displayName);
|
|
87
|
+
const icon = typeof revenexx.icon === "string" ? revenexx.icon.replace(PHP_TRIM, "") : "";
|
|
88
|
+
return {
|
|
89
|
+
name: str(obj.name),
|
|
90
|
+
version: str(obj.version),
|
|
91
|
+
displayName: displayName !== "" ? displayName : void 0,
|
|
92
|
+
icon: icon !== "" ? icon : void 0
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function buildManifest(nodes, credentials = [], templates = []) {
|
|
96
|
+
const manifest = {
|
|
97
|
+
manifestVersion: MANIFEST_VERSION,
|
|
98
|
+
nodes: extractManifests(nodes)
|
|
99
|
+
};
|
|
100
|
+
if (credentials.length > 0) {
|
|
101
|
+
manifest.credentials = extractCredentialManifests(credentials);
|
|
102
|
+
}
|
|
103
|
+
if (templates.length > 0) {
|
|
104
|
+
manifest.templates = templates;
|
|
105
|
+
}
|
|
106
|
+
return manifest;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
isMultiPort,
|
|
111
|
+
outputProblems,
|
|
112
|
+
outputWarnings,
|
|
113
|
+
assertBranchNamed,
|
|
114
|
+
extractManifest,
|
|
115
|
+
extractManifests,
|
|
116
|
+
extractCredentialManifest,
|
|
117
|
+
extractCredentialManifests,
|
|
118
|
+
MANIFEST_VERSION,
|
|
119
|
+
packageIconProblem,
|
|
120
|
+
parsePackageMeta,
|
|
121
|
+
buildManifest
|
|
122
|
+
};
|
package/dist/cli.cjs
CHANGED
|
@@ -28,6 +28,41 @@ var fs2 = __toESM(require("fs"), 1);
|
|
|
28
28
|
var import_node_path2 = require("path");
|
|
29
29
|
var import_node_url = require("url");
|
|
30
30
|
|
|
31
|
+
// src/branches.ts
|
|
32
|
+
function isMultiPort(outputs) {
|
|
33
|
+
if (!outputs) return false;
|
|
34
|
+
return outputs.length > 1 || outputs.some((p) => p.resolveOutputs === true);
|
|
35
|
+
}
|
|
36
|
+
var isGenerated = (p) => p.sourceFromConfig !== void 0 || p.resolveOutputs === true;
|
|
37
|
+
function outputProblems(description) {
|
|
38
|
+
const outputs = description.outputs ?? [];
|
|
39
|
+
if (!isMultiPort(outputs)) return [];
|
|
40
|
+
const problems = [];
|
|
41
|
+
const seen = /* @__PURE__ */ new Set();
|
|
42
|
+
for (const [i, port] of outputs.entries()) {
|
|
43
|
+
if (isGenerated(port)) continue;
|
|
44
|
+
if (port.name === void 0 || port.name === "") {
|
|
45
|
+
problems.push(
|
|
46
|
+
`${description.slug}: output ${i + 1} has no name, so a branch cannot choose it \u2014 a node with several outputs must name each one.`
|
|
47
|
+
);
|
|
48
|
+
} else if (seen.has(port.name)) {
|
|
49
|
+
problems.push(`${description.slug}: output name "${port.name}" is declared twice.`);
|
|
50
|
+
} else {
|
|
51
|
+
seen.add(port.name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const port of outputs) {
|
|
55
|
+
const fb = port.fallback?.name;
|
|
56
|
+
if (isGenerated(port) && fb !== void 0 && seen.has(fb)) {
|
|
57
|
+
problems.push(`${description.slug}: the fallback name "${fb}" is also the name of a fixed output, so a branch "${fb}" is ambiguous.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return problems;
|
|
61
|
+
}
|
|
62
|
+
function outputWarnings(nodes) {
|
|
63
|
+
return nodes.flatMap((n) => outputProblems(n.description));
|
|
64
|
+
}
|
|
65
|
+
|
|
31
66
|
// src/images.ts
|
|
32
67
|
var fs = __toESM(require("fs"), 1);
|
|
33
68
|
var import_node_path = require("path");
|
|
@@ -174,6 +209,9 @@ async function runManifest() {
|
|
|
174
209
|
'\u26A0 package.json has no "revenexx.displayName" \u2014 the node palette will fall back to the raw package name.'
|
|
175
210
|
);
|
|
176
211
|
}
|
|
212
|
+
for (const w of outputWarnings(mod.NODES)) {
|
|
213
|
+
console.warn(`\u26A0 ${w} This will be an error in the next major version.`);
|
|
214
|
+
}
|
|
177
215
|
const manifest = buildManifest(mod.NODES, credentials, templates);
|
|
178
216
|
const outDir = (0, import_node_path2.resolve)(projectRoot, "dist");
|
|
179
217
|
const outFile = (0, import_node_path2.resolve)(outDir, "manifest.json");
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
buildManifest,
|
|
4
|
+
outputWarnings,
|
|
4
5
|
packageIconProblem,
|
|
5
6
|
parsePackageMeta
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-XO73F62E.js";
|
|
7
8
|
|
|
8
9
|
// src/cli.ts
|
|
9
10
|
import * as fs2 from "fs";
|
|
@@ -107,6 +108,9 @@ async function runManifest() {
|
|
|
107
108
|
'\u26A0 package.json has no "revenexx.displayName" \u2014 the node palette will fall back to the raw package name.'
|
|
108
109
|
);
|
|
109
110
|
}
|
|
111
|
+
for (const w of outputWarnings(mod.NODES)) {
|
|
112
|
+
console.warn(`\u26A0 ${w} This will be an error in the next major version.`);
|
|
113
|
+
}
|
|
110
114
|
const manifest = buildManifest(mod.NODES, credentials, templates);
|
|
111
115
|
const outDir = resolve2(projectRoot, "dist");
|
|
112
116
|
const outFile = resolve2(outDir, "manifest.json");
|
package/dist/errors.d.ts
CHANGED
|
@@ -3,3 +3,24 @@ export declare class NodeError extends Error {
|
|
|
3
3
|
readonly meta?: Record<string, unknown>;
|
|
4
4
|
constructor(code: string, message: string, meta?: Record<string, unknown>);
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* The two codes a step fails with when the VPN route it was configured with
|
|
8
|
+
* cannot be used (PO-825). Both are failures of the world around the run, not of
|
|
9
|
+
* the data a record carried, and neither is retried by the platform.
|
|
10
|
+
*
|
|
11
|
+
* - `VPN_UNAVAILABLE` — the connection exists but is not `active`;
|
|
12
|
+
* message `VPN connection <name> is <status>`.
|
|
13
|
+
* - `VPN_ROUTE_MISSING` — the connection or the route no longer exists;
|
|
14
|
+
* message `VPN route <connection>/<route> no longer exists`.
|
|
15
|
+
*/
|
|
16
|
+
export declare const VPN_UNAVAILABLE: 'VPN_UNAVAILABLE';
|
|
17
|
+
export declare const VPN_ROUTE_MISSING: 'VPN_ROUTE_MISSING';
|
|
18
|
+
/** Every VPN failure code, for a reader that has to recognise one. */
|
|
19
|
+
export declare const VPN_ERROR_CODES: readonly ["VPN_UNAVAILABLE", "VPN_ROUTE_MISSING"];
|
|
20
|
+
/** One of {@link VPN_ERROR_CODES}. */
|
|
21
|
+
export type VpnErrorCode = (typeof VPN_ERROR_CODES)[number];
|
|
22
|
+
/**
|
|
23
|
+
* Whether `code` is a VPN failure code, narrowing it. `VPN_ERROR_CODES.includes`
|
|
24
|
+
* does not accept a plain `string`, which is what `NodeError.code` is.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isVpnErrorCode(code: string): code is VpnErrorCode;
|
package/dist/index.cjs
CHANGED
|
@@ -50,6 +50,11 @@ __export(index_exports, {
|
|
|
50
50
|
RetryableError: () => RetryableError,
|
|
51
51
|
SimpleValueCredential: () => SimpleValueCredential,
|
|
52
52
|
VALUELESS_OPERATORS: () => VALUELESS_OPERATORS,
|
|
53
|
+
VPN_ERROR_CODES: () => VPN_ERROR_CODES,
|
|
54
|
+
VPN_ROUTE_MISSING: () => VPN_ROUTE_MISSING,
|
|
55
|
+
VPN_UNAVAILABLE: () => VPN_UNAVAILABLE,
|
|
56
|
+
allowedTargets: () => allowedTargets,
|
|
57
|
+
assertBranchNamed: () => assertBranchNamed,
|
|
53
58
|
assertPublicUrl: () => assertPublicUrl,
|
|
54
59
|
backoffDelay: () => backoffDelay,
|
|
55
60
|
buildManifest: () => buildManifest,
|
|
@@ -63,13 +68,19 @@ __export(index_exports, {
|
|
|
63
68
|
extractManifests: () => extractManifests,
|
|
64
69
|
headerCrossesOrigin: () => headerCrossesOrigin,
|
|
65
70
|
httpErrorResult: () => httpErrorResult,
|
|
71
|
+
installAllowedTargets: () => installAllowedTargets,
|
|
72
|
+
isAllowedTarget: () => isAllowedTarget,
|
|
66
73
|
isBlockedAddress: () => isBlockedAddress,
|
|
74
|
+
isMultiPort: () => isMultiPort,
|
|
67
75
|
isNodeWithIteration: () => isNodeWithIteration,
|
|
68
76
|
isOAuthAuthorizeCredential: () => isOAuthAuthorizeCredential,
|
|
69
77
|
isOperator: () => isOperator,
|
|
78
|
+
isVpnErrorCode: () => isVpnErrorCode,
|
|
70
79
|
maxBytesConfigField: () => maxBytesConfigField,
|
|
71
80
|
normalizeCredentialType: () => normalizeCredentialType,
|
|
72
81
|
normalizeLocalized: () => normalizeLocalized,
|
|
82
|
+
outputProblems: () => outputProblems,
|
|
83
|
+
outputWarnings: () => outputWarnings,
|
|
73
84
|
packageIconProblem: () => packageIconProblem,
|
|
74
85
|
parsePackageMeta: () => parsePackageMeta,
|
|
75
86
|
readArrayBuffer: () => readArrayBuffer,
|
|
@@ -209,6 +220,12 @@ var NodeError = class extends Error {
|
|
|
209
220
|
this.meta = meta;
|
|
210
221
|
}
|
|
211
222
|
};
|
|
223
|
+
var VPN_UNAVAILABLE = "VPN_UNAVAILABLE";
|
|
224
|
+
var VPN_ROUTE_MISSING = "VPN_ROUTE_MISSING";
|
|
225
|
+
var VPN_ERROR_CODES = Object.freeze([VPN_UNAVAILABLE, VPN_ROUTE_MISSING]);
|
|
226
|
+
function isVpnErrorCode(code) {
|
|
227
|
+
return VPN_ERROR_CODES.includes(code);
|
|
228
|
+
}
|
|
212
229
|
|
|
213
230
|
// src/nodeErrors.ts
|
|
214
231
|
function toErrorOutput(err) {
|
|
@@ -294,10 +311,184 @@ function extractCredentialManifests(credentials) {
|
|
|
294
311
|
return credentials.map(extractCredentialManifest);
|
|
295
312
|
}
|
|
296
313
|
|
|
314
|
+
// src/branches.ts
|
|
315
|
+
function isMultiPort(outputs) {
|
|
316
|
+
if (!outputs) return false;
|
|
317
|
+
return outputs.length > 1 || outputs.some((p) => p.resolveOutputs === true);
|
|
318
|
+
}
|
|
319
|
+
var isGenerated = (p) => p.sourceFromConfig !== void 0 || p.resolveOutputs === true;
|
|
320
|
+
function outputProblems(description) {
|
|
321
|
+
const outputs = description.outputs ?? [];
|
|
322
|
+
if (!isMultiPort(outputs)) return [];
|
|
323
|
+
const problems = [];
|
|
324
|
+
const seen = /* @__PURE__ */ new Set();
|
|
325
|
+
for (const [i, port] of outputs.entries()) {
|
|
326
|
+
if (isGenerated(port)) continue;
|
|
327
|
+
if (port.name === void 0 || port.name === "") {
|
|
328
|
+
problems.push(
|
|
329
|
+
`${description.slug}: output ${i + 1} has no name, so a branch cannot choose it \u2014 a node with several outputs must name each one.`
|
|
330
|
+
);
|
|
331
|
+
} else if (seen.has(port.name)) {
|
|
332
|
+
problems.push(`${description.slug}: output name "${port.name}" is declared twice.`);
|
|
333
|
+
} else {
|
|
334
|
+
seen.add(port.name);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
for (const port of outputs) {
|
|
338
|
+
const fb = port.fallback?.name;
|
|
339
|
+
if (isGenerated(port) && fb !== void 0 && seen.has(fb)) {
|
|
340
|
+
problems.push(`${description.slug}: the fallback name "${fb}" is also the name of a fixed output, so a branch "${fb}" is ambiguous.`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return problems;
|
|
344
|
+
}
|
|
345
|
+
function outputWarnings(nodes) {
|
|
346
|
+
return nodes.flatMap((n) => outputProblems(n.description));
|
|
347
|
+
}
|
|
348
|
+
function assertBranchNamed(node, result, config) {
|
|
349
|
+
const description = "execute" in node ? node.description : node;
|
|
350
|
+
const outputs = description.outputs ?? [];
|
|
351
|
+
if (!isMultiPort(outputs)) return;
|
|
352
|
+
const branch = result.branch;
|
|
353
|
+
if (typeof branch !== "string" || branch === "") {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`${description.slug} has several outputs, so its result must name the branch it took; without it every output fires.`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (outputs.some((p) => !isGenerated(p) && p.name === branch)) return;
|
|
359
|
+
const producible = outputs.some((p) => {
|
|
360
|
+
if (p.resolveOutputs === true) return true;
|
|
361
|
+
if (p.sourceFromConfig === void 0) return false;
|
|
362
|
+
if (p.fallback?.name === branch) return true;
|
|
363
|
+
return config === void 0 || config[p.sourceFromConfig] === branch;
|
|
364
|
+
});
|
|
365
|
+
if (!producible) {
|
|
366
|
+
const names = outputs.map((p) => p.name ?? p.fallback?.name).filter((n) => n);
|
|
367
|
+
throw new Error(
|
|
368
|
+
`${description.slug} returned branch "${branch}", which none of its outputs can produce (${names.join(", ")}).`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
297
373
|
// src/ssrf.ts
|
|
298
374
|
var import_node_diagnostics_channel = require("diagnostics_channel");
|
|
375
|
+
var import_node_dns = __toESM(require("dns"), 1);
|
|
376
|
+
var import_node_net2 = require("net");
|
|
377
|
+
var import_ipaddr2 = __toESM(require("ipaddr.js"), 1);
|
|
378
|
+
var import_undici = require("undici");
|
|
379
|
+
|
|
380
|
+
// src/allowedTargets.ts
|
|
299
381
|
var import_node_net = require("net");
|
|
300
382
|
var import_ipaddr = __toESM(require("ipaddr.js"), 1);
|
|
383
|
+
var ALLOWED_TARGETS_SYMBOL = /* @__PURE__ */ Symbol.for("revenexx.ssrf.allowedTargets");
|
|
384
|
+
var EMPTY = Object.freeze([]);
|
|
385
|
+
var V4_ENTRY = /^(\d{1,3}(?:\.\d{1,3}){3}):(\d{1,5})$/;
|
|
386
|
+
var V6_ENTRY = /^\[([0-9A-Fa-f:.]+)\]:(\d{1,5})$/;
|
|
387
|
+
function canonicalAddress(ip) {
|
|
388
|
+
const bare = ip.startsWith("[") && ip.endsWith("]") ? ip.slice(1, -1) : ip;
|
|
389
|
+
if ((0, import_node_net.isIP)(bare) === 0) return void 0;
|
|
390
|
+
try {
|
|
391
|
+
return import_ipaddr.default.process(bare).toString();
|
|
392
|
+
} catch {
|
|
393
|
+
return void 0;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function validPort(port) {
|
|
397
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
398
|
+
}
|
|
399
|
+
var TARGET_RANGES = /* @__PURE__ */ new Set(["private", "uniqueLocal"]);
|
|
400
|
+
function loopbackTargetsAllowed() {
|
|
401
|
+
const raw = process.env["RVNXX_SSRF_TARGETS_ALLOW_LOOPBACK"];
|
|
402
|
+
return raw != null && ["1", "true", "yes"].includes(raw.trim().toLowerCase());
|
|
403
|
+
}
|
|
404
|
+
function parseTarget(entry, allowLoopback) {
|
|
405
|
+
if (typeof entry !== "string") return void 0;
|
|
406
|
+
const v4 = V4_ENTRY.exec(entry);
|
|
407
|
+
const m = v4 ?? V6_ENTRY.exec(entry);
|
|
408
|
+
if (!m) return void 0;
|
|
409
|
+
const host = m[1];
|
|
410
|
+
const port = Number(m[2]);
|
|
411
|
+
if (v4 && !import_ipaddr.default.IPv4.isValidFourPartDecimal(host)) return void 0;
|
|
412
|
+
if (!validPort(port)) return void 0;
|
|
413
|
+
const address = canonicalAddress(host);
|
|
414
|
+
if (address === void 0) return void 0;
|
|
415
|
+
const range = import_ipaddr.default.parse(address).range();
|
|
416
|
+
if (!TARGET_RANGES.has(range) && !(allowLoopback && range === "loopback")) return void 0;
|
|
417
|
+
return `${address}:${port}`;
|
|
418
|
+
}
|
|
419
|
+
var allowListStats = { validations: 0 };
|
|
420
|
+
var keysByList = /* @__PURE__ */ new WeakMap();
|
|
421
|
+
function keysOf(list) {
|
|
422
|
+
const allowLoopback = loopbackTargetsAllowed();
|
|
423
|
+
let byMode = keysByList.get(list);
|
|
424
|
+
if (byMode === void 0) {
|
|
425
|
+
byMode = /* @__PURE__ */ new Map();
|
|
426
|
+
keysByList.set(list, byMode);
|
|
427
|
+
}
|
|
428
|
+
const cached = byMode.get(allowLoopback);
|
|
429
|
+
if (cached !== void 0) return cached;
|
|
430
|
+
allowListStats.validations++;
|
|
431
|
+
const keys = /* @__PURE__ */ new Set();
|
|
432
|
+
let result = keys;
|
|
433
|
+
for (const entry of list) {
|
|
434
|
+
const key = parseTarget(entry, allowLoopback);
|
|
435
|
+
if (key === void 0) {
|
|
436
|
+
result = null;
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
keys.add(key);
|
|
440
|
+
}
|
|
441
|
+
byMode.set(allowLoopback, result);
|
|
442
|
+
return result;
|
|
443
|
+
}
|
|
444
|
+
function installedList(holder) {
|
|
445
|
+
const descriptor = Object.getOwnPropertyDescriptor(holder, ALLOWED_TARGETS_SYMBOL);
|
|
446
|
+
if (descriptor === void 0 || !("value" in descriptor)) return null;
|
|
447
|
+
if (descriptor.writable || descriptor.configurable) return null;
|
|
448
|
+
const value = descriptor.value;
|
|
449
|
+
if (!Array.isArray(value) || !Object.isFrozen(value)) return null;
|
|
450
|
+
const keys = keysOf(value);
|
|
451
|
+
return keys === null ? null : { list: value, keys };
|
|
452
|
+
}
|
|
453
|
+
function readAllowedTargets(holder) {
|
|
454
|
+
return installedList(holder)?.list ?? EMPTY;
|
|
455
|
+
}
|
|
456
|
+
function isAllowedTargetOn(holder, ip, port) {
|
|
457
|
+
const installed = installedList(holder);
|
|
458
|
+
if (installed === null || !validPort(port)) return false;
|
|
459
|
+
const address = canonicalAddress(ip);
|
|
460
|
+
return address !== void 0 && installed.keys.has(`${address}:${port}`);
|
|
461
|
+
}
|
|
462
|
+
function installAllowedTargetsOn(holder, targets) {
|
|
463
|
+
if (Object.getOwnPropertyDescriptor(holder, ALLOWED_TARGETS_SYMBOL) !== void 0) {
|
|
464
|
+
throw new Error("The SSRF allow-list is already installed, and is installed once per process");
|
|
465
|
+
}
|
|
466
|
+
if (!Array.isArray(targets)) throw new TypeError('The SSRF allow-list must be an array of "<ip>:<port>" strings');
|
|
467
|
+
for (const entry of targets) {
|
|
468
|
+
if (parseTarget(entry, loopbackTargetsAllowed()) === void 0) {
|
|
469
|
+
throw new TypeError(
|
|
470
|
+
`Not an allowed target: ${JSON.stringify(entry)} (expected "<ip>:<port>" or "[v6]:<port>" at an RFC 1918 or unique-local address)`
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
Object.defineProperty(holder, ALLOWED_TARGETS_SYMBOL, {
|
|
475
|
+
value: Object.freeze([...targets]),
|
|
476
|
+
writable: false,
|
|
477
|
+
configurable: false,
|
|
478
|
+
enumerable: false
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
function installAllowedTargets(targets) {
|
|
482
|
+
installAllowedTargetsOn(globalThis, targets);
|
|
483
|
+
}
|
|
484
|
+
function allowedTargets() {
|
|
485
|
+
return readAllowedTargets(globalThis);
|
|
486
|
+
}
|
|
487
|
+
function isAllowedTarget(ip, port) {
|
|
488
|
+
return isAllowedTargetOn(globalThis, ip, port);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/ssrf.ts
|
|
301
492
|
var ssrfResolver = {
|
|
302
493
|
lookup: async (hostname) => {
|
|
303
494
|
const { lookup } = await import("dns/promises");
|
|
@@ -309,15 +500,15 @@ function isIPv6(addr) {
|
|
|
309
500
|
return addr.kind() === "ipv6";
|
|
310
501
|
}
|
|
311
502
|
var PUBLIC_RANGE = "unicast";
|
|
312
|
-
var GLOBAL_UNICAST_V6 =
|
|
503
|
+
var GLOBAL_UNICAST_V6 = import_ipaddr2.default.parseCIDR("2000::/3");
|
|
313
504
|
var V4_EMBEDDING_RANGES = /* @__PURE__ */ new Set(["ipv4Mapped", "rfc6052"]);
|
|
314
505
|
function embeddedIpv4(addr) {
|
|
315
|
-
return
|
|
506
|
+
return import_ipaddr2.default.fromByteArray(addr.toByteArray().slice(12));
|
|
316
507
|
}
|
|
317
508
|
function isBlockedAddress(ip) {
|
|
318
509
|
let addr;
|
|
319
510
|
try {
|
|
320
|
-
addr =
|
|
511
|
+
addr = import_ipaddr2.default.parse(ip);
|
|
321
512
|
} catch {
|
|
322
513
|
return true;
|
|
323
514
|
}
|
|
@@ -403,8 +594,73 @@ function judgeConnection(message) {
|
|
|
403
594
|
if (guardRelaxedForLocalDev()) return;
|
|
404
595
|
const peer = socket.remoteAddress;
|
|
405
596
|
if (peer != null && !isBlockedAddress(peer)) return;
|
|
597
|
+
const remotePort = socket.remotePort;
|
|
598
|
+
if (peer != null && allowedTargets().length > 0 && !Number.isInteger(remotePort)) {
|
|
599
|
+
reportUnreadablePeerPort();
|
|
600
|
+
socket.destroy(unreadablePortError(normalizeHost(hostname)));
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (peer != null && isAllowedTarget(peer, remotePort)) return;
|
|
406
604
|
socket.destroy(connectBlockedError(normalizeHost(hostname), peer));
|
|
407
605
|
}
|
|
606
|
+
var unreadablePeerPortLogged = false;
|
|
607
|
+
function reportUnreadablePeerPort() {
|
|
608
|
+
if (unreadablePeerPortLogged) return;
|
|
609
|
+
unreadablePeerPortLogged = true;
|
|
610
|
+
console.warn(
|
|
611
|
+
"[ssrf] a connection reported no remote port: the SSRF allow-list cannot be consulted, so private peers are refused."
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
function unreadablePortError(host) {
|
|
615
|
+
return new NodeError(
|
|
616
|
+
"BLOCKED_ADDRESS",
|
|
617
|
+
`Blocked request to ${host}: the port the connection landed on could not be read, so the allow-list could not be consulted`,
|
|
618
|
+
{ status: 0 }
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
function judgingLookup(port) {
|
|
622
|
+
return (hostname, options, callback) => {
|
|
623
|
+
import_node_dns.default.lookup(hostname, { ...options, all: true }, (err, addresses) => {
|
|
624
|
+
if (err) {
|
|
625
|
+
callback(err, "");
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (addresses.length === 0) {
|
|
629
|
+
callback(new NodeError("BLOCKED_ADDRESS", `Could not resolve host: ${hostname}`, { status: 0 }), "");
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (!guardRelaxedForLocalDev()) {
|
|
633
|
+
for (const a of addresses) {
|
|
634
|
+
if (isBlockedAddress(a.address) && !isAllowedTarget(a.address, port)) {
|
|
635
|
+
callback(blockedError(normalizeHost(hostname), a.address), "");
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (options.all) callback(null, addresses);
|
|
641
|
+
else callback(null, addresses[0].address, addresses[0].family);
|
|
642
|
+
});
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
var connectors = /* @__PURE__ */ new Map();
|
|
646
|
+
function connectorFor(port) {
|
|
647
|
+
let connector = connectors.get(port);
|
|
648
|
+
if (connector === void 0) {
|
|
649
|
+
connector = (0, import_undici.buildConnector)({ lookup: judgingLookup(port) });
|
|
650
|
+
connectors.set(port, connector);
|
|
651
|
+
}
|
|
652
|
+
return connector;
|
|
653
|
+
}
|
|
654
|
+
var dispatcher;
|
|
655
|
+
function guardedDispatcher() {
|
|
656
|
+
dispatcher ??= new import_undici.Agent({
|
|
657
|
+
connect: (opts, callback) => {
|
|
658
|
+
const port = opts.port === "" ? opts.protocol === "https:" ? 443 : 80 : Number(opts.port);
|
|
659
|
+
return connectorFor(port)(opts, callback);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
return dispatcher;
|
|
663
|
+
}
|
|
408
664
|
function guardConnectionsTo(url) {
|
|
409
665
|
if (!connectGuardInstalled) {
|
|
410
666
|
connectGuardInstalled = true;
|
|
@@ -457,8 +713,10 @@ async function assertPublicUrl(url, opts = {}) {
|
|
|
457
713
|
if (lower === "localhost" || lower.endsWith(".localhost")) {
|
|
458
714
|
throw new NodeError("BLOCKED_ADDRESS", `Blocked loopback host: ${host}`, { status: 0 });
|
|
459
715
|
}
|
|
460
|
-
|
|
461
|
-
|
|
716
|
+
const port = u.port === "" ? u.protocol === "https:" ? 443 : 80 : Number(u.port);
|
|
717
|
+
const refused = (address) => isBlockedAddress(address) && !isAllowedTarget(address, port);
|
|
718
|
+
if ((0, import_node_net2.isIP)(host) !== 0) {
|
|
719
|
+
if (refused(host)) throw blockedError(host, host);
|
|
462
720
|
return;
|
|
463
721
|
}
|
|
464
722
|
const lookup = opts.lookup ?? ssrfResolver.lookup;
|
|
@@ -467,7 +725,7 @@ async function assertPublicUrl(url, opts = {}) {
|
|
|
467
725
|
throw new NodeError("BLOCKED_ADDRESS", `Could not resolve host: ${host}`, { status: 0 });
|
|
468
726
|
}
|
|
469
727
|
for (const a of addresses) {
|
|
470
|
-
if (
|
|
728
|
+
if (refused(a.address)) throw blockedError(host, a.address);
|
|
471
729
|
}
|
|
472
730
|
}
|
|
473
731
|
|
|
@@ -567,7 +825,7 @@ async function timedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
|
|
|
567
825
|
const timer = setTimeout(() => ac.abort(), effectiveMs);
|
|
568
826
|
const signal = ctxSignal ? AbortSignal.any([ctxSignal, ac.signal]) : ac.signal;
|
|
569
827
|
try {
|
|
570
|
-
return await fetch(url, { ...fetchOptions, signal });
|
|
828
|
+
return await fetch(url, { ...fetchOptions, signal, dispatcher: guardedDispatcher() });
|
|
571
829
|
} catch (err) {
|
|
572
830
|
const blocked = connectionRefusal(err);
|
|
573
831
|
if (blocked) throw blocked;
|
|
@@ -1235,6 +1493,11 @@ function base64Url(buf) {
|
|
|
1235
1493
|
RetryableError,
|
|
1236
1494
|
SimpleValueCredential,
|
|
1237
1495
|
VALUELESS_OPERATORS,
|
|
1496
|
+
VPN_ERROR_CODES,
|
|
1497
|
+
VPN_ROUTE_MISSING,
|
|
1498
|
+
VPN_UNAVAILABLE,
|
|
1499
|
+
allowedTargets,
|
|
1500
|
+
assertBranchNamed,
|
|
1238
1501
|
assertPublicUrl,
|
|
1239
1502
|
backoffDelay,
|
|
1240
1503
|
buildManifest,
|
|
@@ -1248,13 +1511,19 @@ function base64Url(buf) {
|
|
|
1248
1511
|
extractManifests,
|
|
1249
1512
|
headerCrossesOrigin,
|
|
1250
1513
|
httpErrorResult,
|
|
1514
|
+
installAllowedTargets,
|
|
1515
|
+
isAllowedTarget,
|
|
1251
1516
|
isBlockedAddress,
|
|
1517
|
+
isMultiPort,
|
|
1252
1518
|
isNodeWithIteration,
|
|
1253
1519
|
isOAuthAuthorizeCredential,
|
|
1254
1520
|
isOperator,
|
|
1521
|
+
isVpnErrorCode,
|
|
1255
1522
|
maxBytesConfigField,
|
|
1256
1523
|
normalizeCredentialType,
|
|
1257
1524
|
normalizeLocalized,
|
|
1525
|
+
outputProblems,
|
|
1526
|
+
outputWarnings,
|
|
1258
1527
|
packageIconProblem,
|
|
1259
1528
|
parsePackageMeta,
|
|
1260
1529
|
readArrayBuffer,
|
package/dist/index.d.ts
CHANGED
|
@@ -6,9 +6,11 @@ export * from './credentialType.js';
|
|
|
6
6
|
export * from './errors.js';
|
|
7
7
|
export * from './nodeErrors.js';
|
|
8
8
|
export * from './extract.js';
|
|
9
|
+
export * from './branches.js';
|
|
9
10
|
export * from './fetch.js';
|
|
10
11
|
export { assertPublicUrl, isBlockedAddress } from './ssrf.js';
|
|
11
12
|
export type { LookupAddress, LookupFn } from './ssrf.js';
|
|
13
|
+
export { allowedTargets, installAllowedTargets, isAllowedTarget } from './allowedTargets.js';
|
|
12
14
|
export * from './retry.js';
|
|
13
15
|
export * from './manifest.js';
|
|
14
16
|
export * from './credentials.js';
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MANIFEST_VERSION,
|
|
3
|
+
assertBranchNamed,
|
|
3
4
|
buildManifest,
|
|
4
5
|
extractCredentialManifest,
|
|
5
6
|
extractCredentialManifests,
|
|
6
7
|
extractManifest,
|
|
7
8
|
extractManifests,
|
|
9
|
+
isMultiPort,
|
|
10
|
+
outputProblems,
|
|
11
|
+
outputWarnings,
|
|
8
12
|
packageIconProblem,
|
|
9
13
|
parsePackageMeta
|
|
10
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-XO73F62E.js";
|
|
11
15
|
|
|
12
16
|
// src/types.ts
|
|
13
17
|
function isNodeWithIteration(node) {
|
|
@@ -131,6 +135,12 @@ var NodeError = class extends Error {
|
|
|
131
135
|
this.meta = meta;
|
|
132
136
|
}
|
|
133
137
|
};
|
|
138
|
+
var VPN_UNAVAILABLE = "VPN_UNAVAILABLE";
|
|
139
|
+
var VPN_ROUTE_MISSING = "VPN_ROUTE_MISSING";
|
|
140
|
+
var VPN_ERROR_CODES = Object.freeze([VPN_UNAVAILABLE, VPN_ROUTE_MISSING]);
|
|
141
|
+
function isVpnErrorCode(code) {
|
|
142
|
+
return VPN_ERROR_CODES.includes(code);
|
|
143
|
+
}
|
|
134
144
|
|
|
135
145
|
// src/nodeErrors.ts
|
|
136
146
|
function toErrorOutput(err) {
|
|
@@ -204,8 +214,123 @@ function errorPort(opts = {}) {
|
|
|
204
214
|
|
|
205
215
|
// src/ssrf.ts
|
|
206
216
|
import { subscribe } from "diagnostics_channel";
|
|
217
|
+
import dns from "dns";
|
|
218
|
+
import { isIP as isIP2 } from "net";
|
|
219
|
+
import ipaddr2 from "ipaddr.js";
|
|
220
|
+
import { Agent, buildConnector } from "undici";
|
|
221
|
+
|
|
222
|
+
// src/allowedTargets.ts
|
|
207
223
|
import { isIP } from "net";
|
|
208
224
|
import ipaddr from "ipaddr.js";
|
|
225
|
+
var ALLOWED_TARGETS_SYMBOL = /* @__PURE__ */ Symbol.for("revenexx.ssrf.allowedTargets");
|
|
226
|
+
var EMPTY = Object.freeze([]);
|
|
227
|
+
var V4_ENTRY = /^(\d{1,3}(?:\.\d{1,3}){3}):(\d{1,5})$/;
|
|
228
|
+
var V6_ENTRY = /^\[([0-9A-Fa-f:.]+)\]:(\d{1,5})$/;
|
|
229
|
+
function canonicalAddress(ip) {
|
|
230
|
+
const bare = ip.startsWith("[") && ip.endsWith("]") ? ip.slice(1, -1) : ip;
|
|
231
|
+
if (isIP(bare) === 0) return void 0;
|
|
232
|
+
try {
|
|
233
|
+
return ipaddr.process(bare).toString();
|
|
234
|
+
} catch {
|
|
235
|
+
return void 0;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function validPort(port) {
|
|
239
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
240
|
+
}
|
|
241
|
+
var TARGET_RANGES = /* @__PURE__ */ new Set(["private", "uniqueLocal"]);
|
|
242
|
+
function loopbackTargetsAllowed() {
|
|
243
|
+
const raw = process.env["RVNXX_SSRF_TARGETS_ALLOW_LOOPBACK"];
|
|
244
|
+
return raw != null && ["1", "true", "yes"].includes(raw.trim().toLowerCase());
|
|
245
|
+
}
|
|
246
|
+
function parseTarget(entry, allowLoopback) {
|
|
247
|
+
if (typeof entry !== "string") return void 0;
|
|
248
|
+
const v4 = V4_ENTRY.exec(entry);
|
|
249
|
+
const m = v4 ?? V6_ENTRY.exec(entry);
|
|
250
|
+
if (!m) return void 0;
|
|
251
|
+
const host = m[1];
|
|
252
|
+
const port = Number(m[2]);
|
|
253
|
+
if (v4 && !ipaddr.IPv4.isValidFourPartDecimal(host)) return void 0;
|
|
254
|
+
if (!validPort(port)) return void 0;
|
|
255
|
+
const address = canonicalAddress(host);
|
|
256
|
+
if (address === void 0) return void 0;
|
|
257
|
+
const range = ipaddr.parse(address).range();
|
|
258
|
+
if (!TARGET_RANGES.has(range) && !(allowLoopback && range === "loopback")) return void 0;
|
|
259
|
+
return `${address}:${port}`;
|
|
260
|
+
}
|
|
261
|
+
var allowListStats = { validations: 0 };
|
|
262
|
+
var keysByList = /* @__PURE__ */ new WeakMap();
|
|
263
|
+
function keysOf(list) {
|
|
264
|
+
const allowLoopback = loopbackTargetsAllowed();
|
|
265
|
+
let byMode = keysByList.get(list);
|
|
266
|
+
if (byMode === void 0) {
|
|
267
|
+
byMode = /* @__PURE__ */ new Map();
|
|
268
|
+
keysByList.set(list, byMode);
|
|
269
|
+
}
|
|
270
|
+
const cached = byMode.get(allowLoopback);
|
|
271
|
+
if (cached !== void 0) return cached;
|
|
272
|
+
allowListStats.validations++;
|
|
273
|
+
const keys = /* @__PURE__ */ new Set();
|
|
274
|
+
let result = keys;
|
|
275
|
+
for (const entry of list) {
|
|
276
|
+
const key = parseTarget(entry, allowLoopback);
|
|
277
|
+
if (key === void 0) {
|
|
278
|
+
result = null;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
keys.add(key);
|
|
282
|
+
}
|
|
283
|
+
byMode.set(allowLoopback, result);
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
function installedList(holder) {
|
|
287
|
+
const descriptor = Object.getOwnPropertyDescriptor(holder, ALLOWED_TARGETS_SYMBOL);
|
|
288
|
+
if (descriptor === void 0 || !("value" in descriptor)) return null;
|
|
289
|
+
if (descriptor.writable || descriptor.configurable) return null;
|
|
290
|
+
const value = descriptor.value;
|
|
291
|
+
if (!Array.isArray(value) || !Object.isFrozen(value)) return null;
|
|
292
|
+
const keys = keysOf(value);
|
|
293
|
+
return keys === null ? null : { list: value, keys };
|
|
294
|
+
}
|
|
295
|
+
function readAllowedTargets(holder) {
|
|
296
|
+
return installedList(holder)?.list ?? EMPTY;
|
|
297
|
+
}
|
|
298
|
+
function isAllowedTargetOn(holder, ip, port) {
|
|
299
|
+
const installed = installedList(holder);
|
|
300
|
+
if (installed === null || !validPort(port)) return false;
|
|
301
|
+
const address = canonicalAddress(ip);
|
|
302
|
+
return address !== void 0 && installed.keys.has(`${address}:${port}`);
|
|
303
|
+
}
|
|
304
|
+
function installAllowedTargetsOn(holder, targets) {
|
|
305
|
+
if (Object.getOwnPropertyDescriptor(holder, ALLOWED_TARGETS_SYMBOL) !== void 0) {
|
|
306
|
+
throw new Error("The SSRF allow-list is already installed, and is installed once per process");
|
|
307
|
+
}
|
|
308
|
+
if (!Array.isArray(targets)) throw new TypeError('The SSRF allow-list must be an array of "<ip>:<port>" strings');
|
|
309
|
+
for (const entry of targets) {
|
|
310
|
+
if (parseTarget(entry, loopbackTargetsAllowed()) === void 0) {
|
|
311
|
+
throw new TypeError(
|
|
312
|
+
`Not an allowed target: ${JSON.stringify(entry)} (expected "<ip>:<port>" or "[v6]:<port>" at an RFC 1918 or unique-local address)`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
Object.defineProperty(holder, ALLOWED_TARGETS_SYMBOL, {
|
|
317
|
+
value: Object.freeze([...targets]),
|
|
318
|
+
writable: false,
|
|
319
|
+
configurable: false,
|
|
320
|
+
enumerable: false
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
function installAllowedTargets(targets) {
|
|
324
|
+
installAllowedTargetsOn(globalThis, targets);
|
|
325
|
+
}
|
|
326
|
+
function allowedTargets() {
|
|
327
|
+
return readAllowedTargets(globalThis);
|
|
328
|
+
}
|
|
329
|
+
function isAllowedTarget(ip, port) {
|
|
330
|
+
return isAllowedTargetOn(globalThis, ip, port);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/ssrf.ts
|
|
209
334
|
var ssrfResolver = {
|
|
210
335
|
lookup: async (hostname) => {
|
|
211
336
|
const { lookup } = await import("dns/promises");
|
|
@@ -217,15 +342,15 @@ function isIPv6(addr) {
|
|
|
217
342
|
return addr.kind() === "ipv6";
|
|
218
343
|
}
|
|
219
344
|
var PUBLIC_RANGE = "unicast";
|
|
220
|
-
var GLOBAL_UNICAST_V6 =
|
|
345
|
+
var GLOBAL_UNICAST_V6 = ipaddr2.parseCIDR("2000::/3");
|
|
221
346
|
var V4_EMBEDDING_RANGES = /* @__PURE__ */ new Set(["ipv4Mapped", "rfc6052"]);
|
|
222
347
|
function embeddedIpv4(addr) {
|
|
223
|
-
return
|
|
348
|
+
return ipaddr2.fromByteArray(addr.toByteArray().slice(12));
|
|
224
349
|
}
|
|
225
350
|
function isBlockedAddress(ip) {
|
|
226
351
|
let addr;
|
|
227
352
|
try {
|
|
228
|
-
addr =
|
|
353
|
+
addr = ipaddr2.parse(ip);
|
|
229
354
|
} catch {
|
|
230
355
|
return true;
|
|
231
356
|
}
|
|
@@ -311,8 +436,73 @@ function judgeConnection(message) {
|
|
|
311
436
|
if (guardRelaxedForLocalDev()) return;
|
|
312
437
|
const peer = socket.remoteAddress;
|
|
313
438
|
if (peer != null && !isBlockedAddress(peer)) return;
|
|
439
|
+
const remotePort = socket.remotePort;
|
|
440
|
+
if (peer != null && allowedTargets().length > 0 && !Number.isInteger(remotePort)) {
|
|
441
|
+
reportUnreadablePeerPort();
|
|
442
|
+
socket.destroy(unreadablePortError(normalizeHost(hostname)));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (peer != null && isAllowedTarget(peer, remotePort)) return;
|
|
314
446
|
socket.destroy(connectBlockedError(normalizeHost(hostname), peer));
|
|
315
447
|
}
|
|
448
|
+
var unreadablePeerPortLogged = false;
|
|
449
|
+
function reportUnreadablePeerPort() {
|
|
450
|
+
if (unreadablePeerPortLogged) return;
|
|
451
|
+
unreadablePeerPortLogged = true;
|
|
452
|
+
console.warn(
|
|
453
|
+
"[ssrf] a connection reported no remote port: the SSRF allow-list cannot be consulted, so private peers are refused."
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
function unreadablePortError(host) {
|
|
457
|
+
return new NodeError(
|
|
458
|
+
"BLOCKED_ADDRESS",
|
|
459
|
+
`Blocked request to ${host}: the port the connection landed on could not be read, so the allow-list could not be consulted`,
|
|
460
|
+
{ status: 0 }
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
function judgingLookup(port) {
|
|
464
|
+
return (hostname, options, callback) => {
|
|
465
|
+
dns.lookup(hostname, { ...options, all: true }, (err, addresses) => {
|
|
466
|
+
if (err) {
|
|
467
|
+
callback(err, "");
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (addresses.length === 0) {
|
|
471
|
+
callback(new NodeError("BLOCKED_ADDRESS", `Could not resolve host: ${hostname}`, { status: 0 }), "");
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (!guardRelaxedForLocalDev()) {
|
|
475
|
+
for (const a of addresses) {
|
|
476
|
+
if (isBlockedAddress(a.address) && !isAllowedTarget(a.address, port)) {
|
|
477
|
+
callback(blockedError(normalizeHost(hostname), a.address), "");
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (options.all) callback(null, addresses);
|
|
483
|
+
else callback(null, addresses[0].address, addresses[0].family);
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
var connectors = /* @__PURE__ */ new Map();
|
|
488
|
+
function connectorFor(port) {
|
|
489
|
+
let connector = connectors.get(port);
|
|
490
|
+
if (connector === void 0) {
|
|
491
|
+
connector = buildConnector({ lookup: judgingLookup(port) });
|
|
492
|
+
connectors.set(port, connector);
|
|
493
|
+
}
|
|
494
|
+
return connector;
|
|
495
|
+
}
|
|
496
|
+
var dispatcher;
|
|
497
|
+
function guardedDispatcher() {
|
|
498
|
+
dispatcher ??= new Agent({
|
|
499
|
+
connect: (opts, callback) => {
|
|
500
|
+
const port = opts.port === "" ? opts.protocol === "https:" ? 443 : 80 : Number(opts.port);
|
|
501
|
+
return connectorFor(port)(opts, callback);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
return dispatcher;
|
|
505
|
+
}
|
|
316
506
|
function guardConnectionsTo(url) {
|
|
317
507
|
if (!connectGuardInstalled) {
|
|
318
508
|
connectGuardInstalled = true;
|
|
@@ -365,8 +555,10 @@ async function assertPublicUrl(url, opts = {}) {
|
|
|
365
555
|
if (lower === "localhost" || lower.endsWith(".localhost")) {
|
|
366
556
|
throw new NodeError("BLOCKED_ADDRESS", `Blocked loopback host: ${host}`, { status: 0 });
|
|
367
557
|
}
|
|
368
|
-
|
|
369
|
-
|
|
558
|
+
const port = u.port === "" ? u.protocol === "https:" ? 443 : 80 : Number(u.port);
|
|
559
|
+
const refused = (address) => isBlockedAddress(address) && !isAllowedTarget(address, port);
|
|
560
|
+
if (isIP2(host) !== 0) {
|
|
561
|
+
if (refused(host)) throw blockedError(host, host);
|
|
370
562
|
return;
|
|
371
563
|
}
|
|
372
564
|
const lookup = opts.lookup ?? ssrfResolver.lookup;
|
|
@@ -375,7 +567,7 @@ async function assertPublicUrl(url, opts = {}) {
|
|
|
375
567
|
throw new NodeError("BLOCKED_ADDRESS", `Could not resolve host: ${host}`, { status: 0 });
|
|
376
568
|
}
|
|
377
569
|
for (const a of addresses) {
|
|
378
|
-
if (
|
|
570
|
+
if (refused(a.address)) throw blockedError(host, a.address);
|
|
379
571
|
}
|
|
380
572
|
}
|
|
381
573
|
|
|
@@ -475,7 +667,7 @@ async function timedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
|
|
|
475
667
|
const timer = setTimeout(() => ac.abort(), effectiveMs);
|
|
476
668
|
const signal = ctxSignal ? AbortSignal.any([ctxSignal, ac.signal]) : ac.signal;
|
|
477
669
|
try {
|
|
478
|
-
return await fetch(url, { ...fetchOptions, signal });
|
|
670
|
+
return await fetch(url, { ...fetchOptions, signal, dispatcher: guardedDispatcher() });
|
|
479
671
|
} catch (err) {
|
|
480
672
|
const blocked = connectionRefusal(err);
|
|
481
673
|
if (blocked) throw blocked;
|
|
@@ -1107,6 +1299,11 @@ export {
|
|
|
1107
1299
|
RetryableError,
|
|
1108
1300
|
SimpleValueCredential,
|
|
1109
1301
|
VALUELESS_OPERATORS,
|
|
1302
|
+
VPN_ERROR_CODES,
|
|
1303
|
+
VPN_ROUTE_MISSING,
|
|
1304
|
+
VPN_UNAVAILABLE,
|
|
1305
|
+
allowedTargets,
|
|
1306
|
+
assertBranchNamed,
|
|
1110
1307
|
assertPublicUrl,
|
|
1111
1308
|
backoffDelay,
|
|
1112
1309
|
buildManifest,
|
|
@@ -1120,13 +1317,19 @@ export {
|
|
|
1120
1317
|
extractManifests,
|
|
1121
1318
|
headerCrossesOrigin,
|
|
1122
1319
|
httpErrorResult,
|
|
1320
|
+
installAllowedTargets,
|
|
1321
|
+
isAllowedTarget,
|
|
1123
1322
|
isBlockedAddress,
|
|
1323
|
+
isMultiPort,
|
|
1124
1324
|
isNodeWithIteration,
|
|
1125
1325
|
isOAuthAuthorizeCredential,
|
|
1126
1326
|
isOperator,
|
|
1327
|
+
isVpnErrorCode,
|
|
1127
1328
|
maxBytesConfigField,
|
|
1128
1329
|
normalizeCredentialType,
|
|
1129
1330
|
normalizeLocalized,
|
|
1331
|
+
outputProblems,
|
|
1332
|
+
outputWarnings,
|
|
1130
1333
|
packageIconProblem,
|
|
1131
1334
|
parsePackageMeta,
|
|
1132
1335
|
readArrayBuffer,
|
package/dist/ssrf.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Agent } from 'undici';
|
|
1
2
|
import { NodeError } from './errors.js';
|
|
2
3
|
/**
|
|
3
4
|
* A resolved DNS address, mirroring the shape of Node's `dns.LookupAddress`.
|
|
@@ -39,6 +40,13 @@ export declare const ssrfResolver: {
|
|
|
39
40
|
* Promised behaviour: specs/ssrf-guard.md (AC-9, AC-11, AC-19).
|
|
40
41
|
*/
|
|
41
42
|
export declare function isBlockedAddress(ip: string): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The dispatcher `safeFetch` hands every request to. One per process, so
|
|
45
|
+
* keep-alive survives across calls; its pool holds only connections a `safeFetch`
|
|
46
|
+
* call opened. Deliberately **not** re-exported from `index.ts`, for the reason
|
|
47
|
+
* {@link guardConnectionsTo} is not.
|
|
48
|
+
*/
|
|
49
|
+
export declare function guardedDispatcher(): Agent;
|
|
42
50
|
/**
|
|
43
51
|
* Put `url`'s host **and port** under the connect-time guard and return the
|
|
44
52
|
* release for it. Ref-counted, so concurrent calls to one target do not release
|
|
@@ -48,8 +56,8 @@ export declare function isBlockedAddress(ip: string): boolean;
|
|
|
48
56
|
*
|
|
49
57
|
* The window is the call, not the connection. A connect that completes after the
|
|
50
58
|
* release — the fetch it belonged to having already timed out or aborted — finds
|
|
51
|
-
* its target unregistered and is not judged;
|
|
52
|
-
*
|
|
59
|
+
* its target unregistered and is not judged here; the address it connected to was
|
|
60
|
+
* judged before the socket opened, by {@link guardedDispatcher}.
|
|
53
61
|
*
|
|
54
62
|
* Deliberately **not** re-exported from `index.ts`. The guard is engaged by
|
|
55
63
|
* `safeFetch`, which is the one sanctioned way out to the network (PO-185); an
|
|
@@ -78,6 +86,11 @@ export declare function connectionRefusal(err: unknown): NodeError | undefined;
|
|
|
78
86
|
* actually reached (PO-184). A caller that opens its own request after
|
|
79
87
|
* `assertPublicUrl` gets the check and not that second half.
|
|
80
88
|
*
|
|
89
|
+
* A private address passes only where exactly that address **and** the URL's
|
|
90
|
+
* effective port are on the run's allow-list (PO-828, see `isAllowedTarget`) —
|
|
91
|
+
* every other answer still decides as before, so one allowed address among
|
|
92
|
+
* private ones does not carry the rest.
|
|
93
|
+
*
|
|
81
94
|
* Pass `signal` (the per-request timeout/cancellation budget) so a hung or
|
|
82
95
|
* hostile DNS resolve cannot block the guard past that budget — see
|
|
83
96
|
* {@link resolveHost}.
|
package/dist/types.d.ts
CHANGED
|
@@ -3,7 +3,31 @@ export type LocalizedString = string | Record<string, string>;
|
|
|
3
3
|
export type DataType = 'any' | 'object' | 'array' | 'string' | 'number' | 'boolean';
|
|
4
4
|
export type OutputKind = 'default' | 'branch' | 'error';
|
|
5
5
|
export type NodeCategory = 'trigger' | 'action' | 'transform' | 'control' | 'io';
|
|
6
|
-
export type ConfigType = 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'object' | 'array' | 'expression' | 'secret-ref' | 'credentials-ref' | 'state-ref' | 'dynamic-schema';
|
|
6
|
+
export type ConfigType = 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'object' | 'array' | 'expression' | 'secret-ref' | 'credentials-ref' | 'state-ref' | 'dynamic-schema' | 'vpn-route';
|
|
7
|
+
/**
|
|
8
|
+
* What a node receives at run time for a `vpn-route` setting (PO-831).
|
|
9
|
+
*
|
|
10
|
+
* The editor stores `{ connection: string; route: string }`, or `null` (or no
|
|
11
|
+
* value) for the public internet. Before `execute`, the worker replaces a stored
|
|
12
|
+
* reference with the route as it stands now, so the node never resolves it
|
|
13
|
+
* itself; a connection that is not active fails the step as `VPN_UNAVAILABLE`,
|
|
14
|
+
* one that is gone as `VPN_ROUTE_MISSING` (see `VPN_ERROR_CODES`). A setting left
|
|
15
|
+
* on the public internet stays `null` or absent.
|
|
16
|
+
*/
|
|
17
|
+
export interface ResolvedVpnRoute {
|
|
18
|
+
/** The VPN connection's name. */
|
|
19
|
+
connection: string;
|
|
20
|
+
/** The route's name within that connection. */
|
|
21
|
+
route: string;
|
|
22
|
+
kind: 'http' | 'tcp';
|
|
23
|
+
/**
|
|
24
|
+
* Where the run reaches the route: for `http` a base URL
|
|
25
|
+
* (`http://10.31.0.7:8080/sap-odata/`), for `tcp` a `host:port`
|
|
26
|
+
* (`10.31.0.7:20001`). The address is on the run's allow-list, so
|
|
27
|
+
* `safeFetch` reaches it although it is private.
|
|
28
|
+
*/
|
|
29
|
+
endpoint: string;
|
|
30
|
+
}
|
|
7
31
|
/**
|
|
8
32
|
* Semantic category of an image, driving how the registry/CDN organises and
|
|
9
33
|
* displays it.
|
|
@@ -186,6 +210,15 @@ export interface IConfigFieldBase {
|
|
|
186
210
|
* the node whose config names it.
|
|
187
211
|
*/
|
|
188
212
|
stateRole?: 'mapping' | 'cursor' | 'dedupe' | 'digest';
|
|
213
|
+
/**
|
|
214
|
+
* Only meaningful when `type === 'vpn-route'`: which kind of route the picker
|
|
215
|
+
* offers (PO-831) — `http` for a node that sends HTTP requests through the
|
|
216
|
+
* relay's proxy, `tcp` for one that opens a socket. Defaults to `'http'`.
|
|
217
|
+
*
|
|
218
|
+
* **Accepted by the registry only once integrations#406 is deployed** — until
|
|
219
|
+
* then a package declaring it is refused on upload.
|
|
220
|
+
*/
|
|
221
|
+
routeKind?: 'http' | 'tcp';
|
|
189
222
|
}
|
|
190
223
|
export interface IConfigField extends IConfigFieldBase {
|
|
191
224
|
/**
|
|
@@ -368,6 +401,14 @@ export interface INodeState {
|
|
|
368
401
|
}
|
|
369
402
|
export interface INodeResult {
|
|
370
403
|
outputs: Record<string, unknown>;
|
|
404
|
+
/**
|
|
405
|
+
* The output port the node took. Output ports are alternatives — exactly one
|
|
406
|
+
* fires — so a node with more than one output (an `error` port counts) must
|
|
407
|
+
* name it; only a node with a single output may omit it. The worker fails such
|
|
408
|
+
* a step from the integrations release that carries PO-751; until then a result
|
|
409
|
+
* without `branch` fires every output. See `isMultiPort` for what counts as
|
|
410
|
+
* several, and `assertBranchNamed` to check a result in the node's own tests.
|
|
411
|
+
*/
|
|
371
412
|
branch?: string;
|
|
372
413
|
}
|
|
373
414
|
/**
|
|
@@ -496,6 +537,29 @@ export interface ICredentialDescription {
|
|
|
496
537
|
images?: IImage[];
|
|
497
538
|
authKind: CredentialAuthKind;
|
|
498
539
|
fields: ICredentialField[];
|
|
540
|
+
/**
|
|
541
|
+
* Which of this type's fields a VPN route replaces (PO-829). A type without it
|
|
542
|
+
* cannot be routed through a VPN, and the studio offers no choice.
|
|
543
|
+
*
|
|
544
|
+
* - `tcp` — the route's relay address and port replace the `hostField` and
|
|
545
|
+
* `portField` values (both keys of `fields`).
|
|
546
|
+
* - `http` — the route's endpoint replaces the base of the `urlField` value,
|
|
547
|
+
* keeping any path the author typed after it.
|
|
548
|
+
*
|
|
549
|
+
* The stored configuration is never rewritten; the platform substitutes the
|
|
550
|
+
* route's current endpoint each time the credential is tested or resolved.
|
|
551
|
+
*
|
|
552
|
+
* **Accepted by the registry only once integrations#406 is deployed** — until
|
|
553
|
+
* then a package declaring it is refused on upload.
|
|
554
|
+
*/
|
|
555
|
+
network?: {
|
|
556
|
+
kind: 'tcp';
|
|
557
|
+
hostField: string;
|
|
558
|
+
portField: string;
|
|
559
|
+
} | {
|
|
560
|
+
kind: 'http';
|
|
561
|
+
urlField: string;
|
|
562
|
+
};
|
|
499
563
|
}
|
|
500
564
|
/**
|
|
501
565
|
* Runtime context handed to a credential's `test`/`resolve`. Runs in the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revenexx/integrations-node-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "TypeScript interfaces and utilities for Revenexx integration nodes",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "revenexx GmbH",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"typescript": "^7.0.2"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"ipaddr.js": "2.5.0"
|
|
66
|
+
"ipaddr.js": "2.5.0",
|
|
67
|
+
"undici": "6.29.1"
|
|
67
68
|
}
|
|
68
69
|
}
|
package/dist/chunk-5BCAXRU3.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
// src/extract.ts
|
|
2
|
-
function extractManifest(node) {
|
|
3
|
-
return node.description;
|
|
4
|
-
}
|
|
5
|
-
function extractManifests(nodes) {
|
|
6
|
-
return nodes.map(extractManifest);
|
|
7
|
-
}
|
|
8
|
-
function extractCredentialManifest(credential) {
|
|
9
|
-
return credential.description;
|
|
10
|
-
}
|
|
11
|
-
function extractCredentialManifests(credentials) {
|
|
12
|
-
return credentials.map(extractCredentialManifest);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// src/manifest.ts
|
|
16
|
-
var MANIFEST_VERSION = "v0-draft";
|
|
17
|
-
var PACKAGE_ICON = /^lucide:[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
18
|
-
var PHP_TRIM = /^[ \t\n\r\0\v]+|[ \t\n\r\0\v]+$/g;
|
|
19
|
-
function packageIconProblem(icon) {
|
|
20
|
-
if (icon === void 0 || PACKAGE_ICON.test(icon)) return void 0;
|
|
21
|
-
return `package.json "revenexx.icon" must be a Lucide icon name such as "lucide:bell", got "${icon}".`;
|
|
22
|
-
}
|
|
23
|
-
function parsePackageMeta(raw) {
|
|
24
|
-
const obj = raw && typeof raw === "object" ? raw : {};
|
|
25
|
-
const str = (v) => typeof v === "string" ? v.trim() : "";
|
|
26
|
-
const revenexx = obj.revenexx && typeof obj.revenexx === "object" ? obj.revenexx : {};
|
|
27
|
-
const displayName = str(revenexx.displayName);
|
|
28
|
-
const icon = typeof revenexx.icon === "string" ? revenexx.icon.replace(PHP_TRIM, "") : "";
|
|
29
|
-
return {
|
|
30
|
-
name: str(obj.name),
|
|
31
|
-
version: str(obj.version),
|
|
32
|
-
displayName: displayName !== "" ? displayName : void 0,
|
|
33
|
-
icon: icon !== "" ? icon : void 0
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
function buildManifest(nodes, credentials = [], templates = []) {
|
|
37
|
-
const manifest = {
|
|
38
|
-
manifestVersion: MANIFEST_VERSION,
|
|
39
|
-
nodes: extractManifests(nodes)
|
|
40
|
-
};
|
|
41
|
-
if (credentials.length > 0) {
|
|
42
|
-
manifest.credentials = extractCredentialManifests(credentials);
|
|
43
|
-
}
|
|
44
|
-
if (templates.length > 0) {
|
|
45
|
-
manifest.templates = templates;
|
|
46
|
-
}
|
|
47
|
-
return manifest;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export {
|
|
51
|
-
extractManifest,
|
|
52
|
-
extractManifests,
|
|
53
|
-
extractCredentialManifest,
|
|
54
|
-
extractCredentialManifests,
|
|
55
|
-
MANIFEST_VERSION,
|
|
56
|
-
packageIconProblem,
|
|
57
|
-
parsePackageMeta,
|
|
58
|
-
buildManifest
|
|
59
|
-
};
|