@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.
@@ -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;
@@ -92,6 +92,53 @@ function parsePackageMeta(raw) {
92
92
  icon: icon !== "" ? icon : void 0
93
93
  };
94
94
  }
95
+ var SLUG_PATTERN = "^[a-z][a-z0-9-]{0,62}$";
96
+ var NAMESPACED_SLUG_PATTERN = "^[a-z][a-z0-9-]{0,62}:[a-z][a-z0-9-]{0,62}$";
97
+ var SEMVER_PATTERN = "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$";
98
+ var CONFIG_KEY_PATTERN = "^[a-zA-Z][a-zA-Z0-9-]*$";
99
+ var PATTERN_HINT = {
100
+ [SLUG_PATTERN]: 'lowercase kebab-case, such as "not-found"',
101
+ [NAMESPACED_SLUG_PATTERN]: '"<namespace>:<name>" in lowercase kebab-case, such as "core:http-request"',
102
+ [SEMVER_PATTERN]: 'a semantic version, such as "1.0.0"',
103
+ [CONFIG_KEY_PATTERN]: 'a letter, then letters, digits or "-", such as "postalCode"'
104
+ };
105
+ function nodeManifestProblems(nodes) {
106
+ const problems = [];
107
+ for (const [index, node] of nodes.entries()) {
108
+ const who = `node ${index + 1} (${typeof node.slug === "string" ? node.slug : "no slug"})`;
109
+ const check = (path, value, pattern) => {
110
+ if (value === void 0 || typeof value === "string" && new RegExp(pattern).test(value)) return;
111
+ problems.push(
112
+ `${who}: ${path} ${JSON.stringify(value)} is refused by the registry \u2014 it must match ${pattern} (${PATTERN_HINT[pattern]}).`
113
+ );
114
+ };
115
+ check("slug", node.slug, NAMESPACED_SLUG_PATTERN);
116
+ check("version", node.version, SEMVER_PATTERN);
117
+ for (const [i, port] of (node.outputs ?? []).entries()) {
118
+ check(`outputs[${i}].name`, port.name, SLUG_PATTERN);
119
+ check(`outputs[${i}].sourceFromConfig`, port.sourceFromConfig, SLUG_PATTERN);
120
+ check(`outputs[${i}].fallback.name`, port.fallback?.name, SLUG_PATTERN);
121
+ }
122
+ const checkField = (field, at) => {
123
+ check(`${at}.key`, field.key, CONFIG_KEY_PATTERN);
124
+ for (const [j, key] of (field.dependsOn ?? []).entries()) check(`${at}.dependsOn[${j}]`, key, CONFIG_KEY_PATTERN);
125
+ check(`${at}.showIf.key`, field.showIf?.key, CONFIG_KEY_PATTERN);
126
+ const types = field.credentialType;
127
+ if (Array.isArray(types)) {
128
+ for (const [j, type] of types.entries()) check(`${at}.credentialType[${j}]`, type, NAMESPACED_SLUG_PATTERN);
129
+ } else {
130
+ check(`${at}.credentialType`, types, NAMESPACED_SLUG_PATTERN);
131
+ }
132
+ walk(field.properties, `${at}.properties`);
133
+ if (field.items) checkField(field.items, `${at}.items`);
134
+ };
135
+ const walk = (fields, path) => {
136
+ for (const [i, field] of (fields ?? []).entries()) checkField(field, `${path}[${i}]`);
137
+ };
138
+ walk(node.config, "config");
139
+ }
140
+ return problems;
141
+ }
95
142
  function buildManifest(nodes, credentials = [], templates = []) {
96
143
  const manifest = {
97
144
  manifestVersion: MANIFEST_VERSION,
@@ -118,5 +165,10 @@ export {
118
165
  MANIFEST_VERSION,
119
166
  packageIconProblem,
120
167
  parsePackageMeta,
168
+ SLUG_PATTERN,
169
+ NAMESPACED_SLUG_PATTERN,
170
+ SEMVER_PATTERN,
171
+ CONFIG_KEY_PATTERN,
172
+ nodeManifestProblems,
121
173
  buildManifest
122
174
  };
package/dist/cli.cjs CHANGED
@@ -147,6 +147,53 @@ function parsePackageMeta(raw) {
147
147
  icon: icon !== "" ? icon : void 0
148
148
  };
149
149
  }
150
+ var SLUG_PATTERN = "^[a-z][a-z0-9-]{0,62}$";
151
+ var NAMESPACED_SLUG_PATTERN = "^[a-z][a-z0-9-]{0,62}:[a-z][a-z0-9-]{0,62}$";
152
+ var SEMVER_PATTERN = "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$";
153
+ var CONFIG_KEY_PATTERN = "^[a-zA-Z][a-zA-Z0-9-]*$";
154
+ var PATTERN_HINT = {
155
+ [SLUG_PATTERN]: 'lowercase kebab-case, such as "not-found"',
156
+ [NAMESPACED_SLUG_PATTERN]: '"<namespace>:<name>" in lowercase kebab-case, such as "core:http-request"',
157
+ [SEMVER_PATTERN]: 'a semantic version, such as "1.0.0"',
158
+ [CONFIG_KEY_PATTERN]: 'a letter, then letters, digits or "-", such as "postalCode"'
159
+ };
160
+ function nodeManifestProblems(nodes) {
161
+ const problems = [];
162
+ for (const [index, node] of nodes.entries()) {
163
+ const who = `node ${index + 1} (${typeof node.slug === "string" ? node.slug : "no slug"})`;
164
+ const check = (path, value, pattern) => {
165
+ if (value === void 0 || typeof value === "string" && new RegExp(pattern).test(value)) return;
166
+ problems.push(
167
+ `${who}: ${path} ${JSON.stringify(value)} is refused by the registry \u2014 it must match ${pattern} (${PATTERN_HINT[pattern]}).`
168
+ );
169
+ };
170
+ check("slug", node.slug, NAMESPACED_SLUG_PATTERN);
171
+ check("version", node.version, SEMVER_PATTERN);
172
+ for (const [i, port] of (node.outputs ?? []).entries()) {
173
+ check(`outputs[${i}].name`, port.name, SLUG_PATTERN);
174
+ check(`outputs[${i}].sourceFromConfig`, port.sourceFromConfig, SLUG_PATTERN);
175
+ check(`outputs[${i}].fallback.name`, port.fallback?.name, SLUG_PATTERN);
176
+ }
177
+ const checkField = (field, at) => {
178
+ check(`${at}.key`, field.key, CONFIG_KEY_PATTERN);
179
+ for (const [j, key] of (field.dependsOn ?? []).entries()) check(`${at}.dependsOn[${j}]`, key, CONFIG_KEY_PATTERN);
180
+ check(`${at}.showIf.key`, field.showIf?.key, CONFIG_KEY_PATTERN);
181
+ const types = field.credentialType;
182
+ if (Array.isArray(types)) {
183
+ for (const [j, type] of types.entries()) check(`${at}.credentialType[${j}]`, type, NAMESPACED_SLUG_PATTERN);
184
+ } else {
185
+ check(`${at}.credentialType`, types, NAMESPACED_SLUG_PATTERN);
186
+ }
187
+ walk(field.properties, `${at}.properties`);
188
+ if (field.items) checkField(field.items, `${at}.items`);
189
+ };
190
+ const walk = (fields, path) => {
191
+ for (const [i, field] of (fields ?? []).entries()) checkField(field, `${path}[${i}]`);
192
+ };
193
+ walk(node.config, "config");
194
+ }
195
+ return problems;
196
+ }
150
197
  function buildManifest(nodes, credentials = [], templates = []) {
151
198
  const manifest = {
152
199
  manifestVersion: MANIFEST_VERSION,
@@ -213,6 +260,8 @@ async function runManifest() {
213
260
  console.warn(`\u26A0 ${w} This will be an error in the next major version.`);
214
261
  }
215
262
  const manifest = buildManifest(mod.NODES, credentials, templates);
263
+ const nameProblems = nodeManifestProblems(manifest.nodes);
264
+ if (nameProblems.length > 0) fail(nameProblems.join("\n"));
216
265
  const outDir = (0, import_node_path2.resolve)(projectRoot, "dist");
217
266
  const outFile = (0, import_node_path2.resolve)(outDir, "manifest.json");
218
267
  fs2.mkdirSync(outDir, { recursive: true });
package/dist/cli.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  buildManifest,
4
+ nodeManifestProblems,
4
5
  outputWarnings,
5
6
  packageIconProblem,
6
7
  parsePackageMeta
7
- } from "./chunk-XO73F62E.js";
8
+ } from "./chunk-CA6JVLLF.js";
8
9
 
9
10
  // src/cli.ts
10
11
  import * as fs2 from "fs";
@@ -112,6 +113,8 @@ async function runManifest() {
112
113
  console.warn(`\u26A0 ${w} This will be an error in the next major version.`);
113
114
  }
114
115
  const manifest = buildManifest(mod.NODES, credentials, templates);
116
+ const nameProblems = nodeManifestProblems(manifest.nodes);
117
+ if (nameProblems.length > 0) fail(nameProblems.join("\n"));
115
118
  const outDir = resolve2(projectRoot, "dist");
116
119
  const outFile = resolve2(outDir, "manifest.json");
117
120
  fs2.mkdirSync(outDir, { recursive: true });
package/dist/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;