@revenexx/integrations-node-sdk 1.9.1 → 1.11.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,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-5BCAXRU3.js";
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/fetch.d.ts CHANGED
@@ -37,11 +37,33 @@ export interface SafeFetchRetry {
37
37
  attempts: number;
38
38
  delayMs?: number;
39
39
  }
40
+ /**
41
+ * Binds a request to the one origin its credential belongs to (PO-688).
42
+ *
43
+ * Setting it declares the whole request secret-bearing — headers, query, path
44
+ * and body alike — so nothing has to name which part carries the secret: a
45
+ * request to any other origin is refused with `CREDENTIAL_ORIGIN`, on the first
46
+ * hop and on every redirect, before that origin is looked up or sent anything.
47
+ */
48
+ export interface SafeFetchCredential {
49
+ /** The only origin this request may reach, e.g. `https://api.pipedrive.com`. */
50
+ origin: string | URL;
51
+ /**
52
+ * What a redirect off that origin does. `'refuse'` (the default) fails the
53
+ * call. `'strip'` follows it the way an unbound request would — every header
54
+ * not known to be harmless dropped, a body refused — for a download that is
55
+ * redirected to a CDN or a signed storage link. A secret in the query or the
56
+ * path is not protected on such a hop.
57
+ */
58
+ crossOriginRedirect?: 'refuse' | 'strip';
59
+ }
40
60
  export interface SafeFetchOptions extends RequestInit {
41
61
  timeoutMs?: number;
42
62
  /** Pass `ctx.signal` so the workflow engine can cancel the request. */
43
63
  signal?: AbortSignal;
44
64
  retry?: SafeFetchRetry;
65
+ /** Bind the request to the origin of the credential it carries. */
66
+ credential?: SafeFetchCredential;
45
67
  }
46
68
  export declare function safeFetch(url: string | URL, options?: SafeFetchOptions): Promise<Response>;
47
69
  /**
package/dist/index.cjs CHANGED
@@ -50,6 +50,7 @@ __export(index_exports, {
50
50
  RetryableError: () => RetryableError,
51
51
  SimpleValueCredential: () => SimpleValueCredential,
52
52
  VALUELESS_OPERATORS: () => VALUELESS_OPERATORS,
53
+ assertBranchNamed: () => assertBranchNamed,
53
54
  assertPublicUrl: () => assertPublicUrl,
54
55
  backoffDelay: () => backoffDelay,
55
56
  buildManifest: () => buildManifest,
@@ -64,12 +65,15 @@ __export(index_exports, {
64
65
  headerCrossesOrigin: () => headerCrossesOrigin,
65
66
  httpErrorResult: () => httpErrorResult,
66
67
  isBlockedAddress: () => isBlockedAddress,
68
+ isMultiPort: () => isMultiPort,
67
69
  isNodeWithIteration: () => isNodeWithIteration,
68
70
  isOAuthAuthorizeCredential: () => isOAuthAuthorizeCredential,
69
71
  isOperator: () => isOperator,
70
72
  maxBytesConfigField: () => maxBytesConfigField,
71
73
  normalizeCredentialType: () => normalizeCredentialType,
72
74
  normalizeLocalized: () => normalizeLocalized,
75
+ outputProblems: () => outputProblems,
76
+ outputWarnings: () => outputWarnings,
73
77
  packageIconProblem: () => packageIconProblem,
74
78
  parsePackageMeta: () => parsePackageMeta,
75
79
  readArrayBuffer: () => readArrayBuffer,
@@ -294,6 +298,65 @@ function extractCredentialManifests(credentials) {
294
298
  return credentials.map(extractCredentialManifest);
295
299
  }
296
300
 
301
+ // src/branches.ts
302
+ function isMultiPort(outputs) {
303
+ if (!outputs) return false;
304
+ return outputs.length > 1 || outputs.some((p) => p.resolveOutputs === true);
305
+ }
306
+ var isGenerated = (p) => p.sourceFromConfig !== void 0 || p.resolveOutputs === true;
307
+ function outputProblems(description) {
308
+ const outputs = description.outputs ?? [];
309
+ if (!isMultiPort(outputs)) return [];
310
+ const problems = [];
311
+ const seen = /* @__PURE__ */ new Set();
312
+ for (const [i, port] of outputs.entries()) {
313
+ if (isGenerated(port)) continue;
314
+ if (port.name === void 0 || port.name === "") {
315
+ problems.push(
316
+ `${description.slug}: output ${i + 1} has no name, so a branch cannot choose it \u2014 a node with several outputs must name each one.`
317
+ );
318
+ } else if (seen.has(port.name)) {
319
+ problems.push(`${description.slug}: output name "${port.name}" is declared twice.`);
320
+ } else {
321
+ seen.add(port.name);
322
+ }
323
+ }
324
+ for (const port of outputs) {
325
+ const fb = port.fallback?.name;
326
+ if (isGenerated(port) && fb !== void 0 && seen.has(fb)) {
327
+ problems.push(`${description.slug}: the fallback name "${fb}" is also the name of a fixed output, so a branch "${fb}" is ambiguous.`);
328
+ }
329
+ }
330
+ return problems;
331
+ }
332
+ function outputWarnings(nodes) {
333
+ return nodes.flatMap((n) => outputProblems(n.description));
334
+ }
335
+ function assertBranchNamed(node, result, config) {
336
+ const description = "execute" in node ? node.description : node;
337
+ const outputs = description.outputs ?? [];
338
+ if (!isMultiPort(outputs)) return;
339
+ const branch = result.branch;
340
+ if (typeof branch !== "string" || branch === "") {
341
+ throw new Error(
342
+ `${description.slug} has several outputs, so its result must name the branch it took; without it every output fires.`
343
+ );
344
+ }
345
+ if (outputs.some((p) => !isGenerated(p) && p.name === branch)) return;
346
+ const producible = outputs.some((p) => {
347
+ if (p.resolveOutputs === true) return true;
348
+ if (p.sourceFromConfig === void 0) return false;
349
+ if (p.fallback?.name === branch) return true;
350
+ return config === void 0 || config[p.sourceFromConfig] === branch;
351
+ });
352
+ if (!producible) {
353
+ const names = outputs.map((p) => p.name ?? p.fallback?.name).filter((n) => n);
354
+ throw new Error(
355
+ `${description.slug} returned branch "${branch}", which none of its outputs can produce (${names.join(", ")}).`
356
+ );
357
+ }
358
+ }
359
+
297
360
  // src/ssrf.ts
298
361
  var import_node_diagnostics_channel = require("diagnostics_channel");
299
362
  var import_node_net = require("net");
@@ -542,6 +605,26 @@ function clampResponseBytes(maxBytes) {
542
605
  if (!Number.isFinite(maxBytes) || maxBytes < 1) return MAX_RESPONSE_BYTES;
543
606
  return Math.min(maxBytes, MAX_RESPONSE_BYTES);
544
607
  }
608
+ function boundOrigin(credential) {
609
+ let parsed;
610
+ try {
611
+ parsed = new URL(credential.origin);
612
+ } catch {
613
+ parsed = void 0;
614
+ }
615
+ if (!parsed || parsed.protocol !== "http:" && parsed.protocol !== "https:") {
616
+ throw new NodeError("CREDENTIAL_ORIGIN", "The origin a credential is bound to is not an http(s) address", { status: 0 });
617
+ }
618
+ return parsed.origin;
619
+ }
620
+ function credentialOriginRefusal(target, bound, hop) {
621
+ const what = hop === void 0 ? "a request" : `a ${hop} redirect`;
622
+ return new NodeError(
623
+ "CREDENTIAL_ORIGIN",
624
+ `Refused ${what} to ${target}: the credential it carries is bound to ${bound}`,
625
+ { status: 0 }
626
+ );
627
+ }
545
628
  async function timedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
546
629
  const ac = new AbortController();
547
630
  const timer = setTimeout(() => ac.abort(), effectiveMs);
@@ -581,7 +664,16 @@ function hasContent(body) {
581
664
  if (body instanceof URLSearchParams) return body.toString().length > 0;
582
665
  return true;
583
666
  }
584
- async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
667
+ function refuseUserinfo(url, status = 0) {
668
+ if (url.username !== "" || url.password !== "") {
669
+ throw new NodeError(
670
+ "INVALID_URL",
671
+ status === 0 ? "Refused a URL with a user name or password in it: pass them in a credential, not in the address" : `Refused a ${status} redirect to a URL with a user name or password in it`,
672
+ { status: 0 }
673
+ );
674
+ }
675
+ }
676
+ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding) {
585
677
  let currentUrl = url;
586
678
  let currentInit = { ...fetchOptions, redirect: "manual" };
587
679
  await guardUrl(currentUrl, ctxSignal, effectiveMs);
@@ -610,6 +702,10 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
610
702
  } catch {
611
703
  throw new NodeError("BLOCKED_ADDRESS", "Blocked redirect with an invalid Location header", { status: 0 });
612
704
  }
705
+ refuseUserinfo(nextUrl, redirectStatus);
706
+ if (binding && binding.crossOriginRedirect === "refuse" && nextUrl.origin !== binding.origin) {
707
+ throw credentialOriginRefusal(nextUrl.origin, binding.origin, redirectStatus);
708
+ }
613
709
  if (base.protocol === "https:" && nextUrl.protocol !== "https:") {
614
710
  throw new NodeError("BLOCKED_ADDRESS", "Blocked https\u2192http downgrade on redirect", { status: 0 });
615
711
  }
@@ -630,7 +726,9 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
630
726
  );
631
727
  }
632
728
  await guardUrl(nextUrl, ctxSignal, effectiveMs);
633
- if (nextUrl.origin !== base.origin) {
729
+ if (binding && nextUrl.origin !== binding.origin) {
730
+ for (const name of [...headers.keys()]) headers.delete(name);
731
+ } else if (nextUrl.origin !== base.origin) {
634
732
  for (const name of [...headers.keys()]) {
635
733
  if (!headerCrossesOrigin(name)) headers.delete(name);
636
734
  }
@@ -640,7 +738,7 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
640
738
  }
641
739
  }
642
740
  async function safeFetch(url, options = {}) {
643
- const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: ctxSignal, retry, ...fetchOptions } = options;
741
+ const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: ctxSignal, retry, credential, ...fetchOptions } = options;
644
742
  const effectiveMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.min(timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
645
743
  const rawAttempts = retry?.attempts ?? DEFAULT_RETRY_ATTEMPTS;
646
744
  const safeAttempts = Number.isFinite(rawAttempts) ? Math.min(Math.max(0, rawAttempts), MAX_RETRY_ATTEMPTS) : DEFAULT_RETRY_ATTEMPTS;
@@ -648,14 +746,26 @@ async function safeFetch(url, options = {}) {
648
746
  const retryDelayMs = retry?.delayMs ?? DEFAULT_RETRY_DELAY_MS;
649
747
  if (ctxSignal?.aborted) throw ctxSignal.reason;
650
748
  if (fetchOptions.headers !== void 0) fetchOptions.headers = sendableHeaders(fetchOptions.headers);
749
+ let parsed;
750
+ try {
751
+ parsed = url instanceof URL ? url : new URL(url);
752
+ } catch {
753
+ throw new NodeError("INVALID_URL", "Refused an address that cannot be read as a URL", { status: 0 });
754
+ }
755
+ refuseUserinfo(parsed);
756
+ let binding;
757
+ if (credential) {
758
+ binding = { origin: boundOrigin(credential), crossOriginRedirect: credential.crossOriginRedirect ?? "refuse" };
759
+ if (parsed.origin !== binding.origin) throw credentialOriginRefusal(parsed.origin, binding.origin);
760
+ }
651
761
  let lastError;
652
762
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
653
763
  if (ctxSignal?.aborted) throw ctxSignal.reason;
654
764
  try {
655
- return await guardedFetch(url, fetchOptions, ctxSignal, effectiveMs);
765
+ return await guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding);
656
766
  } catch (err) {
657
767
  if (ctxSignal?.aborted) throw ctxSignal.reason;
658
- if (err instanceof NodeError && (err.code === "BLOCKED_ADDRESS" || err.code === "TOO_MANY_REDIRECTS")) {
768
+ if (err instanceof NodeError && (err.code === "BLOCKED_ADDRESS" || err.code === "TOO_MANY_REDIRECTS" || err.code === "CREDENTIAL_ORIGIN" || err.code === "INVALID_URL")) {
659
769
  throw err;
660
770
  }
661
771
  lastError = err;
@@ -971,15 +1081,23 @@ var BaseCredential = class {
971
1081
  });
972
1082
  const text = await readText(res, MAX_TOKEN_RESPONSE_BYTES);
973
1083
  if (!res.ok) {
974
- throw new Error(`token endpoint ${url} -> ${res.status}${formatOAuthError(text)}`);
1084
+ throw new Error(`token endpoint ${endpointForMessage(url)} -> ${res.status}${formatOAuthError(text)}`);
975
1085
  }
976
1086
  try {
977
1087
  return JSON.parse(text);
978
1088
  } catch {
979
- throw new Error(`token endpoint ${url} returned non-JSON body`);
1089
+ throw new Error(`token endpoint ${endpointForMessage(url)} returned non-JSON body`);
980
1090
  }
981
1091
  }
982
1092
  };
1093
+ function endpointForMessage(url) {
1094
+ try {
1095
+ const u = new URL(url);
1096
+ return `${u.origin}${u.pathname}`;
1097
+ } catch {
1098
+ return "(unreadable address)";
1099
+ }
1100
+ }
983
1101
  function formatOAuthError(body) {
984
1102
  try {
985
1103
  const parsed = JSON.parse(body);
@@ -1180,6 +1298,7 @@ function base64Url(buf) {
1180
1298
  RetryableError,
1181
1299
  SimpleValueCredential,
1182
1300
  VALUELESS_OPERATORS,
1301
+ assertBranchNamed,
1183
1302
  assertPublicUrl,
1184
1303
  backoffDelay,
1185
1304
  buildManifest,
@@ -1194,12 +1313,15 @@ function base64Url(buf) {
1194
1313
  headerCrossesOrigin,
1195
1314
  httpErrorResult,
1196
1315
  isBlockedAddress,
1316
+ isMultiPort,
1197
1317
  isNodeWithIteration,
1198
1318
  isOAuthAuthorizeCredential,
1199
1319
  isOperator,
1200
1320
  maxBytesConfigField,
1201
1321
  normalizeCredentialType,
1202
1322
  normalizeLocalized,
1323
+ outputProblems,
1324
+ outputWarnings,
1203
1325
  packageIconProblem,
1204
1326
  parsePackageMeta,
1205
1327
  readArrayBuffer,
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ 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';
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-5BCAXRU3.js";
14
+ } from "./chunk-XO73F62E.js";
11
15
 
12
16
  // src/types.ts
13
17
  function isNodeWithIteration(node) {
@@ -450,6 +454,26 @@ function clampResponseBytes(maxBytes) {
450
454
  if (!Number.isFinite(maxBytes) || maxBytes < 1) return MAX_RESPONSE_BYTES;
451
455
  return Math.min(maxBytes, MAX_RESPONSE_BYTES);
452
456
  }
457
+ function boundOrigin(credential) {
458
+ let parsed;
459
+ try {
460
+ parsed = new URL(credential.origin);
461
+ } catch {
462
+ parsed = void 0;
463
+ }
464
+ if (!parsed || parsed.protocol !== "http:" && parsed.protocol !== "https:") {
465
+ throw new NodeError("CREDENTIAL_ORIGIN", "The origin a credential is bound to is not an http(s) address", { status: 0 });
466
+ }
467
+ return parsed.origin;
468
+ }
469
+ function credentialOriginRefusal(target, bound, hop) {
470
+ const what = hop === void 0 ? "a request" : `a ${hop} redirect`;
471
+ return new NodeError(
472
+ "CREDENTIAL_ORIGIN",
473
+ `Refused ${what} to ${target}: the credential it carries is bound to ${bound}`,
474
+ { status: 0 }
475
+ );
476
+ }
453
477
  async function timedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
454
478
  const ac = new AbortController();
455
479
  const timer = setTimeout(() => ac.abort(), effectiveMs);
@@ -489,7 +513,16 @@ function hasContent(body) {
489
513
  if (body instanceof URLSearchParams) return body.toString().length > 0;
490
514
  return true;
491
515
  }
492
- async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
516
+ function refuseUserinfo(url, status = 0) {
517
+ if (url.username !== "" || url.password !== "") {
518
+ throw new NodeError(
519
+ "INVALID_URL",
520
+ status === 0 ? "Refused a URL with a user name or password in it: pass them in a credential, not in the address" : `Refused a ${status} redirect to a URL with a user name or password in it`,
521
+ { status: 0 }
522
+ );
523
+ }
524
+ }
525
+ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding) {
493
526
  let currentUrl = url;
494
527
  let currentInit = { ...fetchOptions, redirect: "manual" };
495
528
  await guardUrl(currentUrl, ctxSignal, effectiveMs);
@@ -518,6 +551,10 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
518
551
  } catch {
519
552
  throw new NodeError("BLOCKED_ADDRESS", "Blocked redirect with an invalid Location header", { status: 0 });
520
553
  }
554
+ refuseUserinfo(nextUrl, redirectStatus);
555
+ if (binding && binding.crossOriginRedirect === "refuse" && nextUrl.origin !== binding.origin) {
556
+ throw credentialOriginRefusal(nextUrl.origin, binding.origin, redirectStatus);
557
+ }
521
558
  if (base.protocol === "https:" && nextUrl.protocol !== "https:") {
522
559
  throw new NodeError("BLOCKED_ADDRESS", "Blocked https\u2192http downgrade on redirect", { status: 0 });
523
560
  }
@@ -538,7 +575,9 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
538
575
  );
539
576
  }
540
577
  await guardUrl(nextUrl, ctxSignal, effectiveMs);
541
- if (nextUrl.origin !== base.origin) {
578
+ if (binding && nextUrl.origin !== binding.origin) {
579
+ for (const name of [...headers.keys()]) headers.delete(name);
580
+ } else if (nextUrl.origin !== base.origin) {
542
581
  for (const name of [...headers.keys()]) {
543
582
  if (!headerCrossesOrigin(name)) headers.delete(name);
544
583
  }
@@ -548,7 +587,7 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
548
587
  }
549
588
  }
550
589
  async function safeFetch(url, options = {}) {
551
- const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: ctxSignal, retry, ...fetchOptions } = options;
590
+ const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: ctxSignal, retry, credential, ...fetchOptions } = options;
552
591
  const effectiveMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.min(timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
553
592
  const rawAttempts = retry?.attempts ?? DEFAULT_RETRY_ATTEMPTS;
554
593
  const safeAttempts = Number.isFinite(rawAttempts) ? Math.min(Math.max(0, rawAttempts), MAX_RETRY_ATTEMPTS) : DEFAULT_RETRY_ATTEMPTS;
@@ -556,14 +595,26 @@ async function safeFetch(url, options = {}) {
556
595
  const retryDelayMs = retry?.delayMs ?? DEFAULT_RETRY_DELAY_MS;
557
596
  if (ctxSignal?.aborted) throw ctxSignal.reason;
558
597
  if (fetchOptions.headers !== void 0) fetchOptions.headers = sendableHeaders(fetchOptions.headers);
598
+ let parsed;
599
+ try {
600
+ parsed = url instanceof URL ? url : new URL(url);
601
+ } catch {
602
+ throw new NodeError("INVALID_URL", "Refused an address that cannot be read as a URL", { status: 0 });
603
+ }
604
+ refuseUserinfo(parsed);
605
+ let binding;
606
+ if (credential) {
607
+ binding = { origin: boundOrigin(credential), crossOriginRedirect: credential.crossOriginRedirect ?? "refuse" };
608
+ if (parsed.origin !== binding.origin) throw credentialOriginRefusal(parsed.origin, binding.origin);
609
+ }
559
610
  let lastError;
560
611
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
561
612
  if (ctxSignal?.aborted) throw ctxSignal.reason;
562
613
  try {
563
- return await guardedFetch(url, fetchOptions, ctxSignal, effectiveMs);
614
+ return await guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding);
564
615
  } catch (err) {
565
616
  if (ctxSignal?.aborted) throw ctxSignal.reason;
566
- if (err instanceof NodeError && (err.code === "BLOCKED_ADDRESS" || err.code === "TOO_MANY_REDIRECTS")) {
617
+ if (err instanceof NodeError && (err.code === "BLOCKED_ADDRESS" || err.code === "TOO_MANY_REDIRECTS" || err.code === "CREDENTIAL_ORIGIN" || err.code === "INVALID_URL")) {
567
618
  throw err;
568
619
  }
569
620
  lastError = err;
@@ -844,15 +895,23 @@ var BaseCredential = class {
844
895
  });
845
896
  const text = await readText(res, MAX_TOKEN_RESPONSE_BYTES);
846
897
  if (!res.ok) {
847
- throw new Error(`token endpoint ${url} -> ${res.status}${formatOAuthError(text)}`);
898
+ throw new Error(`token endpoint ${endpointForMessage(url)} -> ${res.status}${formatOAuthError(text)}`);
848
899
  }
849
900
  try {
850
901
  return JSON.parse(text);
851
902
  } catch {
852
- throw new Error(`token endpoint ${url} returned non-JSON body`);
903
+ throw new Error(`token endpoint ${endpointForMessage(url)} returned non-JSON body`);
853
904
  }
854
905
  }
855
906
  };
907
+ function endpointForMessage(url) {
908
+ try {
909
+ const u = new URL(url);
910
+ return `${u.origin}${u.pathname}`;
911
+ } catch {
912
+ return "(unreadable address)";
913
+ }
914
+ }
856
915
  function formatOAuthError(body) {
857
916
  try {
858
917
  const parsed = JSON.parse(body);
@@ -1052,6 +1111,7 @@ export {
1052
1111
  RetryableError,
1053
1112
  SimpleValueCredential,
1054
1113
  VALUELESS_OPERATORS,
1114
+ assertBranchNamed,
1055
1115
  assertPublicUrl,
1056
1116
  backoffDelay,
1057
1117
  buildManifest,
@@ -1066,12 +1126,15 @@ export {
1066
1126
  headerCrossesOrigin,
1067
1127
  httpErrorResult,
1068
1128
  isBlockedAddress,
1129
+ isMultiPort,
1069
1130
  isNodeWithIteration,
1070
1131
  isOAuthAuthorizeCredential,
1071
1132
  isOperator,
1072
1133
  maxBytesConfigField,
1073
1134
  normalizeCredentialType,
1074
1135
  normalizeLocalized,
1136
+ outputProblems,
1137
+ outputWarnings,
1075
1138
  packageIconProblem,
1076
1139
  parsePackageMeta,
1077
1140
  readArrayBuffer,
package/dist/types.d.ts CHANGED
@@ -368,6 +368,14 @@ export interface INodeState {
368
368
  }
369
369
  export interface INodeResult {
370
370
  outputs: Record<string, unknown>;
371
+ /**
372
+ * The output port the node took. Output ports are alternatives — exactly one
373
+ * fires — so a node with more than one output (an `error` port counts) must
374
+ * name it; only a node with a single output may omit it. The worker fails such
375
+ * a step from the integrations release that carries PO-751; until then a result
376
+ * without `branch` fires every output. See `isMultiPort` for what counts as
377
+ * several, and `assertBranchNamed` to check a result in the node's own tests.
378
+ */
371
379
  branch?: string;
372
380
  }
373
381
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/integrations-node-sdk",
3
- "version": "1.9.1",
3
+ "version": "1.11.0",
4
4
  "description": "TypeScript interfaces and utilities for Revenexx integration nodes",
5
5
  "license": "MIT",
6
6
  "author": "revenexx GmbH",
@@ -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
- };