@uipath/maestro-builder-sdk 6.70.2 → 6.70.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/check.js CHANGED
@@ -16,6 +16,7 @@ import { checkEventResolution, checkEventSubscription, eventParamsFor, } from '.
16
16
  import { checkCookedEscapes } from './flow-expr-check.js';
17
17
  import { FLOW_EXPR_DIALECT, checkBindingLabels, checkConnectorLookups as coreCheckConnectorLookups, checkConnectorSchema as coreCheckConnectorSchema, } from './core/connector-checks.js';
18
18
  import { nearestName } from './core/edit-distance.js';
19
+ import { escapeRegExp } from './core/escape-regexp.js';
19
20
  import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome, outcomeIdOf } from './core/hitl-routing.js';
20
21
  import { hasExits } from './core/step-ports.js';
21
22
  import { actionTails, armReachesJoin } from './flow-parallel-hitl.js';
@@ -1916,12 +1917,14 @@ function checkTrigger(spec, diags, opts = {}, entryPointId) {
1916
1917
  * INPUT — which is what `input('x')` lowers to, since inputs are published as the
1917
1918
  * trigger node's output. Matching only the bare form (as three call sites used
1918
1919
  * to) made every input-typed check silently stop firing.
1920
+ *
1921
+ * The trigger id is matched as literal text.
1919
1922
  */
1920
1923
  function declaredRefName(js, triggerId) {
1921
1924
  const bare = /^\$vars\.([A-Za-z0-9_]+)$/.exec(js.trim());
1922
1925
  if (bare)
1923
1926
  return bare[1];
1924
- const viaTrigger = new RegExp(`^\\$vars\\.${triggerId}\\.output\\.([A-Za-z0-9_]+)$`).exec(js.trim());
1927
+ const viaTrigger = new RegExp(`^\\$vars\\.${escapeRegExp(triggerId)}\\.output\\.([A-Za-z0-9_]+)$`).exec(js.trim());
1925
1928
  return viaTrigger?.[1];
1926
1929
  }
1927
1930
  /**
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `s` with every regex metacharacter escaped, so `new RegExp(escapeRegExp(s))`
3
+ * matches `s` as literal text. Use it wherever an id or name is spliced into a
4
+ * pattern. FB005 holds the name here.
5
+ */
6
+ export declare function escapeRegExp(s: string): string;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `s` with every regex metacharacter escaped, so `new RegExp(escapeRegExp(s))`
3
+ * matches `s` as literal text. Use it wherever an id or name is spliced into a
4
+ * pattern. FB005 holds the name here.
5
+ */
6
+ export function escapeRegExp(s) {
7
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
8
+ }
@@ -30,6 +30,7 @@
30
30
  * (which node / task / field) it came from.
31
31
  */
32
32
  import { typescript } from './typescript-module.js';
33
+ import { escapeRegExp } from './escape-regexp.js';
33
34
  /**
34
35
  * A JSONPath wildcard/filter step — the spelling that reaches for JavaScript and
35
36
  * misses. Used only to add a suggestion; the parse decides.
@@ -196,9 +197,6 @@ function maskStringLiterals(expr) {
196
197
  return expr.replace(/'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"/g, (m) => m[0] + ' '.repeat(m.length - 2) + m[m.length - 1]);
197
198
  }
198
199
  const SEGMENT = /\??\.([A-Za-z_$][\w$]*)|\[(\d+)\]/g;
199
- function escapeRegExp(s) {
200
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
201
- }
202
200
  /**
203
201
  * A `<namespace>.<root><path>` reference matcher. The root is a JS identifier;
204
202
  * the path is any run of `.ident`, `?.ident`, or `[<digits>]` selectors — the
@@ -233,7 +233,8 @@ const JSON_TEXT_NEGATIONS = new Set(['not', 'never', 'no', 'dont', 'without', 'r
233
233
  const isJsonTextNegation = (w) => JSON_TEXT_NEGATIONS.has(w) || w.endsWith("n't") || w.endsWith('n’t');
234
234
  const JSON_TEXT_WORD = "[A-Za-z][\\w'’-]*";
235
235
  /** Words are separated by spaces, tabs or a comma — never a newline or a full stop. */
236
- const JSON_TEXT_SEP = '[ \\t]*,?[ \\t]+';
236
+ // A comma binds to the spaces before it, so a space run matches one way.
237
+ const JSON_TEXT_SEP = '(?:[ \\t]*,)?[ \\t]+';
237
238
  const JSON_TEXT_QUAL = `(?:${[...JSON_TEXT_QUALIFIERS].sort((a, b) => b.length - a.length).join('|')})\\b`;
238
239
  // `(?<![-\w])` skips `--output json`; `(?!\.\w)` skips `JSON.parse` / `JSON.stringify`;
239
240
  // `(?!-(?:ld|rpc)\b)` skips JSON-LD / JSON-RPC (`\b` matches before the hyphen);
@@ -26,7 +26,7 @@ import { sortedByCodePoint, sortedStrings } from "../compat/compare.js";
26
26
  import { afterLast, pyRstrip, pyStr, pyStrip, splitOnce } from "../compat/pystr.js";
27
27
  import { referenceOf, specsFromFields } from "../lookup-specs.js";
28
28
  import { isEventNodeType, } from "../model/library.js";
29
- import { symbolIndex } from "../naming.js";
29
+ import { assertModuleConnectorKey, symbolIndex } from "../naming.js";
30
30
  import { descriptorInputFields, lookupSpecs, markLookupFields } from "./fields.js";
31
31
  import { NamedTypeRegistry } from "./type-registry.js";
32
32
  import { buildTypeTree, pyTruthy, subtreeRequired } from "./type-tree.js";
@@ -360,6 +360,7 @@ export function renderDescriptorModules(library, options = {}) {
360
360
  const key = row.connectorKey;
361
361
  if (subset.size > 0 && !subset.has(key))
362
362
  continue;
363
+ assertModuleConnectorKey(key, `the index.json row for ${JSON.stringify(row.nodeType)} at ${JSON.stringify(row.path)}`);
363
364
  const group = byConnector.get(key);
364
365
  if (group)
365
366
  group.push(row);
@@ -16,6 +16,7 @@ import { pyJsonString, pyJsonStringify } from "../codecs/pyjson.js";
16
16
  import { sortedByCodePoint, sortedStrings } from "../compat/compare.js";
17
17
  import { afterLast, pyRstrip, pyStrip, upperFirst } from "../compat/pystr.js";
18
18
  import { TRIGGER_PREFIX } from "../model/library.js";
19
+ import { assertModuleConnectorKey } from "../naming.js";
19
20
  /** The trigger generator's own pascal: same split, `Event` as the empty fallback. */
20
21
  function pascalEvent(eventId) {
21
22
  const joined = eventId
@@ -64,6 +65,7 @@ export function renderTriggerModules(catalogue, options = {}) {
64
65
  const key = row.connectorKey;
65
66
  if (subset.size > 0 && !subset.has(key))
66
67
  continue;
68
+ assertModuleConnectorKey(key, `the trigger catalogue row for ${JSON.stringify(nodeType)}`);
67
69
  const group = byConnector.get(key);
68
70
  if (group)
69
71
  group.push(row);
@@ -1,3 +1,7 @@
1
+ /** One plain path segment: a connector key names a file or folder, so it must not climb out of it. */
2
+ export declare const CONNECTOR_KEY_SEGMENT: RegExp;
3
+ /** Refuse a key that cannot be a module file stem: the key comes from an editable project file. */
4
+ export declare function assertModuleConnectorKey(key: unknown, row: string): asserts key is string;
1
5
  /** `send-message-to-user` → `SendMessageToUser`. */
2
6
  export declare function pascal(actionId: string): string;
3
7
  /** A small deterministic singulariser for array field names used as type names. */
@@ -15,6 +15,16 @@
15
15
  */
16
16
  import { sortedByCodePoint, sortedStrings } from "./compat/compare.js";
17
17
  import { afterLast, beforeLast, upperFirst } from "./compat/pystr.js";
18
+ import { Refusal } from "./errors.js";
19
+ /** One plain path segment: a connector key names a file or folder, so it must not climb out of it. */
20
+ export const CONNECTOR_KEY_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
21
+ /** Refuse a key that cannot be a module file stem: the key comes from an editable project file. */
22
+ export function assertModuleConnectorKey(key, row) {
23
+ if (typeof key === "string" && CONNECTOR_KEY_SEGMENT.test(key))
24
+ return;
25
+ throw new Refusal(`connector key ${JSON.stringify(key) ?? String(key)} of ${row} cannot name a module file: ` +
26
+ "it must be one plain path segment (a letter or digit, then letters, digits, '_', '.' or '-')", "CONNECTOR_KEY_INVALID");
27
+ }
18
28
  /** `send-message-to-user` → `SendMessageToUser`. */
19
29
  export function pascal(actionId) {
20
30
  const joined = actionId
@@ -39,6 +39,8 @@ export declare function describeNextLine(cli: string, nodeType: string, objectNa
39
39
  export declare function discardedNote(count: number): string;
40
40
  /** A write failed after the run succeeded, so the files written before it remain. */
41
41
  export declare function partialWriteMessage(path: string, reason: string): string;
42
+ /** A write that a link would carry out of the folder it belongs in. */
43
+ export declare function writeOutsideMessage(path: string, root: string): string;
42
44
  /**
43
45
  * A refusal whose message is the COMPLETE text the Python passed to
44
46
  * `sys.exit`, prefix included. Every other {@link Refusal} reaching the exit
@@ -54,6 +54,11 @@ export function partialWriteMessage(path, reason) {
54
54
  return prefixed(`could not write ${path} (${reason}). The files written before it remain, so the overlay or bindings.json `
55
55
  + "may be partly updated; fix the cause and run the same prepare again.");
56
56
  }
57
+ /** A write that a link would carry out of the folder it belongs in. */
58
+ export function writeOutsideMessage(path, root) {
59
+ return prefixed(`refusing to write ${path}: it is a link, or a link above it leads outside ${root}. `
60
+ + "Nothing was written; remove the link and run the same prepare again.");
61
+ }
57
62
  /**
58
63
  * A refusal whose message is the COMPLETE text the Python passed to
59
64
  * `sys.exit`, prefix included. Every other {@link Refusal} reaching the exit
@@ -30,7 +30,8 @@ import { SCHEMA_VERSION } from "../library-build/canonical.js";
30
30
  import { sidecarPath, unwrapV1Def } from "../library-reader.js";
31
31
  import { catalogReferences, loadInvariantCollections, specsFromFields } from "../lookup-specs.js";
32
32
  import { isEventNodeType } from "../model/library.js";
33
- import { afterWrites, existsSync, mkdirSync, readFileSync, stagedReadText, writeFileSync } from "./staged-files.js";
33
+ import { CONNECTOR_KEY_SEGMENT } from "../naming.js";
34
+ import { afterWrites, existsSync, mkdirSync, readFileSync, stagedReadText, writeInsideSync } from "./staged-files.js";
34
35
  import { isPlainObject, pyGet, pyOr, pyStrAny, pyTruthy, pyTypeName } from "./support.js";
35
36
  export const INDEX_FILENAME = "index.json";
36
37
  export const DESCRIPTORS_ROOT = "descriptors";
@@ -89,7 +90,7 @@ export function migrateLegacyDescriptorRoots(dst, entries) {
89
90
  if (child.isDirectory())
90
91
  walk(join(dir, child.name), join(destination, child.name));
91
92
  else if (child.isFile()) {
92
- writeFileSync(join(destination, child.name), readFileSync(join(dir, child.name)));
93
+ writeInsideSync(dst, join(destination, child.name), readFileSync(join(dir, child.name)));
93
94
  moved.push([join(dir, child.name), join(destination, child.name)]);
94
95
  }
95
96
  }
@@ -98,8 +99,6 @@ export function migrateLegacyDescriptorRoots(dst, entries) {
98
99
  afterWrites(() => moveOver(sourceRoot, moved));
99
100
  }
100
101
  }
101
- /** A connector key as the legacy layout named its folder: one plain path segment. */
102
- const LEGACY_ROOT = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
103
102
  /**
104
103
  * `<dst>/<legacyRoot>`, or `undefined` unless it is a real folder strictly
105
104
  * inside `dst`. The name comes from `index.json`, a project file anyone with
@@ -107,7 +106,7 @@ const LEGACY_ROOT = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
107
106
  * overlay.
108
107
  */
109
108
  function legacyRootDir(dst, legacyRoot) {
110
- if (!LEGACY_ROOT.test(legacyRoot))
109
+ if (!CONNECTOR_KEY_SEGMENT.test(legacyRoot))
111
110
  return undefined;
112
111
  const root = resolve(dst, legacyRoot);
113
112
  const inside = relative(resolve(dst), root);
@@ -223,13 +222,13 @@ export function mergeFilesInto(files, dst) {
223
222
  migrateLegacyDescriptorRoots(dst, dstEntries);
224
223
  const srcEntries = isPlainObject(srcIdx) ? pyGet(srcIdx, "entries", []) : [];
225
224
  dstIdx.entries = mergeIndexEntries(dstEntries, Array.isArray(srcEntries) ? srcEntries : []);
226
- writeFileSync(join(dst, INDEX_FILENAME), formatOverlayIndex(dstIdx), "utf8");
225
+ writeInsideSync(dst, join(dst, INDEX_FILENAME), formatOverlayIndex(dstIdx));
227
226
  for (const [rel, content] of files) {
228
227
  if (basename(rel) === INDEX_FILENAME)
229
228
  continue;
230
229
  const out = join(dst, ...descriptorOverlayPath(rel).split("/"));
231
230
  mkdirSync(dirname(out), { recursive: true });
232
- writeFileSync(out, content);
231
+ writeInsideSync(dst, out, content);
233
232
  }
234
233
  }
235
234
  /** Every file under `root`, as `POSIX-relative path → bytes`. */
@@ -23,7 +23,7 @@ import { Refusal } from "../errors.js";
23
23
  import { formatRequiredInputGaps, renderDescriptorModules } from "../generators/descriptors.js";
24
24
  import { loadLibrary } from "../library-reader.js";
25
25
  import { overlayHasGenericExpanded } from "./overlay.js";
26
- import { mkdirSync, stagedReadText, writeFileSync } from "./staged-files.js";
26
+ import { mkdirSync, stagedReadText, writeInsideSync } from "./staged-files.js";
27
27
  /**
28
28
  * Write `<out>/<connectorKey>.ts` for every connector in the overlay and
29
29
  * return the module names written; nothing at all under `--json-only`.
@@ -42,7 +42,7 @@ export function regenerateDescriptors(out, importSpecifier, jsonOnly = false) {
42
42
  });
43
43
  mkdirSync(out, { recursive: true });
44
44
  for (const [name, text] of output.modules)
45
- writeFileSync(join(out, name), text, "utf8");
45
+ writeInsideSync(out, join(out, name), text);
46
46
  if (output.requiredInputGaps.length > 0) {
47
47
  throw new Refusal(formatRequiredInputGaps(output.requiredInputGaps), "REQUIRED_INPUT_GAP");
48
48
  }
@@ -8,6 +8,8 @@ export declare function stagedWrites<T>(work: () => Promise<T>): Promise<T>;
8
8
  /** Run `step` once the held files are written: at once outside a run, never for a run that throws. */
9
9
  export declare function afterWrites(step: () => void): void;
10
10
  export declare function writeFileSync(path: string, content: string | Uint8Array, encoding?: "utf8"): void;
11
+ /** {@link writeFileSync}, refused unless the write lands inside `root` once links are followed. */
12
+ export declare function writeInsideSync(root: string, path: string, content: string | Uint8Array): void;
11
13
  /** A held file's parent is made when it is written, so there is nothing to make yet. */
12
14
  export declare function mkdirSync(path: string, options: {
13
15
  recursive: true;
@@ -13,11 +13,13 @@
13
13
  * helper called on its own (the tests, `mergeInto`) writes at once as before.
14
14
  */
15
15
  import * as fs from "node:fs";
16
- import { basename, dirname, resolve } from "node:path";
16
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
17
17
  import { defaultReadText } from "../../core/read-text.js";
18
18
  import { Refusal, messageOf } from "../errors.js";
19
- import { PrepareRefusal, discardedNote, partialWriteMessage } from "./messages.js";
19
+ import { PrepareRefusal, discardedNote, partialWriteMessage, writeOutsideMessage } from "./messages.js";
20
20
  let pending;
21
+ /** The folder each held file written by {@link writeInsideSync} must land in. */
22
+ let confinedTo = new Map();
21
23
  let afterwards = [];
22
24
  /**
23
25
  * Run `work` with its writes held, then write them all, or none when it throws.
@@ -30,6 +32,8 @@ export async function stagedWrites(work) {
30
32
  }
31
33
  const files = new Map();
32
34
  pending = files;
35
+ const roots = new Map();
36
+ confinedTo = roots;
33
37
  const then = [];
34
38
  afterwards = then;
35
39
  let result;
@@ -46,6 +50,11 @@ export async function stagedWrites(work) {
46
50
  }
47
51
  // An index written before a failed write would name descriptors that are not there.
48
52
  const indexLast = [...files].sort(([a], [b]) => Number(basename(a) === "index.json") - Number(basename(b) === "index.json"));
53
+ for (const [path] of indexLast) {
54
+ const root = roots.get(path);
55
+ if (root !== undefined)
56
+ assertLandsInside(root, path);
57
+ }
49
58
  for (const [path, content] of indexLast) {
50
59
  try {
51
60
  fs.mkdirSync(dirname(path), { recursive: true });
@@ -75,6 +84,58 @@ export function writeFileSync(path, content, encoding) {
75
84
  else
76
85
  fs.writeFileSync(path, content, encoding);
77
86
  }
87
+ /** {@link writeFileSync}, refused unless the write lands inside `root` once links are followed. */
88
+ export function writeInsideSync(root, path, content) {
89
+ if (pending !== undefined) {
90
+ pending.set(resolve(path), content);
91
+ confinedTo.set(resolve(path), root);
92
+ return;
93
+ }
94
+ assertLandsInside(root, path);
95
+ fs.mkdirSync(dirname(path), { recursive: true });
96
+ fs.writeFileSync(path, content);
97
+ }
98
+ /** A project can ship links, and a write follows them, so a link at or above `path` must not lead out of `root`. */
99
+ function assertLandsInside(root, path) {
100
+ const refuse = () => {
101
+ throw new PrepareRefusal(writeOutsideMessage(path, root), "PREPARE_WRITE_OUTSIDE_OUT");
102
+ };
103
+ if (isLink(path))
104
+ refuse();
105
+ // Nothing under a folder that is not there yet can be a link.
106
+ if (!fs.existsSync(root))
107
+ return;
108
+ let dir = dirname(resolve(path));
109
+ while (!existsLexically(dir))
110
+ dir = dirname(dir);
111
+ let real;
112
+ try {
113
+ real = fs.realpathSync(dir);
114
+ }
115
+ catch {
116
+ return refuse();
117
+ }
118
+ const inside = relative(fs.realpathSync(root), real);
119
+ if (inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside))
120
+ refuse();
121
+ }
122
+ function isLink(path) {
123
+ try {
124
+ return fs.lstatSync(path).isSymbolicLink();
125
+ }
126
+ catch {
127
+ return false;
128
+ }
129
+ }
130
+ function existsLexically(path) {
131
+ try {
132
+ fs.lstatSync(path);
133
+ return true;
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ }
78
139
  /** A held file's parent is made when it is written, so there is nothing to make yet. */
79
140
  export function mkdirSync(path, options) {
80
141
  if (pending === undefined)
@@ -93,8 +154,10 @@ export function readFileSync(path, encoding) {
93
154
  }
94
155
  /** Drops a held file; only outside a run does it reach the disk. */
95
156
  export function rmSync(path) {
96
- if (pending !== undefined)
157
+ if (pending !== undefined) {
97
158
  pending.delete(resolve(path));
159
+ confinedTo.delete(resolve(path));
160
+ }
98
161
  else
99
162
  fs.rmSync(path);
100
163
  }
@@ -121,6 +121,14 @@ async function download(url) {
121
121
  }
122
122
  return new Uint8Array(await response.arrayBuffer());
123
123
  }
124
+ /** `http://u:p@h` -> `http://***@h` */
125
+ function redactProxyCredentials(proxy) {
126
+ const at = proxy.lastIndexOf("@");
127
+ if (at < 0)
128
+ return proxy;
129
+ const scheme = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i.exec(proxy)?.[0] ?? "";
130
+ return `${scheme}***${proxy.slice(at)}`;
131
+ }
124
132
  /**
125
133
  * Node's built-in fetch ignores HTTP(S)_PROXY unless told otherwise, and it
126
134
  * fails in a way that looks like the server is down. Say so up front, and name
@@ -133,7 +141,7 @@ function warnAboutProxy() {
133
141
  process.env.http_proxy;
134
142
  if (!proxy)
135
143
  return;
136
- console.error(`registry pull: a proxy is set (${proxy}) and Node's fetch does not use it by default.\n` +
144
+ console.error(`registry pull: a proxy is set (${redactProxyCredentials(proxy)}) and Node's fetch does not use it by default.\n` +
137
145
  " If the download fails, either run with NODE_USE_ENV_PROXY=1 (Node 24+),\n" +
138
146
  " or fetch the zip yourself and pass --archive <path>.");
139
147
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.70.2",
3
+ "version": "6.70.4",
4
4
  "description": "Build UiPath Flow, Case, and BPMN artifacts by writing TypeScript.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.uipath.com/maestro",
@@ -100,5 +100,5 @@
100
100
  "@types/node": "^22.7.0",
101
101
  "esbuild": "^0.28.1"
102
102
  },
103
- "gitref": "f3203eee21b892b6b70e41a5bf791aa30fdc0fc2"
103
+ "gitref": "e9ad3d42f515a19663813c7befb4f6afdea2b87c"
104
104
  }