@revenexx/integrations-node-sdk 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,8 @@ packages and the workflow engine that runs them.
11
11
  ```
12
12
  integrations-node-sdk ← this package (types & helpers only)
13
13
  └── integrations-nodes-core ← implements INode, ships dist/manifest.json
14
- └── integrations-worker ← registers nodes, executes workflows
14
+ └── integrations-worker ← reads the manifest, executes workflows
15
+ (no SDK dependency — couples via manifestVersion)
15
16
  ```
16
17
 
17
18
  ## Installation
@@ -14,6 +14,17 @@ function extractCredentialManifests(credentials) {
14
14
 
15
15
  // src/manifest.ts
16
16
  var MANIFEST_VERSION = "v0-draft";
17
+ function parsePackageMeta(raw) {
18
+ const obj = raw && typeof raw === "object" ? raw : {};
19
+ const str = (v) => typeof v === "string" ? v.trim() : "";
20
+ const revenexx = obj.revenexx && typeof obj.revenexx === "object" ? obj.revenexx : {};
21
+ const displayName = str(revenexx.displayName);
22
+ return {
23
+ name: str(obj.name),
24
+ version: str(obj.version),
25
+ displayName: displayName !== "" ? displayName : void 0
26
+ };
27
+ }
17
28
  function buildManifest(nodes, credentials = [], templates = []) {
18
29
  const manifest = {
19
30
  manifestVersion: MANIFEST_VERSION,
@@ -34,5 +45,6 @@ export {
34
45
  extractCredentialManifest,
35
46
  extractCredentialManifests,
36
47
  MANIFEST_VERSION,
48
+ parsePackageMeta,
37
49
  buildManifest
38
50
  };
package/dist/cli.cjs CHANGED
@@ -24,9 +24,58 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli.ts
27
+ var fs2 = __toESM(require("fs"), 1);
28
+ var import_node_path2 = require("path");
29
+ var import_node_url = require("url");
30
+
31
+ // src/images.ts
27
32
  var fs = __toESM(require("fs"), 1);
28
33
  var import_node_path = require("path");
29
- var import_node_url = require("url");
34
+ function collectImageSources(manifest) {
35
+ const sources = /* @__PURE__ */ new Set();
36
+ for (const group of [manifest.nodes, manifest.credentials, manifest.templates]) {
37
+ for (const item of group ?? []) {
38
+ for (const image of item.images ?? []) {
39
+ if (typeof image.src === "string" && image.src.length > 0) {
40
+ sources.add(image.src);
41
+ }
42
+ }
43
+ }
44
+ }
45
+ return [...sources];
46
+ }
47
+ function copyImages(sources, outDir, rootDir) {
48
+ let copied = 0;
49
+ for (const src of sources) {
50
+ if (!isSafeRelativePath(src)) {
51
+ console.warn(` \u26A0 unsafe image path (absolute or escaping the package root), skipping: ${src}`);
52
+ continue;
53
+ }
54
+ const from = (0, import_node_path.resolve)(rootDir, src);
55
+ if (!fs.existsSync(from)) {
56
+ console.warn(` \u26A0 image not found, skipping: ${src}`);
57
+ continue;
58
+ }
59
+ if (!fs.statSync(from).isFile()) {
60
+ console.warn(` \u26A0 image path is not a file, skipping: ${src}`);
61
+ continue;
62
+ }
63
+ const to = (0, import_node_path.resolve)(outDir, src);
64
+ fs.mkdirSync((0, import_node_path.dirname)(to), { recursive: true });
65
+ fs.copyFileSync(from, to);
66
+ copied++;
67
+ }
68
+ if (sources.length > 0) {
69
+ console.log(` copied ${copied}/${sources.length} image(s) into dist/`);
70
+ }
71
+ }
72
+ function isSafeRelativePath(src) {
73
+ if ((0, import_node_path.isAbsolute)(src)) {
74
+ return false;
75
+ }
76
+ const normalized = (0, import_node_path.normalize)(src);
77
+ return normalized !== ".." && !normalized.startsWith(".." + import_node_path.sep);
78
+ }
30
79
 
31
80
  // src/extract.ts
32
81
  function extractManifest(node) {
@@ -44,6 +93,17 @@ function extractCredentialManifests(credentials) {
44
93
 
45
94
  // src/manifest.ts
46
95
  var MANIFEST_VERSION = "v0-draft";
96
+ function parsePackageMeta(raw) {
97
+ const obj = raw && typeof raw === "object" ? raw : {};
98
+ const str = (v) => typeof v === "string" ? v.trim() : "";
99
+ const revenexx = obj.revenexx && typeof obj.revenexx === "object" ? obj.revenexx : {};
100
+ const displayName = str(revenexx.displayName);
101
+ return {
102
+ name: str(obj.name),
103
+ version: str(obj.version),
104
+ displayName: displayName !== "" ? displayName : void 0
105
+ };
106
+ }
47
107
  function buildManifest(nodes, credentials = [], templates = []) {
48
108
  const manifest = {
49
109
  manifestVersion: MANIFEST_VERSION,
@@ -64,9 +124,20 @@ function fail(message) {
64
124
  console.error(`Error: ${message}`);
65
125
  process.exit(1);
66
126
  }
127
+ function readPackageJson(root) {
128
+ const pkgPath = (0, import_node_path2.resolve)(root, "package.json");
129
+ if (!fs2.existsSync(pkgPath)) {
130
+ return {};
131
+ }
132
+ try {
133
+ return JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
134
+ } catch {
135
+ return {};
136
+ }
137
+ }
67
138
  async function runManifest() {
68
- const distEntry = (0, import_node_path.resolve)(projectRoot, "dist", "index.js");
69
- if (!fs.existsSync(distEntry)) {
139
+ const distEntry = (0, import_node_path2.resolve)(projectRoot, "dist", "index.js");
140
+ if (!fs2.existsSync(distEntry)) {
70
141
  fail("dist/index.js is missing. Run the build (tsup) before `rvnxx-nodes manifest`.");
71
142
  }
72
143
  const mod = await import((0, import_node_url.pathToFileURL)(distEntry).href);
@@ -81,15 +152,23 @@ async function runManifest() {
81
152
  }
82
153
  const credentials = mod.CREDENTIALS ?? [];
83
154
  const templates = mod.TEMPLATES ?? [];
155
+ const meta = parsePackageMeta(readPackageJson(projectRoot));
156
+ const hasPackage = meta.name !== "" && meta.version !== "";
157
+ if (hasPackage && !meta.displayName) {
158
+ console.warn(
159
+ '\u26A0 package.json has no "revenexx.displayName" \u2014 the node palette will fall back to the raw package name.'
160
+ );
161
+ }
84
162
  const manifest = buildManifest(mod.NODES, credentials, templates);
85
- const outDir = (0, import_node_path.resolve)(projectRoot, "dist");
86
- const outFile = (0, import_node_path.resolve)(outDir, "manifest.json");
87
- fs.mkdirSync(outDir, { recursive: true });
88
- fs.writeFileSync(outFile, JSON.stringify(manifest, null, 2), "utf-8");
163
+ const outDir = (0, import_node_path2.resolve)(projectRoot, "dist");
164
+ const outFile = (0, import_node_path2.resolve)(outDir, "manifest.json");
165
+ fs2.mkdirSync(outDir, { recursive: true });
166
+ fs2.writeFileSync(outFile, JSON.stringify(manifest, null, 2), "utf-8");
89
167
  const credentialCount = manifest.credentials?.length ?? 0;
90
168
  const templateCount = manifest.templates?.length ?? 0;
169
+ const bundlePrefix = hasPackage ? `package ${meta.displayName ? `"${meta.displayName}" ` : ""}(${meta.name}), ` : "";
91
170
  console.log(
92
- `\u2713 dist/manifest.json \u2014 ${manifest.nodes.length} node(s), ${credentialCount} credential(s), ${templateCount} template(s)`
171
+ `\u2713 dist/manifest.json \u2014 ${bundlePrefix}${manifest.nodes.length} node(s), ${credentialCount} credential(s), ${templateCount} template(s)`
93
172
  );
94
173
  for (const m of manifest.nodes) {
95
174
  console.log(` node ${m.slug}@${m.version}`);
@@ -100,6 +179,7 @@ async function runManifest() {
100
179
  for (const t of manifest.templates ?? []) {
101
180
  console.log(` template ${t.slug}@${t.version} (${t.level})`);
102
181
  }
182
+ copyImages(collectImageSources(manifest), outDir, projectRoot);
103
183
  }
104
184
  async function main() {
105
185
  const command = process.argv[2];
package/dist/cli.js CHANGED
@@ -1,20 +1,83 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- buildManifest
4
- } from "./chunk-X5HI7WPI.js";
3
+ buildManifest,
4
+ parsePackageMeta
5
+ } from "./chunk-WB3BYMLA.js";
5
6
 
6
7
  // src/cli.ts
7
- import * as fs from "fs";
8
- import { resolve } from "path";
8
+ import * as fs2 from "fs";
9
+ import { resolve as resolve2 } from "path";
9
10
  import { pathToFileURL } from "url";
11
+
12
+ // src/images.ts
13
+ import * as fs from "fs";
14
+ import { dirname, isAbsolute, normalize, resolve, sep } from "path";
15
+ function collectImageSources(manifest) {
16
+ const sources = /* @__PURE__ */ new Set();
17
+ for (const group of [manifest.nodes, manifest.credentials, manifest.templates]) {
18
+ for (const item of group ?? []) {
19
+ for (const image of item.images ?? []) {
20
+ if (typeof image.src === "string" && image.src.length > 0) {
21
+ sources.add(image.src);
22
+ }
23
+ }
24
+ }
25
+ }
26
+ return [...sources];
27
+ }
28
+ function copyImages(sources, outDir, rootDir) {
29
+ let copied = 0;
30
+ for (const src of sources) {
31
+ if (!isSafeRelativePath(src)) {
32
+ console.warn(` \u26A0 unsafe image path (absolute or escaping the package root), skipping: ${src}`);
33
+ continue;
34
+ }
35
+ const from = resolve(rootDir, src);
36
+ if (!fs.existsSync(from)) {
37
+ console.warn(` \u26A0 image not found, skipping: ${src}`);
38
+ continue;
39
+ }
40
+ if (!fs.statSync(from).isFile()) {
41
+ console.warn(` \u26A0 image path is not a file, skipping: ${src}`);
42
+ continue;
43
+ }
44
+ const to = resolve(outDir, src);
45
+ fs.mkdirSync(dirname(to), { recursive: true });
46
+ fs.copyFileSync(from, to);
47
+ copied++;
48
+ }
49
+ if (sources.length > 0) {
50
+ console.log(` copied ${copied}/${sources.length} image(s) into dist/`);
51
+ }
52
+ }
53
+ function isSafeRelativePath(src) {
54
+ if (isAbsolute(src)) {
55
+ return false;
56
+ }
57
+ const normalized = normalize(src);
58
+ return normalized !== ".." && !normalized.startsWith(".." + sep);
59
+ }
60
+
61
+ // src/cli.ts
10
62
  var projectRoot = process.cwd();
11
63
  function fail(message) {
12
64
  console.error(`Error: ${message}`);
13
65
  process.exit(1);
14
66
  }
67
+ function readPackageJson(root) {
68
+ const pkgPath = resolve2(root, "package.json");
69
+ if (!fs2.existsSync(pkgPath)) {
70
+ return {};
71
+ }
72
+ try {
73
+ return JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
74
+ } catch {
75
+ return {};
76
+ }
77
+ }
15
78
  async function runManifest() {
16
- const distEntry = resolve(projectRoot, "dist", "index.js");
17
- if (!fs.existsSync(distEntry)) {
79
+ const distEntry = resolve2(projectRoot, "dist", "index.js");
80
+ if (!fs2.existsSync(distEntry)) {
18
81
  fail("dist/index.js is missing. Run the build (tsup) before `rvnxx-nodes manifest`.");
19
82
  }
20
83
  const mod = await import(pathToFileURL(distEntry).href);
@@ -29,15 +92,23 @@ async function runManifest() {
29
92
  }
30
93
  const credentials = mod.CREDENTIALS ?? [];
31
94
  const templates = mod.TEMPLATES ?? [];
95
+ const meta = parsePackageMeta(readPackageJson(projectRoot));
96
+ const hasPackage = meta.name !== "" && meta.version !== "";
97
+ if (hasPackage && !meta.displayName) {
98
+ console.warn(
99
+ '\u26A0 package.json has no "revenexx.displayName" \u2014 the node palette will fall back to the raw package name.'
100
+ );
101
+ }
32
102
  const manifest = buildManifest(mod.NODES, credentials, templates);
33
- const outDir = resolve(projectRoot, "dist");
34
- const outFile = resolve(outDir, "manifest.json");
35
- fs.mkdirSync(outDir, { recursive: true });
36
- fs.writeFileSync(outFile, JSON.stringify(manifest, null, 2), "utf-8");
103
+ const outDir = resolve2(projectRoot, "dist");
104
+ const outFile = resolve2(outDir, "manifest.json");
105
+ fs2.mkdirSync(outDir, { recursive: true });
106
+ fs2.writeFileSync(outFile, JSON.stringify(manifest, null, 2), "utf-8");
37
107
  const credentialCount = manifest.credentials?.length ?? 0;
38
108
  const templateCount = manifest.templates?.length ?? 0;
109
+ const bundlePrefix = hasPackage ? `package ${meta.displayName ? `"${meta.displayName}" ` : ""}(${meta.name}), ` : "";
39
110
  console.log(
40
- `\u2713 dist/manifest.json \u2014 ${manifest.nodes.length} node(s), ${credentialCount} credential(s), ${templateCount} template(s)`
111
+ `\u2713 dist/manifest.json \u2014 ${bundlePrefix}${manifest.nodes.length} node(s), ${credentialCount} credential(s), ${templateCount} template(s)`
41
112
  );
42
113
  for (const m of manifest.nodes) {
43
114
  console.log(` node ${m.slug}@${m.version}`);
@@ -48,6 +119,7 @@ async function runManifest() {
48
119
  for (const t of manifest.templates ?? []) {
49
120
  console.log(` template ${t.slug}@${t.version} (${t.level})`);
50
121
  }
122
+ copyImages(collectImageSources(manifest), outDir, projectRoot);
51
123
  }
52
124
  async function main() {
53
125
  const command = process.argv[2];
package/dist/index.cjs CHANGED
@@ -42,6 +42,7 @@ __export(index_exports, {
42
42
  isOAuthAuthorizeCredential: () => isOAuthAuthorizeCredential,
43
43
  normalizeCredentialType: () => normalizeCredentialType,
44
44
  normalizeLocalized: () => normalizeLocalized,
45
+ parsePackageMeta: () => parsePackageMeta,
45
46
  retryConfigFields: () => retryConfigFields,
46
47
  safeFetch: () => safeFetch,
47
48
  timeoutConfigField: () => timeoutConfigField
@@ -189,6 +190,17 @@ function retryConfigFields(opts) {
189
190
 
190
191
  // src/manifest.ts
191
192
  var MANIFEST_VERSION = "v0-draft";
193
+ function parsePackageMeta(raw) {
194
+ const obj = raw && typeof raw === "object" ? raw : {};
195
+ const str = (v) => typeof v === "string" ? v.trim() : "";
196
+ const revenexx = obj.revenexx && typeof obj.revenexx === "object" ? obj.revenexx : {};
197
+ const displayName = str(revenexx.displayName);
198
+ return {
199
+ name: str(obj.name),
200
+ version: str(obj.version),
201
+ displayName: displayName !== "" ? displayName : void 0
202
+ };
203
+ }
192
204
  function buildManifest(nodes, credentials = [], templates = []) {
193
205
  const manifest = {
194
206
  manifestVersion: MANIFEST_VERSION,
@@ -431,6 +443,7 @@ function base64Url(buf) {
431
443
  isOAuthAuthorizeCredential,
432
444
  normalizeCredentialType,
433
445
  normalizeLocalized,
446
+ parsePackageMeta,
434
447
  retryConfigFields,
435
448
  safeFetch,
436
449
  timeoutConfigField
package/dist/index.d.cts CHANGED
@@ -2,7 +2,25 @@ type LocalizedString = string | Record<string, string>;
2
2
  type DataType = 'any' | 'object' | 'array' | 'string' | 'number' | 'boolean';
3
3
  type OutputKind = 'default' | 'branch' | 'error';
4
4
  type NodeCategory = 'trigger' | 'action' | 'transform' | 'control' | 'io';
5
- type ConfigType = 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'object' | 'array' | 'expression' | 'secret-ref' | 'credentials-ref';
5
+ type ConfigType = 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'object' | 'array' | 'expression' | 'secret-ref' | 'credentials-ref' | 'dynamic-schema';
6
+ /**
7
+ * Semantic category of an image, driving how the registry/CDN organises and
8
+ * displays it.
9
+ */
10
+ type ImageCategory = 'screenshot' | 'logo' | 'banner' | 'icon' | 'other';
11
+ /**
12
+ * A single image a node, credential type, or template ships alongside its
13
+ * description (e.g. a screenshot, logo, or banner). The file is bundled into
14
+ * the package tarball and uploaded to the Revenexx CDN when the package is
15
+ * published.
16
+ */
17
+ interface IImage {
18
+ /** Path to the image file, relative to the package root (e.g. `images/screenshot.png`). */
19
+ src: string;
20
+ alt: LocalizedString;
21
+ title?: LocalizedString;
22
+ category: ImageCategory;
23
+ }
6
24
  interface IInputPort {
7
25
  dataType: DataType;
8
26
  required?: boolean;
@@ -20,6 +38,14 @@ interface IOutputPort {
20
38
  description?: LocalizedString;
21
39
  fields?: Record<string, IOutputField>;
22
40
  sourceFromConfig?: string;
41
+ /**
42
+ * Marks a generic port set resolved at author time by the node's
43
+ * `resolveOutputs` callback (PO-143), rather than a static `name` or a
44
+ * config-driven `sourceFromConfig`. Set at most one of `name`,
45
+ * `sourceFromConfig`, `resolveOutputs` — this is a server-validated
46
+ * constraint on publish, not enforced by this (intentionally flat) type.
47
+ */
48
+ resolveOutputs?: boolean;
23
49
  fallback?: {
24
50
  name: string;
25
51
  label?: LocalizedString;
@@ -49,6 +75,23 @@ interface IConfigFieldBase {
49
75
  multiline?: boolean;
50
76
  validation?: IConfigValidation;
51
77
  options?: IConfigOption[];
78
+ /**
79
+ * When true, a *known* field's `options` are resolved at author time via the
80
+ * node's `loadOptions` (a live/dependent dropdown) instead of a static
81
+ * `options[]`. Applies to `select` / `multiselect`; left unset for ordinary
82
+ * static fields.
83
+ *
84
+ * Independent of `type: 'dynamic-schema'`: that field type triggers
85
+ * `resolveConfigSchema` on its own and does NOT require `dynamic: true`.
86
+ */
87
+ dynamic?: boolean;
88
+ /**
89
+ * Config keys whose values drive this field's dynamic resolution. The editor
90
+ * re-resolves when one of them changes. A key listed here is
91
+ * *dependency-driving* and MUST be a literal (it may not set
92
+ * `expressionAllowed`), so its value is known at author time.
93
+ */
94
+ dependsOn?: string[];
52
95
  /**
53
96
  * Only meaningful when `type === 'credentials-ref'`: the namespaced slug(s) of
54
97
  * the credential type(s) this field accepts (e.g. `revenexx:smtp`). The editor
@@ -70,6 +113,8 @@ interface INodeDescription {
70
113
  name: LocalizedString;
71
114
  description?: LocalizedString;
72
115
  icon?: string;
116
+ /** Associated images (screenshots, logos, banners) shipped with the package. */
117
+ images?: IImage[];
73
118
  inputs: Record<string, IInputPort>;
74
119
  outputs: IOutputPort[];
75
120
  config?: IConfigField[];
@@ -99,18 +144,73 @@ interface INodeResult {
99
144
  outputs: Record<string, unknown>;
100
145
  branch?: string;
101
146
  }
147
+ /**
148
+ * Context handed to a node's *author-time* resolvers (`loadOptions`,
149
+ * `resolveConfigSchema`, `resolveOutputs`). These run in the node-runtime host
150
+ * (a Node side-container) while a user is editing the workflow — never in
151
+ * `execute`. The result is snapshotted into the workflow blob at save, so the
152
+ * runtime never calls these (PO-143).
153
+ */
154
+ interface INodeAuthorContext {
155
+ signal: AbortSignal;
156
+ logger: {
157
+ info(message: string, meta?: Record<string, unknown>): void;
158
+ warn(message: string, meta?: Record<string, unknown>): void;
159
+ error(message: string, meta?: Record<string, unknown>): void;
160
+ };
161
+ /** The user's current partial config values. */
162
+ config: Record<string, unknown>;
163
+ /**
164
+ * Lazily resolve a `secret-ref` config field's value (a key in the tenant
165
+ * secret store → a single string). Mirrors {@link INodeContext.secrets} so a
166
+ * resolver that authenticates via a secret-ref uses the same call it would in
167
+ * `execute`. Resolves against the internal secret endpoint; only the keys the
168
+ * resolver actually asks for are fetched.
169
+ */
170
+ secrets: {
171
+ get(key: string): Promise<string>;
172
+ };
173
+ /**
174
+ * Lazily resolve a `credentials-ref` instance's access data via the
175
+ * credentials broker (id → structured material, e.g. `{ accessToken }`).
176
+ * Mirrors {@link INodeContext.credentials}.
177
+ */
178
+ credentials: {
179
+ get(credentialsId: string): Promise<Record<string, unknown>>;
180
+ };
181
+ /** Preferred locale for resolved labels, when the caller supplies one. */
182
+ locale?: string;
183
+ }
102
184
  interface INode {
103
185
  description: INodeDescription;
104
186
  execute(ctx: INodeContext, inputs: Record<string, unknown>): Promise<INodeResult>;
187
+ /**
188
+ * Author-time: resolve the options of a `dynamic` field (a live/dependent
189
+ * dropdown). `fieldKey` is the config key being resolved. Optional — declare
190
+ * it only for nodes with `dynamic` `select`/`multiselect` fields.
191
+ */
192
+ loadOptions?(ctx: INodeAuthorContext, fieldKey: string): Promise<IConfigOption[]>;
193
+ /**
194
+ * Author-time: resolve the flat set of typed config fields that replaces a
195
+ * `type: 'dynamic-schema'` marker (e.g. an API's parameters once app + API
196
+ * are chosen). Returns fields in the same config-field grammar; the node is
197
+ * responsible for flattening any nested API shape into flat keys.
198
+ */
199
+ resolveConfigSchema?(ctx: INodeAuthorContext): Promise<IConfigField[]>;
200
+ /**
201
+ * Author-time: resolve a generic output-port set for an output marked
202
+ * `resolveOutputs` (e.g. ports derived from a connected resource's schema).
203
+ */
204
+ resolveOutputs?(ctx: INodeAuthorContext): Promise<IOutputPort[]>;
105
205
  }
106
206
  /**
107
207
  * Optional capability interface for nodes that iterate over a collection.
108
- * Implement this alongside {@link INode} to signal to the worker that this
109
- * node drives iteration — the worker will call {@link extractItems} instead
208
+ * Implement this alongside {@link INode} to signal to the runtime that this
209
+ * node drives iteration — the runtime will call {@link extractItems} instead
110
210
  * of relying on slug-based detection.
111
211
  *
112
212
  * This interface is the designated dispatch point for future child-workflow
113
- * execution: once per-iteration isolation is needed, the worker can swap the
213
+ * execution: once per-iteration isolation is needed, the runtime can swap the
114
214
  * inline loop for `executeChild` calls without any changes to the node or SDK.
115
215
  */
116
216
  interface INodeWithIteration {
@@ -159,6 +259,8 @@ interface ICredentialDescription {
159
259
  name: LocalizedString;
160
260
  description?: LocalizedString;
161
261
  icon?: string;
262
+ /** Associated images (screenshots, logos, banners) shipped with the package. */
263
+ images?: IImage[];
162
264
  authKind: CredentialAuthKind;
163
265
  fields: ICredentialField[];
164
266
  }
@@ -270,6 +372,8 @@ interface ITemplateDescription {
270
372
  industries?: string[];
271
373
  /** Free-form vendor tags (e.g. `pipedrive`, `slack`). */
272
374
  vendors?: string[];
375
+ /** Associated images (screenshots, logos, banners) shipped with the package. */
376
+ images?: IImage[];
273
377
  /** Workflow-blob schema version `definition` targets (e.g. `v0-draft`). */
274
378
  blobVersion: string;
275
379
  /** The workflow blob instantiated when a user picks this template. */
@@ -284,7 +388,7 @@ interface ITemplateDescription {
284
388
  /**
285
389
  * Reduce a {@link LocalizedString} to a single plain string.
286
390
  *
287
- * Every consumer (worker, UI, the Laravel side) repeats the same "is it a
391
+ * Every consumer (the UI, the Laravel side) repeats the same "is it a
288
392
  * string or a localized map?" branch to render a name/label; this centralizes
289
393
  * it so the rules are defined once:
290
394
  *
@@ -302,8 +406,8 @@ declare function normalizeLocalized(value: LocalizedString | undefined | null, f
302
406
  *
303
407
  * The union `string | string[]` exists so node authors can write the common
304
408
  * single-type case as a bare string, but every consumer (the editor that
305
- * lists tenant credential instances, the worker that resolves the chosen
306
- * instance, the Laravel side) needs a list to iterate. This centralizes the
409
+ * lists tenant credential instances, the Laravel side) needs a list to
410
+ * iterate. This centralizes the
307
411
  * "is it a string or an array?" branch — analogous to {@link normalizeLocalized}
308
412
  * for `LocalizedString` — so the rules are defined once:
309
413
  *
@@ -374,10 +478,41 @@ interface NodeManifest {
374
478
  */
375
479
  templates?: ITemplateDescription[];
376
480
  }
481
+ /**
482
+ * The registry-relevant fields of a node package's `package.json`, as read by
483
+ * the CLI. `name`/`version` identify the package; `displayName` is the
484
+ * human-readable bundle label shown in the editor's node palette (e.g.
485
+ * „Business Central"). All three are read straight from `package.json` by the
486
+ * integrations server on upload — the CLI reads them only to warn about a
487
+ * missing label and to annotate the manifest log line.
488
+ *
489
+ * The label lives under a namespaced `revenexx` group in `package.json`
490
+ * (`{ "revenexx": { "displayName": "…" } }`), not a bespoke top-level key, so
491
+ * it can't collide with unrelated tooling that squats on `displayName`.
492
+ */
493
+ interface NodePackageMeta {
494
+ name: string;
495
+ version: string;
496
+ /** Human-readable bundle label (e.g. „Business Central"); optional. */
497
+ displayName?: string;
498
+ }
499
+ /**
500
+ * Extracts {@link NodePackageMeta} from parsed `package.json` contents, keeping
501
+ * only the registry-relevant fields and coercing anything malformed to a safe
502
+ * shape. All three fields are trimmed; a blank or whitespace-only value becomes
503
+ * `''` (`name`/`version`) or `undefined` (`displayName`, matching how the server
504
+ * treats it), so whitespace can't masquerade as a present value in tooling.
505
+ * The bundle label is read from the `revenexx` group (`revenexx.displayName`).
506
+ * Does not validate that `name`/`version` are present — the integrations server
507
+ * enforces that on upload; this is a typed, lenient read for tooling.
508
+ */
509
+ declare function parsePackageMeta(raw: unknown): NodePackageMeta;
377
510
  /**
378
511
  * Builds the manifest envelope from a package's `NODES` (and optional
379
512
  * `CREDENTIALS` / `TEMPLATES`) exports. The result is what gets written to
380
- * `dist/manifest.json` and uploaded to the registry inside the tarball.
513
+ * `dist/manifest.json` and uploaded to the registry inside the tarball. The
514
+ * bundle label is NOT carried here — the integrations server reads it straight
515
+ * from `package.json` (`revenexx.displayName`).
381
516
  */
382
517
  declare function buildManifest(nodes: INode[], credentials?: ICredential[], templates?: ITemplateDescription[]): NodeManifest;
383
518
 
@@ -489,4 +624,4 @@ declare abstract class OAuth2AuthCodeCredential extends BaseCredential implement
489
624
  test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
490
625
  }
491
626
 
492
- export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IInputPort, type INode, type INodeContext, type INodeDescription, type INodeResult, type INodeWithIteration, type IOutputField, type IOutputPort, type ITemplateDescription, type ITemplateTrigger, type LocalizedString, MANIFEST_VERSION, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, type OutputKind, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, buildManifest, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isNodeWithIteration, isOAuthAuthorizeCredential, normalizeCredentialType, normalizeLocalized, retryConfigFields, safeFetch, timeoutConfigField };
627
+ export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IImage, type IInputPort, type INode, type INodeAuthorContext, type INodeContext, type INodeDescription, type INodeResult, type INodeWithIteration, type IOutputField, type IOutputPort, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, MANIFEST_VERSION, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, type OutputKind, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, buildManifest, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isNodeWithIteration, isOAuthAuthorizeCredential, normalizeCredentialType, normalizeLocalized, parsePackageMeta, retryConfigFields, safeFetch, timeoutConfigField };
package/dist/index.d.ts CHANGED
@@ -2,7 +2,25 @@ type LocalizedString = string | Record<string, string>;
2
2
  type DataType = 'any' | 'object' | 'array' | 'string' | 'number' | 'boolean';
3
3
  type OutputKind = 'default' | 'branch' | 'error';
4
4
  type NodeCategory = 'trigger' | 'action' | 'transform' | 'control' | 'io';
5
- type ConfigType = 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'object' | 'array' | 'expression' | 'secret-ref' | 'credentials-ref';
5
+ type ConfigType = 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'object' | 'array' | 'expression' | 'secret-ref' | 'credentials-ref' | 'dynamic-schema';
6
+ /**
7
+ * Semantic category of an image, driving how the registry/CDN organises and
8
+ * displays it.
9
+ */
10
+ type ImageCategory = 'screenshot' | 'logo' | 'banner' | 'icon' | 'other';
11
+ /**
12
+ * A single image a node, credential type, or template ships alongside its
13
+ * description (e.g. a screenshot, logo, or banner). The file is bundled into
14
+ * the package tarball and uploaded to the Revenexx CDN when the package is
15
+ * published.
16
+ */
17
+ interface IImage {
18
+ /** Path to the image file, relative to the package root (e.g. `images/screenshot.png`). */
19
+ src: string;
20
+ alt: LocalizedString;
21
+ title?: LocalizedString;
22
+ category: ImageCategory;
23
+ }
6
24
  interface IInputPort {
7
25
  dataType: DataType;
8
26
  required?: boolean;
@@ -20,6 +38,14 @@ interface IOutputPort {
20
38
  description?: LocalizedString;
21
39
  fields?: Record<string, IOutputField>;
22
40
  sourceFromConfig?: string;
41
+ /**
42
+ * Marks a generic port set resolved at author time by the node's
43
+ * `resolveOutputs` callback (PO-143), rather than a static `name` or a
44
+ * config-driven `sourceFromConfig`. Set at most one of `name`,
45
+ * `sourceFromConfig`, `resolveOutputs` — this is a server-validated
46
+ * constraint on publish, not enforced by this (intentionally flat) type.
47
+ */
48
+ resolveOutputs?: boolean;
23
49
  fallback?: {
24
50
  name: string;
25
51
  label?: LocalizedString;
@@ -49,6 +75,23 @@ interface IConfigFieldBase {
49
75
  multiline?: boolean;
50
76
  validation?: IConfigValidation;
51
77
  options?: IConfigOption[];
78
+ /**
79
+ * When true, a *known* field's `options` are resolved at author time via the
80
+ * node's `loadOptions` (a live/dependent dropdown) instead of a static
81
+ * `options[]`. Applies to `select` / `multiselect`; left unset for ordinary
82
+ * static fields.
83
+ *
84
+ * Independent of `type: 'dynamic-schema'`: that field type triggers
85
+ * `resolveConfigSchema` on its own and does NOT require `dynamic: true`.
86
+ */
87
+ dynamic?: boolean;
88
+ /**
89
+ * Config keys whose values drive this field's dynamic resolution. The editor
90
+ * re-resolves when one of them changes. A key listed here is
91
+ * *dependency-driving* and MUST be a literal (it may not set
92
+ * `expressionAllowed`), so its value is known at author time.
93
+ */
94
+ dependsOn?: string[];
52
95
  /**
53
96
  * Only meaningful when `type === 'credentials-ref'`: the namespaced slug(s) of
54
97
  * the credential type(s) this field accepts (e.g. `revenexx:smtp`). The editor
@@ -70,6 +113,8 @@ interface INodeDescription {
70
113
  name: LocalizedString;
71
114
  description?: LocalizedString;
72
115
  icon?: string;
116
+ /** Associated images (screenshots, logos, banners) shipped with the package. */
117
+ images?: IImage[];
73
118
  inputs: Record<string, IInputPort>;
74
119
  outputs: IOutputPort[];
75
120
  config?: IConfigField[];
@@ -99,18 +144,73 @@ interface INodeResult {
99
144
  outputs: Record<string, unknown>;
100
145
  branch?: string;
101
146
  }
147
+ /**
148
+ * Context handed to a node's *author-time* resolvers (`loadOptions`,
149
+ * `resolveConfigSchema`, `resolveOutputs`). These run in the node-runtime host
150
+ * (a Node side-container) while a user is editing the workflow — never in
151
+ * `execute`. The result is snapshotted into the workflow blob at save, so the
152
+ * runtime never calls these (PO-143).
153
+ */
154
+ interface INodeAuthorContext {
155
+ signal: AbortSignal;
156
+ logger: {
157
+ info(message: string, meta?: Record<string, unknown>): void;
158
+ warn(message: string, meta?: Record<string, unknown>): void;
159
+ error(message: string, meta?: Record<string, unknown>): void;
160
+ };
161
+ /** The user's current partial config values. */
162
+ config: Record<string, unknown>;
163
+ /**
164
+ * Lazily resolve a `secret-ref` config field's value (a key in the tenant
165
+ * secret store → a single string). Mirrors {@link INodeContext.secrets} so a
166
+ * resolver that authenticates via a secret-ref uses the same call it would in
167
+ * `execute`. Resolves against the internal secret endpoint; only the keys the
168
+ * resolver actually asks for are fetched.
169
+ */
170
+ secrets: {
171
+ get(key: string): Promise<string>;
172
+ };
173
+ /**
174
+ * Lazily resolve a `credentials-ref` instance's access data via the
175
+ * credentials broker (id → structured material, e.g. `{ accessToken }`).
176
+ * Mirrors {@link INodeContext.credentials}.
177
+ */
178
+ credentials: {
179
+ get(credentialsId: string): Promise<Record<string, unknown>>;
180
+ };
181
+ /** Preferred locale for resolved labels, when the caller supplies one. */
182
+ locale?: string;
183
+ }
102
184
  interface INode {
103
185
  description: INodeDescription;
104
186
  execute(ctx: INodeContext, inputs: Record<string, unknown>): Promise<INodeResult>;
187
+ /**
188
+ * Author-time: resolve the options of a `dynamic` field (a live/dependent
189
+ * dropdown). `fieldKey` is the config key being resolved. Optional — declare
190
+ * it only for nodes with `dynamic` `select`/`multiselect` fields.
191
+ */
192
+ loadOptions?(ctx: INodeAuthorContext, fieldKey: string): Promise<IConfigOption[]>;
193
+ /**
194
+ * Author-time: resolve the flat set of typed config fields that replaces a
195
+ * `type: 'dynamic-schema'` marker (e.g. an API's parameters once app + API
196
+ * are chosen). Returns fields in the same config-field grammar; the node is
197
+ * responsible for flattening any nested API shape into flat keys.
198
+ */
199
+ resolveConfigSchema?(ctx: INodeAuthorContext): Promise<IConfigField[]>;
200
+ /**
201
+ * Author-time: resolve a generic output-port set for an output marked
202
+ * `resolveOutputs` (e.g. ports derived from a connected resource's schema).
203
+ */
204
+ resolveOutputs?(ctx: INodeAuthorContext): Promise<IOutputPort[]>;
105
205
  }
106
206
  /**
107
207
  * Optional capability interface for nodes that iterate over a collection.
108
- * Implement this alongside {@link INode} to signal to the worker that this
109
- * node drives iteration — the worker will call {@link extractItems} instead
208
+ * Implement this alongside {@link INode} to signal to the runtime that this
209
+ * node drives iteration — the runtime will call {@link extractItems} instead
110
210
  * of relying on slug-based detection.
111
211
  *
112
212
  * This interface is the designated dispatch point for future child-workflow
113
- * execution: once per-iteration isolation is needed, the worker can swap the
213
+ * execution: once per-iteration isolation is needed, the runtime can swap the
114
214
  * inline loop for `executeChild` calls without any changes to the node or SDK.
115
215
  */
116
216
  interface INodeWithIteration {
@@ -159,6 +259,8 @@ interface ICredentialDescription {
159
259
  name: LocalizedString;
160
260
  description?: LocalizedString;
161
261
  icon?: string;
262
+ /** Associated images (screenshots, logos, banners) shipped with the package. */
263
+ images?: IImage[];
162
264
  authKind: CredentialAuthKind;
163
265
  fields: ICredentialField[];
164
266
  }
@@ -270,6 +372,8 @@ interface ITemplateDescription {
270
372
  industries?: string[];
271
373
  /** Free-form vendor tags (e.g. `pipedrive`, `slack`). */
272
374
  vendors?: string[];
375
+ /** Associated images (screenshots, logos, banners) shipped with the package. */
376
+ images?: IImage[];
273
377
  /** Workflow-blob schema version `definition` targets (e.g. `v0-draft`). */
274
378
  blobVersion: string;
275
379
  /** The workflow blob instantiated when a user picks this template. */
@@ -284,7 +388,7 @@ interface ITemplateDescription {
284
388
  /**
285
389
  * Reduce a {@link LocalizedString} to a single plain string.
286
390
  *
287
- * Every consumer (worker, UI, the Laravel side) repeats the same "is it a
391
+ * Every consumer (the UI, the Laravel side) repeats the same "is it a
288
392
  * string or a localized map?" branch to render a name/label; this centralizes
289
393
  * it so the rules are defined once:
290
394
  *
@@ -302,8 +406,8 @@ declare function normalizeLocalized(value: LocalizedString | undefined | null, f
302
406
  *
303
407
  * The union `string | string[]` exists so node authors can write the common
304
408
  * single-type case as a bare string, but every consumer (the editor that
305
- * lists tenant credential instances, the worker that resolves the chosen
306
- * instance, the Laravel side) needs a list to iterate. This centralizes the
409
+ * lists tenant credential instances, the Laravel side) needs a list to
410
+ * iterate. This centralizes the
307
411
  * "is it a string or an array?" branch — analogous to {@link normalizeLocalized}
308
412
  * for `LocalizedString` — so the rules are defined once:
309
413
  *
@@ -374,10 +478,41 @@ interface NodeManifest {
374
478
  */
375
479
  templates?: ITemplateDescription[];
376
480
  }
481
+ /**
482
+ * The registry-relevant fields of a node package's `package.json`, as read by
483
+ * the CLI. `name`/`version` identify the package; `displayName` is the
484
+ * human-readable bundle label shown in the editor's node palette (e.g.
485
+ * „Business Central"). All three are read straight from `package.json` by the
486
+ * integrations server on upload — the CLI reads them only to warn about a
487
+ * missing label and to annotate the manifest log line.
488
+ *
489
+ * The label lives under a namespaced `revenexx` group in `package.json`
490
+ * (`{ "revenexx": { "displayName": "…" } }`), not a bespoke top-level key, so
491
+ * it can't collide with unrelated tooling that squats on `displayName`.
492
+ */
493
+ interface NodePackageMeta {
494
+ name: string;
495
+ version: string;
496
+ /** Human-readable bundle label (e.g. „Business Central"); optional. */
497
+ displayName?: string;
498
+ }
499
+ /**
500
+ * Extracts {@link NodePackageMeta} from parsed `package.json` contents, keeping
501
+ * only the registry-relevant fields and coercing anything malformed to a safe
502
+ * shape. All three fields are trimmed; a blank or whitespace-only value becomes
503
+ * `''` (`name`/`version`) or `undefined` (`displayName`, matching how the server
504
+ * treats it), so whitespace can't masquerade as a present value in tooling.
505
+ * The bundle label is read from the `revenexx` group (`revenexx.displayName`).
506
+ * Does not validate that `name`/`version` are present — the integrations server
507
+ * enforces that on upload; this is a typed, lenient read for tooling.
508
+ */
509
+ declare function parsePackageMeta(raw: unknown): NodePackageMeta;
377
510
  /**
378
511
  * Builds the manifest envelope from a package's `NODES` (and optional
379
512
  * `CREDENTIALS` / `TEMPLATES`) exports. The result is what gets written to
380
- * `dist/manifest.json` and uploaded to the registry inside the tarball.
513
+ * `dist/manifest.json` and uploaded to the registry inside the tarball. The
514
+ * bundle label is NOT carried here — the integrations server reads it straight
515
+ * from `package.json` (`revenexx.displayName`).
381
516
  */
382
517
  declare function buildManifest(nodes: INode[], credentials?: ICredential[], templates?: ITemplateDescription[]): NodeManifest;
383
518
 
@@ -489,4 +624,4 @@ declare abstract class OAuth2AuthCodeCredential extends BaseCredential implement
489
624
  test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
490
625
  }
491
626
 
492
- export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IInputPort, type INode, type INodeContext, type INodeDescription, type INodeResult, type INodeWithIteration, type IOutputField, type IOutputPort, type ITemplateDescription, type ITemplateTrigger, type LocalizedString, MANIFEST_VERSION, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, type OutputKind, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, buildManifest, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isNodeWithIteration, isOAuthAuthorizeCredential, normalizeCredentialType, normalizeLocalized, retryConfigFields, safeFetch, timeoutConfigField };
627
+ export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IImage, type IInputPort, type INode, type INodeAuthorContext, type INodeContext, type INodeDescription, type INodeResult, type INodeWithIteration, type IOutputField, type IOutputPort, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, MANIFEST_VERSION, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, type OutputKind, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, buildManifest, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isNodeWithIteration, isOAuthAuthorizeCredential, normalizeCredentialType, normalizeLocalized, parsePackageMeta, retryConfigFields, safeFetch, timeoutConfigField };
package/dist/index.js CHANGED
@@ -4,8 +4,9 @@ import {
4
4
  extractCredentialManifest,
5
5
  extractCredentialManifests,
6
6
  extractManifest,
7
- extractManifests
8
- } from "./chunk-X5HI7WPI.js";
7
+ extractManifests,
8
+ parsePackageMeta
9
+ } from "./chunk-WB3BYMLA.js";
9
10
 
10
11
  // src/types.ts
11
12
  function isNodeWithIteration(node) {
@@ -359,6 +360,7 @@ export {
359
360
  isOAuthAuthorizeCredential,
360
361
  normalizeCredentialType,
361
362
  normalizeLocalized,
363
+ parsePackageMeta,
362
364
  retryConfigFields,
363
365
  safeFetch,
364
366
  timeoutConfigField
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/integrations-node-sdk",
3
- "version": "0.12.1",
3
+ "version": "0.14.0",
4
4
  "description": "TypeScript interfaces and utilities for Revenexx integration nodes",
5
5
  "license": "MIT",
6
6
  "author": "revenexx GmbH",