@uipath/maestro-builder-sdk 6.11.4 → 6.11.6

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/dist/check-cli.js CHANGED
@@ -9,7 +9,7 @@ import { reportDiagnostics } from './cli-result.js';
9
9
  import { existsSync } from 'node:fs';
10
10
  import { resolveFlowFile, loadBuiltFlow } from './load.js';
11
11
  import { check } from './check.js';
12
- import { eventResolveErrorFor, openCliLibrary } from './cli-library.js';
12
+ import { byoaCheckOpts, eventResolveErrorFor, openCliLibrary } from './cli-library.js';
13
13
  import { Bindings } from './bindings.js';
14
14
  import { connectorInputErrors } from './serialize.js';
15
15
  import { runWhenInvokedDirectly } from './cli-run.js';
@@ -41,6 +41,7 @@ function checkOpts(argv) {
41
41
  return bindings ? { bindings } : {};
42
42
  return {
43
43
  eventResolveError: eventResolveErrorFor(library),
44
+ ...byoaCheckOpts(library),
44
45
  eventParams: (connector, event, object) => {
45
46
  try {
46
47
  return library.eventParameterFields(connector, event, object);
package/dist/check.d.ts CHANGED
@@ -2,6 +2,7 @@ import { type LookupResolutions, type LookupSpec } from './core/lookups.js';
2
2
  import type { SchemaReadiness } from './core/library.js';
3
3
  import type { BuiltFlow } from './flow-sdk.js';
4
4
  import { type BindingsView } from './core/binding-messages.js';
5
+ import { type ByoaEventFacts, type ByoaLedger } from './core/byoa.js';
5
6
  export interface Diagnostic {
6
7
  level: 'error' | 'warning';
7
8
  code: string;
@@ -116,6 +117,18 @@ export interface CheckOpts {
116
117
  lookups?: (connector: string, action: string, version?: string, object?: string) => Readonly<Record<string, LookupSpec>> | undefined;
117
118
  /** Everything `registry prepare --resolve` has recorded in the local overlay. */
118
119
  resolutions?: LookupResolutions;
120
+ /**
121
+ * A connector event's BYOA and webhook-URL facts, or `undefined` when unknown.
122
+ * Supplied by the CLIs from `library.eventFacts` — the call `serialize` makes
123
+ * for the same refusal — so check and compile cannot disagree; omitted (no
124
+ * library) → EVENT_BYOA_UNVERIFIED is not checked.
125
+ */
126
+ eventFacts?: (connector: string, event: string, object?: string) => ByoaEventFacts | undefined;
127
+ /**
128
+ * The BYOA checks `registry prepare` recorded — the `byoa` section of the
129
+ * overlay's `resolutions.json`, as `library.byoaVerifications` loads it.
130
+ */
131
+ byoa?: ByoaLedger;
119
132
  /**
120
133
  * Whether a connector step's schema is materialized locally — the fact behind
121
134
  * the OBJECT_UNPREPARED and CUSTOM_FIELDS_UNPREPARED warnings. Supplied by the
package/dist/check.js CHANGED
@@ -8,6 +8,7 @@
8
8
  import { Expr, SCHEDULE_PRESETS, DELAY_PRESETS, VARIANT_OPERATION, parseIxpProjectType, flatNameOf, flatNameSegmentsOf, FLAT_SEP } from './flow-sdk.js';
9
9
  import { FlowNode } from './core/node-classes.js';
10
10
  import { isLookupToken, resolvedValue, unresolvedLookupMessage, } from './core/lookups.js';
11
+ import { QUEUE_ITEM_RECORD_SCHEMA } from './core/queue-record.js';
11
12
  import { EVENT_FILTER_OPERATORS, readEventFilter, eventFilterProblem } from './event-filters.js';
12
13
  import { extractRefs } from './core/expr-check.js';
13
14
  import { bindingSelfNameMessage, bindingUndeclaredMessage, undeclaredBinding } from './core/binding-messages.js';
@@ -17,6 +18,7 @@ import { childStepLists, declaredExits, hasExits, stopsAtEnd } from './core/step
17
18
  import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
18
19
  import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js';
19
20
  import { prepareCommand, resolveFlagArg } from './core/cli-spelling.js';
21
+ import { EVENT_BYOA_UNVERIFIED, byoaUnverified } from './core/byoa.js';
20
22
  export function check(built, opts = {}) {
21
23
  return checkFlow(built, [], opts);
22
24
  }
@@ -145,20 +147,12 @@ function checkFlow(built, ancestors, opts = {}) {
145
147
  // form's ANSWER fields (plus `Action`), so a read of it is checked against the
146
148
  // form the author wrote rather than waved through (see HITL_READ_* below).
147
149
  const hitlForms = new Map();
148
- const apiWorkflows = new Map();
149
- const publishedFunctions = new Map();
150
- // Agentic-process step name → its call. Same reason as api-workflow's map: the
151
- // step publishes the job's output arguments and the only offline statement of
152
- // what those are is the step's own `returns`.
150
+ // Agentic-process step name → its call, for the form-field rules below. Its
151
+ // output READS go through OUTPUT_READ_RULES like every `returns` family.
153
152
  const agenticProcesses = new Map();
154
- // Published-agent step name → its call. Same reason again: the step publishes the
155
- // agent job's output arguments, and the only offline statement of what those are
156
- // is the step's own `returns`.
157
- const agents = new Map();
158
- // Inline-agent step name → its call. NOT the same reason: an inline agent's
159
- // contract is AUTHORED rather than looked up on a tenant, so a read outside the
160
- // declared `returns` is a mistake the compiler can be certain about.
161
- const inlineAgents = new Map();
153
+ // Step name → the read rule for its output, bound to its inputs. Filled from
154
+ // OUTPUT_READ_RULES for every kind whose inputs take a `returns`.
155
+ const outputReaders = new Map();
162
156
  const connectorSteps = new Map();
163
157
  // Subflow step name → the child flow it runs. A subflow's output is the
164
158
  // OBJECT of the child's declared outputs, so a read of it is checked against
@@ -434,11 +428,9 @@ function checkFlow(built, ancestors, opts = {}) {
434
428
  checkRpaWorkflow(s.name, s.spec.inputs, diags);
435
429
  if (s.spec.kind === 'apiWorkflow') {
436
430
  checkApiWorkflow(s.name, s.spec.inputs, diags);
437
- apiWorkflows.set(s.name, s.spec.inputs);
438
431
  }
439
432
  if (s.spec.kind === 'publishedFunction') {
440
433
  checkPublishedFunction(s.name, s.spec.inputs, diags);
441
- publishedFunctions.set(s.name, s.spec.inputs);
442
434
  }
443
435
  if (s.spec.kind === 'sendMessage' || s.spec.kind === 'waitForMessage'
444
436
  || s.spec.kind === 'conversationContext') {
@@ -456,14 +448,20 @@ function checkFlow(built, ancestors, opts = {}) {
456
448
  }
457
449
  if (s.spec.kind === 'agent') {
458
450
  checkAgent(s.name, s.spec.inputs, diags);
459
- agents.set(s.name, s.spec.inputs);
460
451
  }
461
452
  if (s.spec.kind === 'inlineAgent') {
462
453
  checkInlineAgent(s.name, s.spec.inputs, diags);
463
- inlineAgents.set(s.name, s.spec.inputs);
464
454
  }
465
455
  if (s.spec.kind === 'queueItem')
466
456
  checkQueueItem(s.name, s.spec.inputs, diags);
457
+ {
458
+ const rule = OUTPUT_READ_RULES[s.spec.kind];
459
+ if (typeof rule === 'function') {
460
+ const stepName = s.name;
461
+ const inputs = s.spec.inputs;
462
+ outputReaders.set(stepName, (field, rest, at, d) => rule(stepName, field, rest, inputs, at, d));
463
+ }
464
+ }
467
465
  if (s.spec.kind === 'documentClassify' || s.spec.kind === 'dynamicExtract') {
468
466
  const fr = s.spec.inputs.fileRef;
469
467
  const empty = fr === undefined || fr === null || (typeof fr === 'string' && fr.trim() === '');
@@ -1168,49 +1166,13 @@ function checkFlow(built, ancestors, opts = {}) {
1168
1166
  checkHitlRead(name, field, rest, form, step, diags);
1169
1167
  continue;
1170
1168
  }
1171
- // An API workflow publishes the job's output arguments, and the only offline
1172
- // statement of what those are is the step's own `returns`. A read of anything
1173
- // else is `undefined` on every rung and on the platform — and with no
1174
- // `returns` at all the node declares no output schema, so nothing downstream
1175
- // can tell a real field from a typo.
1176
- const apiwf = apiWorkflows.get(name);
1177
- if (apiwf) {
1178
- checkApiWorkflowRead(name, field, rest, apiwf, step, diags);
1179
- continue;
1180
- }
1181
- // A published function publishes its result the same way, and the same
1182
- // three read mistakes are decidable from the same place — so the reader is
1183
- // shared, with the family named in the codes.
1184
- const fn = publishedFunctions.get(name);
1185
- if (fn) {
1186
- checkPublishedFunctionRead(name, field, rest, fn, step, diags);
1187
- continue;
1188
- }
1189
- // An agentic process publishes its job's output arguments the same way, so
1190
- // the same three read mistakes are decidable from the same place.
1191
- const agentic = agenticProcesses.get(name);
1192
- if (agentic) {
1193
- checkAgenticProcessRead(name, field, rest, agentic, step, diags);
1194
- continue;
1195
- }
1196
- // A published agent publishes its job's output arguments the same way its
1197
- // three siblings do, so the same three read mistakes are decidable — with one
1198
- // extra that only this family has: a read whose CASE differs from the declared
1199
- // field. That used to be a live-only defect (the poll verb PascalCased the
1200
- // answer, flow-builder-sdk #122), so the wrong case was right live and wrong
1201
- // offline for a while. Now it is simply wrong, and worth naming as itself
1202
- // rather than as an unknown field.
1203
- const published = agents.get(name);
1204
- if (published) {
1205
- checkAgentRead(name, field, rest, published, step, diags);
1206
- continue;
1207
- }
1208
- // An inline agent's readable fields are exactly its declared `returns` — the
1209
- // node's `agentOutputVariables`, which is also what the engine's live path
1210
- // coerces the model's completion to. Nothing on a tenant to consult.
1211
- const inline = inlineAgents.get(name);
1212
- if (inline) {
1213
- checkInlineAgentRead(name, field, rest, inline, step, diags);
1169
+ // Every family that declares its output's fields — the author's `returns`, or
1170
+ // a shape the SDK fixes — has its read rule in OUTPUT_READ_RULES, keyed by
1171
+ // kind. A read outside the declared set is undefined on every rung and on
1172
+ // the platform, so it is refused here rather than at `flow debug`.
1173
+ const readOutput = outputReaders.get(name);
1174
+ if (readOutput) {
1175
+ readOutput(field, rest, step, diags);
1214
1176
  continue;
1215
1177
  }
1216
1178
  // A loop step exposes a fixed set of fields, and only those resolve at run
@@ -1992,6 +1954,13 @@ function eventParamsFor(subscription, opts) {
1992
1954
  * the fallback case for a connector the shipped library was not generated
1993
1955
  * against. A warning, because compile still succeeds. Prints the one
1994
1956
  * command that makes the event checkable.
1957
+ * - `EVENT_BYOA_UNVERIFIED` — an ERROR: the event only fires on a BYOA
1958
+ * (bring-your-own-app) connection, and the bound connection id has no record
1959
+ * of `prepare` having checked it (`core/byoa.ts` has the rule and when it
1960
+ * stays silent). A standard connection deploys and the trigger never fires;
1961
+ * the usual way in is binding the connection by preparing one of the
1962
+ * connector's ACTIONS, which checks nothing about BYOA. `compile` throws the
1963
+ * same text.
1995
1964
  * - `EVENT_FILTER_UNKNOWN_FIELD` — a filter leaf naming a field outside the
1996
1965
  * prepared vocabulary is an ERROR, not a warning, because the platform's own
1997
1966
  * configure DROPS an unmatched leaf and reports Success: the deployed
@@ -2027,6 +1996,9 @@ function checkEventResolution(subscription, where, kind, opts, error, warn, even
2027
1996
  return;
2028
1997
  }
2029
1998
  const object = typeof sub.object === 'string' && sub.object !== '' ? sub.object : undefined;
1999
+ const byoa = opts.eventFacts && byoaUnverified(where, { connector: connectorKey, event: eventName, ...(object ? { object } : {}), connection: sub.connection }, opts.eventFacts(connectorKey, eventName, object), opts.byoa, opts.bindings);
2000
+ if (byoa)
2001
+ error(EVENT_BYOA_UNVERIFIED, byoa);
2030
2002
  if (eventParams?.objectUnprepared && object) {
2031
2003
  // The generic template answers for every object, so resolution SUCCEEDS
2032
2004
  // and nothing downstream notices that the object named here has no
@@ -2815,6 +2787,175 @@ function checkPublishedFunctionRead(name, field, rest, spec, step, diags) {
2815
2787
  suggestion: `returns: { ${wanted}: 'string' }`,
2816
2788
  });
2817
2789
  }
2790
+ /**
2791
+ * The fields every HTTP response carries at the top of `output`, whatever the
2792
+ * API answered. `core.action.http`'s definition declares all three; the author's
2793
+ * `returns` describes what is INSIDE `body`, never beside it.
2794
+ */
2795
+ const HTTP_ENVELOPE_FIELDS = ['body', 'statusCode', 'headers'];
2796
+ /**
2797
+ * A read of an `http()` step's result.
2798
+ *
2799
+ * The response is an envelope — `{ body, statusCode, headers }` — and `returns`
2800
+ * declares the parsed JSON body's top-level fields, so a declared field is read
2801
+ * at `output.body.<field>` (`out('<step>', 'body.<field>')`). That nesting is the
2802
+ * one thing this family does not share with the process families, and it is
2803
+ * exactly where flow-builder-sdk#850's run went wrong: a branch kept reading
2804
+ * `output.currentDayForecast…` (the Azure Maps connector's path) after the step
2805
+ * became an Open-Meteo `http()`, passed `check` and `validate`, and faulted at
2806
+ * `flow debug` as `[400302]`.
2807
+ */
2808
+ function checkHttpRead(name, field, rest, spec, step, diags) {
2809
+ if (field === undefined)
2810
+ return; // `out('fetch')` — the whole response.
2811
+ if (field === 'error')
2812
+ return; // the failure envelope (managed http's error port).
2813
+ if (field !== 'output') {
2814
+ diags.push({
2815
+ level: 'error', code: 'HTTP_READ_NOT_OUTPUT', step,
2816
+ message: `HTTP step "${name}" publishes its response under "output", but "$vars.${name}.${field}" `
2817
+ + `(read in "${step}") asks for "${field}", which is undefined at run time.`,
2818
+ suggestion: `out('${name}', 'body.<field>')`,
2819
+ });
2820
+ return;
2821
+ }
2822
+ const [top, inner] = (rest ?? '').replace(/^\./, '').split('.');
2823
+ if (!top)
2824
+ return; // `$vars.<name>.output` — the whole envelope.
2825
+ const declared = Object.keys(spec?.returns ?? {});
2826
+ if (!HTTP_ENVELOPE_FIELDS.includes(top)) {
2827
+ // The commonest form: a declared body field read one level too high.
2828
+ const hoisted = declared.includes(top) ? top : undefined;
2829
+ const nearest = hoisted ?? nearestName(top, declared);
2830
+ diags.push({
2831
+ level: 'error', code: 'HTTP_READ_UNKNOWN_FIELD', step,
2832
+ message: `"$vars.${name}.output.${top}" (read in "${step}") is not a field of HTTP step "${name}"'s `
2833
+ + `response. An HTTP response publishes ${HTTP_ENVELOPE_FIELDS.map((f) => `"${f}"`).join(', ')}; `
2834
+ + `the fields \`returns\` declares live under "body"`
2835
+ + (hoisted ? `, so "${top}" is read as "$vars.${name}.output.body.${top}".` : '.')
2836
+ + ` A read outside that shape resolves to undefined and faults a gateway at run time.`,
2837
+ ...(nearest !== undefined ? { suggestion: `out('${name}', 'body.${nearest}')` } : {}),
2838
+ });
2839
+ return;
2840
+ }
2841
+ if (top !== 'body' || !inner)
2842
+ return; // `statusCode`, `headers`, or the whole body.
2843
+ if (declared.length === 0) {
2844
+ diags.push({
2845
+ level: 'error', code: 'HTTP_READ_WITHOUT_RETURNS', step,
2846
+ message: `"$vars.${name}.output.body.${inner}" (read in "${step}") reads a field of HTTP step "${name}"'s `
2847
+ + `response body, but the step declares no \`returns\` — so the node carries no body schema and nothing `
2848
+ + `can tell a real field from a typo. Only the API being called knows its body; declare the fields you read.`,
2849
+ suggestion: `returns: { ${inner}: 'string' }`,
2850
+ });
2851
+ return;
2852
+ }
2853
+ if (declared.includes(inner))
2854
+ return;
2855
+ const nearest = nearestName(inner, declared);
2856
+ diags.push({
2857
+ level: 'error', code: 'HTTP_READ_UNKNOWN_FIELD', step,
2858
+ message: `HTTP step "${name}" declares no body field "${inner}" (read as "$vars.${name}.output.body.${inner}" `
2859
+ + `in "${step}"). Its \`returns\` names: ${declared.join(', ')}. A read outside that set resolves to undefined.`,
2860
+ ...(nearest !== undefined ? { suggestion: `out('${name}', 'body.${nearest}')` } : {}),
2861
+ });
2862
+ }
2863
+ /**
2864
+ * The read rule the process-shaped families share: the result is published
2865
+ * under `output`, keyed by a known field list — the author's `returns`, or a
2866
+ * shape the SDK itself fixes. `declared: undefined` means "the author declared
2867
+ * nothing", which is its own mistake (`<PREFIX>_READ_WITHOUT_RETURNS`).
2868
+ */
2869
+ function checkDeclaredOutputRead(o) {
2870
+ const { prefix, what, name, field, rest, step, diags } = o;
2871
+ if (field === undefined || field === 'error')
2872
+ return;
2873
+ if (field !== 'output') {
2874
+ diags.push({
2875
+ level: 'error', code: `${prefix}_READ_NOT_OUTPUT`, step,
2876
+ message: `${what} "${name}" publishes its result under "output", but "$vars.${name}.${field}" `
2877
+ + `(read in "${step}") asks for "${field}", which is undefined at run time.`,
2878
+ suggestion: `out('${name}', '<field>')`,
2879
+ });
2880
+ return;
2881
+ }
2882
+ const wanted = (rest ?? '').replace(/^\./, '').split('.')[0];
2883
+ if (!wanted)
2884
+ return;
2885
+ if (o.declared === undefined) {
2886
+ diags.push({
2887
+ level: 'error', code: `${prefix}_READ_WITHOUT_RETURNS`, step,
2888
+ message: `"$vars.${name}.output.${wanted}" (read in "${step}") reads a field of ${what} "${name}"'s result, `
2889
+ + `but the step declares no \`returns\` — so the node carries no output schema and nothing can tell a `
2890
+ + `real field from a typo. ${o.withoutReturns}`,
2891
+ suggestion: `returns: { ${wanted}: 'string' }`,
2892
+ });
2893
+ return;
2894
+ }
2895
+ if (o.declared.includes(wanted))
2896
+ return;
2897
+ const nearest = nearestName(wanted, o.declared);
2898
+ diags.push({
2899
+ level: 'error', code: `${prefix}_READ_UNKNOWN_FIELD`, step,
2900
+ message: `${what} "${name}" declares no output "${wanted}" (read as "$vars.${name}.output.${wanted}" in `
2901
+ + `"${step}"). It publishes: ${o.declared.join(', ')}. A read outside that set resolves to undefined.`,
2902
+ ...(nearest !== undefined ? { suggestion: `out('${name}', '${nearest}')` } : {}),
2903
+ });
2904
+ }
2905
+ function checkRpaWorkflowRead(name, field, rest, spec, step, diags) {
2906
+ const declared = Object.keys(spec?.returns ?? {});
2907
+ checkDeclaredOutputRead({
2908
+ prefix: 'RPA', what: 'RPA process', name, field, rest, step, diags,
2909
+ declared: declared.length ? declared : undefined,
2910
+ withoutReturns: `The process's output arguments live on the tenant; declare the ones you read. `
2911
+ + `\`uip maestro flow registry get uipath.core.rpa-workflow.${spec?.key}\` shows what the platform `
2912
+ + `synthesizes for it.`,
2913
+ });
2914
+ }
2915
+ /**
2916
+ * A queue step publishes one of two shapes: a plain enqueue publishes the queue
2917
+ * ITEM's Orchestrator record (fixed, declared by the serializer from
2918
+ * `QUEUE_ITEM_RECORD_SCHEMA`), a `wait: true` publishes the processed result,
2919
+ * whose fields only the author's `returns` states.
2920
+ */
2921
+ function checkQueueItemRead(name, field, rest, spec, step, diags) {
2922
+ const waitReturns = Object.keys(spec?.returns ?? {});
2923
+ checkDeclaredOutputRead({
2924
+ prefix: 'QUEUE', what: spec?.wait ? 'Queue wait' : 'Queue item', name, field, rest, step, diags,
2925
+ declared: !spec?.wait
2926
+ ? Object.keys(QUEUE_ITEM_RECORD_SCHEMA.properties)
2927
+ : waitReturns.length ? waitReturns : undefined,
2928
+ withoutReturns: `What the consumer writes into the processed item's result is that automation's business; `
2929
+ + `declare the fields you read.`,
2930
+ });
2931
+ }
2932
+ /**
2933
+ * THE GATE (flow-builder-sdk#850). Each `returns`-bearing kind maps to the rule
2934
+ * that checks a read of its output, or to a stated reason it has none.
2935
+ *
2936
+ * Before this table each family's rule was wired by hand — a map filled in the
2937
+ * walk plus a branch in the `$vars` dispatch — and nothing tied "this family
2938
+ * takes `returns`" to "this family has a read rule". `http`, `rpaWorkflow` and
2939
+ * `queueItem` shipped without one, so a read of an undeclared field passed
2940
+ * `check` and `validate` and only faulted live. The type below is exhaustive
2941
+ * over the derived kinds: give a new family a `returns` and `tsc` refuses to
2942
+ * build until it lands here.
2943
+ */
2944
+ const OUTPUT_READ_RULES = {
2945
+ http: checkHttpRead,
2946
+ rpaWorkflow: checkRpaWorkflowRead,
2947
+ apiWorkflow: checkApiWorkflowRead,
2948
+ publishedFunction: checkPublishedFunctionRead,
2949
+ agenticProcess: checkAgenticProcessRead,
2950
+ agent: checkAgentRead,
2951
+ inlineAgent: checkInlineAgentRead,
2952
+ queueItem: checkQueueItemRead,
2953
+ script: {
2954
+ exempt: 'Not yet: only the field-map form of a script\'s `returns` gives a list to check against (the '
2955
+ + 'type form and the inferred form do not), and script is the most-used step, so its rule lands with '
2956
+ + 'its own false-positive sweep — flow-builder-sdk#855.',
2957
+ },
2958
+ };
2818
2959
  function checkApiWorkflowRead(name, field, rest, spec, step, diags) {
2819
2960
  if (field === undefined)
2820
2961
  return; // `out('call')` — the whole output object.
@@ -19,3 +19,10 @@ export declare function openCliLibrary(argv: readonly string[]): Library | undef
19
19
  * exactly when compile would throw, with the same text.
20
20
  */
21
21
  export declare function eventResolveErrorFor(library: Library): NonNullable<CheckOpts['eventResolveError']>;
22
+ /**
23
+ * `check`'s EVENT_BYOA_UNVERIFIED inputs: the event facts and the recorded BYOA
24
+ * checks, from the same `library.eventFacts` / `library.byoaVerifications` that
25
+ * `serialize` reads for the same refusal. Both CLIs spread this into their
26
+ * check options, so the inner loop and the emit rung refuse the same flows.
27
+ */
28
+ export declare function byoaCheckOpts(library: Library): Pick<CheckOpts, 'eventFacts' | 'byoa'>;
@@ -54,3 +54,22 @@ export function eventResolveErrorFor(library) {
54
54
  }
55
55
  };
56
56
  }
57
+ /**
58
+ * `check`'s EVENT_BYOA_UNVERIFIED inputs: the event facts and the recorded BYOA
59
+ * checks, from the same `library.eventFacts` / `library.byoaVerifications` that
60
+ * `serialize` reads for the same refusal. Both CLIs spread this into their
61
+ * check options, so the inner loop and the emit rung refuse the same flows.
62
+ */
63
+ export function byoaCheckOpts(library) {
64
+ return {
65
+ eventFacts: (connector, event, object) => {
66
+ try {
67
+ return library.eventFacts(connector, event, object);
68
+ }
69
+ catch {
70
+ return undefined; // unknown fails open, same as every library callback
71
+ }
72
+ },
73
+ byoa: library.byoaVerifications,
74
+ };
75
+ }
@@ -23,12 +23,13 @@ import { basename, dirname, join, resolve } from 'node:path';
23
23
  import { resolveFlowFile, loadBuiltFlow } from './load.js';
24
24
  import { serializeWithSidecars, connectorInputErrors } from './serialize.js';
25
25
  import { check } from './check.js';
26
- import { eventResolveErrorFor, openCliLibrary } from './cli-library.js';
26
+ import { byoaCheckOpts, eventResolveErrorFor, openCliLibrary } from './cli-library.js';
27
27
  import { Bindings } from './bindings.js';
28
28
  import { runWhenInvokedDirectly } from './cli-run.js';
29
29
  import { connectionResources, mergeBindingsV2, readBindingsV2 } from './bindings-v2.js';
30
30
  import { ensureTypeScriptRuntime } from './node-runtime.js';
31
31
  import { BindingRefusal } from './core/binding-messages.js';
32
+ import { ByoaRefusal } from './core/byoa.js';
32
33
  function opt(argv, name) {
33
34
  const i = argv.indexOf(name);
34
35
  return i >= 0 ? argv[i + 1] : undefined;
@@ -96,6 +97,7 @@ export async function run(argv) {
96
97
  // printed as `✗ [EVENT_UNKNOWN]` before stopping instead of hiding
97
98
  // behind "1 error(s); not emitting".
98
99
  eventResolveError: eventResolveErrorFor(library),
100
+ ...byoaCheckOpts(library),
99
101
  eventParams: (connector, event) => {
100
102
  try {
101
103
  return library.eventParameterFields(connector, event);
@@ -134,16 +136,17 @@ export async function run(argv) {
134
136
  // check()'s. Warnings are never fatal: the .flow is still written, exit stays 0.
135
137
  // A binding LABEL the serializer refuses (its own name as the resourceKey, or a
136
138
  // label a populated bindings.json does not declare) is an error, not a
137
- // warning: the check pass above reports it first, and this is the same refusal
138
- // in the same `✗ [CODE]` shape when that pass was skipped (`--no-check`, or an
139
- // emit-only project). Nothing is written.
139
+ // warning, and so is a BYOA-only event on a connection `prepare` never checked
140
+ // (EVENT_BYOA_UNVERIFIED): the check pass above reports each first, and this is
141
+ // the same refusal in the same `✗ [CODE]` shape when that pass was skipped
142
+ // (`--no-check`, or an emit-only project). Nothing is written.
140
143
  const warnings = [];
141
144
  let emitted;
142
145
  try {
143
146
  emitted = serializeWithSidecars(built, { library, bindings, warn: (w) => warnings.push(w) });
144
147
  }
145
148
  catch (error) {
146
- if (!(error instanceof BindingRefusal))
149
+ if (!(error instanceof BindingRefusal || error instanceof ByoaRefusal))
147
150
  throw error;
148
151
  for (const w of warnings)
149
152
  console.error(` ⚠ [${w.code}] ${w.message}`);
@@ -82,6 +82,16 @@ interface HttpInputsBase {
82
82
  * fields, and nothing downstream can tell a real field from a typo. Same rule,
83
83
  * and the same word, as
84
84
  * `rpaWorkflow`'s `returns`.
85
+ *
86
+ * **Where a declared field is read:** under `body`, never beside it. The
87
+ * response is an envelope — `{ body, statusCode, headers }` — and these are the
88
+ * body's fields, so `returns: { items: 'array' }` is read as
89
+ * `out('fetch', 'body.items')`, which is `$vars.fetch.output.body.items`.
90
+ * `$vars.fetch.output.items` is undefined at run time.
91
+ *
92
+ * @enforcedBy HTTP_READ_UNKNOWN_FIELD A read must be `output.body.<declared field>`,
93
+ * `output.statusCode` or `output.headers`.
94
+ * @enforcedBy HTTP_READ_WITHOUT_RETURNS Reading a body field requires declaring it here.
85
95
  */
86
96
  returns?: Record<string, ReturnFieldType>;
87
97
  /**
@@ -991,6 +1001,9 @@ export interface RpaWorkflowInputs {
991
1001
  * the fields here is the same thing the platform's own definition does when it
992
1002
  * can reach the process, and it is what lets a misspelled read be caught at
993
1003
  * `uip maestro flow check` instead of at run time.
1004
+ *
1005
+ * @enforcedBy RPA_READ_UNKNOWN_FIELD A read of the output must name a field declared here.
1006
+ * @enforcedBy RPA_READ_WITHOUT_RETURNS Reading a field off the result requires declaring it here.
994
1007
  */
995
1008
  returns?: Record<string, ReturnFieldType>;
996
1009
  }
@@ -2035,6 +2048,11 @@ export interface QueueItemInputs {
2035
2048
  * `wait: false` needs none: what it publishes is the queue ITEM's own
2036
2049
  * Orchestrator record (`Key`, `Status`, `Reference`, `SpecificContent`, …),
2037
2050
  * which is the same for every queue, so the serializer declares it for you.
2051
+ *
2052
+ * @enforcedBy QUEUE_READ_UNKNOWN_FIELD A read must name a field of the record
2053
+ * (`wait: false`) or of this `returns` (`wait: true`).
2054
+ * @enforcedBy QUEUE_READ_WITHOUT_RETURNS Reading a field off a `wait: true` result
2055
+ * requires declaring it here.
2038
2056
  */
2039
2057
  returns?: Record<string, ReturnFieldType>;
2040
2058
  }
@@ -0,0 +1,69 @@
1
+ /** The diagnostic code, in `check` and on the error `serialize` throws. */
2
+ export declare const EVENT_BYOA_UNVERIFIED = "EVENT_BYOA_UNVERIFIED";
3
+ /** One verification `prepare` recorded. Only a PASSING check is ever written. */
4
+ export interface ByoaRecord {
5
+ byoa: true;
6
+ /** The connection's `Name`, so a person reading the file can tell which one it is. */
7
+ connectionName?: string;
8
+ /** `YYYY-MM-DD`: a resolved answer is a snapshot, like every other ledger row. */
9
+ verifiedAt?: string;
10
+ }
11
+ /**
12
+ * The ledger's `byoa` section: `byoa[<event node type>][<connection id>]`.
13
+ *
14
+ * Keyed per EVENT, not per connector — Slack `send-reply` needs no BYOA, the
15
+ * message trigger does — and per CONNECTION, so swapping the connection in
16
+ * bindings.json un-verifies the event until it is prepared again.
17
+ */
18
+ export type ByoaLedger = Record<string, Record<string, ByoaRecord>>;
19
+ /** The event facts the rule reads, as `Library.eventFacts` answers them. */
20
+ export interface ByoaEventFacts {
21
+ byoaConnection: boolean;
22
+ isWebhookUrlVisible: boolean;
23
+ }
24
+ /**
25
+ * The key an event's record is stored under: the WAIT form of the node type,
26
+ * the form `prepare` stores an event's overlay and lookup rows under, whichever
27
+ * role (`onEvent` or `waitForEvent`) the flow uses it in.
28
+ */
29
+ export declare function byoaLedgerKey(connectorKey: string, event: string): string;
30
+ /** The record for `connectionId` under `nodeType`, matching the id case-insensitively. */
31
+ export declare function byoaRecordFor(ledger: ByoaLedger | undefined, nodeType: string, connectionId: string): ByoaRecord | undefined;
32
+ /** What `compile` throws for the refusal; `code` lets the CLI print it as `✗ [CODE]`. */
33
+ export declare class ByoaRefusal extends Error {
34
+ readonly code = "EVENT_BYOA_UNVERIFIED";
35
+ constructor(message: string);
36
+ }
37
+ /** One subscription, as the rule needs it. */
38
+ export interface ByoaSubscription {
39
+ connector: string;
40
+ event: string;
41
+ object?: string;
42
+ /** The `connection:` label as authored — a bindings.json name, or a connection id. */
43
+ connection?: unknown;
44
+ }
45
+ /**
46
+ * Why a BYOA-only event cannot be trusted to fire on its bound connection, or
47
+ * `undefined` when there is nothing to refuse. The ONE condition behind
48
+ * `check`'s EVENT_BYOA_UNVERIFIED error and `compile`'s refusal, so the two
49
+ * cannot disagree.
50
+ *
51
+ * Refuses when the event's facts say BYOA and the connection resolves to a real
52
+ * connection id with no `byoa` record for this event. Silent when:
53
+ *
54
+ * - the facts are unknown or say no BYOA;
55
+ * - the connection does not resolve to a real id — no bindings.json, a label it
56
+ * does not declare, or a placeholder (`0000…`, `<…>`, the label itself). There
57
+ * is no id to look a record up by, and nothing is lost: such a flow cannot
58
+ * run its trigger on any connection, and BINDING_SELF_NAME /
59
+ * BINDING_UNDECLARED / CONNECTION_STUB already say so.
60
+ *
61
+ * @param where - The call site, as the other event diagnostics name it (`trigger`, `waitForEvent "x"`).
62
+ * @param sub - The subscription.
63
+ * @param facts - The event's facts from the library, or `undefined` when unknown.
64
+ * @param ledger - The overlay ledger's `byoa` section.
65
+ * @param bindings - Resolves the `connection:` label to an id.
66
+ */
67
+ export declare function byoaUnverified(where: string, sub: ByoaSubscription, facts: ByoaEventFacts | undefined, ledger: ByoaLedger | undefined, bindings: {
68
+ resolve(name?: string): string | undefined;
69
+ } | undefined): string | undefined;