@uipath/maestro-builder-sdk 6.17.5 → 6.18.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 (61) hide show
  1. package/dist/bindings-v2.js +15 -17
  2. package/dist/bpmn/bpmn-sdk.js +3 -2
  3. package/dist/bpmn/check.js +3 -2
  4. package/dist/bpmn/decompile.js +123 -137
  5. package/dist/bpmn/intsvc.js +7 -2
  6. package/dist/bpmn/serialize.js +1 -1
  7. package/dist/bpmn/typed-node.js +5 -4
  8. package/dist/case/compile-cli.js +1 -3
  9. package/dist/case/decompile.js +103 -109
  10. package/dist/case/entry-points.js +2 -1
  11. package/dist/case/merge.js +3 -5
  12. package/dist/case/serialize.js +1 -1
  13. package/dist/check.js +26 -10
  14. package/dist/core/ceql.js +5 -2
  15. package/dist/core/connector-inputs.js +4 -4
  16. package/dist/core/event-checks.js +17 -2
  17. package/dist/core/event-filters.d.ts +38 -4
  18. package/dist/core/event-filters.js +63 -7
  19. package/dist/core/expr.d.ts +1 -1
  20. package/dist/core/expr.js +3 -3
  21. package/dist/core/json-file.d.ts +2 -0
  22. package/dist/core/json-file.js +4 -0
  23. package/dist/core/library.js +2 -1
  24. package/dist/core/node-envelope.d.ts +1 -0
  25. package/dist/core/node-envelope.js +1 -1
  26. package/dist/core/own-entry.d.ts +9 -0
  27. package/dist/core/own-entry.js +11 -0
  28. package/dist/core/ts-literal.d.ts +22 -0
  29. package/dist/core/ts-literal.js +37 -0
  30. package/dist/core/typed-raw-node.js +4 -3
  31. package/dist/decompile.js +272 -300
  32. package/dist/merge.js +10 -12
  33. package/dist/registry/compat/pystr.d.ts +2 -0
  34. package/dist/registry/compat/pystr.js +35 -0
  35. package/dist/registry/compat/pyvalue.d.ts +16 -0
  36. package/dist/registry/compat/pyvalue.js +35 -0
  37. package/dist/registry/generators/markdown/pyvalue.d.ts +1 -4
  38. package/dist/registry/generators/markdown/pyvalue.js +2 -30
  39. package/dist/registry/generators/markdown/sections/inputs.js +3 -2
  40. package/dist/registry/generators/type-tree.d.ts +1 -7
  41. package/dist/registry/generators/type-tree.js +5 -22
  42. package/dist/registry/library-build/build.js +6 -6
  43. package/dist/registry/library-build/canonical.js +25 -25
  44. package/dist/registry/library-build/context.js +5 -5
  45. package/dist/registry/library-build/enrich.js +18 -18
  46. package/dist/registry/library-build/events.js +25 -24
  47. package/dist/registry/library-build/generic-expand.js +21 -20
  48. package/dist/registry/library-build/plan.js +8 -8
  49. package/dist/registry/library-build/py.d.ts +3 -10
  50. package/dist/registry/library-build/py.js +9 -52
  51. package/dist/registry/library-build/schema-action.js +6 -6
  52. package/dist/registry/prepare/event.js +2 -2
  53. package/dist/registry/prepare/source-import.js +2 -9
  54. package/dist/registry/prepare/source-labels.js +2 -9
  55. package/dist/registry/prepare/support.d.ts +2 -6
  56. package/dist/registry/prepare/support.js +3 -58
  57. package/dist/registry/pull-cli.js +6 -14
  58. package/dist/registry/search-cli.js +2 -2
  59. package/dist/schema-version.js +0 -1
  60. package/dist/serialize.js +86 -76
  61. package/package.json +2 -2
@@ -48,9 +48,7 @@
48
48
  */
49
49
  import { collectPreservation } from './decompile.js';
50
50
  import { applyPreservation } from './serialize.js';
51
- function clone(value) {
52
- return JSON.parse(JSON.stringify(value));
53
- }
51
+ import { jsonClone } from '../core/json-file.js';
54
52
  /** Stage labels in a document, in order. */
55
53
  function stageLabels(document) {
56
54
  return (document.nodes ?? [])
@@ -102,11 +100,11 @@ function warnOrphanedIdentities(preservation, edited, warn) {
102
100
  export function mergeCase(original, edited, options = {}) {
103
101
  const preservation = collectPreservation(original);
104
102
  if (!preservation)
105
- return clone(edited);
103
+ return jsonClone(edited);
106
104
  if (options.warn)
107
105
  warnOrphanedIdentities(preservation, edited, options.warn);
108
106
  // `applyPreservation` is what `compile` runs with the payload a source
109
107
  // carries. Handing it the ORIGINAL's payload and the EDITED graph is the whole
110
108
  // merge: same operation, different provenance.
111
- return applyPreservation(clone(edited), preservation, { overwrite: true });
109
+ return applyPreservation(jsonClone(edited), preservation, { overwrite: true });
112
110
  }
@@ -1414,7 +1414,7 @@ function emitCaseExit(c, i, ids, resolver) {
1414
1414
  }
1415
1415
  /** One `slaRules[]` entry: the deadline + its escalations. `id` (+ `displayName`
1416
1416
  * when named) is preallocated so a `sla-status-change` rule can reference it. */
1417
- function emitSla(sla, index, count, ids, resolver) {
1417
+ function emitSla(sla, index, count, _ids, resolver) {
1418
1418
  const displayName = sla.displayName ?? (index === count - 1 ? 'Default SLA' : `Conditional SLA ${count - index - 1}`);
1419
1419
  const out = {
1420
1420
  id: resolver.slaId.get(sla),
package/dist/check.js CHANGED
@@ -25,6 +25,7 @@ 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';
27
27
  import { planFlowGlobals, planSharedInputs, sharedInputRoots } from './flow-variables.js';
28
+ import { ownEntry } from './core/own-entry.js';
28
29
  export function check(built, opts = {}) {
29
30
  let diags = [];
30
31
  const uses = collectConnectionUses(() => { diags = checkFlow(built, [], opts); });
@@ -1094,7 +1095,7 @@ function checkFlow(built, ancestors, opts = {}) {
1094
1095
  // per-family rules below, because the envelope carries the same fields
1095
1096
  // whatever the node publishes on its success path; which VARIABLE holds it
1096
1097
  // is a per-family runtime boundary `serialize` rewrites, not something the
1097
- // author writes (`errorEnvelopeVar`).
1098
+ // author writes (`ERROR_ENVELOPE_VAR`).
1098
1099
  if (field === 'error') {
1099
1100
  const tail = rest ? rest.slice(1).split('.')[0] : undefined;
1100
1101
  if (tail !== undefined)
@@ -2394,14 +2395,16 @@ function checkVoice(step, kind, inputs, diags) {
2394
2395
  'gpt-realtime-2': ['alloy', 'ash', 'ballad', 'coral', 'echo', 'sage', 'shimmer', 'verse'],
2395
2396
  };
2396
2397
  const model = typeof v.model === 'string' ? v.model : undefined;
2397
- if (model !== undefined && !(model in PERSONAS)) {
2398
+ // An own-key read: `in` also matched 'toString', and `PERSONAS[model].includes` then threw.
2399
+ const personas = model !== undefined ? ownEntry(PERSONAS, model) : undefined;
2400
+ if (model !== undefined && personas === undefined) {
2398
2401
  at('VOICE_MODEL_UNKNOWN', `voiceAgent step "${step}" names the voice model ${JSON.stringify(model)}. The platform's catalog `
2399
2402
  + `today is ${Object.keys(PERSONAS).map((m) => `'${m}'`).join(' and ')} — a voice model, not one of `
2400
2403
  + `the text models an inline agent takes.`);
2401
2404
  }
2402
- else if (typeof v.persona === 'string' && model !== undefined && !PERSONAS[model].includes(v.persona)) {
2405
+ else if (typeof v.persona === 'string' && personas !== undefined && !personas.includes(v.persona)) {
2403
2406
  at('VOICE_PERSONA_UNKNOWN', `voiceAgent step "${step}"'s persona ${JSON.stringify(v.persona)} is not one of `
2404
- + `${JSON.stringify(model)}'s voices (${PERSONAS[model].join(', ')}). A persona belongs to its model.`);
2407
+ + `${JSON.stringify(model)}'s voices (${personas.join(', ')}). A persona belongs to its model.`);
2405
2408
  }
2406
2409
  }
2407
2410
  }
@@ -2813,7 +2816,7 @@ function checkAgenticProcess(step, inputs, diags) {
2813
2816
  + `\`form: 'flow'\` if the sibling is a Flow.`, `location: 'published', folderPath: 'Shared/<folder>'`);
2814
2817
  }
2815
2818
  if (inSolution && blank(inputs?.projectId)) {
2816
- const folder = (AGENTIC_PROCESS_FORMS[inputs?.form ?? 'bpmn'] ?? AGENTIC_PROCESS_FORMS.bpmn).solutionFolder;
2819
+ const folder = (ownEntry(AGENTIC_PROCESS_FORMS, inputs?.form ?? 'bpmn') ?? AGENTIC_PROCESS_FORMS.bpmn).solutionFolder;
2817
2820
  at('AGENTIC_LOCAL_NO_PROJECT_ID', `In-solution agentic process step "${step}" has no \`projectId\`. Read \`resource.projectKey\` from `
2818
2821
  + `\`resources/solution_folder/process/${folder}/<project>.json\`.`, `projectId: '<resource.projectKey>'`);
2819
2822
  }
@@ -3605,6 +3608,13 @@ function checkInlineAgent(step, inputs, diags, opts = {}) {
3605
3608
  at('INLINE_AGENT_ESCALATION_NO_FIELDS', `Inline agent step "${step}"'s quick-form escalation declares no \`fields\` — the reviewer gets an `
3606
3609
  + `empty form.`);
3607
3610
  }
3611
+ for (const f of Array.isArray(e.fields) ? e.fields : []) {
3612
+ if (!FORM_FIELD_TYPES.includes(String(f?.type))) {
3613
+ at('INLINE_AGENT_ESCALATION_FIELD_TYPE_UNKNOWN', `Inline agent step "${step}"'s quick-form escalation gives the field "${String(f?.id ?? '?')}" the type `
3614
+ + `${JSON.stringify(f?.type)}, which the form does not recognize — it would be published as a plain `
3615
+ + `string. Valid: ${FORM_FIELD_TYPES.join(', ')}.`, formFieldTypeHint(f?.type));
3616
+ }
3617
+ }
3608
3618
  continue;
3609
3619
  }
3610
3620
  if (e?.fields !== undefined) {
@@ -4440,8 +4450,15 @@ function checkHttp(step, inputs, diags) {
4440
4450
  }
4441
4451
  }
4442
4452
  }
4453
+ /** Every FormFieldType, once; `Record<FormFieldType, …>` makes tsc refuse a missing or extra one. */
4454
+ const FORM_FIELD_TYPE_SET = { text: true, number: true, boolean: true, 'date-time': true };
4443
4455
  /** Form-field types the platform recognizes; anything else normalizes to text. */
4444
- const FORM_FIELD_TYPES = ['text', 'number', 'boolean', 'date-time'];
4456
+ const FORM_FIELD_TYPES = Object.keys(FORM_FIELD_TYPE_SET);
4457
+ /** The `suggestion` for an unknown form-field type: the nearest valid one, else `type: 'text'`. */
4458
+ function formFieldTypeHint(type) {
4459
+ const nearest = nearestName(String(type), FORM_FIELD_TYPES);
4460
+ return nearest !== undefined ? `type: '${nearest}'` : `type: 'text'`;
4461
+ }
4445
4462
  /** Names the human task's own result already uses — an answer cannot claim one. */
4446
4463
  const RESERVED_ANSWER_IDS = new Set(['output', 'status', 'Action']);
4447
4464
  /**
@@ -4717,11 +4734,10 @@ function checkHitl(step, inputs, diags) {
4717
4734
  return;
4718
4735
  }
4719
4736
  if (!FORM_FIELD_TYPES.includes(String(f?.type))) {
4720
- const nearest = nearestName(String(f?.type), FORM_FIELD_TYPES);
4721
4737
  at('HITL_FIELD_TYPE_UNKNOWN', `Human task "${step}" gives the form field "${id}" the type ${JSON.stringify(f?.type)}, which `
4722
4738
  + `the platform does not recognize — it normalizes an unknown type to text, so the reviewer `
4723
4739
  + `gets a text box and the value comes back as a string. Valid: `
4724
- + `${FORM_FIELD_TYPES.join(', ')}.`, nearest !== undefined ? `type: '${nearest}'` : `type: 'text'`);
4740
+ + `${FORM_FIELD_TYPES.join(', ')}.`, formFieldTypeHint(f?.type));
4725
4741
  }
4726
4742
  if (isAnswerField(f))
4727
4743
  answers++; // an `output`, or an `inOut` whose reviewed value comes back
@@ -5715,7 +5731,7 @@ function checkStepRefs(built, diags) {
5715
5731
  }
5716
5732
  };
5717
5733
  collect(built.steps, undefined, '', undefined);
5718
- for (const { target, port, from, after, scope, arm } of found) {
5734
+ for (const { target, from, after, scope, arm } of found) {
5719
5735
  const where = from ? `after "${from}"` : 'at the start of the flow';
5720
5736
  const say = (level, code, message, suggestion) => {
5721
5737
  diags.push({ level, code, message, step: from ?? 'stepToRef', ...(suggestion ? { suggestion } : {}) });
@@ -5781,7 +5797,7 @@ function checkStepRefs(built, diags) {
5781
5797
  * checks and what it does not. It checks the FIELD NAME against the envelope's
5782
5798
  * own fields, which are the same on every family. It does NOT police the prefix:
5783
5799
  * `.error` vs `.output` is a per-family runtime boundary, measured per family and
5784
- * rewritten by `serialize` (`errorEnvelopeVar`), so an author writing `.error`
5800
+ * rewritten by `serialize` (`ERROR_ENVELOPE_VAR`), so an author writing `.error`
5785
5801
  * is right by construction and an author hand-writing `.output` is caught by
5786
5802
  * ERROR_ENVELOPE_VIA_OUTPUT instead.
5787
5803
  *
package/dist/core/ceql.js CHANGED
@@ -33,6 +33,7 @@
33
33
  * rather than a wrong one.
34
34
  */
35
35
  import { stableId } from './stable-id.js';
36
+ import { ownEntry } from './own-entry.js';
36
37
  /** Raised when an expression is not CEQL this parser can represent. */
37
38
  export class CeqlParseError extends Error {
38
39
  expression;
@@ -128,9 +129,11 @@ function readOperator(s) {
128
129
  }
129
130
  }
130
131
  const alias = /^([A-Za-z]+)\b/.exec(s.rest);
131
- if (alias && ODATA_ALIASES[alias[1].toLowerCase()]) {
132
+ const word = alias?.[1].toLowerCase();
133
+ const odata = word !== undefined ? ownEntry(ODATA_ALIASES, word) : undefined;
134
+ if (alias && odata) {
132
135
  const wrong = alias[1];
133
- throw new CeqlParseError(`\`${wrong}\` is an OData operator; CEQL uses \`${ODATA_ALIASES[wrong.toLowerCase()]}\`.`, s.src);
136
+ throw new CeqlParseError(`\`${wrong}\` is an OData operator; CEQL uses \`${odata}\`.`, s.src);
134
137
  }
135
138
  for (const [text, op] of SYMBOL_OPERATORS) {
136
139
  if (s.src.startsWith(text, s.pos)) {
@@ -28,6 +28,8 @@ import { editDistance } from './edit-distance.js';
28
28
  /** Flow's dialect, re-exported so `serialize.ts` need not reach past this module. */
29
29
  export { FLOW_EXPR_DIALECT } from './connector-checks.js';
30
30
  import { FLOW_EXPR_DIALECT as FLOW_DIALECT } from './connector-checks.js';
31
+ import { ownEntry } from './own-entry.js';
32
+ import { isRecord } from './node-envelope.js';
31
33
  /**
32
34
  * Validate a connector's activity inputs against its declared field list and
33
35
  * return them ROUTED to the wire slots the platform reads. Catches the three
@@ -155,7 +157,8 @@ export function buildConnectorInputs(rc, inputs, opts) {
155
157
  partValues.set(f.name, render(nested.value));
156
158
  continue;
157
159
  }
158
- const slot = SLOT_FOR_LOCATION[f.location ?? 'body'] ?? 'bodyParameters';
160
+ const location = f.location ?? 'body';
161
+ const slot = ownEntry(SLOT_FOR_LOCATION, location) ?? 'bodyParameters';
159
162
  routed[slot][f.name] = render(nested.value);
160
163
  }
161
164
  if (errs.length) {
@@ -318,9 +321,6 @@ function place(target, parts, index, value) {
318
321
  target[part.key] = next;
319
322
  place(next, parts, index + 1, value);
320
323
  }
321
- function isRecord(value) {
322
- return typeof value === 'object' && value !== null && !Array.isArray(value);
323
- }
324
324
  function connectorPath(name) {
325
325
  return name.split('.').map((raw) => ({
326
326
  key: raw.endsWith('[*]') ? raw.slice(0, -3) : raw,
@@ -20,7 +20,7 @@
20
20
  */
21
21
  import { isLookupToken, resolvedValue, unresolvedLookupMessage, } from './lookups.js';
22
22
  import { prepareCommand } from './cli-spelling.js';
23
- import { EVENT_FILTER_OPERATORS, readEventFilter, eventFilterProblem } from './event-filters.js';
23
+ import { EVENT_FILTER_OPERATORS, readEventFilter, eventFilterProblem, jmesStringProblem } from './event-filters.js';
24
24
  import { EVENT_BYOA_UNVERIFIED, byoaUnverified } from './byoa.js';
25
25
  /**
26
26
  * Ask the library what event parameters a subscription's operation declares.
@@ -228,6 +228,18 @@ export function checkEventSubscription(subscription, where, at, warn, eventParam
228
228
  const scopeObject = scopeEntries.find(([k]) => /^(objectName|entityName|object)$/i.test(k))?.[1];
229
229
  const object = typeof sub.object === 'string' && sub.object !== '' ? sub.object : undefined;
230
230
  const say = warn ?? at;
231
+ // A scope value is folded into the match expression as `k == '<value>'`, so it
232
+ // meets the same JMESPath backslash ambiguity a filter value does. A nested
233
+ // `where` (a dotted parameter written as an object) is checked leaf by leaf.
234
+ const scopeStrings = (value, path) => typeof value === 'string' ? [[path, value]]
235
+ : value && typeof value === 'object' && !Array.isArray(value)
236
+ ? Object.entries(value).flatMap(([k, v]) => scopeStrings(v, `${path}.${k}`))
237
+ : [];
238
+ for (const [path, value] of scopeEntries.flatMap(([k, v]) => scopeStrings(v, k))) {
239
+ const ambiguous = jmesStringProblem(value);
240
+ if (ambiguous)
241
+ at('EVENT_FILTER_BACKSLASH', `${where}'s \`where.${path}\`: ${ambiguous}`);
242
+ }
231
243
  // The OBJECT half of the contract. A GENERIC event (`record-created` on Data
232
244
  // Fabric, Salesforce, ServiceNow, Jira, …) fires for ONE object of the connection
233
245
  // and its library entry names none — so `object` is REQUIRED, and its absence is
@@ -313,8 +325,11 @@ export function checkEventSubscription(subscription, where, at, warn, eventParam
313
325
  at('EVENT_FILTER_NO_VALUE', `${where}'s filters[${i}] gives \`${keys[0]}\` a value that is not a string, number or boolean.`);
314
326
  }
315
327
  else {
328
+ const ambiguous = typeof read.value === 'string' ? jmesStringProblem(read.value) : undefined;
316
329
  const problem = eventFilterProblem(read);
317
- if (problem)
330
+ if (ambiguous)
331
+ at('EVENT_FILTER_BACKSLASH', `${where}'s filters[${i}] on \`${read.field}\`: ${ambiguous}`);
332
+ else if (problem)
318
333
  at('EVENT_FILTER_BAD_VALUE', `${where}'s filters[${i}] on \`${read.field}\`: ${problem}`);
319
334
  }
320
335
  });
@@ -22,7 +22,10 @@
22
22
  * | … and `lessThanOrEqual` / `greaterThan` / `greaterThanOrEqual` likewise. |
23
23
  *
24
24
  * Literals follow the designer's `_formatLiteral`: a string is single-quoted, a
25
- * number or boolean is a JMESPath backtick literal. A DATE is the one case the
25
+ * number or boolean is a JMESPath backtick literal. A plain string keeps the
26
+ * designer's bytes; a `'` inside it is escaped (`jmesString`), where the designer
27
+ * would emit broken — or injectable — JMESPath, and a backslash no two engines read
28
+ * alike is refused. A DATE is the one case the
26
29
  * designer decides from the field's type (`DateTimeOffset` → `to_number(...)` on
27
30
  * both sides, because JMESPath compares only numbers). Offline the SDK has no
28
31
  * field types for a connection-scoped object, so the VALUE decides: an ISO-8601
@@ -97,11 +100,42 @@ export declare function readEventFilter(leaf: unknown): {
97
100
  };
98
101
  export declare function isIsoDateString(value: unknown): value is string;
99
102
  /**
100
- * Why a filter cannot be lowered, or `undefined` when it can. A comparison on a
101
- * plain string is the one refused shape: JMESPath `<`/`>` compare numbers only,
102
- * so the emitted expression would match nothing and nobody would be told.
103
+ * Why a filter cannot be lowered, or `undefined` when it can. Two shapes are
104
+ * refused. A comparison on a plain string: JMESPath `<`/`>` compare numbers only,
105
+ * so the emitted expression would match nothing and nobody would be told. A string
106
+ * value with a backslash the engines read differently (`jmesStringProblem`).
103
107
  */
104
108
  export declare function eventFilterProblem(read: ReadEventFilter): string | undefined;
109
+ /**
110
+ * A JMESPath raw-string literal: `'…'` with each `'` written `\'`. A backslash is
111
+ * kept as it is, so a value without `'` keeps the designer's bytes.
112
+ *
113
+ * The designer's `_formatLiteral` (@uipath/maestro-sdk JmesHelper) splices the value
114
+ * unescaped, so `O'Brien` ends the literal early and the rest of the value is parsed
115
+ * as expression. Escaping `'` closes that.
116
+ *
117
+ * A backslash is the one character the readers disagree on. The specification's
118
+ * raw-string grammar reads `\\` as one backslash and keeps any other escaped
119
+ * character with its backslash (`preserved-escape`). The reference jmespath.js/py
120
+ * lexers keep `\\` as two characters. So `Finance\Q3` and `C:\tmp` read the same
121
+ * everywhere when emitted unchanged, while doubling them would make them miss on a
122
+ * reference engine. Which reader Integration Service runs is unconfirmed, so a value
123
+ * whose backslash HAS no common spelling is refused (`jmesStringProblem`): one before
124
+ * another `\`, before a `'`, or at the end. With those refused, an emitted `\'` is
125
+ * always an escaped quote under every reader, which is what keeps the literal from
126
+ * closing early (`x\' || `true` || '` is refused, not emitted). jmespath.js still
127
+ * unescapes only the first `\'`, so a value with two quotes reads differently there.
128
+ *
129
+ * This is the one spelling for Flow filters, Flow scope clauses and BPMN; FB011 holds
130
+ * every JMESPath string splice to it. It throws on a refused value: `check` reports
131
+ * one first (`EVENT_FILTER_BACKSLASH`), and serialize refuses it with the node's name.
132
+ */
133
+ export declare function jmesString(value: string): string;
134
+ /**
135
+ * Why `value` has no JMESPath raw-string spelling every reader agrees on, or
136
+ * `undefined` when it has one. See `jmesString`.
137
+ */
138
+ export declare function jmesStringProblem(value: string): string | undefined;
105
139
  /**
106
140
  * The JMESPath condition for one filter — the grammar the designer emits,
107
141
  * without the outer parentheses (the caller groups conditions).
@@ -22,7 +22,10 @@
22
22
  * | … and `lessThanOrEqual` / `greaterThan` / `greaterThanOrEqual` likewise. |
23
23
  *
24
24
  * Literals follow the designer's `_formatLiteral`: a string is single-quoted, a
25
- * number or boolean is a JMESPath backtick literal. A DATE is the one case the
25
+ * number or boolean is a JMESPath backtick literal. A plain string keeps the
26
+ * designer's bytes; a `'` inside it is escaped (`jmesString`), where the designer
27
+ * would emit broken — or injectable — JMESPath, and a backslash no two engines read
28
+ * alike is refused. A DATE is the one case the
26
29
  * designer decides from the field's type (`DateTimeOffset` → `to_number(...)` on
27
30
  * both sides, because JMESPath compares only numbers). Offline the SDK has no
28
31
  * field types for a connection-scoped object, so the VALUE decides: an ISO-8601
@@ -31,6 +34,7 @@
31
34
  * strings is null), so `check` refuses it rather than emit a filter that never
32
35
  * matches.
33
36
  */
37
+ import { ownEntry } from './own-entry.js';
34
38
  /** The designer's operator vocabulary this SDK emits, keyed by the authored key. */
35
39
  export const EVENT_FILTER_OPERATORS = {
36
40
  contains: { designer: 'Contains', kind: 'function', jmes: 'contains' },
@@ -82,12 +86,18 @@ export function isIsoDateString(value) {
82
86
  return typeof value === 'string' && ISO_DATE.test(value);
83
87
  }
84
88
  /**
85
- * Why a filter cannot be lowered, or `undefined` when it can. A comparison on a
86
- * plain string is the one refused shape: JMESPath `<`/`>` compare numbers only,
87
- * so the emitted expression would match nothing and nobody would be told.
89
+ * Why a filter cannot be lowered, or `undefined` when it can. Two shapes are
90
+ * refused. A comparison on a plain string: JMESPath `<`/`>` compare numbers only,
91
+ * so the emitted expression would match nothing and nobody would be told. A string
92
+ * value with a backslash the engines read differently (`jmesStringProblem`).
88
93
  */
89
94
  export function eventFilterProblem(read) {
90
95
  const op = EVENT_FILTER_OPERATORS[read.operator];
96
+ if (typeof read.value === 'string') {
97
+ const ambiguous = jmesStringProblem(read.value);
98
+ if (ambiguous)
99
+ return ambiguous;
100
+ }
91
101
  if (op.kind === 'function' && typeof read.value !== 'string') {
92
102
  return `\`${read.operator}\` is a text test, so its value must be a string — got ${JSON.stringify(read.value)}.`;
93
103
  }
@@ -99,9 +109,54 @@ export function eventFilterProblem(read) {
99
109
  }
100
110
  return undefined;
101
111
  }
112
+ /**
113
+ * A JMESPath raw-string literal: `'…'` with each `'` written `\'`. A backslash is
114
+ * kept as it is, so a value without `'` keeps the designer's bytes.
115
+ *
116
+ * The designer's `_formatLiteral` (@uipath/maestro-sdk JmesHelper) splices the value
117
+ * unescaped, so `O'Brien` ends the literal early and the rest of the value is parsed
118
+ * as expression. Escaping `'` closes that.
119
+ *
120
+ * A backslash is the one character the readers disagree on. The specification's
121
+ * raw-string grammar reads `\\` as one backslash and keeps any other escaped
122
+ * character with its backslash (`preserved-escape`). The reference jmespath.js/py
123
+ * lexers keep `\\` as two characters. So `Finance\Q3` and `C:\tmp` read the same
124
+ * everywhere when emitted unchanged, while doubling them would make them miss on a
125
+ * reference engine. Which reader Integration Service runs is unconfirmed, so a value
126
+ * whose backslash HAS no common spelling is refused (`jmesStringProblem`): one before
127
+ * another `\`, before a `'`, or at the end. With those refused, an emitted `\'` is
128
+ * always an escaped quote under every reader, which is what keeps the literal from
129
+ * closing early (`x\' || `true` || '` is refused, not emitted). jmespath.js still
130
+ * unescapes only the first `\'`, so a value with two quotes reads differently there.
131
+ *
132
+ * This is the one spelling for Flow filters, Flow scope clauses and BPMN; FB011 holds
133
+ * every JMESPath string splice to it. It throws on a refused value: `check` reports
134
+ * one first (`EVENT_FILTER_BACKSLASH`), and serialize refuses it with the node's name.
135
+ */
136
+ export function jmesString(value) {
137
+ const problem = jmesStringProblem(value);
138
+ if (problem)
139
+ throw new Error(`jmesString: ${problem}`);
140
+ return `'${value.replace(/'/g, "\\'")}'`;
141
+ }
142
+ /** A backslash whose raw-string reading depends on the engine: before `\` or `'`, or last. */
143
+ const AMBIGUOUS_BACKSLASH = /\\(?=[\\']|$)/;
144
+ /**
145
+ * Why `value` has no JMESPath raw-string spelling every reader agrees on, or
146
+ * `undefined` when it has one. See `jmesString`.
147
+ */
148
+ export function jmesStringProblem(value) {
149
+ if (!AMBIGUOUS_BACKSLASH.test(value))
150
+ return undefined;
151
+ return `${JSON.stringify(value)} has a backslash at its end or before another \`\\\` or a \`'\`. JMESPath engines `
152
+ + `disagree on how to read that: the specification reads \`\\\\\` as one backslash, while the reference `
153
+ + `jmespath.js/py lexers keep two. Which one Integration Service runs is unconfirmed, so the subscription could `
154
+ + `silently never match. Remove that backslash, or narrow on a substring without it (\`contains\`). A backslash `
155
+ + `before any other character (\`C:\\tmp\`, \`Finance\\Q3\`) reads the same everywhere and is fine.`;
156
+ }
102
157
  /** The JMESPath literal for one filter value, per the designer's `_formatLiteral`. */
103
158
  function jmesLiteral(value) {
104
- return typeof value === 'string' ? `'${value}'` : `\`${JSON.stringify(value)}\``;
159
+ return typeof value === 'string' ? jmesString(value) : `\`${JSON.stringify(value)}\``;
105
160
  }
106
161
  /**
107
162
  * The JMESPath condition for one filter — the grammar the designer emits,
@@ -112,7 +167,7 @@ export function eventFilterJmes(read) {
112
167
  if (op.kind === 'function')
113
168
  return `${op.jmes}(${read.field},${jmesLiteral(read.value)})`;
114
169
  if (isIsoDateString(read.value))
115
- return `to_number(${read.field})${op.jmes}to_number('${read.value}')`;
170
+ return `to_number(${read.field})${op.jmes}to_number(${jmesString(read.value)})`;
116
171
  return `${read.field}${op.jmes}${jmesLiteral(read.value)}`;
117
172
  }
118
173
  /** The designer's tree leaf for one filter — `uiId` is the caller's (it is what keeps a rebuild byte-identical). */
@@ -128,7 +183,8 @@ export function eventFilterTreeLeaf(read) {
128
183
  * Returns `undefined` for an operator this SDK does not author (`In`, `IsEmpty`, …).
129
184
  */
130
185
  export function eventFilterFromTreeLeaf(leaf) {
131
- const key = DESIGNER_TO_KEY[String(leaf?.operator ?? '')];
186
+ const op = String(leaf?.operator ?? '');
187
+ const key = ownEntry(DESIGNER_TO_KEY, op);
132
188
  if (!key || typeof leaf?.id !== 'string')
133
189
  return undefined;
134
190
  const raw = leaf.value?.value;
@@ -196,7 +196,7 @@ export type ErrorEnvelopeField = 'code' | 'message' | 'detail' | 'category' | 's
196
196
  *
197
197
  * So the author writes one shape, `.error.<field>`, and `serialize` rewrites it
198
198
  * to `.output.<field>` for the one family that publishes it there
199
- * (`errorEnvelopeVar` in `serialize.ts`). Do not hand-write either prefix: which
199
+ * (`ERROR_ENVELOPE_VAR` in `serialize.ts`). Do not hand-write either prefix: which
200
200
  * one is right is not a property of the expression, it is a property of the node
201
201
  * the step happens to be, and only the builder knows that.
202
202
  *
package/dist/core/expr.js CHANGED
@@ -209,7 +209,7 @@ export function ran(step) {
209
209
  *
210
210
  * So the author writes one shape, `.error.<field>`, and `serialize` rewrites it
211
211
  * to `.output.<field>` for the one family that publishes it there
212
- * (`errorEnvelopeVar` in `serialize.ts`). Do not hand-write either prefix: which
212
+ * (`ERROR_ENVELOPE_VAR` in `serialize.ts`). Do not hand-write either prefix: which
213
213
  * one is right is not a property of the expression, it is a property of the node
214
214
  * the step happens to be, and only the builder knows that.
215
215
  *
@@ -258,7 +258,7 @@ export function ran(step) {
258
258
  */
259
259
  export function err(step, field) {
260
260
  // ONE shape for every family. `serialize` rewrites the prefix for the families
261
- // that publish the envelope elsewhere — see `errorEnvelopeVar`.
261
+ // that publish the envelope elsewhere — see `ERROR_ENVELOPE_VAR`.
262
262
  return new Expr(`$vars.${step}.error${field ? '.' + field : ''}`);
263
263
  }
264
264
  /**
@@ -414,7 +414,7 @@ function segmentEscapes(raw, cooked) {
414
414
  let value;
415
415
  if (next === '\n' || next === '\u2028' || next === '\u2029')
416
416
  value = ''; // line continuation
417
- else if (next in SINGLE_ESCAPES)
417
+ else if (Object.hasOwn(SINGLE_ESCAPES, next))
418
418
  value = SINGLE_ESCAPES[next];
419
419
  else if (next === '0' && !/[0-9]/.test(raw[i + 2] ?? ''))
420
420
  value = '\0';
@@ -1 +1,3 @@
1
1
  export declare function writeJsonFile(path: string, value: unknown): void;
2
+ /** `JSON.parse(JSON.stringify(value))`: a deep copy of a JSON value that drops `undefined` and functions, the semantics every copy relied on. */
3
+ export declare function jsonClone<T>(value: T): T;
@@ -10,3 +10,7 @@ import { writeFileSync } from 'node:fs';
10
10
  export function writeJsonFile(path, value) {
11
11
  writeFileSync(path, JSON.stringify(value, null, 2) + '\n');
12
12
  }
13
+ /** `JSON.parse(JSON.stringify(value))`: a deep copy of a JSON value that drops `undefined` and functions, the semantics every copy relied on. */
14
+ export function jsonClone(value) {
15
+ return JSON.parse(JSON.stringify(value));
16
+ }
@@ -14,6 +14,7 @@ import { editDistance } from './edit-distance.js';
14
14
  import { loadListEnvelopes, lookupListEnvelope } from './list-envelope.js';
15
15
  import { lookupSpecOf } from './lookups.js';
16
16
  import { loadByoaLedger, loadInvariantCollections, loadLookupResolutions } from './lookup-store.js';
17
+ import { ownEntry } from './own-entry.js';
17
18
  /**
18
19
  * The refusal {@link Library.resolve} throws, with a `code` for each reason.
19
20
  *
@@ -1265,7 +1266,7 @@ function readFields(v1def, entry) {
1265
1266
  const out = (v1def?.inputDefinition?.fields ?? []).map((f) => ({ ...f, location: 'body' }));
1266
1267
  for (const p of params) {
1267
1268
  const name = typeof p?.name === 'string' ? p.name : undefined;
1268
- const location = PARAM_LOCATIONS[p?.type];
1269
+ const location = typeof p?.type === 'string' ? ownEntry(PARAM_LOCATIONS, p.type) : undefined;
1269
1270
  if (!name || !location)
1270
1271
  continue;
1271
1272
  const field = {
@@ -24,6 +24,7 @@
24
24
  * so a missing field is an SDK emit bug caught before a file is written — on
25
25
  * every path, `compile --no-check` and direct `serialize()` included.
26
26
  */
27
+ export declare const isRecord: (v: unknown) => v is Record<string, unknown>;
27
28
  /** Every way `node` breaks the envelope, as `<field>: <problem>` strings. Empty when it conforms. */
28
29
  export declare function nodeEnvelopeProblems(node: unknown): string[];
29
30
  /**
@@ -25,7 +25,7 @@
25
25
  * every path, `compile --no-check` and direct `serialize()` included.
26
26
  */
27
27
  const TYPE_VERSION_RE = /^\d+\.\d+(\.\d+)?$/;
28
- const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
28
+ export const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
29
29
  const isNonEmptyString = (v) => typeof v === 'string' && v.length > 0;
30
30
  /** Every way `node` breaks the envelope, as `<field>: <problem>` strings. Empty when it conforms. */
31
31
  export function nodeEnvelopeProblems(node) {
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A lookup table's OWN entry for `key`, or `undefined` — never an Object.prototype
3
+ * member. `TABLE[key]` on a plain object answers `toString`, `constructor` and
4
+ * `__proto__` with inherited functions, and `key in TABLE` says they are present;
5
+ * an untrusted key (a node type from a file, a field type from a registry) then
6
+ * reads a function where the caller expects data. FB012 refuses the unguarded
7
+ * forms; this is the guarded read every table shares. FB005 holds the name here.
8
+ */
9
+ export declare function ownEntry<V>(table: Readonly<Record<string, V>>, key: string): V | undefined;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * A lookup table's OWN entry for `key`, or `undefined` — never an Object.prototype
3
+ * member. `TABLE[key]` on a plain object answers `toString`, `constructor` and
4
+ * `__proto__` with inherited functions, and `key in TABLE` says they are present;
5
+ * an untrusted key (a node type from a file, a field type from a registry) then
6
+ * reads a function where the caller expects data. FB012 refuses the unguarded
7
+ * forms; this is the guarded read every table shares. FB005 holds the name here.
8
+ */
9
+ export function ownEntry(table, key) {
10
+ return Object.hasOwn(table, key) ? table[key] : undefined;
11
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * A value as an inert TypeScript literal in decompiled source.
3
+ *
4
+ * `JSON.stringify` escapes quotes, backslashes and control characters, so an
5
+ * untrusted `.flow`/`.bpmn`/caseplan value — a label, an expression, a URL — can
6
+ * never break out of the literal and become code in a file `compile` later
7
+ * executes. U+2028/U+2029 are escaped too: legal in JSON, illegal in pre-ES2019
8
+ * string literals. The three decompilers each carried a `str()`/`literal()` copy
9
+ * and two had drifted (only some escaped U+2028 in `literal`; one coerced null to
10
+ * `''`). FB005 holds `str`/`literal` to this file, and FB011 holds a decompiler's
11
+ * quoted call argument to these functions. `tsNumber` is the unquoted sibling for
12
+ * numbers: an untrusted `count` spliced with `String()` became code.
13
+ *
14
+ * A missing value is refused, as the decompilers' old `str()` did; the one place
15
+ * that writes the expression `undefined` (Flow's `emitVal`) says so itself.
16
+ */
17
+ /** A TS expression for `value`: its JSON with U+2028/U+2029 escaped. Throws for `undefined`, and for a value JSON cannot carry (a function or symbol). */
18
+ export declare function tsLiteral(value: unknown): string;
19
+ /** `value` as a TS string literal, null and undefined as `""` (the BPMN decompiler's text fields). */
20
+ export declare function tsText(value: unknown): string;
21
+ /** `value` as a TS number literal: `Number(value)`, refused unless finite — a caseplan or `.flow` number is untrusted text until checked. */
22
+ export declare function tsNumber(value: unknown): string;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * A value as an inert TypeScript literal in decompiled source.
3
+ *
4
+ * `JSON.stringify` escapes quotes, backslashes and control characters, so an
5
+ * untrusted `.flow`/`.bpmn`/caseplan value — a label, an expression, a URL — can
6
+ * never break out of the literal and become code in a file `compile` later
7
+ * executes. U+2028/U+2029 are escaped too: legal in JSON, illegal in pre-ES2019
8
+ * string literals. The three decompilers each carried a `str()`/`literal()` copy
9
+ * and two had drifted (only some escaped U+2028 in `literal`; one coerced null to
10
+ * `''`). FB005 holds `str`/`literal` to this file, and FB011 holds a decompiler's
11
+ * quoted call argument to these functions. `tsNumber` is the unquoted sibling for
12
+ * numbers: an untrusted `count` spliced with `String()` became code.
13
+ *
14
+ * A missing value is refused, as the decompilers' old `str()` did; the one place
15
+ * that writes the expression `undefined` (Flow's `emitVal`) says so itself.
16
+ */
17
+ const LINE_SEPARATORS = /[\u2028\u2029]/g;
18
+ /** A TS expression for `value`: its JSON with U+2028/U+2029 escaped. Throws for `undefined`, and for a value JSON cannot carry (a function or symbol). */
19
+ export function tsLiteral(value) {
20
+ if (value === undefined)
21
+ throw new Error('cannot write undefined as a TypeScript literal.');
22
+ const json = JSON.stringify(value);
23
+ if (json === undefined)
24
+ throw new Error(`cannot write a ${typeof value} as a TypeScript literal.`);
25
+ return json.replace(LINE_SEPARATORS, (c) => `\\u${c.charCodeAt(0).toString(16)}`);
26
+ }
27
+ /** `value` as a TS string literal, null and undefined as `""` (the BPMN decompiler's text fields). */
28
+ export function tsText(value) {
29
+ return tsLiteral(String(value ?? ''));
30
+ }
31
+ /** `value` as a TS number literal: `Number(value)`, refused unless finite — a caseplan or `.flow` number is untrusted text until checked. */
32
+ export function tsNumber(value) {
33
+ const n = Number(value);
34
+ if (!Number.isFinite(n))
35
+ throw new Error(`decompile: expected a number, got ${JSON.stringify(value)}.`);
36
+ return String(n);
37
+ }
@@ -3,6 +3,7 @@
3
3
  * the SDK has a typed factory for, and the Data Fabric input keys it may carry.
4
4
  * Not re-exported from the package entry point.
5
5
  */
6
+ import { ownEntry } from './own-entry.js';
6
7
  /**
7
8
  * Every node type the SDK authors as a STEP through a typed factory, with the
8
9
  * factory to use. `raw-node-typed-family.test.ts` pins the key set to the
@@ -135,7 +136,7 @@ const DATAFABRIC_HINT = ' The factory writes the query and write keys: `filters`
135
136
  export function typedFamilyRawNodeWarning(nodeType) {
136
137
  if (typeof nodeType !== 'string')
137
138
  return undefined;
138
- const entry = TYPED_FACTORIES[nodeType];
139
+ const entry = ownEntry(TYPED_FACTORIES, nodeType);
139
140
  if (entry === undefined)
140
141
  return undefined;
141
142
  const hint = nodeType.startsWith('core.datafabric.') ? DATAFABRIC_HINT : '';
@@ -182,9 +183,9 @@ export function dataFabricUnknownInputs(nodeType, version, inputs) {
182
183
  }
183
184
  if (unknown.length === 0)
184
185
  return undefined;
185
- const bundled = TYPED_FACTORIES[nodeType]?.bundled.includes(version) ?? false;
186
+ const bundled = ownEntry(TYPED_FACTORIES, nodeType)?.bundled.includes(version) ?? false;
186
187
  const named = unknown.map((k) => `\`${k}\``).join(', ');
187
- const factory = TYPED_FACTORIES[nodeType]?.factory;
188
+ const factory = ownEntry(TYPED_FACTORIES, nodeType)?.factory;
188
189
  const base = `${nodeType}@${version} carries ${named}, which the platform never reads: Data Fabric `
189
190
  + `nodes read only \`inputs.entityConfig\`, with the keys ${[...DATAFABRIC_ENTITY_CONFIG_KEYS].join(', ')} `
190
191
  + `(flow-workbench entity-vdo.ts \`EntityConfig\` and the entity panels). \`validate\` cannot see `