@uipath/maestro-builder-sdk 6.4.0 → 6.5.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.
Files changed (161) hide show
  1. package/dist/bpmn/uipath-moddle.v1.json +21 -0
  2. package/dist/cli/commands/registry.js +68 -12
  3. package/dist/registry/archive/entry-policy.d.ts +36 -0
  4. package/dist/registry/archive/entry-policy.js +60 -0
  5. package/dist/registry/archive/extract.d.ts +31 -0
  6. package/dist/registry/archive/extract.js +92 -0
  7. package/dist/registry/archive/zip-reader.d.ts +38 -0
  8. package/dist/registry/archive/zip-reader.js +256 -0
  9. package/dist/registry/codecs/pyjson.d.ts +62 -0
  10. package/dist/registry/codecs/pyjson.js +208 -0
  11. package/dist/registry/compat/compare.d.ts +18 -0
  12. package/dist/registry/compat/compare.js +32 -0
  13. package/dist/registry/compat/paths.d.ts +4 -0
  14. package/dist/registry/compat/paths.js +17 -0
  15. package/dist/registry/compat/pystr.d.ts +50 -0
  16. package/dist/registry/compat/pystr.js +221 -0
  17. package/dist/registry/concurrency.d.ts +10 -0
  18. package/dist/registry/concurrency.js +34 -0
  19. package/dist/registry/curated-overlay.d.ts +36 -4
  20. package/dist/registry/curated-overlay.js +87 -14
  21. package/dist/registry/errors.d.ts +26 -0
  22. package/dist/registry/errors.js +38 -0
  23. package/dist/registry/extractor.d.ts +9 -10
  24. package/dist/registry/extractor.js +5 -15
  25. package/dist/registry/files.d.ts +26 -0
  26. package/dist/registry/files.js +160 -0
  27. package/dist/registry/generators/descriptors.d.ts +81 -0
  28. package/dist/registry/generators/descriptors.js +405 -0
  29. package/dist/registry/generators/fields.d.ts +46 -0
  30. package/dist/registry/generators/fields.js +111 -0
  31. package/dist/registry/generators/markdown/common.d.ts +21 -0
  32. package/dist/registry/generators/markdown/common.js +55 -0
  33. package/dist/registry/generators/markdown/index.d.ts +33 -0
  34. package/dist/registry/generators/markdown/index.js +37 -0
  35. package/dist/registry/generators/markdown/page.d.ts +8 -0
  36. package/dist/registry/generators/markdown/page.js +52 -0
  37. package/dist/registry/generators/markdown/pyvalue.d.ts +34 -0
  38. package/dist/registry/generators/markdown/pyvalue.js +106 -0
  39. package/dist/registry/generators/markdown/sections/choosing-object.d.ts +2 -0
  40. package/dist/registry/generators/markdown/sections/choosing-object.js +52 -0
  41. package/dist/registry/generators/markdown/sections/example.d.ts +22 -0
  42. package/dist/registry/generators/markdown/sections/example.js +295 -0
  43. package/dist/registry/generators/markdown/sections/filtering.d.ts +2 -0
  44. package/dist/registry/generators/markdown/sections/filtering.js +76 -0
  45. package/dist/registry/generators/markdown/sections/header.d.ts +2 -0
  46. package/dist/registry/generators/markdown/sections/header.js +37 -0
  47. package/dist/registry/generators/markdown/sections/inputs.d.ts +30 -0
  48. package/dist/registry/generators/markdown/sections/inputs.js +151 -0
  49. package/dist/registry/generators/markdown/sections/lookups.d.ts +12 -0
  50. package/dist/registry/generators/markdown/sections/lookups.js +150 -0
  51. package/dist/registry/generators/markdown/sections/outputs.d.ts +3 -0
  52. package/dist/registry/generators/markdown/sections/outputs.js +9 -0
  53. package/dist/registry/generators/markdown/sections/resolving-fields.d.ts +2 -0
  54. package/dist/registry/generators/markdown/sections/resolving-fields.js +139 -0
  55. package/dist/registry/generators/markdown/sections/schema-dynamic.d.ts +2 -0
  56. package/dist/registry/generators/markdown/sections/schema-dynamic.js +86 -0
  57. package/dist/registry/generators/materialize.d.ts +30 -0
  58. package/dist/registry/generators/materialize.js +132 -0
  59. package/dist/registry/generators/md-symbols.d.ts +22 -0
  60. package/dist/registry/generators/md-symbols.js +58 -0
  61. package/dist/registry/generators/triggers.d.ts +15 -0
  62. package/dist/registry/generators/triggers.js +85 -0
  63. package/dist/registry/generators/type-registry.d.ts +50 -0
  64. package/dist/registry/generators/type-registry.js +154 -0
  65. package/dist/registry/generators/type-tree.d.ts +65 -0
  66. package/dist/registry/generators/type-tree.js +179 -0
  67. package/dist/registry/library-build/build.d.ts +57 -0
  68. package/dist/registry/library-build/build.js +286 -0
  69. package/dist/registry/library-build/canonical.d.ts +119 -0
  70. package/dist/registry/library-build/canonical.js +512 -0
  71. package/dist/registry/library-build/cli.d.ts +45 -0
  72. package/dist/registry/library-build/cli.js +182 -0
  73. package/dist/registry/library-build/context.d.ts +107 -0
  74. package/dist/registry/library-build/context.js +267 -0
  75. package/dist/registry/library-build/enrich.d.ts +36 -0
  76. package/dist/registry/library-build/enrich.js +158 -0
  77. package/dist/registry/library-build/events.d.ts +108 -0
  78. package/dist/registry/library-build/events.js +476 -0
  79. package/dist/registry/library-build/generic-expand.d.ts +77 -0
  80. package/dist/registry/library-build/generic-expand.js +363 -0
  81. package/dist/registry/library-build/plan.d.ts +71 -0
  82. package/dist/registry/library-build/plan.js +270 -0
  83. package/dist/registry/library-build/py.d.ts +45 -0
  84. package/dist/registry/library-build/py.js +167 -0
  85. package/dist/registry/library-build/schema-action.d.ts +38 -0
  86. package/dist/registry/library-build/schema-action.js +107 -0
  87. package/dist/registry/library-build/write.d.ts +6 -0
  88. package/dist/registry/library-build/write.js +26 -0
  89. package/dist/registry/library-reader.d.ts +20 -0
  90. package/dist/registry/library-reader.js +50 -0
  91. package/dist/registry/lookup-specs.d.ts +55 -0
  92. package/dist/registry/lookup-specs.js +140 -0
  93. package/dist/registry/materialize-cli.js +28 -14
  94. package/dist/registry/materializer.d.ts +25 -3
  95. package/dist/registry/materializer.js +45 -29
  96. package/dist/registry/model/library.d.ts +254 -0
  97. package/dist/registry/model/library.js +23 -0
  98. package/dist/registry/naming.d.ts +54 -0
  99. package/dist/registry/naming.js +142 -0
  100. package/dist/registry/orphans.d.ts +4 -0
  101. package/dist/registry/orphans.js +86 -0
  102. package/dist/registry/prepare/actions.d.ts +26 -0
  103. package/dist/registry/prepare/actions.js +94 -0
  104. package/dist/registry/prepare/connections.d.ts +181 -0
  105. package/dist/registry/prepare/connections.js +399 -0
  106. package/dist/registry/prepare/event.d.ts +96 -0
  107. package/dist/registry/prepare/event.js +268 -0
  108. package/dist/registry/prepare/handoff.d.ts +21 -0
  109. package/dist/registry/prepare/handoff.js +91 -0
  110. package/dist/registry/prepare/ledger.d.ts +104 -0
  111. package/dist/registry/prepare/ledger.js +216 -0
  112. package/dist/registry/prepare/messages.d.ts +190 -0
  113. package/dist/registry/prepare/messages.js +472 -0
  114. package/dist/registry/prepare/objects.d.ts +130 -0
  115. package/dist/registry/prepare/objects.js +318 -0
  116. package/dist/registry/prepare/overlay.d.ts +149 -0
  117. package/dist/registry/prepare/overlay.js +487 -0
  118. package/dist/registry/prepare/parents.d.ts +41 -0
  119. package/dist/registry/prepare/parents.js +122 -0
  120. package/dist/registry/prepare/pipeline.d.ts +91 -0
  121. package/dist/registry/prepare/pipeline.js +376 -0
  122. package/dist/registry/prepare/regenerate.d.ts +9 -0
  123. package/dist/registry/prepare/regenerate.js +50 -0
  124. package/dist/registry/prepare/resolve.d.ts +199 -0
  125. package/dist/registry/prepare/resolve.js +470 -0
  126. package/dist/registry/prepare/retry.d.ts +83 -0
  127. package/dist/registry/prepare/retry.js +109 -0
  128. package/dist/registry/prepare/source-import.d.ts +54 -0
  129. package/dist/registry/prepare/source-import.js +192 -0
  130. package/dist/registry/prepare/support.d.ts +41 -0
  131. package/dist/registry/prepare/support.js +178 -0
  132. package/dist/registry/prepare-cli.d.ts +16 -5
  133. package/dist/registry/prepare-cli.js +60 -53
  134. package/dist/registry/pull-cli.js +8 -7
  135. package/dist/registry/reporter.d.ts +26 -0
  136. package/dist/registry/reporter.js +41 -0
  137. package/dist/registry/uip/client.d.ts +99 -0
  138. package/dist/registry/uip/client.js +226 -0
  139. package/dist/registry/uip/commands.d.ts +40 -0
  140. package/dist/registry/uip/commands.js +80 -0
  141. package/dist/registry/uip/envelope.d.ts +37 -0
  142. package/dist/registry/uip/envelope.js +84 -0
  143. package/dist/registry/uip/response-cache.d.ts +28 -0
  144. package/dist/registry/uip/response-cache.js +57 -0
  145. package/lib/README.md +10 -8
  146. package/package.json +3 -3
  147. package/dist/generators/_connections.py +0 -331
  148. package/dist/generators/_events.py +0 -355
  149. package/dist/generators/_lookups.py +0 -114
  150. package/dist/generators/_naming.py +0 -185
  151. package/dist/generators/_objects.py +0 -299
  152. package/dist/generators/_resolve.py +0 -319
  153. package/dist/generators/convert_library_to_md.py +0 -1441
  154. package/dist/generators/extract_library.py +0 -125
  155. package/dist/generators/generate_connectors_ts.py +0 -936
  156. package/dist/generators/generate_library_json.py +0 -2527
  157. package/dist/generators/generate_triggers_ts.py +0 -110
  158. package/dist/generators/materialize_registry.py +0 -155
  159. package/dist/generators/prepare_connector.py +0 -1860
  160. package/dist/registry/python.d.ts +0 -26
  161. package/dist/registry/python.js +0 -46
@@ -0,0 +1,181 @@
1
+ import { Refusal } from "../errors.js";
2
+ import type { UipClient } from "../uip/client.js";
3
+ /** One row of `uip is connections list`: `Id`, `Name`, `ConnectorKey`, `State`, `Folder`, `FolderKey`, … */
4
+ export type ConnectionRow = Record<string, unknown>;
5
+ /** Which listing the candidates came from, as the refusals name it. */
6
+ export type ConnectionScope = "your folder" | "the tenant";
7
+ /** No usable connection, with the reason and the way forward (`_connections.ConnectionError_`). */
8
+ export declare class ConnectionRefusal extends Refusal {
9
+ constructor(message: string, code: string);
10
+ }
11
+ /**
12
+ * `uip is connections list --output json`, as rows.
13
+ *
14
+ * A non-zero exit and a non-JSON reply are both refusals, never an empty list:
15
+ * "you have no connection" and "the listing failed" have opposite remedies.
16
+ */
17
+ export declare function listConnections(client: UipClient, options: {
18
+ allFolders: boolean;
19
+ byoa?: boolean;
20
+ }): Promise<ConnectionRow[]>;
21
+ /**
22
+ * The rows for `connectorKey` that can serve a request, optionally by `Name`.
23
+ *
24
+ * A Failed connection is listed like any other and cannot serve a request;
25
+ * selecting one produces an error about the operation rather than about the
26
+ * connection, which sends the reader the wrong way.
27
+ */
28
+ export declare function enabledConnections(rows: readonly ConnectionRow[], connectorKey: string, name?: string): ConnectionRow[];
29
+ /**
30
+ * The candidates, one pasteable `--connection-id` line each
31
+ * (`_connections.candidate_listing`): `<indent><Name> --connection-id <Id> (folder: <Folder>)`.
32
+ */
33
+ export declare function candidateListing(rows: readonly ConnectionRow[], indent?: string): string;
34
+ /**
35
+ * The one enabled connection for `connectorKey` among `rows`, or throw.
36
+ *
37
+ * Pure: `rows` is whatever listing the caller fetched, and `scope` is how the
38
+ * refusal names it. Several candidates is a refusal that lists every one with
39
+ * its id — a NAME is not a key on a tenant. The eval tenant holds three Slack
40
+ * connections all named `is-sandboxes` (two Enabled, in different folders), so
41
+ * the old remedy — "name the one you want with --connection <name>" — handed
42
+ * back the very flag the caller had just used, and the doc-following author
43
+ * dead-ended (placeholder GUIDs hand-written into bindings.json, 401 at the
44
+ * Slack node).
45
+ */
46
+ export declare function selectConnection(rows: readonly ConnectionRow[], connectorKey: string, name?: string, scope?: ConnectionScope): ConnectionRow;
47
+ /**
48
+ * The one enabled connection for `connectorKey`, fetched, or throw.
49
+ *
50
+ * Tries the DEFAULT listing first and only widens to `--all-folders` if that
51
+ * finds nothing. That order matters and is not just politeness to the API: the
52
+ * default listing is the caller's own folder, which on a real tenant is one
53
+ * connection where the tenant-wide view is many. Widening first would turn an
54
+ * unambiguous case into an ambiguous one.
55
+ */
56
+ export declare function discoverConnection(client: UipClient, connectorKey: string, name?: string): Promise<ConnectionRow>;
57
+ /**
58
+ * Every enabled connection for `connectorKey`, own folder FIRST
59
+ * (`_connections.candidate_connections`).
60
+ *
61
+ * {@link discoverConnection} answers "which one" and stops at the first
62
+ * non-empty scope, which is right for picking a connection and wrong for
63
+ * retrying one: when the own-folder listing holds exactly one, the tenant-wide
64
+ * connections are never even looked at. So a lookup that exhausts the
65
+ * personal-workspace connection reports a bad VALUE while the record sits in
66
+ * another folder's connection (flow-builder-sdk#744, measured on two Jira
67
+ * connections where only the second carried project `TS`).
68
+ *
69
+ * Ordering is the same preference discovery encodes — the caller's own folder
70
+ * is the likeliest and is tried first — but nothing is dropped. Deduplicated
71
+ * by `Id`, because the tenant-wide listing repeats the own-folder rows; a row
72
+ * with no `Id` is never deduplicated, as the Python's `if identifier and …`
73
+ * did not.
74
+ *
75
+ * This does NOT decide anything, and in particular does not throw on several
76
+ * matches: choosing among them is the caller's job, and the only honest way to
77
+ * choose is evidence (a lookup that resolves on exactly one). That keeps this
78
+ * module's rule intact — ambiguity is never resolved by guessing.
79
+ */
80
+ export declare function candidateConnections(client: UipClient, connectorKey: string, name?: string): Promise<ConnectionRow[]>;
81
+ /** The enabled connections for `connectorKey`, one per line, pasteable. */
82
+ export declare function enabledListing(rows: readonly ConnectionRow[], connectorKey: string): string;
83
+ /**
84
+ * The record for an explicitly named connection among `rows`, or throw unless it can serve.
85
+ *
86
+ * An explicit `--connection-id` used to skip every check discovery applies, and
87
+ * that skip is where a whole class of silent failures came from. A connection
88
+ * in `State: Failed` is listed like any other, and `uip is resources describe`
89
+ * against it answers `Result: Success` with NO request fields — the tenant has
90
+ * no schema to give for a connection it cannot reach. `prepare` then wrote an
91
+ * overlay with zero body inputs, printed "0 input field(s)" as a success, and
92
+ * the next `compile` rejected every real field as unknown. Measured on the eval
93
+ * tenant (Salesforce `Opportunity` / `Update`): the Failed connection returned
94
+ * 0 request fields, the Enabled one 13.
95
+ *
96
+ * So: resolve the id against the listing, and refuse — naming the state found
97
+ * and the enabled alternatives — rather than describe through it.
98
+ */
99
+ export declare function checkConnection(rows: readonly ConnectionRow[], connectorKey: string, connectionId: string): ConnectionRow;
100
+ /** {@link checkConnection} against the tenant-wide listing — the id may live in another folder. */
101
+ export declare function lookupConnection(client: UipClient, connectorKey: string, connectionId: string): Promise<ConnectionRow>;
102
+ /**
103
+ * The enabled BYOA connections for `connectorKey`.
104
+ *
105
+ * THROWS on a failed listing rather than answering an empty list. The
106
+ * difference is the whole point: "you have no BYOA connection" tells the caller
107
+ * to go create one, and a listing that failed says nothing of the kind.
108
+ * Answering [] for both turns a broken call into confident, wrong advice.
109
+ */
110
+ export declare function byoaConnections(client: UipClient, connectorKey: string): Promise<ConnectionRow[]>;
111
+ /** The BYOA candidates, one pasteable line each, or why there are none. */
112
+ export declare function byoaListing(rows: readonly ConnectionRow[], connectorKey: string): string;
113
+ /** `bindings.json` as this module reads and writes it; rows are the author's and stay loose. */
114
+ export type BindingsJson = Record<string, unknown> & {
115
+ schemaVersion?: unknown;
116
+ bindings: unknown[];
117
+ };
118
+ export interface BindingsMerge {
119
+ document: BindingsJson;
120
+ /** True when the connection row's `resourceKey` changed (or the row was added). */
121
+ wroteConnection: boolean;
122
+ /** True when the folder row's `resourceKey` changed (or the row was added). */
123
+ wroteFolder: boolean;
124
+ }
125
+ /**
126
+ * Add connection + folder entries to a parsed `bindings.json`, without touching the input.
127
+ *
128
+ * ADDITIVE AND IDEMPOTENT. An existing entry under the same symbolic name (its
129
+ * `name` or `id`) is updated, never duplicated, and every other entry is left
130
+ * exactly as it was — `bindings.json` is an authored file that may already
131
+ * carry bindings for connectors this run knows nothing about. An updated row
132
+ * keeps its key order and gains any missing key at the end; a new row is
133
+ * written `id, name, resource, resourceKey, default, propertyAttribute`.
134
+ *
135
+ * `document` is what the file parsed to, or `undefined` when there was no file.
136
+ * As in the Python, a parsed value that is not an object is replaced by the
137
+ * default document.
138
+ */
139
+ export declare function mergeBindingsDocument(document: unknown, symbolicConnection: string, symbolicFolder: string, connection: ConnectionRow): BindingsMerge;
140
+ /**
141
+ * `json.dump(document, indent=2)` + newline: the bytes `bindings.json` is written in.
142
+ *
143
+ * `allowIntegerKeys` for the same reason the ledger passes it (`objects.ts`):
144
+ * `bindings.json` is the author's file and may carry an integer-like key
145
+ * anywhere in a row this module never looks at. `json.dump` wrote such a file
146
+ * back; refusing it here left the file untouched and the connection unbound,
147
+ * with an error about a key the author did not know they had. The one thing
148
+ * that cannot be kept is that key's position — `JSON.parse` has already
149
+ * hoisted it to the front of its object — which Python preserved.
150
+ */
151
+ export declare function formatBindings(document: BindingsJson): string;
152
+ /**
153
+ * Add connection + folder entries to `bindings.json` at `path`, in place.
154
+ *
155
+ * Returns `[wroteConnection, wroteFolder]` describing what changed, so the
156
+ * caller can report the specific thing rather than "updated bindings". The file
157
+ * is always rewritten, as the Python did. A file that will not parse is REFUSED,
158
+ * not overwritten: it is the author's, and not ours to discard.
159
+ */
160
+ export declare function mergeBindings(path: string, symbolicConnection: string, symbolicFolder: string, connection: ConnectionRow): [wroteConnection: boolean, wroteFolder: boolean];
161
+ /**
162
+ * Remember `bindings.json` byte-for-byte, before a stage that writes it.
163
+ *
164
+ * `undefined` means the file could not be read — normally that it did not
165
+ * exist — and {@link restoreBindings} will remove whatever was written since.
166
+ */
167
+ export declare function snapshotBindings(path: string | undefined): Uint8Array | undefined;
168
+ /**
169
+ * Put `bindings.json` back exactly as {@link snapshotBindings} found it.
170
+ *
171
+ * A refusal must leave `bindings.json` as it was: the BYOA fact needs a
172
+ * connection to discover, so stage 0 has already written one by the time the
173
+ * event says it is the wrong KIND — without the rollback the overlay keeps
174
+ * pointing at a connection the tool just forbade. A failure to restore is
175
+ * reported through `warn` (stderr by default), never thrown: the refusal being
176
+ * delivered is the message that matters.
177
+ *
178
+ * `shown` is the path as the caller typed it (`args.bindings`), which is what
179
+ * the Python printed; `path` is where the file actually is.
180
+ */
181
+ export declare function restoreBindings(path: string | undefined, snapshot: Uint8Array | undefined, warn?: (message: string) => void, shown?: string | undefined): void;
@@ -0,0 +1,399 @@
1
+ /**
2
+ * Find the connection for a connector, and write its bindings.
3
+ *
4
+ * `prepare` already knows which connector it is preparing. Making the author go
5
+ * and find that connector's connection id by hand — and then its folder key, and
6
+ * then hand-write `bindings.json` around both — is work the command can do
7
+ * itself from information it already has. Measured on the eval corpus, that
8
+ * hand-work was 4 to 7 tool calls per task.
9
+ *
10
+ * THE ONE THING THIS MUST NOT DO IS GUESS. A tenant can carry several
11
+ * connections for the same connector — the test tenant has five for Slack — and
12
+ * they are not interchangeable: a user who exists in one workspace does not
13
+ * exist in another, so picking the wrong one makes a lookup unresolvable for a
14
+ * reason that points nowhere near the actual mistake. That failure was observed
15
+ * directly (`skill-flow-slack-dm-lookup` named the wrong workspace and burned
16
+ * its whole turn budget), which is why ambiguity here is an error listing the
17
+ * candidates and never a silent choice.
18
+ *
19
+ * Ported from `integrations/scripts/_connections.py`. The decisions are pure
20
+ * functions over an already-fetched listing; the `async` wrappers fetch through
21
+ * a {@link UipClient} and hand the rows to them. Every refusal's text is kept
22
+ * verbatim — agents read these messages and follow them.
23
+ */
24
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
25
+ import { pyJsonStringify } from "../codecs/pyjson.js";
26
+ import { pyStrip } from "../compat/pystr.js";
27
+ import { Refusal, messageOf } from "../errors.js";
28
+ import { isConnectionsList } from "../uip/commands.js";
29
+ import { dataItems, parseEnvelope } from "../uip/envelope.js";
30
+ import { isPlainObject, notJsonReason, pyGet, pyOr, pyRepr, pyStrAny, pyTruthy } from "./support.js";
31
+ /** No usable connection, with the reason and the way forward (`_connections.ConnectionError_`). */
32
+ export class ConnectionRefusal extends Refusal {
33
+ constructor(message, code) {
34
+ super(message, code);
35
+ this.name = "ConnectionRefusal";
36
+ }
37
+ }
38
+ /**
39
+ * `uip is connections list --output json`, as rows.
40
+ *
41
+ * A non-zero exit and a non-JSON reply are both refusals, never an empty list:
42
+ * "you have no connection" and "the listing failed" have opposite remedies.
43
+ */
44
+ export async function listConnections(client, options) {
45
+ const result = await client.run(isConnectionsList({ allFolders: options.allFolders, byoa: options.byoa }));
46
+ if (result.code !== 0) {
47
+ throw new ConnectionRefusal(`listing connections failed: ${pyStrip(result.stderr) || "no output"}`, "CONNECTION_LISTING_FAILED");
48
+ }
49
+ const envelope = parseEnvelope(result.stdout);
50
+ if (envelope === undefined) {
51
+ throw new ConnectionRefusal(`connection listing was not JSON: ${notJsonReason(result.stdout)}`, "CONNECTION_LISTING_NOT_JSON");
52
+ }
53
+ return dataItems(envelope).filter(isPlainObject);
54
+ }
55
+ /**
56
+ * The rows for `connectorKey` that can serve a request, optionally by `Name`.
57
+ *
58
+ * A Failed connection is listed like any other and cannot serve a request;
59
+ * selecting one produces an error about the operation rather than about the
60
+ * connection, which sends the reader the wrong way.
61
+ */
62
+ export function enabledConnections(rows, connectorKey, name) {
63
+ let out = rows.filter((row) => row.ConnectorKey === connectorKey && pyStrAny(pyGet(row, "State", "")).toLowerCase() === "enabled");
64
+ if (name)
65
+ out = out.filter((row) => row.Name === name);
66
+ return out;
67
+ }
68
+ /**
69
+ * The candidates, one pasteable `--connection-id` line each
70
+ * (`_connections.candidate_listing`): `<indent><Name> --connection-id <Id> (folder: <Folder>)`.
71
+ */
72
+ export function candidateListing(rows, indent = " ") {
73
+ return rows
74
+ .map((row) => `${indent}${pyStrAny(row.Name)} --connection-id ${pyStrAny(row.Id)} (folder: ${pyStrAny(row.Folder)})`)
75
+ .join("\n");
76
+ }
77
+ /** {@link candidateListing} at the refusals' four-space indent. */
78
+ function candidateLines(rows) {
79
+ return candidateListing(rows);
80
+ }
81
+ /**
82
+ * The one enabled connection for `connectorKey` among `rows`, or throw.
83
+ *
84
+ * Pure: `rows` is whatever listing the caller fetched, and `scope` is how the
85
+ * refusal names it. Several candidates is a refusal that lists every one with
86
+ * its id — a NAME is not a key on a tenant. The eval tenant holds three Slack
87
+ * connections all named `is-sandboxes` (two Enabled, in different folders), so
88
+ * the old remedy — "name the one you want with --connection <name>" — handed
89
+ * back the very flag the caller had just used, and the doc-following author
90
+ * dead-ended (placeholder GUIDs hand-written into bindings.json, 401 at the
91
+ * Slack node).
92
+ */
93
+ export function selectConnection(rows, connectorKey, name, scope = "the tenant") {
94
+ const matched = enabledConnections(rows, connectorKey, name);
95
+ if (matched.length === 0) {
96
+ const qualifier = name ? ` named ${pyRepr(name)}` : "";
97
+ throw new ConnectionRefusal(`no enabled ${connectorKey} connection${qualifier} found in ${scope}. ` +
98
+ "List what exists with:\n" +
99
+ " uip is connections list --all-folders", "CONNECTION_NONE");
100
+ }
101
+ if (matched.length > 1) {
102
+ const listed = candidateLines(matched);
103
+ const names = new Set(matched.map((row) => pyStrAny(row.Name)));
104
+ // Python tested `name is not None`: an empty-string name takes this branch too.
105
+ const remedy = (name !== undefined && name !== null) || names.size < matched.length
106
+ ? "They share a name, so --connection <name> cannot pick one; pass " +
107
+ "the id of the one in the folder you mean with --connection-id:"
108
+ : "Name the one you want with --connection <name>, or pass its id " +
109
+ "with --connection-id:";
110
+ throw new ConnectionRefusal(`${matched.length} enabled ${connectorKey} connections in ${scope}, and they ` +
111
+ "are not interchangeable — a record that exists in one workspace need " +
112
+ `not exist in another. ${remedy}\n${listed}`, "CONNECTION_AMBIGUOUS");
113
+ }
114
+ return matched[0];
115
+ }
116
+ /**
117
+ * The one enabled connection for `connectorKey`, fetched, or throw.
118
+ *
119
+ * Tries the DEFAULT listing first and only widens to `--all-folders` if that
120
+ * finds nothing. That order matters and is not just politeness to the API: the
121
+ * default listing is the caller's own folder, which on a real tenant is one
122
+ * connection where the tenant-wide view is many. Widening first would turn an
123
+ * unambiguous case into an ambiguous one.
124
+ */
125
+ export async function discoverConnection(client, connectorKey, name) {
126
+ let rows = enabledConnections(await listConnections(client, { allFolders: false }), connectorKey, name);
127
+ let scope = "your folder";
128
+ if (rows.length === 0) {
129
+ rows = enabledConnections(await listConnections(client, { allFolders: true }), connectorKey, name);
130
+ scope = "the tenant";
131
+ }
132
+ return selectConnection(rows, connectorKey, name, scope);
133
+ }
134
+ /**
135
+ * Every enabled connection for `connectorKey`, own folder FIRST
136
+ * (`_connections.candidate_connections`).
137
+ *
138
+ * {@link discoverConnection} answers "which one" and stops at the first
139
+ * non-empty scope, which is right for picking a connection and wrong for
140
+ * retrying one: when the own-folder listing holds exactly one, the tenant-wide
141
+ * connections are never even looked at. So a lookup that exhausts the
142
+ * personal-workspace connection reports a bad VALUE while the record sits in
143
+ * another folder's connection (flow-builder-sdk#744, measured on two Jira
144
+ * connections where only the second carried project `TS`).
145
+ *
146
+ * Ordering is the same preference discovery encodes — the caller's own folder
147
+ * is the likeliest and is tried first — but nothing is dropped. Deduplicated
148
+ * by `Id`, because the tenant-wide listing repeats the own-folder rows; a row
149
+ * with no `Id` is never deduplicated, as the Python's `if identifier and …`
150
+ * did not.
151
+ *
152
+ * This does NOT decide anything, and in particular does not throw on several
153
+ * matches: choosing among them is the caller's job, and the only honest way to
154
+ * choose is evidence (a lookup that resolves on exactly one). That keeps this
155
+ * module's rule intact — ambiguity is never resolved by guessing.
156
+ */
157
+ export async function candidateConnections(client, connectorKey, name) {
158
+ const ordered = [];
159
+ const seen = new Set();
160
+ for (const allFolders of [false, true]) {
161
+ for (const row of enabledConnections(await listConnections(client, { allFolders }), connectorKey, name)) {
162
+ const identifier = pyStrAny(pyGet(row, "Id", ""));
163
+ if (identifier && seen.has(identifier))
164
+ continue;
165
+ seen.add(identifier);
166
+ ordered.push(row);
167
+ }
168
+ }
169
+ return ordered;
170
+ }
171
+ /** The enabled connections for `connectorKey`, one per line, pasteable. */
172
+ export function enabledListing(rows, connectorKey) {
173
+ const enabled = enabledConnections(rows, connectorKey);
174
+ if (enabled.length === 0) {
175
+ return (` (none — this tenant has no Enabled ${connectorKey} connection; ` +
176
+ "repair or create one in Integration Service first)");
177
+ }
178
+ return candidateLines(enabled);
179
+ }
180
+ /**
181
+ * The record for an explicitly named connection among `rows`, or throw unless it can serve.
182
+ *
183
+ * An explicit `--connection-id` used to skip every check discovery applies, and
184
+ * that skip is where a whole class of silent failures came from. A connection
185
+ * in `State: Failed` is listed like any other, and `uip is resources describe`
186
+ * against it answers `Result: Success` with NO request fields — the tenant has
187
+ * no schema to give for a connection it cannot reach. `prepare` then wrote an
188
+ * overlay with zero body inputs, printed "0 input field(s)" as a success, and
189
+ * the next `compile` rejected every real field as unknown. Measured on the eval
190
+ * tenant (Salesforce `Opportunity` / `Update`): the Failed connection returned
191
+ * 0 request fields, the Enabled one 13.
192
+ *
193
+ * So: resolve the id against the listing, and refuse — naming the state found
194
+ * and the enabled alternatives — rather than describe through it.
195
+ */
196
+ export function checkConnection(rows, connectorKey, connectionId) {
197
+ const match = rows.find((row) => pyStrAny(pyGet(row, "Id", "")) === pyStrAny(connectionId));
198
+ const alternatives = enabledListing(rows, connectorKey);
199
+ const remedy = "Or omit --connection-id and prepare selects the connection itself.";
200
+ if (match === undefined) {
201
+ throw new ConnectionRefusal(`connection ${connectionId} is not in this tenant's listing ` +
202
+ `(\`uip is connections list --all-folders\`). Enabled ${connectorKey} ` +
203
+ `connections:\n${alternatives}\n` +
204
+ remedy, "CONNECTION_UNKNOWN_ID");
205
+ }
206
+ const actualKey = match.ConnectorKey;
207
+ if (pyTruthy(actualKey) && actualKey !== connectorKey) {
208
+ throw new ConnectionRefusal(`connection ${pyStrAny(match.Name)} (${connectionId}) belongs to connector ` +
209
+ `${pyStrAny(actualKey)}, not ${connectorKey}. Enabled ${connectorKey} ` +
210
+ `connections:\n${alternatives}\n` +
211
+ remedy, "CONNECTION_WRONG_CONNECTOR");
212
+ }
213
+ const state = pyStrAny(pyOr(pyGet(match, "State", ""), ""));
214
+ if (state.toLowerCase() !== "enabled") {
215
+ const shown = state || "in an unknown state";
216
+ throw new ConnectionRefusal(`connection ${pyStrAny(match.Name)} (${connectionId}) is ${shown}, not ` +
217
+ "Enabled. A connection that is not Enabled answers field discovery " +
218
+ "with an EMPTY schema, so the overlay would carry no fields and the " +
219
+ "next compile would still reject every input as unknown. Enabled " +
220
+ `${connectorKey} connections:\n${alternatives}\n` +
221
+ remedy, "CONNECTION_NOT_ENABLED");
222
+ }
223
+ return match;
224
+ }
225
+ /** {@link checkConnection} against the tenant-wide listing — the id may live in another folder. */
226
+ export async function lookupConnection(client, connectorKey, connectionId) {
227
+ return checkConnection(await listConnections(client, { allFolders: true }), connectorKey, connectionId);
228
+ }
229
+ /**
230
+ * The enabled BYOA connections for `connectorKey`.
231
+ *
232
+ * THROWS on a failed listing rather than answering an empty list. The
233
+ * difference is the whole point: "you have no BYOA connection" tells the caller
234
+ * to go create one, and a listing that failed says nothing of the kind.
235
+ * Answering [] for both turns a broken call into confident, wrong advice.
236
+ */
237
+ export async function byoaConnections(client, connectorKey) {
238
+ return enabledConnections(await listConnections(client, { allFolders: true, byoa: true }), connectorKey);
239
+ }
240
+ /** The BYOA candidates, one pasteable line each, or why there are none. */
241
+ export function byoaListing(rows, connectorKey) {
242
+ if (rows.length === 0) {
243
+ return (` (none — this tenant has no enabled BYOA ${connectorKey} ` +
244
+ "connection; create one in Integration Service with your own " +
245
+ "OAuth app, then re-run)");
246
+ }
247
+ return candidateLines(rows);
248
+ }
249
+ function requiredKey(connection, key) {
250
+ if (!Object.prototype.hasOwnProperty.call(connection, key)) {
251
+ throw new Error(`connection record has no ${pyRepr(key)}`); // Python: KeyError
252
+ }
253
+ return pyStrAny(connection[key]);
254
+ }
255
+ /**
256
+ * Add connection + folder entries to a parsed `bindings.json`, without touching the input.
257
+ *
258
+ * ADDITIVE AND IDEMPOTENT. An existing entry under the same symbolic name (its
259
+ * `name` or `id`) is updated, never duplicated, and every other entry is left
260
+ * exactly as it was — `bindings.json` is an authored file that may already
261
+ * carry bindings for connectors this run knows nothing about. An updated row
262
+ * keeps its key order and gains any missing key at the end; a new row is
263
+ * written `id, name, resource, resourceKey, default, propertyAttribute`.
264
+ *
265
+ * `document` is what the file parsed to, or `undefined` when there was no file.
266
+ * As in the Python, a parsed value that is not an object is replaced by the
267
+ * default document.
268
+ */
269
+ export function mergeBindingsDocument(document, symbolicConnection, symbolicFolder, connection) {
270
+ let out;
271
+ if (isPlainObject(document)) {
272
+ const copy = { ...document };
273
+ if (!Object.prototype.hasOwnProperty.call(copy, "schemaVersion"))
274
+ copy.schemaVersion = "1";
275
+ if (!Object.prototype.hasOwnProperty.call(copy, "bindings"))
276
+ copy.bindings = [];
277
+ if (!Array.isArray(copy.bindings)) {
278
+ throw new TypeError(`bindings.json: "bindings" is not a list`); // Python: TypeError iterating it
279
+ }
280
+ copy.bindings = copy.bindings.map((entry) => (isPlainObject(entry) ? { ...entry } : entry));
281
+ out = copy;
282
+ }
283
+ else {
284
+ out = { schemaVersion: "1", bindings: [] };
285
+ }
286
+ const bindings = out.bindings;
287
+ const upsert = (symbolic, key, attribute) => {
288
+ for (const entry of bindings) {
289
+ if (!isPlainObject(entry))
290
+ continue;
291
+ if (entry.name === symbolic || entry.id === symbolic) {
292
+ const changed = entry.resourceKey !== key;
293
+ Object.assign(entry, {
294
+ id: pyGet(entry, "id", symbolic),
295
+ name: symbolic,
296
+ resource: "Connection",
297
+ resourceKey: key,
298
+ default: key,
299
+ propertyAttribute: attribute,
300
+ });
301
+ return changed;
302
+ }
303
+ }
304
+ bindings.push({
305
+ id: symbolic,
306
+ name: symbolic,
307
+ resource: "Connection",
308
+ resourceKey: key,
309
+ default: key,
310
+ propertyAttribute: attribute,
311
+ });
312
+ return true;
313
+ };
314
+ const wroteConnection = upsert(symbolicConnection, requiredKey(connection, "Id"), "ConnectionId");
315
+ const wroteFolder = upsert(symbolicFolder, requiredKey(connection, "FolderKey"), "FolderKey");
316
+ return { document: out, wroteConnection, wroteFolder };
317
+ }
318
+ /**
319
+ * `json.dump(document, indent=2)` + newline: the bytes `bindings.json` is written in.
320
+ *
321
+ * `allowIntegerKeys` for the same reason the ledger passes it (`objects.ts`):
322
+ * `bindings.json` is the author's file and may carry an integer-like key
323
+ * anywhere in a row this module never looks at. `json.dump` wrote such a file
324
+ * back; refusing it here left the file untouched and the connection unbound,
325
+ * with an error about a key the author did not know they had. The one thing
326
+ * that cannot be kept is that key's position — `JSON.parse` has already
327
+ * hoisted it to the front of its object — which Python preserved.
328
+ */
329
+ export function formatBindings(document) {
330
+ return `${pyJsonStringify(document, { indent: 2, allowIntegerKeys: true })}\n`;
331
+ }
332
+ /**
333
+ * Add connection + folder entries to `bindings.json` at `path`, in place.
334
+ *
335
+ * Returns `[wroteConnection, wroteFolder]` describing what changed, so the
336
+ * caller can report the specific thing rather than "updated bindings". The file
337
+ * is always rewritten, as the Python did. A file that will not parse is REFUSED,
338
+ * not overwritten: it is the author's, and not ours to discard.
339
+ */
340
+ export function mergeBindings(path, symbolicConnection, symbolicFolder, connection) {
341
+ let loaded = undefined;
342
+ if (existsSync(path)) {
343
+ try {
344
+ loaded = JSON.parse(readFileSync(path, "utf8"));
345
+ }
346
+ catch {
347
+ throw new ConnectionRefusal(`${path} exists but is not readable JSON. Fix or remove it; ` +
348
+ "this command will not overwrite a file it cannot parse.", "BINDINGS_UNREADABLE");
349
+ }
350
+ }
351
+ const merged = mergeBindingsDocument(loaded, symbolicConnection, symbolicFolder, connection);
352
+ writeFileSync(path, formatBindings(merged.document), "utf8");
353
+ return [merged.wroteConnection, merged.wroteFolder];
354
+ }
355
+ /**
356
+ * Remember `bindings.json` byte-for-byte, before a stage that writes it.
357
+ *
358
+ * `undefined` means the file could not be read — normally that it did not
359
+ * exist — and {@link restoreBindings} will remove whatever was written since.
360
+ */
361
+ export function snapshotBindings(path) {
362
+ if (!path)
363
+ return undefined;
364
+ try {
365
+ return new Uint8Array(readFileSync(path));
366
+ }
367
+ catch {
368
+ return undefined;
369
+ }
370
+ }
371
+ /**
372
+ * Put `bindings.json` back exactly as {@link snapshotBindings} found it.
373
+ *
374
+ * A refusal must leave `bindings.json` as it was: the BYOA fact needs a
375
+ * connection to discover, so stage 0 has already written one by the time the
376
+ * event says it is the wrong KIND — without the rollback the overlay keeps
377
+ * pointing at a connection the tool just forbade. A failure to restore is
378
+ * reported through `warn` (stderr by default), never thrown: the refusal being
379
+ * delivered is the message that matters.
380
+ *
381
+ * `shown` is the path as the caller typed it (`args.bindings`), which is what
382
+ * the Python printed; `path` is where the file actually is.
383
+ */
384
+ export function restoreBindings(path, snapshot, warn = (message) => process.stderr.write(`${message}\n`), shown = path) {
385
+ if (!path)
386
+ return;
387
+ try {
388
+ if (snapshot === undefined) {
389
+ if (existsSync(path))
390
+ rmSync(path);
391
+ }
392
+ else {
393
+ writeFileSync(path, snapshot);
394
+ }
395
+ }
396
+ catch (error) {
397
+ warn(` WARN: could not restore ${shown} after refusing the connection: ${messageOf(error)}`);
398
+ }
399
+ }
@@ -0,0 +1,96 @@
1
+ import { type BuildContext } from "../library-build/context.js";
2
+ import { type EventFacts } from "../library-build/events.js";
3
+ import { type Dict } from "../library-build/py.js";
4
+ import type { LibraryEntry } from "../model/library.js";
5
+ import type { UipClient } from "../uip/client.js";
6
+ /**
7
+ * `_event_operation`: the operation key every trigger API takes.
8
+ *
9
+ * NOT `operation.name`: that is the object's CRUD verb (`Create` on outlook
10
+ * `email-received`), while the event's own operation is `EMAIL_RECEIVED`,
11
+ * which `extractCanonical` stamps as `eventOperation` from the manifest's
12
+ * `model.context`. `name` happens to fall back to the same value when the
13
+ * manifest omits `connectorMethodInfo.operation`, so reading it works or
14
+ * silently queries the WRONG operation depending on which fallback rung a
15
+ * connector's manifest exercises.
16
+ */
17
+ export declare function eventOperation(operation: Record<string, unknown>): string;
18
+ /** The START and WAIT node types of one event. */
19
+ export declare function eventNodeTypes(connectorKey: string, action: string): {
20
+ trigger: string;
21
+ event: string;
22
+ };
23
+ /** The stored definition and its library entry, both re-keyed to the WAIT form. */
24
+ export interface EventDefinition {
25
+ node: Dict;
26
+ entry: LibraryEntry;
27
+ }
28
+ /**
29
+ * The one stored shape (see the module comment): the definition with the
30
+ * describe folded in, or with the object row's `Parameters` unioned into its
31
+ * own `eventParameters`; the entry's `outputSchema` taken from the definition's
32
+ * `outputResponseDefinition` when that has fields. Pure — copies of both inputs.
33
+ */
34
+ export declare function assembleEventDefinition(node: Dict, entry: LibraryEntry, describe: Dict | undefined, objectRow: Record<string, unknown> | undefined, eventNodeType: string): EventDefinition;
35
+ /** What stage 0 wrote to `bindings.json`, so a refusal here can undo exactly that. */
36
+ export interface BindingsRollback {
37
+ path: string;
38
+ /** The file before stage 0 touched it; `undefined` when it did not exist. */
39
+ snapshot: Uint8Array | undefined;
40
+ /** `path` as the caller typed `--bindings`, for the lines that name it; defaults to `path`. */
41
+ shown?: string;
42
+ }
43
+ /**
44
+ * `_refuse_unless_byoa_connection`: refuse a standard connection on an event
45
+ * object that requires BYOA. V1 measured the failure this prevents — the
46
+ * trigger deploys and then faults at runtime with a webhook error naming
47
+ * nothing about connections — and the requirement is knowable now.
48
+ *
49
+ * A refusal must leave nothing behind: stage 0 has already written this
50
+ * connection into `bindings.json` by the time the event's own metadata says it
51
+ * is the wrong KIND, so the file is restored first. A listing that FAILED is
52
+ * not a tenant with no BYOA connections; the remedies are opposite.
53
+ */
54
+ export declare function refuseUnlessByoaConnection(client: UipClient, connectorKey: string, connectionId: string, rollback: BindingsRollback | undefined, warn: (line: string) => void): Promise<void>;
55
+ export interface EventPrepareOptions {
56
+ connectorKey: string;
57
+ action: string;
58
+ connectionId: string;
59
+ /** `--object`: the ONE object a Generic event fires for. */
60
+ objectName?: string | null;
61
+ allObjects?: boolean;
62
+ /** The raw `-f` flags — an event has no parent fields, so any is a refusal. */
63
+ fields?: readonly string[];
64
+ resolve?: readonly string[];
65
+ refreshLookups?: boolean;
66
+ /** The overlay directory to write (resolved). */
67
+ out: string;
68
+ /** `out` as the caller spelled `--out`; every printed line names this, as the Python printed `args.out`. Defaults to `out`. */
69
+ outShown?: string;
70
+ importSpecifier: string;
71
+ jsonOnly?: boolean;
72
+ /** The symbolic names stage 0 bound, for the `next:` subscription. */
73
+ boundConnection?: string | null;
74
+ boundFolder?: string | null;
75
+ bindings?: BindingsRollback;
76
+ /** How the pasteable `registry prepare` is spelled in the vocabulary print. */
77
+ prepareCommand?: string;
78
+ }
79
+ export interface EventPrepareResult {
80
+ eventNodeType: string;
81
+ operation: string;
82
+ generic: boolean;
83
+ /** The API's spelling of the object, for a generic event. */
84
+ objectName?: string;
85
+ facts: EventFacts;
86
+ whereFields: unknown[];
87
+ filterFields: unknown[];
88
+ definition: EventDefinition;
89
+ }
90
+ /**
91
+ * `_prepare_event`. Returns `undefined` when `<key> <action>` is not an event
92
+ * either, so the caller can raise the ordinary unknown-action refusal;
93
+ * everything else is handled here, refusals included. `ctx.connectionId` must
94
+ * already be the connection: the trigger definition only answers with one.
95
+ */
96
+ export declare function prepareEvent(ctx: BuildContext, options: EventPrepareOptions): Promise<EventPrepareResult | undefined>;