@uipath/maestro-builder-sdk 6.21.2 → 6.23.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.
Files changed (42) hide show
  1. package/dist/api-index.md +138 -138
  2. package/dist/api-members.md +407 -407
  3. package/dist/check.js +43 -331
  4. package/dist/core/actions.d.ts +0 -287
  5. package/dist/core/actions.js +0 -95
  6. package/dist/core/expr.d.ts +1 -1
  7. package/dist/core/expr.js +2 -2
  8. package/dist/core/nodes/node-type.d.ts +15 -0
  9. package/dist/core/step-ports.js +7 -23
  10. package/dist/core/typed-raw-node.js +0 -4
  11. package/dist/decompile.js +44 -115
  12. package/dist/flow-node-registry.js +3 -1
  13. package/dist/flow-node-type.d.ts +59 -0
  14. package/dist/flow-sdk.d.ts +2 -0
  15. package/dist/flow-sdk.js +2 -0
  16. package/dist/flow-step-exits.d.ts +12 -0
  17. package/dist/flow-step-exits.js +30 -0
  18. package/dist/nodes/ai-patterns/flow/factories.d.ts +83 -0
  19. package/dist/nodes/ai-patterns/flow/factories.js +86 -0
  20. package/dist/nodes/ai-patterns/flow/family.d.ts +3 -0
  21. package/dist/nodes/ai-patterns/flow/family.js +8 -0
  22. package/dist/nodes/ai-patterns/flow/index.d.ts +3 -0
  23. package/dist/nodes/ai-patterns/flow/index.js +3 -0
  24. package/dist/nodes/ai-patterns/flow/node-type.d.ts +11 -0
  25. package/dist/nodes/ai-patterns/flow/node-type.js +106 -0
  26. package/dist/nodes/ai-patterns/flow/spec.d.ts +75 -0
  27. package/dist/nodes/ai-patterns/flow/spec.js +1 -0
  28. package/dist/nodes/http/flow/check.d.ts +58 -0
  29. package/dist/nodes/http/flow/check.js +248 -0
  30. package/dist/nodes/http/flow/factories.d.ts +14 -0
  31. package/dist/nodes/http/flow/factories.js +20 -0
  32. package/dist/nodes/http/flow/family.d.ts +8 -0
  33. package/dist/nodes/http/flow/family.js +13 -0
  34. package/dist/nodes/http/flow/index.d.ts +3 -0
  35. package/dist/nodes/http/flow/index.js +3 -0
  36. package/dist/nodes/http/flow/node-type.d.ts +7 -0
  37. package/dist/nodes/http/flow/node-type.js +411 -0
  38. package/dist/nodes/http/flow/spec.d.ts +151 -0
  39. package/dist/nodes/http/flow/spec.js +1 -0
  40. package/dist/serialize.d.ts +2 -2
  41. package/dist/serialize.js +20 -316
  42. package/package.json +2 -2
package/dist/check.js CHANGED
@@ -20,7 +20,7 @@ import { commentText } from './core/comment-text.js';
20
20
  import { nearestName } from './core/edit-distance.js';
21
21
  import { isAnswerField } from './core/hitl-answers.js';
22
22
  import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome, outcomeIdOf } from './core/hitl-routing.js';
23
- import { declaredExits, hasExits } from './core/step-ports.js';
23
+ import { hasExits } from './core/step-ports.js';
24
24
  import { stopsAtEnd } from './flow-step-tree.js';
25
25
  import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
26
26
  import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js';
@@ -28,6 +28,8 @@ import { planFlowGlobals, planSharedInputs, sharedInputRoots } from './flow-vari
28
28
  import { planOutputBacking } from './flow-output-backing.js';
29
29
  import { ownEntry } from './core/own-entry.js';
30
30
  import { FLOW_NODE_TYPES } from './flow-node-registry.js';
31
+ import { stepExits } from './flow-step-exits.js';
32
+ import { plainHttpOnErrorMessage } from './nodes/http/flow/family.js';
31
33
  import { resolveVersion } from './core/nodes/versioned.js';
32
34
  import { declaredErrorPortRefusal } from './core/error-port.js';
33
35
  export function check(built, opts = {}) {
@@ -125,6 +127,14 @@ function checkFlow(built, ancestors, opts = {}) {
125
127
  const registeredSteps = new Map();
126
128
  /** Steps of a registered kind whose version did not resolve (NODE_VERSION_UNREGISTERED), so a family rule can stay silent about them. */
127
129
  const unresolvedSteps = new Set();
130
+ /**
131
+ * Every step of a registered kind, placed or not, with the implementation its
132
+ * node-local rules ran with: an unregistered pin falls back to the version it
133
+ * would get unpinned. The step-level rules that follow a step (its output
134
+ * reads, its `.onError`) ask this one, so a wrong pin is not followed by a
135
+ * second round of mistakes. Family rules use `registeredSteps` only.
136
+ */
137
+ const checkedSteps = new Map();
128
138
  /**
129
139
  * Every expression the flow carries, with the step that failed when the
130
140
  * expression sits inside that step's error handler. The failed step's own
@@ -270,13 +280,14 @@ function checkFlow(built, ancestors, opts = {}) {
270
280
  declared.add(s.name); // a step's output is referenceable by name
271
281
  }
272
282
  if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port === 'error')
273
- checkOnError(s, prevPortSource, diags, prevPortSource && registeredSteps.get(prevPortSource.name));
283
+ checkOnError(s, prevPortSource, diags, prevPortSource && checkedSteps.get(prevPortSource.name));
274
284
  if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port.startsWith('branch-')) {
275
285
  const want = s.port.slice('branch-'.length);
276
286
  const prevSpec = prevAction?.spec;
277
- const declared = prevSpec && 'kind' in prevSpec && prevSpec.kind === 'http'
278
- ? (prevSpec.inputs.branches ?? []).some((b) => b.name === want)
279
- : false;
287
+ // Whichever type the preceding step is, its declared exits say whether it
288
+ // has this port: only an HTTP step names `branch-<name>` exits.
289
+ const exits = prevSpec && 'kind' in prevSpec ? stepExits(prevSpec, prevAction?.options?.version) : undefined;
290
+ const declared = exits !== undefined && hasExits(exits) && exits.some((e) => e.port === s.port);
280
291
  if (!declared) {
281
292
  diags.push({
282
293
  level: 'error', code: 'HTTP_BRANCH_UNKNOWN', step: prevAction?.name,
@@ -375,9 +386,10 @@ function checkFlow(built, ancestors, opts = {}) {
375
386
  prevPortSource = s;
376
387
  const registered = registeredKind(s.spec);
377
388
  if (registered) {
378
- const placed = checkRegisteredStep(registered, s, exprs, errorSource, opts, diags, typeOf, triggerId);
389
+ const { placed, checked } = checkRegisteredStep(registered, s, exprs, errorSource, opts, diags, typeOf, triggerId);
390
+ checkedSteps.set(s.name, checked);
379
391
  if (placed)
380
- registeredSteps.set(s.name, placed);
392
+ registeredSteps.set(s.name, checked);
381
393
  else
382
394
  unresolvedSteps.add(s.name);
383
395
  }
@@ -400,43 +412,6 @@ function checkFlow(built, ancestors, opts = {}) {
400
412
  }
401
413
  checkContribution(maybeCustom, s.options?.version, s.name, own.some((d) => d.severity === 'error'), diags);
402
414
  }
403
- if (s.spec.kind === 'http') {
404
- checkHttp(s.name, s.spec.inputs, diags);
405
- // Only a complete pair: an empty half is HTTP_MANAGED_BINDING_INCOMPLETE, which
406
- // serialize refuses before it resolves either name.
407
- const pair = s.spec.inputs;
408
- const filled = (v) => typeof v === 'string' && v.trim() !== '';
409
- if (pair?.managed && filled(pair.connection) && filled(pair.folder)) {
410
- const httpStep = s.name;
411
- const target = pair.targetConnector;
412
- checkBindingLabels(`managed http "${httpStep}"`, pair, opts, (code, message) => diags.push({ level: 'error', code, step: httpStep, message }), undefined,
413
- // The connector its CONNECTION belongs to: the target, not the wrapper.
414
- { id: httpStep, connector: typeof target === 'string' && target !== '' ? target : 'uipath-uipath-http' });
415
- }
416
- const seen = new Set();
417
- for (const b of s.spec.inputs.branches ?? []) {
418
- addExpr(b.condition.js, s.name);
419
- if (!/^[A-Za-z0-9_-]+$/.test(b.name)) {
420
- diags.push({
421
- level: 'error', code: 'HTTP_BRANCH_NAME', step: s.name,
422
- message: `HTTP branch ${JSON.stringify(b.name)} on "${s.name}" is not port-safe — use letters, digits, _ or - (it becomes the branch-<name> port).`,
423
- });
424
- }
425
- if (seen.has(b.name)) {
426
- diags.push({
427
- level: 'error', code: 'HTTP_BRANCH_DUP', step: s.name,
428
- message: `HTTP branch ${JSON.stringify(b.name)} on "${s.name}" is declared twice.`,
429
- });
430
- }
431
- seen.add(b.name);
432
- if (b.condition.literal) {
433
- diags.push({
434
- level: 'error', code: 'HTTP_BRANCH_CONST', step: s.name,
435
- message: `HTTP branch ${JSON.stringify(b.name)} on "${s.name}" tests a constant — it would fire always or never. Reference the response, e.g. $vars.${s.name}.output.statusCode.`,
436
- });
437
- }
438
- }
439
- }
440
415
  // `.stepSwitch` routes its exits as ARMS, so the `.stepToList` bookkeeping
441
416
  // would report every one of them unrouted. STEP_SWITCH_EXIT_UNROUTED is
442
417
  // that step's version of the same question.
@@ -706,7 +681,7 @@ function checkFlow(built, ancestors, opts = {}) {
706
681
  // on the first cut. The action rules have run by now; what is left is the
707
682
  // arms and the three things only this step can get wrong.
708
683
  if (s0.kind === 'stepSwitch') {
709
- const exits = declaredExits(s0.spec, s0.options?.version);
684
+ const exits = stepExits(s0.spec, s0.options?.version);
710
685
  if (!hasExits(exits)) {
711
686
  diags.push({
712
687
  level: 'error', code: 'STEP_SWITCH_NO_EXITS', step: s0.name,
@@ -1044,7 +1019,7 @@ function checkFlow(built, ancestors, opts = {}) {
1044
1019
  // per-family rules below, because the envelope carries the same fields
1045
1020
  // whatever the node publishes on its success path; which VARIABLE holds it
1046
1021
  // is a per-family runtime boundary `serialize` rewrites, not something the
1047
- // author writes (`ERROR_ENVELOPE_VAR`).
1022
+ // author writes (a node type's `errorEnvelope`).
1048
1023
  if (field === 'error') {
1049
1024
  const tail = rest ? rest.slice(1).split('.')[0] : undefined;
1050
1025
  if (tail !== undefined)
@@ -1095,13 +1070,17 @@ function checkFlow(built, ancestors, opts = {}) {
1095
1070
  }
1096
1071
  // A registered node type declares what its output is; `none` reports here,
1097
1072
  // at the same point as the per-family no-output rules below.
1098
- const placed = registeredSteps.get(name);
1073
+ const placed = checkedSteps.get(name);
1099
1074
  if (placed) {
1100
1075
  const contract = placed.impl.output(placed.spec);
1101
1076
  if (contract.kind === 'none') {
1102
1077
  diags.push(catalogued(placed.entry, step, contract.code, contract.message({ node: name, ref: `$vars.${name}${field ? '.' + field : ''}`, reader: step })));
1103
1078
  continue;
1104
1079
  }
1080
+ if (contract.kind === 'reader') {
1081
+ contract.read({ node: name, ref: `$vars.${name}${field ? '.' + field : ''}${rest}`, reader: step, field, rest }, (code, message, suggestion) => diags.push(catalogued(placed.entry, step, code, message, suggestion)));
1082
+ continue;
1083
+ }
1105
1084
  }
1106
1085
  // A switch only routes; it has no output of its own, so any read of it is
1107
1086
  // undefined at run time. Point the author at the arm's step instead.
@@ -1176,7 +1155,7 @@ function checkFlow(built, ancestors, opts = {}) {
1176
1155
  }
1177
1156
  // A registered node type that states its output's fields: the same read
1178
1157
  // rule, worded by its contract and reported through its family's catalog.
1179
- const fieldsPlaced = registeredSteps.get(name);
1158
+ const fieldsPlaced = checkedSteps.get(name);
1180
1159
  const fieldsContract = fieldsPlaced?.impl.output(fieldsPlaced.spec);
1181
1160
  if (fieldsPlaced && fieldsContract?.kind === 'fields') {
1182
1161
  const found = [];
@@ -2434,79 +2413,6 @@ function checkPublishedFunctionRead(name, field, rest, spec, step, diags) {
2434
2413
  suggestion: `returns: { ${wanted}: 'string' }`,
2435
2414
  });
2436
2415
  }
2437
- /**
2438
- * The fields every HTTP response carries at the top of `output`, whatever the
2439
- * API answered. `core.action.http`'s definition declares all three; the author's
2440
- * `returns` describes what is INSIDE `body`, never beside it.
2441
- */
2442
- const HTTP_ENVELOPE_FIELDS = ['body', 'statusCode', 'headers'];
2443
- /**
2444
- * A read of an `http()` step's result.
2445
- *
2446
- * The response is an envelope — `{ body, statusCode, headers }` — and `returns`
2447
- * declares the parsed JSON body's top-level fields, so a declared field is read
2448
- * at `output.body.<field>` (`out('<step>', 'body.<field>')`). That nesting is the
2449
- * one thing this family does not share with the process families, and it is
2450
- * exactly where flow-builder-sdk#850's run went wrong: a branch kept reading
2451
- * `output.currentDayForecast…` (the Azure Maps connector's path) after the step
2452
- * became an Open-Meteo `http()`, passed `check` and `validate`, and faulted at
2453
- * `flow debug` as `[400302]`.
2454
- */
2455
- function checkHttpRead(name, field, rest, spec, step, diags) {
2456
- if (field === undefined)
2457
- return; // `out('fetch')` — the whole response.
2458
- if (field === 'error')
2459
- return; // the failure envelope (managed http's error port).
2460
- if (field !== 'output') {
2461
- diags.push({
2462
- level: 'error', code: 'HTTP_READ_NOT_OUTPUT', step,
2463
- message: `HTTP step "${name}" publishes its response under "output", but "$vars.${name}.${field}" `
2464
- + `(read in "${step}") asks for "${field}", which is undefined at run time.`,
2465
- suggestion: `out('${name}', 'body.<field>')`,
2466
- });
2467
- return;
2468
- }
2469
- const [top, inner] = (rest ?? '').replace(/^\./, '').split('.');
2470
- if (!top)
2471
- return; // `$vars.<name>.output` — the whole envelope.
2472
- const declared = Object.keys(spec?.returns ?? {});
2473
- if (!HTTP_ENVELOPE_FIELDS.includes(top)) {
2474
- // The commonest form: a declared body field read one level too high.
2475
- const hoisted = declared.includes(top) ? top : undefined;
2476
- const nearest = hoisted ?? nearestName(top, declared);
2477
- diags.push({
2478
- level: 'error', code: 'HTTP_READ_UNKNOWN_FIELD', step,
2479
- message: `"$vars.${name}.output.${top}" (read in "${step}") is not a field of HTTP step "${name}"'s `
2480
- + `response. An HTTP response publishes ${HTTP_ENVELOPE_FIELDS.map((f) => `"${f}"`).join(', ')}; `
2481
- + `the fields \`returns\` declares live under "body"`
2482
- + (hoisted ? `, so "${top}" is read as "$vars.${name}.output.body.${top}".` : '.')
2483
- + ` A read outside that shape resolves to undefined and faults a gateway at run time.`,
2484
- ...(nearest !== undefined ? { suggestion: `out('${name}', 'body.${nearest}')` } : {}),
2485
- });
2486
- return;
2487
- }
2488
- if (top !== 'body' || !inner)
2489
- return; // `statusCode`, `headers`, or the whole body.
2490
- if (declared.length === 0) {
2491
- diags.push({
2492
- level: 'error', code: 'HTTP_READ_WITHOUT_RETURNS', step,
2493
- message: `"$vars.${name}.output.body.${inner}" (read in "${step}") reads a field of HTTP step "${name}"'s `
2494
- + `response body, but the step declares no \`returns\` — so the node carries no body schema and nothing `
2495
- + `can tell a real field from a typo. Only the API being called knows its body; declare the fields you read.`,
2496
- suggestion: `returns: { ${inner}: 'string' }`,
2497
- });
2498
- return;
2499
- }
2500
- if (declared.includes(inner))
2501
- return;
2502
- const nearest = nearestName(inner, declared);
2503
- diags.push({
2504
- level: 'error', code: 'HTTP_READ_UNKNOWN_FIELD', step,
2505
- message: `HTTP step "${name}" declares no body field "${inner}" (read as "$vars.${name}.output.body.${inner}" `
2506
- + `in "${step}"). Its \`returns\` names: ${declared.join(', ')}. A read outside that set resolves to undefined.`,
2507
- ...(nearest !== undefined ? { suggestion: `out('${name}', 'body.${nearest}')` } : {}),
2508
- });
2509
- }
2510
2416
  /**
2511
2417
  * The read rule the process-shaped families share: the result is published
2512
2418
  * under `output`, keyed by a known field list — the author's `returns`, or a
@@ -2572,7 +2478,7 @@ function checkRpaWorkflowRead(name, field, rest, spec, step, diags) {
2572
2478
  * build until it lands here.
2573
2479
  */
2574
2480
  const OUTPUT_READ_RULES = {
2575
- http: checkHttpRead,
2481
+ http: { exempt: 'Registered: the http node types read their own output (nodes/http/flow, a `reader` contract).' },
2576
2482
  rpaWorkflow: checkRpaWorkflowRead,
2577
2483
  apiWorkflow: checkApiWorkflowRead,
2578
2484
  publishedFunction: checkPublishedFunctionRead,
@@ -3912,7 +3818,7 @@ function hitlEndHazards(steps) {
3912
3818
  // is where this arm's token stops on that outcome.
3913
3819
  const spec = st.spec;
3914
3820
  const exits = spec.kind === 'hitl' && hitlDerivesOutcomeHandles(spec.inputs?.variant)
3915
- ? declaredExits(st.spec, st.options?.version) : undefined;
3821
+ ? stepExits(st.spec, st.options?.version) : undefined;
3916
3822
  if (exits && hasExits(exits)) {
3917
3823
  const stopped = exits.filter((e) => {
3918
3824
  const c = st.cases.find((x) => x.value === e.value);
@@ -3966,170 +3872,6 @@ function endDiag(join, h) {
3966
3872
  * reference to hide — which is what the checks below enforce for the cases
3967
3873
  * where types have been stripped at run time.
3968
3874
  */
3969
- /**
3970
- * An ISO-8601 DURATION, which is what both http definitions declare `timeout` to
3971
- * be (`PT15M` by default) and what all 808 deployed http nodes carry.
3972
- *
3973
- * Deliberately the whole grammar rather than the `PT<n>[HMS]` subset the corpus
3974
- * happens to use: a too-narrow fence would refuse `P1D` or `PT1H30M`, which the
3975
- * platform parses perfectly well. What it does refuse is the family of things
3976
- * that are NOT durations at all and would reach the platform as an unparseable
3977
- * string — `'30s'`, `'30'`, `'PT'`, `'15 minutes'`.
3978
- */
3979
- const ISO8601_DURATION = /^P(?!$)(\d+(?:\.\d+)?Y)?(\d+(?:\.\d+)?M)?(\d+(?:\.\d+)?W)?(\d+(?:\.\d+)?D)?(T(?!$)(\d+(?:\.\d+)?H)?(\d+(?:\.\d+)?M)?(\d+(?:\.\d+)?S)?)?$/;
3980
- /**
3981
- * Where `check` stops treating a retry count as plainly reasonable. The
3982
- * platform's own ceiling is NOT measured, and the deployed corpus's largest
3983
- * author-set value is 3 — so above this we WARN. A too-narrow fence is fatal
3984
- * (it refuses something that works); a too-wide one costs nothing.
3985
- */
3986
- const RETRY_COUNT_SOFT_MAX = 5;
3987
- /**
3988
- * The two http settings every deployed request node carries.
3989
- *
3990
- * Both definitions declare the defaults (`timeout: 'PT15M'`, `retryCount: 0`), so
3991
- * omitting them is safe and is what ~99% of deployed nodes do. What is NOT safe
3992
- * is writing them in a form the platform cannot read: an unparseable duration is
3993
- * not a shorter timeout, it is a value the request node fails to configure with,
3994
- * and nothing downstream of `check` would say so in the author's own vocabulary.
3995
- */
3996
- function checkHttp(step, inputs, diags) {
3997
- // `core.action.http` is retired on the platform side: after `registry pull
3998
- // --force` the tenant serves only `core.action.http.v2` (2.3), and a
3999
- // `registry get core.action.http` — bare or at the `@1.3` that `flow migrate`
4000
- // upgrades it to — answers "Node not found". It still compiles, validates and
4001
- // runs, which is why this is a warning and not an error: existing flows are
4002
- // not broken and `decompile` must keep round-tripping them.
4003
- //
4004
- // Moving to the managed node is not a rename. A plain step keeps a non-2xx on
4005
- // the SUCCESS path (`out(step,'statusCode')`); the managed one fails on it, so
4006
- // a status branch becomes an error handler reading the envelope's `status`.
4007
- //
4008
- // It is not a free upgrade either, and for a connection-less URL it is usually
4009
- // the wrong move (flow-builder-sdk#898). With no `connection` the managed node
4010
- // still goes through Integration Service: serialize emits the `uipath-uipath-http`
4011
- // connector on `ImplicitConnection`, so the request leaves from IS's SHARED
4012
- // egress. Measured on slack-weather-pipeline (3/3 runs): agents took the old
4013
- // `managed: true` hint, and Open-Meteo answered that egress 429 "Daily API
4014
- // request limit exceeded" while direct calls from the same box got 200. The
4015
- // plain node goes direct. So the hint only steers toward `managed: true` when
4016
- // the step was given connection settings; the two other real reasons to move
4017
- // (a binding, an `.onError()`) already fire as errors with that suggestion
4018
- // (HTTP_BINDING_REQUIRES_MANAGED, HTTP_ONERROR_V1).
4019
- if (inputs && !inputs.managed) {
4020
- const asked = inputs;
4021
- const wantsConnection = asked.connection !== undefined || asked.folder !== undefined
4022
- || asked.targetConnector !== undefined;
4023
- diags.push(wantsConnection
4024
- ? {
4025
- level: 'warning',
4026
- code: 'HTTP_V1_RETIRED',
4027
- message: `Step "${step}" emits core.action.http, which the tenant registry no longer serves (only `
4028
- + `core.action.http.v2). It sets Integration Service connection settings, which only the managed node `
4029
- + `can use. Moving to managed: true changes behaviour rather than just the node type: a non-2xx stops `
4030
- + `arriving on the success path with statusCode and fails the step instead, so a status branch becomes `
4031
- + `an .onError() handler reading err('${step}', 'status').`,
4032
- step,
4033
- suggestion: `http({ url: …, managed: true, connection, folder })`,
4034
- }
4035
- : {
4036
- level: 'warning',
4037
- code: 'HTTP_V1_RETIRED',
4038
- message: `Step "${step}" emits core.action.http, which the tenant registry no longer serves (only `
4039
- + `core.action.http.v2). It still compiles, validates and runs, and it calls the URL directly. For a URL `
4040
- + `that needs no Integration Service connection, keep managed: false. managed: true is not a drop-in `
4041
- + `replacement: even without a connection it sends the request through Integration Service's shared `
4042
- + `egress (rate limits there are shared, and public APIs can answer 429 where a direct call succeeds), `
4043
- + `and a non-2xx fails the step instead of arriving with statusCode. Switch only if the call needs an `
4044
- + `Integration Service connection or an .onError() handler.`,
4045
- step,
4046
- });
4047
- }
4048
- const connection = inputs?.connection;
4049
- const folder = inputs?.folder;
4050
- const targetConnector = inputs?.targetConnector;
4051
- const bindingConfigured = connection !== undefined || folder !== undefined;
4052
- const targetConfigured = targetConnector !== undefined;
4053
- const nonEmpty = (value) => typeof value === 'string' && value.trim().length > 0;
4054
- if (!inputs?.managed && (bindingConfigured || targetConfigured)) {
4055
- diags.push({
4056
- level: 'error',
4057
- code: 'HTTP_BINDING_REQUIRES_MANAGED',
4058
- message: `Step "${step}" supplies Integration Service connection settings to the standalone HTTP node. `
4059
- + `Only the managed node can use connection, folder, or targetConnector.`,
4060
- step,
4061
- suggestion: `managed: true`,
4062
- });
4063
- }
4064
- else if (inputs?.managed && bindingConfigured && (!nonEmpty(connection) || !nonEmpty(folder))) {
4065
- diags.push({
4066
- level: 'error',
4067
- code: 'HTTP_MANAGED_BINDING_INCOMPLETE',
4068
- message: `Managed HTTP step "${step}" must name both a non-empty \`connection\` and \`folder\`. The platform `
4069
- + `needs the pair to resolve the Integration Service connection at runtime.`,
4070
- step,
4071
- suggestion: `connection: 'http', folder: 'shared'`,
4072
- });
4073
- }
4074
- if (inputs?.managed && targetConfigured && !nonEmpty(targetConnector)) {
4075
- diags.push({
4076
- level: 'error',
4077
- code: 'HTTP_TARGET_CONNECTOR_EMPTY',
4078
- message: `Managed HTTP step "${step}" sets an empty \`targetConnector\`; name its Integration Service connector key.`,
4079
- step,
4080
- suggestion: `targetConnector: 'uipath-salesforce-slack'`,
4081
- });
4082
- }
4083
- else if (inputs?.managed && targetConfigured && !bindingConfigured) {
4084
- diags.push({
4085
- level: 'error',
4086
- code: 'HTTP_TARGET_CONNECTOR_REQUIRES_BINDING',
4087
- message: `Managed HTTP step "${step}" sets \`targetConnector\` without a connection and folder. `
4088
- + `A connector-specific fallback needs the binding pair whose authentication it reuses.`,
4089
- step,
4090
- suggestion: `connection: 'slack', folder: 'shared'`,
4091
- });
4092
- }
4093
- const timeout = inputs?.timeout;
4094
- if (timeout !== undefined) {
4095
- if (typeof timeout !== 'string' || !ISO8601_DURATION.test(timeout.trim())) {
4096
- diags.push({
4097
- level: 'error',
4098
- code: 'HTTP_TIMEOUT_NOT_DURATION',
4099
- message: `Step "${step}" sets timeout to ${JSON.stringify(timeout)}, which is not an ISO-8601 duration. The `
4100
- + `platform reads this field as a duration string (both http definitions default it to "PT15M"), so a `
4101
- + `value it cannot parse configures no timeout at all.`,
4102
- step,
4103
- suggestion: `timeout: 'PT30S' // 30 seconds — also 'PT1M', 'PT15M', 'PT1H'`,
4104
- });
4105
- }
4106
- }
4107
- const retryCount = inputs?.retryCount;
4108
- if (retryCount !== undefined) {
4109
- if (typeof retryCount !== 'number' || !Number.isInteger(retryCount) || retryCount < 0) {
4110
- diags.push({
4111
- level: 'error',
4112
- code: 'HTTP_RETRY_COUNT_INVALID',
4113
- message: `Step "${step}" sets retryCount to ${JSON.stringify(retryCount)}. It is a count of RETRIES after the `
4114
- + `first attempt, so it must be a non-negative whole number — 0 (the definitions' default) means "try `
4115
- + `once", 2 means up to three attempts.`,
4116
- step,
4117
- suggestion: `retryCount: 2`,
4118
- });
4119
- }
4120
- else if (retryCount > RETRY_COUNT_SOFT_MAX) {
4121
- diags.push({
4122
- level: 'warning',
4123
- code: 'HTTP_RETRY_COUNT_HIGH',
4124
- message: `Step "${step}" asks for ${retryCount} retries. The local runtime clamps at ${RETRY_COUNT_SOFT_MAX} and `
4125
- + `the largest value in the deployed corpus is 3, so a local replay will not exercise what you wrote. `
4126
- + `The platform's own ceiling is not measured — this is a warning, not a refusal.`,
4127
- step,
4128
- suggestion: `retryCount: ${RETRY_COUNT_SOFT_MAX}`,
4129
- });
4130
- }
4131
- }
4132
- }
4133
3875
  /**
4134
3876
  * Why a form-field type is refused: a product spelling of a type the SDK
4135
3877
  * models (`"string"` is the platform's own name for text), or a type
@@ -4699,6 +4441,7 @@ function checkRegisteredStep(entry, s, exprs, errorSource, opts, diags, typeOf,
4699
4441
  return name ? { name, type: typeOf.get(name) } : undefined;
4700
4442
  },
4701
4443
  options: opts,
4444
+ bindingLabels: (where, labels, use) => checkBindingLabels(where, labels, opts, (code, message) => diags.push({ level: 'error', code, step: s.name, message }), undefined, use),
4702
4445
  };
4703
4446
  impl.check?.(spec, ctx);
4704
4447
  for (const site of impl.expressions(spec)) {
@@ -4726,11 +4469,15 @@ function checkRegisteredStep(entry, s, exprs, errorSource, opts, diags, typeOf,
4726
4469
  // would get unpinned (select(), else the default) — as decompile lifts an
4727
4470
  // unregistered version — so fixing the pin is not followed by a second round.
4728
4471
  // It stays unplaced: the pin is wrong, so no family rule may rely on it.
4729
- runRules(resolveVersion(entry.type, spec, undefined, `step "${s.name}"`));
4730
- return undefined;
4472
+ const fallback = resolveVersion(entry.type, spec, undefined, `step "${s.name}"`);
4473
+ const fallbackCtx = runRules(fallback);
4474
+ return {
4475
+ placed: false,
4476
+ checked: { entry, impl: fallback, spec, placement: { step: s.name, kind: fallback.kind, spec, version: fallback.version }, ctx: fallbackCtx },
4477
+ };
4731
4478
  }
4732
4479
  const ctx = runRules(impl);
4733
- return { entry, impl, spec, placement: { step: s.name, kind: impl.kind, spec, version: impl.version }, ctx };
4480
+ return { placed: true, checked: { entry, impl, spec, placement: { step: s.name, kind: impl.kind, spec, version: impl.version }, ctx } };
4734
4481
  }
4735
4482
  /** Every non-literal Expr reachable in `value`, as expressions to check. */
4736
4483
  function pushDeepExprs(value, step, errorSource, out) {
@@ -4795,16 +4542,6 @@ function collectActionExprs(s, out, errorSource) {
4795
4542
  if (val && typeof val === 'object')
4796
4543
  return Object.values(val).forEach(pushDeep);
4797
4544
  };
4798
- if (s.spec.kind === 'http') {
4799
- const i = s.spec.inputs;
4800
- push(i.url);
4801
- push(i.body);
4802
- for (const val of Object.values(i.headers ?? {}))
4803
- push(val);
4804
- for (const val of Object.values(i.query ?? {}))
4805
- push(val);
4806
- return;
4807
- }
4808
4545
  if (s.spec.kind === 'hitl') {
4809
4546
  // A human task's only expressions are the values its shown fields display.
4810
4547
  // Without this a typo'd reference inside a form field would reach the
@@ -4933,8 +4670,8 @@ placed) {
4933
4670
  // A registered node type: a bundled one asks its definition at the version it
4934
4671
  // will be emitted at; a per-tenant one answers for itself (its errorPort).
4935
4672
  if (registeredKind(spec)) {
4936
- // No record: the version did not resolve, which NODE_VERSION_UNREGISTERED
4937
- // already reports where the step is checked.
4673
+ // An unregistered pin still has a record, with the version it would get
4674
+ // unpinned; NODE_VERSION_UNREGISTERED reports the pin itself.
4938
4675
  if (!placed)
4939
4676
  return;
4940
4677
  const { impl } = placed;
@@ -5002,36 +4739,11 @@ placed) {
5002
4739
  });
5003
4740
  return;
5004
4741
  }
5005
- // Two separate facts about a PLAIN http step, both measured, both fatal to a
5006
- // handler there:
5007
- //
5008
- // 1. a 4xx/5xx does not FAIL the node — it completes carrying `statusCode`,
5009
- // so the handler only ever runs on a transport failure (DNS, refused,
5010
- // timeout);
5011
- // 2. when it does run there is nothing to read. `core.action.http@1.0.0` and
5012
- // the `@1.3` that `uip maestro flow migrate` upgrades it to both publish
5013
- // `<step>.error = true` and leave `<step>.output` null — no envelope
5014
- // anywhere, unlike every other family. `validate` still says Valid.
5015
- //
5016
- // So this stays an error, and says the second part too: an author who reads
5017
- // the old message ("use the managed node if statuses must route") could
5018
- // reasonably conclude a transport-failure handler is fine as it is.
5019
- if (spec.kind === 'http' && !spec.inputs?.managed)
5020
- diags.push(plainHttpOnErrorRefusal(step.name));
5021
4742
  }
5022
- /** `.onError` after a plain `core.action.http` node (see the two measured facts in {@link checkOnError}). */
4743
+ /** `.onError` after a custom node built on the plain `core.action.http` definition (the http family's refusal). */
5023
4744
  function plainHttpOnErrorRefusal(stepName) {
5024
- return {
5025
- level: 'error',
5026
- code: 'HTTP_ONERROR_V1',
5027
- message: `Step "${stepName}" handles errors on a plain http() request. A 4xx/5xx does NOT fail that node — it `
5028
- + `completes with out('${stepName}', 'statusCode') set — so the handler runs only on a transport failure, `
5029
- + `and when it does the node publishes no failure envelope at all (measured: "<step>.error" is the boolean `
5030
- + `true and "<step>.output" is null, in 1.0.0 and in the 1.3 flow migrate produces), so err() has nothing `
5031
- + `to read. The managed node is the one whose failures route AND carry the envelope.`,
5032
- step: stepName,
5033
- suggestion: `http({ url: …, managed: true })`,
5034
- };
4745
+ const { message, suggestion } = plainHttpOnErrorMessage(stepName);
4746
+ return { level: 'error', code: 'HTTP_ONERROR_V1', message, step: stepName, suggestion };
5035
4747
  }
5036
4748
  /**
5037
4749
  * Reading a value that only ONE arm produces (flow-builder-sdk#200).
@@ -5356,7 +5068,7 @@ function loopExitInArm() {
5356
5068
  * checks and what it does not. It checks the FIELD NAME against the envelope's
5357
5069
  * own fields, which are the same on every family. It does NOT police the prefix:
5358
5070
  * `.error` vs `.output` is a per-family runtime boundary, measured per family and
5359
- * rewritten by `serialize` (`ERROR_ENVELOPE_VAR`), so an author writing `.error`
5071
+ * rewritten by `serialize` (a node type's `errorEnvelope`), so an author writing `.error`
5360
5072
  * is right by construction and an author hand-writing `.output` is caught by
5361
5073
  * ERROR_ENVELOPE_VIA_OUTPUT instead.
5362
5074
  *