@uipath/maestro-builder-sdk 6.14.3 → 6.14.4

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.
@@ -13,6 +13,22 @@ export interface BindingsV2Document {
13
13
  resources: BindingsV2Resource[];
14
14
  [k: string]: unknown;
15
15
  }
16
+ /**
17
+ * Which connector each bound connection belongs to, keyed by connection id —
18
+ * decided by EVERY node on the connection, so the answer does not depend on
19
+ * which step comes first.
20
+ *
21
+ * `bindings.json` cannot answer this: `prepare --connection-id` writes a label,
22
+ * a resource key and a kind (`ConnectionId` / `FolderKey`), never the connector
23
+ * (see `core/binding-messages.ts`). The nodes are the only record of it, so
24
+ * all of them must agree. Two that do not — a Drive step and a Slack step on
25
+ * one connection, or managed HTTP whose `targetConnector` is not the connection's
26
+ * connector — are refused with `CONNECTION_CONNECTOR_CONFLICT`: at most one of
27
+ * them can run, and picking the first silently mislabels the solution resource.
28
+ *
29
+ * @throws BindingRefusal when two nodes on one connection name different connectors.
30
+ */
31
+ export declare function connectionConnectors(flow: unknown): Map<string, string>;
16
32
  /**
17
33
  * The `connection` resources a compiled flow declares — one per distinct
18
34
  * connection, in node order, attributed to the FIRST node that uses it.
@@ -20,6 +36,16 @@ export interface BindingsV2Document {
20
36
  * Both halves of that are pack's behaviour, not a choice made here: a flow with
21
37
  * two Outlook steps on one connection yields a single resource whose
22
38
  * `ActivityName` is the earlier step's.
39
+ *
40
+ * What the resource IS does not follow node order: its `Connector` comes from
41
+ * {@link connectionConnectors} (every node on the connection, managed HTTP read
42
+ * through its `targetConnector`). Pack stamps the same value — measured with
43
+ * `uip maestro flow pack` on managed HTTP → Slack alone, before a native Slack
44
+ * step, and after one: `uipath-salesforce-slack` all three times, where this
45
+ * module used to stamp `uipath-uipath-http` for the first two (#866;
46
+ * uip 1.202.0-dev, 2026-09-30).
47
+ *
48
+ * @throws BindingRefusal when two nodes on one connection name different connectors.
23
49
  */
24
50
  export declare function connectionResources(flow: unknown): BindingsV2Resource[];
25
51
  /**
@@ -42,6 +42,7 @@
42
42
  * the connection rows.
43
43
  */
44
44
  import { existsSync, readFileSync } from 'node:fs';
45
+ import { BindingRefusal, connectionConnectorConflictMessage } from './core/binding-messages.js';
45
46
  /** The document version pack emits. */
46
47
  const DOCUMENT_VERSION = '2.0';
47
48
  /** The per-resource metadata version pack stamps. */
@@ -56,6 +57,93 @@ function str(value) {
56
57
  function isConnection(resource) {
57
58
  return typeof resource === 'string' && resource.toLowerCase() === 'connection';
58
59
  }
60
+ /** The managed-HTTP wrapper's own connector key — never the connection's type when a target is named. */
61
+ const MANAGED_HTTP_CONNECTOR = 'uipath-uipath-http';
62
+ function nodeDetail(node) {
63
+ return isRecord(node.inputs) && isRecord(node.inputs.detail) ? node.inputs.detail : undefined;
64
+ }
65
+ /** The connection a node runs on: `connectionId`, else `connectionResourceId`. */
66
+ function nodeConnectionKey(detail) {
67
+ return str(detail.connectionId) ?? str(detail.connectionResourceId);
68
+ }
69
+ /**
70
+ * The connector a node's CONNECTION belongs to — not the connector the node is.
71
+ *
72
+ * The two differ for managed HTTP: its `detail.connector` is always the wrapper,
73
+ * `uipath-uipath-http`, and the connection it authenticates with is the one
74
+ * `bodyParameters.targetConnector` names (`uipath-google-drive`,
75
+ * `uipath-salesforce-slack`, …). Stamping the wrapper made `uip solution
76
+ * resources refresh` refuse the connection — "Requested type
77
+ * 'uipath-uipath-http'; the resource is registered as type
78
+ * 'uipath-google-drive'" (flow-builder-sdk#866). Every other family's
79
+ * `detail.connector` is its connection's connector.
80
+ */
81
+ function nodeConnectionConnector(detail) {
82
+ const connector = str(detail.connector);
83
+ if (connector === MANAGED_HTTP_CONNECTOR && isRecord(detail.bodyParameters)) {
84
+ return str(detail.bodyParameters.targetConnector) ?? connector;
85
+ }
86
+ return connector;
87
+ }
88
+ /** Every node of the flow and of its inline subflows — they share one `bindings[]`. */
89
+ function allNodes(flow) {
90
+ const nodes = Array.isArray(flow.nodes) ? flow.nodes.filter(isRecord) : [];
91
+ if (isRecord(flow.subflows)) {
92
+ for (const sub of Object.values(flow.subflows)) {
93
+ if (isRecord(sub) && Array.isArray(sub.nodes))
94
+ nodes.push(...sub.nodes.filter(isRecord));
95
+ }
96
+ }
97
+ return nodes;
98
+ }
99
+ /**
100
+ * Which connector each bound connection belongs to, keyed by connection id —
101
+ * decided by EVERY node on the connection, so the answer does not depend on
102
+ * which step comes first.
103
+ *
104
+ * `bindings.json` cannot answer this: `prepare --connection-id` writes a label,
105
+ * a resource key and a kind (`ConnectionId` / `FolderKey`), never the connector
106
+ * (see `core/binding-messages.ts`). The nodes are the only record of it, so
107
+ * all of them must agree. Two that do not — a Drive step and a Slack step on
108
+ * one connection, or managed HTTP whose `targetConnector` is not the connection's
109
+ * connector — are refused with `CONNECTION_CONNECTOR_CONFLICT`: at most one of
110
+ * them can run, and picking the first silently mislabels the solution resource.
111
+ *
112
+ * @throws BindingRefusal when two nodes on one connection name different connectors.
113
+ */
114
+ export function connectionConnectors(flow) {
115
+ const out = new Map();
116
+ if (!isRecord(flow))
117
+ return out;
118
+ const bound = new Set();
119
+ for (const b of Array.isArray(flow.bindings) ? flow.bindings.filter(isRecord) : []) {
120
+ const key = str(b.resourceKey);
121
+ if (key && isConnection(b.resource) && str(b.propertyAttribute)?.toLowerCase() === 'connectionid')
122
+ bound.add(key);
123
+ }
124
+ const users = new Map();
125
+ for (const node of allNodes(flow)) {
126
+ const detail = nodeDetail(node);
127
+ const key = detail && nodeConnectionKey(detail);
128
+ if (!detail || !key || !bound.has(key))
129
+ continue;
130
+ const connector = nodeConnectionConnector(detail);
131
+ if (!connector)
132
+ continue;
133
+ const byConnector = users.get(key) ?? new Map();
134
+ users.set(key, byConnector);
135
+ const steps = byConnector.get(connector) ?? [];
136
+ steps.push((isRecord(node.display) ? str(node.display.label) : undefined) ?? str(node.id) ?? '?');
137
+ byConnector.set(connector, steps);
138
+ }
139
+ for (const [key, byConnector] of users) {
140
+ if (byConnector.size > 1) {
141
+ throw new BindingRefusal('CONNECTION_CONNECTOR_CONFLICT', connectionConnectorConflictMessage(key, byConnector));
142
+ }
143
+ out.set(key, [...byConnector.keys()][0]);
144
+ }
145
+ return out;
146
+ }
59
147
  /**
60
148
  * The `connection` resources a compiled flow declares — one per distinct
61
149
  * connection, in node order, attributed to the FIRST node that uses it.
@@ -63,6 +151,16 @@ function isConnection(resource) {
63
151
  * Both halves of that are pack's behaviour, not a choice made here: a flow with
64
152
  * two Outlook steps on one connection yields a single resource whose
65
153
  * `ActivityName` is the earlier step's.
154
+ *
155
+ * What the resource IS does not follow node order: its `Connector` comes from
156
+ * {@link connectionConnectors} (every node on the connection, managed HTTP read
157
+ * through its `targetConnector`). Pack stamps the same value — measured with
158
+ * `uip maestro flow pack` on managed HTTP → Slack alone, before a native Slack
159
+ * step, and after one: `uipath-salesforce-slack` all three times, where this
160
+ * module used to stamp `uipath-uipath-http` for the first two (#866;
161
+ * uip 1.202.0-dev, 2026-09-30).
162
+ *
163
+ * @throws BindingRefusal when two nodes on one connection name different connectors.
66
164
  */
67
165
  export function connectionResources(flow) {
68
166
  if (!isRecord(flow))
@@ -78,25 +176,31 @@ export function connectionResources(flow) {
78
176
  continue;
79
177
  if (str(binding.propertyAttribute)?.toLowerCase() !== 'connectionid')
80
178
  continue;
81
- if (!byKey.has(key))
82
- byKey.set(key, binding);
179
+ byKey.set(key, [...(byKey.get(key) ?? []), binding]);
83
180
  }
84
181
  if (byKey.size === 0)
85
182
  return [];
183
+ const connectors = connectionConnectors(flow);
86
184
  const nodes = Array.isArray(flow.nodes) ? flow.nodes.filter(isRecord) : [];
87
185
  const resources = [];
88
186
  const seen = new Set();
89
187
  for (const node of nodes) {
90
- const detail = isRecord(node.inputs) && isRecord(node.inputs.detail) ? node.inputs.detail : undefined;
188
+ const detail = nodeDetail(node);
91
189
  if (!detail)
92
190
  continue;
93
- const key = str(detail.connectionId) ?? str(detail.connectionResourceId);
191
+ const key = nodeConnectionKey(detail);
94
192
  if (!key || seen.has(key))
95
193
  continue;
96
- const binding = byKey.get(key);
97
- if (!binding)
194
+ const candidates = byKey.get(key);
195
+ if (!candidates)
98
196
  continue;
99
197
  seen.add(key);
198
+ const connector = connectors.get(key) ?? nodeConnectionConnector(detail) ?? '';
199
+ // The label is the FIRST ConnectionId binding on the connection, even when
200
+ // #865's fix emits one per referencing definition — measured against `uip
201
+ // maestro flow pack` (uip 1.202.0-dev) on both orders of the issue's repro: pack labels
202
+ // the resource after the earlier step's binding, like `ActivityName`.
203
+ const binding = candidates[0];
100
204
  const displayLabel = str(binding.name) ?? key;
101
205
  const label = isRecord(node.display) ? str(node.display.label) : undefined;
102
206
  resources.push({
@@ -116,7 +220,7 @@ export function connectionResources(flow) {
116
220
  BindingsVersion: BINDINGS_VERSION,
117
221
  DisplayLabel: displayLabel,
118
222
  UseConnectionService: 'true',
119
- Connector: str(detail.connector) ?? '',
223
+ Connector: connector,
120
224
  },
121
225
  });
122
226
  }
package/dist/check.js CHANGED
@@ -11,7 +11,7 @@ import { isLookupToken, resolvedValue, unresolvedLookupMessage, } from './core/l
11
11
  import { QUEUE_ITEM_RECORD_SCHEMA } from './core/queue-record.js';
12
12
  import { EVENT_FILTER_OPERATORS, readEventFilter, eventFilterProblem } from './event-filters.js';
13
13
  import { extractRefs } from './core/expr-check.js';
14
- import { bindingSelfNameMessage, bindingUndeclaredMessage, undeclaredBinding } from './core/binding-messages.js';
14
+ import { bindingSelfNameMessage, bindingUndeclaredMessage, connectionConnectorConflictMessage, undeclaredBinding } from './core/binding-messages.js';
15
15
  import { isAnswerField } from './core/hitl-answers.js';
16
16
  import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome } from './core/hitl-routing.js';
17
17
  import { childStepLists, declaredExits, hasExits, stopsAtEnd } from './core/step-ports.js';
@@ -20,7 +20,51 @@ import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js
20
20
  import { prepareCommand, resolveFlagArg } from './core/cli-spelling.js';
21
21
  import { EVENT_BYOA_UNVERIFIED, byoaUnverified } from './core/byoa.js';
22
22
  export function check(built, opts = {}) {
23
- return checkFlow(built, [], opts);
23
+ const outer = connectionUsage;
24
+ connectionUsage = [];
25
+ try {
26
+ const diags = checkFlow(built, [], opts);
27
+ diags.push(...connectionConnectorConflicts(connectionUsage, opts));
28
+ return diags;
29
+ }
30
+ finally {
31
+ connectionUsage = outer;
32
+ }
33
+ }
34
+ /**
35
+ * The connection uses {@link checkBindingLabels} saw during the current
36
+ * `check()` — the flow and every child flow, which share one `bindings[]`.
37
+ * Module state rather than a `CheckOpts` field because `check` is synchronous
38
+ * and this is not an input; `check()` saves and restores it, so a nested call
39
+ * cannot mix two flows.
40
+ */
41
+ let connectionUsage;
42
+ /**
43
+ * CONNECTION_CONNECTOR_CONFLICT — the rule `serialize` enforces on the emitted
44
+ * document (`connectionConnectors`, bindings-v2.ts), reported here with the same
45
+ * sentence so `check` does not print `✓ no issues` for a source `compile` then
46
+ * refuses. Labels are grouped by what they resolve to, so two labels bound to
47
+ * one connection id are one connection, as they are in the `.flow`.
48
+ */
49
+ function connectionConnectorConflicts(uses, opts) {
50
+ const byConnection = new Map();
51
+ for (const use of uses) {
52
+ const key = opts.bindings?.resolve(use.label) ?? use.label;
53
+ const byConnector = byConnection.get(key) ?? new Map();
54
+ byConnection.set(key, byConnector);
55
+ byConnector.set(use.connector, [...(byConnector.get(use.connector) ?? []), use.step]);
56
+ }
57
+ const out = [];
58
+ for (const [key, byConnector] of byConnection) {
59
+ if (byConnector.size < 2)
60
+ continue;
61
+ out.push({
62
+ level: 'error', code: 'CONNECTION_CONNECTOR_CONFLICT',
63
+ step: [...byConnector.values()][1][0],
64
+ message: connectionConnectorConflictMessage(key, byConnector),
65
+ });
66
+ }
67
+ return out;
24
68
  }
25
69
  /**
26
70
  * `ancestors` is the chain of child flows currently being checked, innermost
@@ -358,7 +402,10 @@ function checkFlow(built, ancestors, opts = {}) {
358
402
  const filled = (v) => typeof v === 'string' && v.trim() !== '';
359
403
  if (pair?.managed && filled(pair.connection) && filled(pair.folder)) {
360
404
  const httpStep = s.name;
361
- checkBindingLabels(`managed http "${httpStep}"`, pair, opts, (code, message) => diags.push({ level: 'error', code, step: httpStep, message }));
405
+ const target = pair.targetConnector;
406
+ checkBindingLabels(`managed http "${httpStep}"`, pair, opts, (code, message) => diags.push({ level: 'error', code, step: httpStep, message }), undefined,
407
+ // The connector its CONNECTION belongs to: the target, not the wrapper.
408
+ { step: httpStep, connector: typeof target === 'string' && target !== '' ? target : 'uipath-uipath-http' });
362
409
  }
363
410
  const seen = new Set();
364
411
  for (const b of s.spec.inputs.branches ?? []) {
@@ -537,7 +584,7 @@ function checkFlow(built, ancestors, opts = {}) {
537
584
  });
538
585
  checkEventSubscription(s.spec.subscription, `waitForEvent "${stepName}"`, push('error'), push('warning'), eventParamsFor(s.spec.subscription, opts));
539
586
  checkEventResolution(s.spec.subscription, `waitForEvent "${stepName}"`, 'event', opts, push('error'), push('warning'), eventParamsFor(s.spec.subscription, opts));
540
- checkBindingLabels(`waitForEvent "${stepName}"`, s.spec.subscription, opts, push('error'), eventTarget(s.spec.subscription));
587
+ checkBindingLabels(`waitForEvent "${stepName}"`, s.spec.subscription, opts, push('error'), eventTarget(s.spec.subscription), { step: stepName, connector: eventTarget(s.spec.subscription)?.key });
541
588
  }
542
589
  if (s.spec.kind === 'subflow') {
543
590
  // Only a real child goes in the map: a missing one is reported by
@@ -1453,7 +1500,7 @@ function checkConnectorContract(step, spec, opts, diags) {
1453
1500
  if (inputErr) {
1454
1501
  diags.push({ level: 'error', code: 'CONNECTOR_INPUT', step, message: `Connector step "${step}": ${inputErr}` });
1455
1502
  }
1456
- checkBindingLabels(`Connector step "${step}"`, spec, opts, (code, message) => diags.push({ level: 'error', code, step, message }), { key: spec.key, action: spec.action });
1503
+ checkBindingLabels(`Connector step "${step}"`, spec, opts, (code, message) => diags.push({ level: 'error', code, step, message }), { key: spec.key, action: spec.action }, { step, connector: spec.key });
1457
1504
  }
1458
1505
  /** What `registry prepare` takes for an event subscription: its connector key and curated event name. */
1459
1506
  function eventTarget(sub) {
@@ -1472,7 +1519,13 @@ function eventTarget(sub) {
1472
1519
  * Silent without a bindings.json, and for a label that resolves to anything
1473
1520
  * other than its own name (stub values are compile's CONNECTION_STUB warning).
1474
1521
  */
1475
- function checkBindingLabels(where, labels, opts, report, target) {
1522
+ function checkBindingLabels(where, labels, opts, report, target,
1523
+ /** The step, and the connector its connection must belong to, for CONNECTION_CONNECTOR_CONFLICT. */
1524
+ use) {
1525
+ const label = labels?.connection;
1526
+ if (use?.connector && typeof label === 'string' && label !== '') {
1527
+ connectionUsage?.push({ step: use.step, label, connector: use.connector });
1528
+ }
1476
1529
  const bindings = opts.bindings;
1477
1530
  if (!bindings || !labels)
1478
1531
  return;
@@ -2285,7 +2338,7 @@ function checkTrigger(spec, diags, opts = {}) {
2285
2338
  const sub = spec.subscription;
2286
2339
  checkEventSubscription(sub, 'trigger', at, push('warning'), eventParamsFor(sub, opts));
2287
2340
  checkEventResolution(sub, 'trigger', 'trigger', opts, at, push('warning'), eventParamsFor(sub, opts));
2288
- checkBindingLabels('event trigger', sub, opts, at, eventTarget(sub));
2341
+ checkBindingLabels('event trigger', sub, opts, at, eventTarget(sub), { step: 'trigger', connector: eventTarget(sub)?.key });
2289
2342
  return;
2290
2343
  }
2291
2344
  // manual() is the explicit spelling of the default — nothing to validate.
@@ -1,7 +1,11 @@
1
1
  /** A tenant resource key — an Integration Service connection id or an Orchestrator folder key — is a GUID. */
2
2
  export declare const GUID_RE: RegExp;
3
3
  /** The diagnostic codes a label refusal carries, in `check` and on the error `serialize` throws. */
4
- export type BindingRefusalCode = 'BINDING_SELF_NAME' | 'BINDING_UNDECLARED';
4
+ export type BindingRefusalCode = 'BINDING_SELF_NAME' | 'BINDING_UNDECLARED'
5
+ /** An emitted definition's `<bindings.NAME>` placeholder has no `bindings[]` entry named NAME (#865). */
6
+ | 'BINDING_PLACEHOLDER_UNRESOLVED'
7
+ /** Two steps on one connection disagree on which connector it belongs to (#866). */
8
+ | 'CONNECTION_CONNECTOR_CONFLICT';
5
9
  /**
6
10
  * What `serialize` throws for a label it refuses. It is an ordinary `Error` whose
7
11
  * message is the sentence `check` prints; the `code` lets `compile --no-check`
@@ -58,3 +62,14 @@ export interface PrepareTarget {
58
62
  * `prepare` command for this step's own connector and operation.
59
63
  */
60
64
  export declare function bindingUndeclaredMessage(where: string, name: string, attribute: 'connection' | 'folder', declared: readonly string[], target?: PrepareTarget): string;
65
+ /**
66
+ * Why compile refuses one connection used by steps of different connectors —
67
+ * shared by `serialize` (which throws it, via `connectionConnectors` in
68
+ * `bindings-v2.ts`) and `check`, so both print the same sentence.
69
+ *
70
+ * `users` maps each connector to the steps that use the connection as that
71
+ * connector. For managed HTTP the connector is its `targetConnector` — the
72
+ * connection it authenticates with — not the `uipath-uipath-http` wrapper
73
+ * (flow-builder-sdk#866).
74
+ */
75
+ export declare function connectionConnectorConflictMessage(connection: string, users: ReadonlyMap<string, readonly string[]>): string;
@@ -89,3 +89,24 @@ export function bindingUndeclaredMessage(where, name, attribute, declared, targe
89
89
  + `declares the label with a placeholder resourceKey instead, which compiles with a CONNECTION_STUB warning — `
90
90
  + `never an invented GUID.`;
91
91
  }
92
+ /**
93
+ * Why compile refuses one connection used by steps of different connectors —
94
+ * shared by `serialize` (which throws it, via `connectionConnectors` in
95
+ * `bindings-v2.ts`) and `check`, so both print the same sentence.
96
+ *
97
+ * `users` maps each connector to the steps that use the connection as that
98
+ * connector. For managed HTTP the connector is its `targetConnector` — the
99
+ * connection it authenticates with — not the `uipath-uipath-http` wrapper
100
+ * (flow-builder-sdk#866).
101
+ */
102
+ export function connectionConnectorConflictMessage(connection, users) {
103
+ const who = [...users]
104
+ .map(([connector, steps]) => `${steps.map((s) => `"${s}"`).join(', ')} as ${connector}`)
105
+ .join('; ');
106
+ return `connection "${connection}" is used by steps of different connectors (${who}). An Integration Service `
107
+ + `connection belongs to exactly one connector, so at most one of these steps can run on it, and the solution `
108
+ + `resource would be declared as whichever came first. Give each connector its own connection label in `
109
+ + `bindings.json (\`${prepareCommand('<connector-key>', '<action>')}\` binds one per connector). A managed `
110
+ + `\`http({ managed: true, … })\` step on another connector's connection must name that connector in `
111
+ + `\`targetConnector\`.`;
112
+ }
@@ -54,6 +54,34 @@ export declare function serializeWithSidecars(built: BuiltFlow, opts?: Serialize
54
54
  }>;
55
55
  };
56
56
  export declare function serialize(built: BuiltFlow, opts?: SerializeOpts): any;
57
+ /**
58
+ * Every `<bindings.NAME>` placeholder a node's definition carries in its
59
+ * `model.context` must resolve to a `bindings[]` entry for THAT node.
60
+ *
61
+ * "Resolve" is Studio Web's rule, not a looser one: flow-v1
62
+ * `matchContextBindings` + `getInstanceBindingKey`
63
+ * (flow-schema/src/packaging/resolve-context.ts) take the entries whose
64
+ * `resource` EQUALS the definition's `model.bindings.resource`, scope them to the
65
+ * node's `inputs.detail.connectionResourceId` when that resource is `connection`
66
+ * (or undeclared), and look the placeholder up there BY NAME. Any miss blocks
67
+ * publish (MST-15010), and one that slips through reaches Studio as the literal
68
+ * placeholder: `invalid GUID <bindings.uipath-salesforce-slack connection>`.
69
+ * `uip maestro flow validate` does not check this.
70
+ *
71
+ * That is how #865 shipped — a per-call-site dedupe that let the first step on a
72
+ * shared connection claim it — and UV-16774 before it (a `Connection` resource
73
+ * spelling). Checking the finished document catches that class for every family
74
+ * and every emission order, whichever call site produced it.
75
+ *
76
+ * It holds for real definitions: of the 5,102 in the local registry index
77
+ * (2026-09-30), 4,999 reference a `<bindings.…>` name and every one of them
78
+ * declares that name in `model.bindings.values`, which is what the emitters
79
+ * close. Agent resource definitions are the one exemption (see below).
80
+ *
81
+ * @throws BindingRefusal `BINDING_PLACEHOLDER_UNRESOLVED`, naming each dangling
82
+ * placeholder and the step that carries it.
83
+ */
84
+ export declare function assertBindingPlaceholdersResolve(flow: any): void;
57
85
  /**
58
86
  * What `compile` would refuse these connector inputs with, or `undefined` when it
59
87
  * would accept them — the SAME code path `buildConnectorInputs` runs at emit time,
package/dist/serialize.js CHANGED
@@ -15,6 +15,7 @@ import { FlowAction, FlowTrigger } from './core/node-classes.js';
15
15
  import { connectorRawNodeRefusal } from './core/connector-raw-node.js';
16
16
  import { dataFabricUnknownInputs } from './core/typed-raw-node.js';
17
17
  import { isAnswerField } from './core/hitl-answers.js';
18
+ import { connectionConnectors } from './bindings-v2.js';
18
19
  import { BindingRefusal, GUID_RE, bindingSelfNameMessage, bindingUndeclaredMessage, undeclaredBinding } from './core/binding-messages.js';
19
20
  import { ByoaRefusal, byoaUnverified } from './core/byoa.js';
20
21
  import { OUTCOME_HANDLE_NODE_TYPES, assertHitlExitInvariant, hitlExitModel, hitlExitViolation, outcomeSlug } from './core/hitl-routing.js';
@@ -2306,9 +2307,15 @@ function connectionResourceBindings(spec, existing) {
2306
2307
  if (values.length === 0)
2307
2308
  return { entries: [] };
2308
2309
  const resourceKey = spec.resourceKey ?? spec.connectionId ?? '';
2309
- const have = (attr) => [...existing.values()].some((b) => b.resource === 'connection' && b.resourceKey === resourceKey && b.propertyAttribute === attr);
2310
- if (have('ConnectionId') && have('FolderKey'))
2311
- return { entries: [] };
2310
+ // Keyed on the NAME as well (flow-builder-sdk#865). The name is the definition's
2311
+ // own `<bindings.NAME>` placeholder, and two definitions sharing one connection
2312
+ // can spell it differently — managed HTTP says `uipath-uipath-http connection`,
2313
+ // the Slack connector `uipath-salesforce-slack connection`. Keyed on
2314
+ // resourceKey + attribute alone, whichever step came first claimed the
2315
+ // connection and the other node's placeholder never resolved: Studio showed
2316
+ // `invalid GUID <bindings.uipath-salesforce-slack connection>`.
2317
+ const have = (name, attr) => [...existing.values()].some((b) => b.resource === 'connection' && b.name === name && b.resourceKey === resourceKey
2318
+ && b.propertyAttribute === attr);
2312
2319
  const slug = spec.connectorKey.replace(/[^A-Za-z0-9]/g, '') || 'Connector';
2313
2320
  let base = `b${slug.charAt(0).toUpperCase()}${slug.slice(1)}`;
2314
2321
  for (let n = 2; existing.has(`${base}Connection`) || existing.has(`${base}FolderKey`); n++) {
@@ -2317,7 +2324,7 @@ function connectionResourceBindings(spec, existing) {
2317
2324
  const entries = values.flatMap((v) => {
2318
2325
  const name = typeof v.name === 'string' ? v.name : '';
2319
2326
  const attr = v.propertyAttribute;
2320
- if (!name || (attr !== 'ConnectionId' && attr !== 'FolderKey') || have(attr))
2327
+ if (!name || (attr !== 'ConnectionId' && attr !== 'FolderKey') || have(name, attr))
2321
2328
  return [];
2322
2329
  const isConnection = attr === 'ConnectionId';
2323
2330
  const dflt = isConnection ? spec.connectionId : spec.folderKey;
@@ -6280,8 +6287,84 @@ export function serialize(built, opts = {}) {
6280
6287
  // Passing the target is what lets a version-conditional step exist at all —
6281
6288
  // see migrate.ts's VERSION_CONDITIONAL ladder.
6282
6289
  migrateToSchemaVersion(flow, schemaVersion);
6290
+ // Emitted-document invariants over connections, checked on the FINISHED file
6291
+ // so every family and every emission order is covered by one rule, not by
6292
+ // each call site remembering it (flow-builder-sdk#865/#866).
6293
+ assertBindingPlaceholdersResolve(flow);
6294
+ connectionConnectors(flow);
6283
6295
  return flow;
6284
6296
  }
6297
+ /**
6298
+ * Every `<bindings.NAME>` placeholder a node's definition carries in its
6299
+ * `model.context` must resolve to a `bindings[]` entry for THAT node.
6300
+ *
6301
+ * "Resolve" is Studio Web's rule, not a looser one: flow-v1
6302
+ * `matchContextBindings` + `getInstanceBindingKey`
6303
+ * (flow-schema/src/packaging/resolve-context.ts) take the entries whose
6304
+ * `resource` EQUALS the definition's `model.bindings.resource`, scope them to the
6305
+ * node's `inputs.detail.connectionResourceId` when that resource is `connection`
6306
+ * (or undeclared), and look the placeholder up there BY NAME. Any miss blocks
6307
+ * publish (MST-15010), and one that slips through reaches Studio as the literal
6308
+ * placeholder: `invalid GUID <bindings.uipath-salesforce-slack connection>`.
6309
+ * `uip maestro flow validate` does not check this.
6310
+ *
6311
+ * That is how #865 shipped — a per-call-site dedupe that let the first step on a
6312
+ * shared connection claim it — and UV-16774 before it (a `Connection` resource
6313
+ * spelling). Checking the finished document catches that class for every family
6314
+ * and every emission order, whichever call site produced it.
6315
+ *
6316
+ * It holds for real definitions: of the 5,102 in the local registry index
6317
+ * (2026-09-30), 4,999 reference a `<bindings.…>` name and every one of them
6318
+ * declares that name in `model.bindings.values`, which is what the emitters
6319
+ * close. Agent resource definitions are the one exemption (see below).
6320
+ *
6321
+ * @throws BindingRefusal `BINDING_PLACEHOLDER_UNRESOLVED`, naming each dangling
6322
+ * placeholder and the step that carries it.
6323
+ */
6324
+ export function assertBindingPlaceholdersResolve(flow) {
6325
+ const bindings = Array.isArray(flow?.bindings) ? flow.bindings : [];
6326
+ const defs = new Map();
6327
+ for (const def of Array.isArray(flow?.definitions) ? flow.definitions : []) {
6328
+ defs.set(`${def?.nodeType}@${def?.version}`, def);
6329
+ }
6330
+ const nodes = [...(Array.isArray(flow?.nodes) ? flow.nodes : [])];
6331
+ for (const sub of Object.values(flow?.subflows ?? {})) {
6332
+ if (Array.isArray(sub?.nodes))
6333
+ nodes.push(...sub.nodes);
6334
+ }
6335
+ const dangling = [];
6336
+ for (const node of nodes) {
6337
+ // An agent's resource nodes (`uipath.agent.resource.tool.connector.…`) carry
6338
+ // the same placeholders, but they are resolved through the AGENT project —
6339
+ // `uip agent refresh` generates the connector tool's resource — and the
6340
+ // deployed corpus carries no flow `bindings[]` entry for them.
6341
+ if (typeof node?.type !== 'string' || node.type.startsWith('uipath.agent.resource.'))
6342
+ continue;
6343
+ const model = defs.get(`${node.type}@${node.typeVersion}`)?.model;
6344
+ if (!Array.isArray(model?.context))
6345
+ continue;
6346
+ const resource = model.bindings?.resource;
6347
+ const scope = resource === 'connection' || resource === undefined
6348
+ ? node.inputs?.detail?.connectionResourceId ?? node.inputs?.connectionId ?? undefined
6349
+ : undefined;
6350
+ const candidates = resource === undefined ? bindings : bindings.filter((b) => b?.resource === resource);
6351
+ for (const row of model.context) {
6352
+ const name = typeof row?.value === 'string' ? /^<bindings\.(.+)>$/.exec(row.value)?.[1] : undefined;
6353
+ if (name === undefined)
6354
+ continue;
6355
+ if (candidates.some((b) => b?.name === name && (!scope || b.resourceKey === scope)))
6356
+ continue;
6357
+ dangling.push(`step "${node.display?.label ?? node.id}" (${node.type}): "${row.value}"`
6358
+ + `${scope ? ` on connection ${scope}` : ''}`);
6359
+ }
6360
+ }
6361
+ if (dangling.length === 0)
6362
+ return;
6363
+ throw new BindingRefusal('BINDING_PLACEHOLDER_UNRESOLVED', `serialize: ${dangling.join('; ')} — no bindings[] entry of that name${dangling.length === 1 ? '' : ' (each)'} `
6364
+ + `is emitted for the step, so Studio Web reads the placeholder itself as the value (`
6365
+ + `"invalid GUID <bindings.…>") and refuses to publish. This is a serializer defect, not an authoring `
6366
+ + `error; report it with the flow source.`);
6367
+ }
6285
6368
  /**
6286
6369
  * Validate a connector's activity inputs against its declared field list and
6287
6370
  * return them ROUTED to the wire slots the platform reads. Catches the three
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.14.3",
3
+ "version": "6.14.4",
4
4
  "description": "Build UiPath Flow, Case, and BPMN artifacts by writing TypeScript.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.uipath.com/maestro",
@@ -92,5 +92,5 @@
92
92
  "@types/node": "^22.7.0",
93
93
  "esbuild": "^0.28.1"
94
94
  },
95
- "gitref": "52d87c5c383324464fd4c672ac4e9ec40795d994"
95
+ "gitref": "deacd660d0428eb3144e4b61a76bbfbbfc87cbe8"
96
96
  }