@uipath/maestro-builder-sdk 6.70.9 → 6.71.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,356 @@
1
+ import { BindingRefusal, bindingMissingMessage, missingBindingLabels } from './binding-messages.js';
2
+ import { ByoaRefusal, byoaUnverified } from './byoa.js';
3
+ import { CompileRefusal, LibraryRequiredRefusal } from './compile-refusal.js';
4
+ import { genericEventObjectRefusal } from './event-checks.js';
5
+ import { EVENT_FILTER_OPERATORS, eventFilterJmes, eventFilterProblem, eventFilterTreeLeaf, jmesString, jmesStringProblem, readEventFilter, } from './event-filters.js';
6
+ import { isLookupToken, resolveEventWhere } from './lookups.js';
7
+ import { stableId } from './stable-id.js';
8
+ /** What `registry prepare` takes for an event subscription: its connector key and curated event name. */
9
+ export function eventTarget(sub) {
10
+ return typeof sub.connector === 'string' && sub.connector !== ''
11
+ ? { key: sub.connector, ...(typeof sub.event === 'string' && sub.event !== '' ? { action: sub.event } : {}) }
12
+ : undefined;
13
+ }
14
+ /**
15
+ * Throw `check`'s EVENT_BYOA_UNVERIFIED for a subscription whose event only
16
+ * fires on a BYOA connection and whose bound connection `prepare` never checked
17
+ * — the one condition in `core/byoa.ts`, fed from the same library and
18
+ * bindings, so a flow check refuses never compiles.
19
+ *
20
+ * `where` is check's spelling of the site (`trigger` for `onEvent`, entry points
21
+ * included; `waitForEvent "<step>"`), so the two print the same sentence.
22
+ */
23
+ export function refuseUnverifiedByoa(env, where, sub) {
24
+ const library = env.library;
25
+ if (!library || typeof library.eventFacts !== 'function')
26
+ return;
27
+ const object = typeof sub.object === 'string' && sub.object !== '' ? sub.object : undefined;
28
+ const message = byoaUnverified(where, { connector: sub.connector, event: sub.event, ...(object ? { object } : {}), connection: sub.connection }, library.eventFacts(sub.connector, sub.event, object), library.byoaVerifications, env.bindings);
29
+ if (message)
30
+ throw new ByoaRefusal(message);
31
+ }
32
+ /**
33
+ * Refuse a connector-bound node (connector step, event trigger, `.entryPoint`
34
+ * onEvent, `waitForEvent`) that names no `connection` or no `folder` label.
35
+ * (An inline agent's connector tool is #875's: `check` refuses it as
36
+ * INLINE_AGENT_TOOL_CONNECTOR_NO_CONNECTION, and serialize leaves it unbound.)
37
+ * `resolveBinding` returns `undefined` for an absent label, which used to reach
38
+ * the node as `detail.connectionId: null` with no binding — valid to compile,
39
+ * refused by `flow validate` (flow-builder-sdk#890): BINDING_MISSING.
40
+ */
41
+ export function requireBindingPair(where, labels, target) {
42
+ const missing = missingBindingLabels(labels);
43
+ if (missing.length > 0)
44
+ throw new BindingRefusal('BINDING_MISSING', bindingMissingMessage(where, missing, target));
45
+ }
46
+ /**
47
+ * `library.resolveEvent` for one subscription, or EVENT_UNKNOWN: whatever it
48
+ * throws (a connector or event the library does not carry, a version or object
49
+ * it does not resolve) is that one condition, an event compile cannot place.
50
+ * `where` names the site as `check` always has (`trigger`, `waitForEvent "x"`).
51
+ */
52
+ export function resolveEvent(library, kind, sub, where) {
53
+ try {
54
+ return library.resolveEvent(kind, sub.connector, sub.event, sub.version, sub.object);
55
+ }
56
+ catch (error) {
57
+ if (error instanceof Error && error.message.startsWith('internal SDK error:'))
58
+ throw error;
59
+ throw new CompileRefusal('EVENT_UNKNOWN', `${where}: ${error instanceof Error ? error.message : String(error)}`);
60
+ }
61
+ }
62
+ /**
63
+ * LIBRARY_REQUIRED for an event subscription at `site`, after the refusals that
64
+ * need no library: its binding labels, judged as `eventDetail` judges them, so a
65
+ * run without a library (`check`'s inner loop) still hears about them. The
66
+ * refusal carries the connection the node is bound to, so its collect-mode
67
+ * stand-in still takes part in the file-wide connection rule (`connectionConnectors`).
68
+ */
69
+ export function libraryRequiredForEvent(env, sub, nodeId, site, call) {
70
+ let use;
71
+ if (sub !== null && typeof sub === 'object') {
72
+ // With no library no parameter is declared, so every `where` key lands in
73
+ // eventParameters and folds into the match expression, exactly as
74
+ // `eventDetail` places an undeclared key. A lookup token is a value only
75
+ // a library resolves; it is not judged here.
76
+ if (sub.where !== null && typeof sub.where === 'object') {
77
+ const scope = Object.fromEntries(Object.entries(sub.where).filter(([, v]) => !isLookupToken(v)));
78
+ eventScopeClauses(flattenWhere(scope, new Set()), nodeId);
79
+ }
80
+ if (Array.isArray(sub.filters))
81
+ eventFilterReads(sub.filters, nodeId);
82
+ requireBindingPair(site, sub, eventTarget(sub));
83
+ const connection = env.resolveBinding(sub.connection, 'connection', `event node "${nodeId}"`, eventTarget(sub));
84
+ env.resolveBinding(sub.folder, 'folder', `event node "${nodeId}"`, eventTarget(sub));
85
+ const connector = eventTarget(sub)?.key;
86
+ if (connection !== undefined && connector !== undefined)
87
+ use = { connector, connection };
88
+ }
89
+ return new LibraryRequiredRefusal(`${call} needs a library (pass --library / set $FLOW_SDK_LIBRARY_JSON).`, use);
90
+ }
91
+ /**
92
+ * Flatten a nested `where` onto the DOTTED parameter names the wire uses.
93
+ *
94
+ * The generated `…Where` interface splits a dotted event parameter into an
95
+ * object — `channelData.channel.id` becomes
96
+ * `channelData: ConnectorValue<ChannelDataInput>` — so an author following the
97
+ * types writes the nested shape, while `bucketOf` and the `filterExpression`
98
+ * clause both key on the declared dotted name. Without this the nested shape
99
+ * missed the bucket lookup and then stringified, emitting
100
+ * `channelData == '[object Object]'`: a subscription matching nothing, from a
101
+ * document that compiled clean and passed `check`.
102
+ *
103
+ * Descent STOPS as soon as a path names a declared parameter, so a parameter
104
+ * that genuinely takes an object value is passed through whole rather than
105
+ * shredded. A lookup token is a value, never a branch.
106
+ */
107
+ export function flattenWhere(where, declared) {
108
+ const out = {};
109
+ const walk = (value, path) => {
110
+ if (path && declared.has(path)) {
111
+ out[path] = value;
112
+ return;
113
+ }
114
+ const plain = typeof value === 'object' && value !== null && !Array.isArray(value)
115
+ && !isLookupToken(value)
116
+ && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
117
+ if (plain) {
118
+ for (const [k, v] of Object.entries(value))
119
+ walk(v, path ? `${path}.${k}` : k);
120
+ return;
121
+ }
122
+ if (path)
123
+ out[path] = value;
124
+ };
125
+ walk(where, '');
126
+ return out;
127
+ }
128
+ /**
129
+ * A subscription's `filters`, each leaf read structurally (`readEventFilter`),
130
+ * or the refusal of the first leaf compile cannot lower: no field, no operator
131
+ * or two, a value no operator takes, a backslash JMESPath engines read apart.
132
+ * Needs no library, so a run without one judges the leaves too
133
+ * (`libraryRequiredForEvent`).
134
+ */
135
+ export function eventFilterReads(filters, nodeId) {
136
+ const operators = Object.keys(EVENT_FILTER_OPERATORS).join(', ');
137
+ const at = `event node "${nodeId}": filters`;
138
+ return filters.map((f, i) => {
139
+ const { keys, read } = readEventFilter(f);
140
+ const field = f?.field;
141
+ if (typeof field !== 'string' || field === '') {
142
+ throw new CompileRefusal('EVENT_FILTER_NO_FIELD', `${at}[${i}] has no \`field\` — name a field on the event payload, e.g. 'subject'.`);
143
+ }
144
+ if (keys.length === 0) {
145
+ throw new CompileRefusal('EVENT_FILTER_NO_VALUE', `${at}[${i}] has no operator. Write exactly one of ${operators} with its value — `
146
+ + `\`contains\` is a case-sensitive substring test, the same one Integration Service applies `
147
+ + `server-side; \`lessThan\`/\`greaterThan\` order a number or an ISO date.`, `{ field: '${field}', contains: '<text>' }`);
148
+ }
149
+ if (keys.length > 1) {
150
+ throw new CompileRefusal('EVENT_FILTER_MANY_OPERATORS', `${at}[${i}] carries ${keys.length} operators (${keys.join(', ')}); a condition has exactly `
151
+ + `one. Write two conditions instead — they are ANDed.`);
152
+ }
153
+ if (!read) {
154
+ throw new CompileRefusal('EVENT_FILTER_NO_VALUE', `${at}[${i}] gives \`${keys[0]}\` a value that is not a string, number or boolean.`);
155
+ }
156
+ // An ambiguous backslash first: `eventFilterProblem` would report it too.
157
+ const problem = eventFilterProblem(read);
158
+ const backslash = typeof read.value === 'string' && jmesStringProblem(read.value) !== undefined;
159
+ if (problem) {
160
+ throw new CompileRefusal(backslash ? 'EVENT_FILTER_BACKSLASH' : 'EVENT_FILTER_BAD_VALUE', `${at}[${i}] on \`${read.field}\`: ${problem}`);
161
+ }
162
+ return read;
163
+ });
164
+ }
165
+ /**
166
+ * The match-expression clauses of a subscription's EVENT parameters, one
167
+ * `k == '<value>'` per value, or the refusal of the first value compile cannot
168
+ * write there. The clause compares against a JMESPath string, so the value must
169
+ * be a string, number or boolean: an object or array would be stringified to
170
+ * `'[object Object]'` (or a comma list), a subscription that matches nothing
171
+ * (EVENT_WHERE_NOT_SCALAR). A string with a backslash JMESPath engines read
172
+ * apart is EVENT_FILTER_BACKSLASH. An absent or empty value (`''` or `[]`, so a
173
+ * `where` built from a possibly-empty list) writes no clause.
174
+ *
175
+ * Only event parameters fold into the expression; a query or path parameter is
176
+ * sent with the request that registers the subscription and is never read as
177
+ * JMESPath, so neither rule applies to it.
178
+ */
179
+ export function eventScopeClauses(eventParameters, nodeId) {
180
+ return Object.entries(eventParameters)
181
+ .filter(([, v]) => v !== undefined && v !== null && v !== '' && !(Array.isArray(v) && v.length === 0))
182
+ .map(([k, v]) => {
183
+ if (typeof v !== 'string' && typeof v !== 'number' && typeof v !== 'boolean') {
184
+ throw new CompileRefusal('EVENT_WHERE_NOT_SCALAR', `event node "${nodeId}": \`where.${k}\` is ${Array.isArray(v) ? 'an array' : isLookupToken(v) ? 'an unresolved lookup() nested inside an object' : 'an object'}, `
185
+ + `and an event parameter is matched as one text value (\`${k} == '<value>'\`): written as it is, `
186
+ + `the subscription would compare against ${JSON.stringify(String(v))} and match nothing. `
187
+ + `Give \`${k}\` a string, number or boolean.`, `where: { '${k}': '<value>' }`);
188
+ }
189
+ const problem = jmesStringProblem(String(v));
190
+ if (problem)
191
+ throw new CompileRefusal('EVENT_FILTER_BACKSLASH', `event node "${nodeId}": \`where.${k}\`: ${problem}`);
192
+ return `${k} == ${jmesString(String(v))}`;
193
+ });
194
+ }
195
+ export function eventDetail(rc, sub, nodeId, env, site) {
196
+ requireBindingPair(site, sub, eventTarget(sub));
197
+ const conn = env.resolveBinding(sub.connection, 'connection', `event node "${nodeId}"`, eventTarget(sub));
198
+ const folder = env.resolveBinding(sub.folder, 'folder', `event node "${nodeId}"`, eventTarget(sub));
199
+ const op = (rc.entry?.operation ?? {});
200
+ const filters = sub.filters ?? [];
201
+ // A GENERIC event with no `object` would carry `objectName: ""`: it validates
202
+ // and the platform's dispatcher refuses it. Refused here, at the layer that
203
+ // would otherwise emit it, with the text every family's compile shares.
204
+ const objectName = String(op.objectName ?? '');
205
+ const noObject = genericEventObjectRefusal(rc, sub, (o) => `onEvent(…, { object: '${o}' })`);
206
+ if (noObject) {
207
+ // The object an author put in `where` by mistake is the value to move.
208
+ const scopeObject = sub.where && typeof sub.where === 'object'
209
+ ? Object.entries(sub.where).find(([k]) => /^(objectName|entityName|object)$/i.test(k))?.[1]
210
+ : undefined;
211
+ throw new CompileRefusal('EVENT_GENERIC_NO_OBJECT', `event node "${nodeId}": ${noObject}`, `object: '${scopeObject !== undefined ? String(scopeObject) : '<Object>'}'`);
212
+ }
213
+ // An unresolved `where` lookup is swapped for its recorded id — the id
214
+ // `prepare --resolve` stored under the EVENT-form node type — or refused.
215
+ // Same rule as connector inputs: emitting a placeholder for a value nobody
216
+ // resolved would subscribe the trigger to the wrong folder/channel/queue.
217
+ const where = resolveEventWhere(sub.where, env.library?.lookupResolutions ?? {}, `event node "${nodeId}"`);
218
+ // A subscription's parameters do not all travel in the same slot. The
219
+ // connector declares which, and the library bakes it per field as `bucket`:
220
+ // measured against designer-authored triggers, Jira's `project`/`issuetype`
221
+ // and GitHub's `repo` land in `queryParameters`, Asana's `projectId` in
222
+ // `pathParameters` and its `workspaceId` in `queryParameters`, while
223
+ // Outlook's `parentFolderId` — which the connector declares through the
224
+ // field's own `events.<OPERATION>` map rather than as a typed request
225
+ // parameter — lands in `eventParameters`. Emitting all of them as event
226
+ // parameters puts three of those four in a slot the runtime does not read.
227
+ const bucketOf = new Map();
228
+ const declaredOrder = [];
229
+ for (const f of (rc.v1def
230
+ ?.eventParameters?.fields ?? [])) {
231
+ if (typeof f?.name !== 'string')
232
+ continue;
233
+ declaredOrder.push(f.name);
234
+ if (typeof f?.bucket === 'string')
235
+ bucketOf.set(f.name, f.bucket);
236
+ }
237
+ const eventParameters = {};
238
+ const queryParameters = {};
239
+ const pathParameters = {};
240
+ // FLATTENED first: everything below keys on the DECLARED name, which is dotted
241
+ // (`channelData.channel.id`), while the generated `…Where` interface splits
242
+ // that into an object — so an author following the types writes the nested
243
+ // shape. Un-flattened it missed `bucketOf` (landing in the wrong slot) and
244
+ // then stringified into the match expression as
245
+ // `channelData == '[object Object]'`: a subscription that matches nothing,
246
+ // compiled clean and passed `check`.
247
+ for (const [k, v] of Object.entries(flattenWhere(where, new Set(declaredOrder)))) {
248
+ const slot = bucketOf.get(k);
249
+ (slot === 'query' ? queryParameters : slot === 'path' ? pathParameters : eventParameters)[k] = v;
250
+ }
251
+ // Only EVENT parameters fold into the match expression. They are the
252
+ // subscription's own scope — the mandatory clause the platform AND-joins —
253
+ // whereas a query or path parameter is part of the request that registers
254
+ // the subscription, not a property of the events it matches.
255
+ const scopeClauses = eventScopeClauses(eventParameters, nodeId);
256
+ // Each authored filter is read structurally (which operator, which value) and
257
+ // lowered with the designer's own grammar — see `event-filters.ts` for the
258
+ // measured table.
259
+ const conditions = eventFilterReads(filters, nodeId);
260
+ const filterClauses = conditions.map((c) => `(${eventFilterJmes(c)})`);
261
+ const filterExpression = [
262
+ ...scopeClauses,
263
+ ...(filterClauses.length > 0 ? [`(${filterClauses.join('&&')})`] : []),
264
+ ].join(' && ');
265
+ const instanceParameters = {
266
+ connectorKey: rc.connectorKey,
267
+ objectName,
268
+ activityType: op.activityType,
269
+ version: rc.version,
270
+ eventOperation: op.eventOperation,
271
+ eventMode: op.eventMode,
272
+ supportsStreaming: op.supportsStreaming ?? false,
273
+ };
274
+ const tree = conditions.length === 0 ? null : {
275
+ groupOperator: 0,
276
+ index: 0,
277
+ filters: conditions.map((c, i) => ({
278
+ ...eventFilterTreeLeaf(c),
279
+ // Deterministic, so recompiling the same flow is byte-identical (the
280
+ // designer mints random uuids here; a random one would churn every build).
281
+ uiId: stableId(`${nodeId}:${c.field}:${String(c.value)}:${i}`),
282
+ })),
283
+ groups: [],
284
+ uuId: stableId(`${nodeId}:filters`),
285
+ };
286
+ // The node's own Property binding: how the platform registers the
287
+ // subscription, and where `flow debug` reads its query parameters from.
288
+ // Pinned to designer-authored artifacts — the START form carries every
289
+ // subscription parameter as `{defaultValue, isExpression}` whatever its
290
+ // bucket (Jira `project,issuetype`; Asana `workspaceId,projectId`; Outlook
291
+ // `parentFolderId`), while the mid-flow WAIT form carries an EMPTY value and
292
+ // empty parameter lists: it joins a subscription the trigger registers,
293
+ // rather than registering one of its own.
294
+ const isStartForm = rc.nodeType.includes('.connector.trigger.');
295
+ const allParameters = {
296
+ ...eventParameters, ...queryParameters, ...pathParameters,
297
+ };
298
+ // Declared order, not the order the author happened to write `where` in, so
299
+ // the same subscription always emits the same bytes.
300
+ const orderedNames = [
301
+ ...declaredOrder.filter((n) => n in allParameters),
302
+ ...Object.keys(allParameters).filter((n) => !declaredOrder.includes(n)),
303
+ ];
304
+ const subscriptionValue = {};
305
+ if (isStartForm) {
306
+ for (const name of orderedNames) {
307
+ subscriptionValue[name] = { defaultValue: allParameters[name], isExpression: false };
308
+ }
309
+ }
310
+ const parameterList = isStartForm ? orderedNames.join(',') : '';
311
+ const propertyBinding = {
312
+ resource: 'Property',
313
+ key: op.eventOperation ?? '',
314
+ value: subscriptionValue,
315
+ activityDisplayName: 'connector-activity',
316
+ metadata: {
317
+ ActivityName: 'connector-activity',
318
+ BindingsVersion: '2.2',
319
+ ObjectName: objectName,
320
+ // The trigger resource is keyed by the CONNECTION the subscription is
321
+ // registered against, so two connections keep two trigger resources.
322
+ ...(conn ? { ParentResourceKey: `EventTrigger.${conn}` } : {}),
323
+ TriggerCreateParameters: parameterList,
324
+ DebugQueryParameters: parameterList,
325
+ },
326
+ };
327
+ return {
328
+ connector: rc.connectorKey,
329
+ ...(conn ? { connectionId: conn, connectionResourceId: conn } : {}),
330
+ ...(folder ? { connectionFolderKey: folder } : {}),
331
+ eventParameters,
332
+ // Emitted only when populated, mirroring the connector-activity slots:
333
+ // no archived artifact carries an empty one.
334
+ ...(Object.keys(queryParameters).length ? { queryParameters } : {}),
335
+ ...(Object.keys(pathParameters).length ? { pathParameters } : {}),
336
+ objectName,
337
+ eventType: op.eventOperation,
338
+ eventMode: op.eventMode,
339
+ filterExpression,
340
+ configuration: '=jsonString:' + JSON.stringify({
341
+ essentialConfiguration: {
342
+ instanceParameters,
343
+ objectName,
344
+ packageVersion: rc.version,
345
+ connectorVersion: null,
346
+ executionType: null,
347
+ httpMethod: null,
348
+ path: null,
349
+ filter: tree,
350
+ },
351
+ }),
352
+ ...(rc.activityTypeId ? { uiPathActivityTypeId: rc.activityTypeId } : {}),
353
+ bindings: [propertyBinding],
354
+ errorState: { issues: [] },
355
+ };
356
+ }
@@ -67,7 +67,9 @@ export declare class NodeTypeRegistry<Ctx extends ArtifactContexts> {
67
67
  * whose `typeNamePrefix` it starts with (and whose `typeNameMatches`, if any,
68
68
  * accepts it) — or undefined when no type owns it.
69
69
  * The bare prefix counts: it is what an empty `key` builds, and the old lift
70
- * read it as its factory, so it still does.
70
+ * read it as its factory, so it still does. Two prefix types that would both
71
+ * own a name (a nested prefix the outer narrowing failed to leave alone) are
72
+ * an SDK bug, and throw.
71
73
  */
72
74
  byTypeName(typeName: string): Registered<Ctx> | undefined;
73
75
  /** Every registered kind, sorted. */
@@ -5,6 +5,18 @@ import { CompileRefusal } from '../compile-refusal.js';
5
5
  function owns(p, typeName) {
6
6
  return typeName.startsWith(p.prefix) && (p.matches?.(typeName) ?? true);
7
7
  }
8
+ /**
9
+ * Whether one prefix sits strictly inside the other and the outer one is
10
+ * narrowed to leave it alone: the connector operations' `uipath.connector.`
11
+ * holds the event wait's `uipath.connector.event.`, and the operations'
12
+ * `typeNameMatches` rejects every event name. A name under the inner prefix is
13
+ * the probe; `byTypeName` still refuses a name two prefixes would both own.
14
+ */
15
+ function nests(a, b) {
16
+ const [outer, inner] = a.prefix.length < b.prefix.length ? [a, b] : [b, a];
17
+ return inner.prefix !== outer.prefix && inner.prefix.startsWith(outer.prefix)
18
+ && outer.matches !== undefined && !outer.matches(`${inner.prefix}probe.probe`);
19
+ }
8
20
  export class NodeTypeRegistry {
9
21
  artifact;
10
22
  #builtIn;
@@ -84,7 +96,8 @@ export class NodeTypeRegistry {
84
96
  if (type.typeName !== `${prefix}*`) {
85
97
  problems.push(`${where}: a type that owns the names under "${prefix}" is named "${prefix}*", not "${type.typeName}"`);
86
98
  }
87
- const overlap = index.byPrefix.find((p) => p.prefix.startsWith(prefix) || prefix.startsWith(p.prefix));
99
+ const self = { prefix, ...(type.typeNameMatches ? { matches: type.typeNameMatches.bind(type) } : {}) };
100
+ const overlap = index.byPrefix.find((p) => (p.prefix.startsWith(prefix) || prefix.startsWith(p.prefix)) && !nests(self, p));
88
101
  if (overlap)
89
102
  problems.push(`${where}: prefix "${prefix}" overlaps "${overlap.prefix}" (${placeOf(overlap.entry)})`);
90
103
  const shadowed = [...index.byTypeName.keys()].find((n) => n.startsWith(prefix) && (type.typeNameMatches?.(n) ?? true));
@@ -156,13 +169,19 @@ export class NodeTypeRegistry {
156
169
  * whose `typeNamePrefix` it starts with (and whose `typeNameMatches`, if any,
157
170
  * accepts it) — or undefined when no type owns it.
158
171
  * The bare prefix counts: it is what an empty `key` builds, and the old lift
159
- * read it as its factory, so it still does.
172
+ * read it as its factory, so it still does. Two prefix types that would both
173
+ * own a name (a nested prefix the outer narrowing failed to leave alone) are
174
+ * an SDK bug, and throw.
160
175
  */
161
176
  byTypeName(typeName) {
162
177
  const exact = this.#index.byTypeName.get(typeName);
163
178
  if (exact)
164
179
  return exact;
165
- return this.#index.byPrefix.find((p) => owns(p, typeName))?.entry;
180
+ const owners = this.#index.byPrefix.filter((p) => owns(p, typeName));
181
+ if (owners.length > 1) {
182
+ throw new Error(`internal SDK error: the node type name "${typeName}" is owned by ${owners.map((o) => placeOf(o.entry)).join(' and ')}; exactly one may.`);
183
+ }
184
+ return owners[0]?.entry;
166
185
  }
167
186
  /** Every registered kind, sorted. */
168
187
  kinds() {
@@ -43,6 +43,9 @@ export declare const REFUSALS: {
43
43
  readonly AGENT_READ_WRONG_CASE: "lint";
44
44
  readonly APIWF_EMPTY_RETURNS: "lint";
45
45
  readonly APIWF_KEY_NOT_KEY: "lint";
46
+ readonly APIWF_LOCAL_HAS_FOLDER: "lowering";
47
+ readonly APIWF_LOCAL_NO_PROJECT_ID: "lint";
48
+ readonly APIWF_LOCAL_PROJECT_ID_NOT_GUID: "lint";
46
49
  readonly APIWF_NAME_IS_PATH: "lint";
47
50
  readonly APIWF_NO_FOLDER: "lint";
48
51
  readonly APIWF_NO_KEY: "lowering";
@@ -92,6 +92,9 @@ export const REFUSALS = {
92
92
  AGENT_READ_WRONG_CASE: 'lint',
93
93
  APIWF_EMPTY_RETURNS: 'lint',
94
94
  APIWF_KEY_NOT_KEY: 'lint',
95
+ APIWF_LOCAL_HAS_FOLDER: 'lowering',
96
+ APIWF_LOCAL_NO_PROJECT_ID: 'lint',
97
+ APIWF_LOCAL_PROJECT_ID_NOT_GUID: 'lint',
95
98
  APIWF_NAME_IS_PATH: 'lint',
96
99
  APIWF_NO_FOLDER: 'lint',
97
100
  APIWF_NO_KEY: 'lowering',
package/dist/decompile.js CHANGED
@@ -912,13 +912,6 @@ function emitStepSpec(node, imp, inputNames, o, graph) {
912
912
  return lifted;
913
913
  switch (node.type) {
914
914
  default:
915
- // A mid-flow connector EVENT wait (`uipath.connector.event.<key>.<event>`
916
- // placed as a step rather than as the start node).
917
- if (node.type.startsWith('uipath.connector.event.')) {
918
- const sub = emitEventSubscription(node, imp, inputNames);
919
- if (sub !== undefined)
920
- return `${imp.use('waitForEvent')}(${sub})`;
921
- }
922
915
  // Every family this SDK AUTHORS is recovered above — a guard test asserts
923
916
  // it (`flow-decompile-families.test.ts`). Reaching here means a node type
924
917
  // the SDK cannot author: a hand-written file, a newer platform family, or
@@ -16,6 +16,7 @@ import type { DefinitionReference, NodeContribution } from './core/node-classes.
16
16
  import type { CheckOpts } from './check.js';
17
17
  import type { SerializeOpts } from './serialize.js';
18
18
  import type { DeclaredExit, NoExits } from './core/step-ports.js';
19
+ import type { EventEmitEnv } from './core/event-subscription.js';
19
20
  /**
20
21
  * What a Flow node type's `serialize` returns: the node's type-owned fields.
21
22
  *
@@ -240,6 +241,12 @@ export interface FlowSerializeContext {
240
241
  emitted(typeName: string, version: string): unknown;
241
242
  /** The connector library serialize was given, for a type that resolves a connector operation. */
242
243
  readonly library: SerializeOpts['library'];
244
+ /**
245
+ * What an event subscription's shared emission (`core/event-subscription.ts`)
246
+ * needs: the library, the bindings, and how a label resolves, with compile's
247
+ * refusals and warnings.
248
+ */
249
+ readonly events: EventEmitEnv;
243
250
  /**
244
251
  * Render a value and every `Expr` nested in its objects and arrays, as a
245
252
  * connector's inputs are. A `lookup()` token is refused: it belongs only where a
@@ -408,6 +415,14 @@ export interface FlowCheckContext {
408
415
  key: string;
409
416
  action?: string;
410
417
  }): void;
418
+ /**
419
+ * Run the rules every event subscription shares, against the library facts
420
+ * check was given (`options`): the subscription's own shape and its
421
+ * resolution against the library. `where` is how the site is named
422
+ * (`waitForEvent "<step>"`). What compile refuses is the serializer's,
423
+ * reported from its run; each rule is reported at its own severity.
424
+ */
425
+ eventSubscription(subscription: unknown, where: string): void;
411
426
  /**
412
427
  * Run the rules every Integration Service operation shares that compile does
413
428
  * not: its lookups and its schema readiness, against the library facts check
@@ -564,6 +579,16 @@ export interface FlowNodeType<Spec> extends NodeType<Spec, FlowNodeContribution,
564
579
  * `errorPort`, so the two rungs give one answer; undefined when it cannot.
565
580
  */
566
581
  checkDefinition?(spec: Spec, options: CheckOpts): unknown;
582
+ /**
583
+ * The node a spec builds, as far as the spec itself names it, for a per-tenant
584
+ * prefix type: its concrete type name, and the version the spec pins. A refused
585
+ * step's stand-in names its node by it, before any definition is in hand (an
586
+ * event wait refused for want of a library still names its event).
587
+ */
588
+ namedNode?(spec: Spec): {
589
+ typeName: string;
590
+ version?: string;
591
+ } | undefined;
567
592
  }
568
593
  /** A Flow node type with several versions. */
569
594
  export type FlowVersionedNodeType<Spec> = VersionedNodeType<Spec, FlowNodeContribution, FlowContexts, FlowNodeType<Spec>>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * A mid-flow wait for an Integration Service event — `waitForEvent(subscription)`,
3
+ * a node type per event: `uipath.connector.event.<connectorKey>.<event>`. The
4
+ * type owns every name under `uipath.connector.event.` that names an event,
5
+ * nested inside the connector operations' `uipath.connector.` prefix, whose
6
+ * narrowing leaves it alone. Per-tenant: the definition is the library's event
7
+ * entry, and so is its version (`versionedByDefinition`).
8
+ *
9
+ * The same node type opens a flow as its start trigger (`onEvent`). That place
10
+ * stays the pipeline's until the triggers port: serialize places the trigger, and
11
+ * decompile reads a root `uipath.connector.event.*` node as one. This type is the
12
+ * node as a step, with an incoming edge.
13
+ *
14
+ * The subscription's emission — `inputs.detail`, the library's resolution, the
15
+ * refusals compile makes with and without a library — is shared with the trigger,
16
+ * so it stays in `core/event-subscription.ts` and is called here through
17
+ * `ctx.events`; the subscription's check rules stay in `core/event-checks.ts` and
18
+ * run through `ctx.eventSubscription`, with their codes.
19
+ *
20
+ * Moved from serialize.ts / check.ts / decompile.ts in Phase 4 of
21
+ * docs/NODE_TYPE_ARCHITECTURE_DESIGN.md.
22
+ */
23
+ import type { ActionSpec } from '../../../core/actions.js';
24
+ import type { FlowVersionedNodeType } from '../../../flow-node-type.js';
25
+ export type EventWaitSpec = Extract<ActionSpec, {
26
+ kind: 'waitEvent';
27
+ }>;
28
+ /** `<connector>.<event>` after the prefix, split on the last dot; undefined for a name with no event. */
29
+ export declare function splitEvent(typeName: string): {
30
+ connector: string;
31
+ event: string;
32
+ } | undefined;
33
+ export declare const eventWaitNodeType: FlowVersionedNodeType<EventWaitSpec>;