@elastic.io/mcp 1.0.0-beta

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 (174) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +173 -0
  3. package/README.npm.md +173 -0
  4. package/dist/api/client.d.ts +12 -0
  5. package/dist/api/client.d.ts.map +1 -0
  6. package/dist/api/client.js +129 -0
  7. package/dist/api/client.js.map +1 -0
  8. package/dist/api/jsonapi.d.ts +21 -0
  9. package/dist/api/jsonapi.d.ts.map +1 -0
  10. package/dist/api/jsonapi.js +32 -0
  11. package/dist/api/jsonapi.js.map +1 -0
  12. package/dist/api/logger.d.ts +8 -0
  13. package/dist/api/logger.d.ts.map +1 -0
  14. package/dist/api/logger.js +43 -0
  15. package/dist/api/logger.js.map +1 -0
  16. package/dist/api/samples.d.ts +15 -0
  17. package/dist/api/samples.d.ts.map +1 -0
  18. package/dist/api/samples.js +465 -0
  19. package/dist/api/samples.js.map +1 -0
  20. package/dist/api/verification.d.ts +6 -0
  21. package/dist/api/verification.d.ts.map +1 -0
  22. package/dist/api/verification.js +71 -0
  23. package/dist/api/verification.js.map +1 -0
  24. package/dist/cli.d.ts +3 -0
  25. package/dist/cli.d.ts.map +1 -0
  26. package/dist/cli.js +9 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/config.d.ts +15 -0
  29. package/dist/config.d.ts.map +1 -0
  30. package/dist/config.js +44 -0
  31. package/dist/config.js.map +1 -0
  32. package/dist/embeddings/factory.d.ts +7 -0
  33. package/dist/embeddings/factory.d.ts.map +1 -0
  34. package/dist/embeddings/factory.js +25 -0
  35. package/dist/embeddings/factory.js.map +1 -0
  36. package/dist/embeddings/google.d.ts +19 -0
  37. package/dist/embeddings/google.d.ts.map +1 -0
  38. package/dist/embeddings/google.js +132 -0
  39. package/dist/embeddings/google.js.map +1 -0
  40. package/dist/embeddings/types.d.ts +27 -0
  41. package/dist/embeddings/types.d.ts.map +1 -0
  42. package/dist/embeddings/types.js +2 -0
  43. package/dist/embeddings/types.js.map +1 -0
  44. package/dist/index.d.ts +3 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +227 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/paths.d.ts +2 -0
  49. package/dist/paths.d.ts.map +1 -0
  50. package/dist/paths.js +10 -0
  51. package/dist/paths.js.map +1 -0
  52. package/dist/server.d.ts +29 -0
  53. package/dist/server.d.ts.map +1 -0
  54. package/dist/server.js +385 -0
  55. package/dist/server.js.map +1 -0
  56. package/dist/setup.d.ts +3 -0
  57. package/dist/setup.d.ts.map +1 -0
  58. package/dist/setup.js +338 -0
  59. package/dist/setup.js.map +1 -0
  60. package/dist/tools/confirmation.d.ts +37 -0
  61. package/dist/tools/confirmation.d.ts.map +1 -0
  62. package/dist/tools/confirmation.js +120 -0
  63. package/dist/tools/confirmation.js.map +1 -0
  64. package/dist/tools/constants.d.ts +28 -0
  65. package/dist/tools/constants.d.ts.map +1 -0
  66. package/dist/tools/constants.js +92 -0
  67. package/dist/tools/constants.js.map +1 -0
  68. package/dist/tools/definitions.d.ts +3 -0
  69. package/dist/tools/definitions.d.ts.map +1 -0
  70. package/dist/tools/definitions.js +915 -0
  71. package/dist/tools/definitions.js.map +1 -0
  72. package/dist/tools/flow-build.d.ts +18 -0
  73. package/dist/tools/flow-build.d.ts.map +1 -0
  74. package/dist/tools/flow-build.js +295 -0
  75. package/dist/tools/flow-build.js.map +1 -0
  76. package/dist/tools/flow-evidence.d.ts +62 -0
  77. package/dist/tools/flow-evidence.d.ts.map +1 -0
  78. package/dist/tools/flow-evidence.js +128 -0
  79. package/dist/tools/flow-evidence.js.map +1 -0
  80. package/dist/tools/flow-state-machine.d.ts +69 -0
  81. package/dist/tools/flow-state-machine.d.ts.map +1 -0
  82. package/dist/tools/flow-state-machine.js +292 -0
  83. package/dist/tools/flow-state-machine.js.map +1 -0
  84. package/dist/tools/handlers/auth.d.ts +3 -0
  85. package/dist/tools/handlers/auth.d.ts.map +1 -0
  86. package/dist/tools/handlers/auth.js +410 -0
  87. package/dist/tools/handlers/auth.js.map +1 -0
  88. package/dist/tools/handlers/component.d.ts +3 -0
  89. package/dist/tools/handlers/component.d.ts.map +1 -0
  90. package/dist/tools/handlers/component.js +165 -0
  91. package/dist/tools/handlers/component.js.map +1 -0
  92. package/dist/tools/handlers/data-sample.d.ts +3 -0
  93. package/dist/tools/handlers/data-sample.d.ts.map +1 -0
  94. package/dist/tools/handlers/data-sample.js +83 -0
  95. package/dist/tools/handlers/data-sample.js.map +1 -0
  96. package/dist/tools/handlers/dynamic.d.ts +3 -0
  97. package/dist/tools/handlers/dynamic.d.ts.map +1 -0
  98. package/dist/tools/handlers/dynamic.js +139 -0
  99. package/dist/tools/handlers/dynamic.js.map +1 -0
  100. package/dist/tools/handlers/flow.d.ts +9 -0
  101. package/dist/tools/handlers/flow.d.ts.map +1 -0
  102. package/dist/tools/handlers/flow.js +427 -0
  103. package/dist/tools/handlers/flow.js.map +1 -0
  104. package/dist/tools/handlers/log.d.ts +3 -0
  105. package/dist/tools/handlers/log.d.ts.map +1 -0
  106. package/dist/tools/handlers/log.js +92 -0
  107. package/dist/tools/handlers/log.js.map +1 -0
  108. package/dist/tools/handlers/organization.d.ts +3 -0
  109. package/dist/tools/handlers/organization.d.ts.map +1 -0
  110. package/dist/tools/handlers/organization.js +39 -0
  111. package/dist/tools/handlers/organization.js.map +1 -0
  112. package/dist/tools/handlers/raw.d.ts +3 -0
  113. package/dist/tools/handlers/raw.d.ts.map +1 -0
  114. package/dist/tools/handlers/raw.js +80 -0
  115. package/dist/tools/handlers/raw.js.map +1 -0
  116. package/dist/tools/handlers/search.d.ts +3 -0
  117. package/dist/tools/handlers/search.d.ts.map +1 -0
  118. package/dist/tools/handlers/search.js +105 -0
  119. package/dist/tools/handlers/search.js.map +1 -0
  120. package/dist/tools/handlers/session.d.ts +3 -0
  121. package/dist/tools/handlers/session.d.ts.map +1 -0
  122. package/dist/tools/handlers/session.js +22 -0
  123. package/dist/tools/handlers/session.js.map +1 -0
  124. package/dist/tools/handlers.d.ts +12 -0
  125. package/dist/tools/handlers.d.ts.map +1 -0
  126. package/dist/tools/handlers.js +202 -0
  127. package/dist/tools/handlers.js.map +1 -0
  128. package/dist/tools/helpers.d.ts +44 -0
  129. package/dist/tools/helpers.d.ts.map +1 -0
  130. package/dist/tools/helpers.js +195 -0
  131. package/dist/tools/helpers.js.map +1 -0
  132. package/dist/tools/types.d.ts +39 -0
  133. package/dist/tools/types.d.ts.map +1 -0
  134. package/dist/tools/types.js +2 -0
  135. package/dist/tools/types.js.map +1 -0
  136. package/dist/tools/validation.d.ts +35 -0
  137. package/dist/tools/validation.d.ts.map +1 -0
  138. package/dist/tools/validation.js +800 -0
  139. package/dist/tools/validation.js.map +1 -0
  140. package/dist/types/index.d.ts +47 -0
  141. package/dist/types/index.d.ts.map +1 -0
  142. package/dist/types/index.js +2 -0
  143. package/dist/types/index.js.map +1 -0
  144. package/dist/vectorstore/factory.d.ts +7 -0
  145. package/dist/vectorstore/factory.d.ts.map +1 -0
  146. package/dist/vectorstore/factory.js +25 -0
  147. package/dist/vectorstore/factory.js.map +1 -0
  148. package/dist/vectorstore/qdrant.d.ts +11 -0
  149. package/dist/vectorstore/qdrant.d.ts.map +1 -0
  150. package/dist/vectorstore/qdrant.js +107 -0
  151. package/dist/vectorstore/qdrant.js.map +1 -0
  152. package/dist/vectorstore/types.d.ts +28 -0
  153. package/dist/vectorstore/types.d.ts.map +1 -0
  154. package/dist/vectorstore/types.js +2 -0
  155. package/dist/vectorstore/types.js.map +1 -0
  156. package/package.json +66 -0
  157. package/resources/docs/flow-building-guide.md +581 -0
  158. package/resources/docs/rest-api-pagination.md +58 -0
  159. package/resources/docs/standardized-components-guide.md +66 -0
  160. package/resources/docs/system-instructions.md +207 -0
  161. package/resources/flow-examples/batch-flow-1-collect.json +51 -0
  162. package/resources/flow-examples/batch-flow-2-process.json +70 -0
  163. package/resources/flow-examples/code-component-transform.json +65 -0
  164. package/resources/flow-examples/code-component-trigger.json +52 -0
  165. package/resources/flow-examples/code-component-with-credentials-trigger.json +53 -0
  166. package/resources/flow-examples/code-component-with-credentials.json +67 -0
  167. package/resources/flow-examples/filter-step.json +63 -0
  168. package/resources/flow-examples/polling-sync-trigger.json +50 -0
  169. package/resources/flow-examples/router-branching.json +94 -0
  170. package/resources/flow-examples/splitter-re-assemble.json +72 -0
  171. package/resources/flow-examples/splitter-split.json +68 -0
  172. package/resources/flow-examples/timer-query-action-sync.json +68 -0
  173. package/resources/flow-examples/webhook-async-sync.json +49 -0
  174. package/resources/flow-examples/webhook-request-reply.json +63 -0
@@ -0,0 +1,800 @@
1
+ import { verifyCredentials } from '../api/verification.js';
2
+ import { credentialKey, descriptorFor, evidence, nodeCredentialArgs, nodeMetadataKey, operation } from './flow-evidence.js';
3
+ import { logger } from '../api/logger.js';
4
+ import { deserializeJsonApi } from '../api/jsonapi.js';
5
+ import { resolveCredentialId } from './helpers.js';
6
+ import { DEPRECATED_COMPONENTS, getDeprecatedActionReplacement, BARE_FIELD_NAMES, UNSUPPORTED_ROUTER_JSONATA_FUNCTIONS, REST_API_READER_FIELDS, PLACEHOLDER_FIELD_VALUES, } from './constants.js';
7
+ export { DEPRECATED_COMPONENTS, DEPRECATED_ACTIONS, DEPRECATED_COMPONENT_ACTIONS, getDeprecatedActionReplacement } from './constants.js';
8
+ export function checkFlowLifecycle(flowId, discoveryState, existingFlow, _operation) {
9
+ const errors = [];
10
+ if (!discoveryState.inspectedFlows.has(flowId)) {
11
+ errors.push(`You have not inspected flow '${flowId}' using 'get_flow' in this session. ` +
12
+ `Playbook B requires you to call 'get_flow' FIRST to inspect the current graph before making modifications. ` +
13
+ `Call 'get_flow' with flowId '${flowId}' and review the result before retrying.`);
14
+ }
15
+ if (existingFlow) {
16
+ const status = existingFlow.status || existingFlow.attributes?.status;
17
+ if (status === 'active') {
18
+ errors.push(`Flow '${flowId}' is currently ACTIVE (status: "${status}"). ` +
19
+ `Playbook B requires you to call 'stop_flow' and wait until the flow is inactive BEFORE applying modifications. ` +
20
+ `Call 'stop_flow' with flowId '${flowId}', then poll 'get_flow' until the status is "inactive", then retry.`);
21
+ }
22
+ }
23
+ return errors;
24
+ }
25
+ export function checkFlowBuildingGuideRead(discoveryState) {
26
+ if (!discoveryState.flowBuildingGuideRead) {
27
+ return [
28
+ `You have NOT read the 'elasticio://docs/flow-building-guide' resource in this session. ` +
29
+ `Rule 5 requires you to read this resource BEFORE creating or updating any flow to retrieve the correct JSON payload structures, schemas, and mapping rules. ` +
30
+ `Read the resource 'elasticio://docs/flow-building-guide' first, then retry.`
31
+ ];
32
+ }
33
+ return [];
34
+ }
35
+ function stripStringLiterals(str) {
36
+ let result = '';
37
+ let inDoubleQuote = false;
38
+ let inSingleQuote = false;
39
+ let escaped = false;
40
+ for (let i = 0; i < str.length; i++) {
41
+ const char = str[i];
42
+ if (escaped) {
43
+ escaped = false;
44
+ continue;
45
+ }
46
+ if (char === '\\') {
47
+ escaped = true;
48
+ continue;
49
+ }
50
+ if (char === '"' && !inSingleQuote) {
51
+ inDoubleQuote = !inDoubleQuote;
52
+ continue;
53
+ }
54
+ if (char === "'" && !inDoubleQuote) {
55
+ inSingleQuote = !inSingleQuote;
56
+ continue;
57
+ }
58
+ if (!inDoubleQuote && !inSingleQuote) {
59
+ result += char;
60
+ }
61
+ }
62
+ return result;
63
+ }
64
+ /**
65
+ * Validate REST API reader fields on a node (url quoting, headers format, body format).
66
+ * Shared between validateFlowGraph (pre-submit) and healthCheckFlow (post-submit).
67
+ */
68
+ function validateRestApiReader(reader, stepLabel, errors, warnings) {
69
+ // Check for plain URLs in JSONata-mode fields
70
+ for (const [fieldKey, fieldValue] of Object.entries(reader)) {
71
+ if (typeof fieldValue === 'string') {
72
+ const trimmedVal = fieldValue.trim();
73
+ if (/^https?:\/\//i.test(trimmedVal)) {
74
+ errors.push(`${stepLabel} field 'reader.${fieldKey}' contains a plain URL "${trimmedVal}" which is invalid JSONata syntax ` +
75
+ `(the ":" in the protocol causes a parse error). ` +
76
+ `This field uses JSONata Mode — wrap the URL in escaped double quotes so it is treated as a string literal: ` +
77
+ `"\\\"${trimmedVal}\\\"".`);
78
+ }
79
+ }
80
+ }
81
+ // Headers must be array of {key, value}, not a plain object
82
+ if (reader.headers && !Array.isArray(reader.headers)) {
83
+ errors.push(`${stepLabel} field 'reader.headers' is a plain object, but the REST API V2 component (RESTAPIView) ` +
84
+ `requires headers as an array of {key, value} pairs: [{"key": "Content-Type", "value": "\"application/json\"", "frozen": true}]. ` +
85
+ `Passing a plain object causes "TypeError: r.map is not a function" in the frontend.`);
86
+ }
87
+ // Body format for POST/PUT/PATCH
88
+ const method = reader.method;
89
+ if (method && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
90
+ const body = reader.body;
91
+ if (!body) {
92
+ errors.push(`${stepLabel} uses HTTP method '${method}' but 'reader.body' is missing. ` +
93
+ `For POST/PUT/PATCH requests, you MUST include a body object: ` +
94
+ `{"raw": "<JSONata expression or static JSON>", "formData": [], "urlencoded": [], "contentType": "application/json"}.`);
95
+ }
96
+ else if (typeof body === 'string') {
97
+ errors.push(`${stepLabel} field 'reader.body' is a plain string, but it MUST be an object: ` +
98
+ `{"raw": "${body}", "formData": [], "urlencoded": [], "contentType": "application/json"}. ` +
99
+ `Passing a string causes "TypeError: Cannot create property 'raw' on string" in the frontend.`);
100
+ }
101
+ else if (typeof body === 'object') {
102
+ if (body.raw === undefined) {
103
+ errors.push(`${stepLabel} field 'reader.body' is missing the 'raw' property. ` +
104
+ `Body must include: {"raw": "<content>", "formData": [], "urlencoded": [], "contentType": "application/json"}.`);
105
+ }
106
+ else if (body.raw === '' || body.raw === null) {
107
+ warnings.push(`${stepLabel} field 'reader.body.raw' is empty — the ${method} request will send no data. ` +
108
+ `Set it to a JSONata expression (e.g., "$getPassthrough().step_X.body") or a static JSON string.`);
109
+ }
110
+ if (!Array.isArray(body.formData)) {
111
+ errors.push(`${stepLabel} field 'reader.body.formData' must be an array (use [] if not needed).`);
112
+ }
113
+ if (!Array.isArray(body.urlencoded)) {
114
+ errors.push(`${stepLabel} field 'reader.body.urlencoded' must be an array (use [] if not needed).`);
115
+ }
116
+ if (!body.contentType) {
117
+ errors.push(`${stepLabel} field 'reader.body.contentType' is missing. Set it to the body content type (e.g., "application/json").`);
118
+ }
119
+ }
120
+ }
121
+ }
122
+ /** Validate all nodes in a flow graph: id, command format, deprecated components/actions, fields. */
123
+ function validateNodes(nodes, nodeMap, errors, warnings) {
124
+ for (const node of nodes) {
125
+ if (!node.id) {
126
+ errors.push("Node definition is missing the 'id' field.");
127
+ continue;
128
+ }
129
+ const stepIdRegex = /^step_[1-9]\d*$/;
130
+ if (!stepIdRegex.test(node.id)) {
131
+ errors.push(`Node ID '${node.id}' has an invalid format. ` +
132
+ 'Every step_id must follow the format "step_N" where N is a positive integer starting from 1 ' +
133
+ '(e.g., "step_1", "step_2", "step_3"). ' +
134
+ 'Do not use formats like "step1", "Step_1", "step_0", "step_01", or custom names like "step_lk_from".');
135
+ }
136
+ if (nodeMap.has(node.id))
137
+ errors.push(`Duplicate node ID '${node.id}'.`);
138
+ nodeMap.set(node.id, node);
139
+ const command = node.command;
140
+ if (!command) {
141
+ errors.push(`Node '${node.id}' is missing the 'command' field.`);
142
+ }
143
+ else {
144
+ const formatRegex = /^[^/]+\/[^:]+:[^@]+@[^@]+$/;
145
+ if (!formatRegex.test(command)) {
146
+ errors.push(`Node '${node.id}' has an invalid command format: "${command}". ` +
147
+ 'It must be formatted as "{team_name}/{repo_name}:{action_or_trigger_id}@{latestVersionId}" ' +
148
+ '(e.g., "elasticio/salesforce-v2:getUpdatedObjectsPolling@latestVersionId"). ' +
149
+ 'Retrieve this from the \'get_component\' tool response.');
150
+ }
151
+ // Check for deprecated components by command prefix
152
+ const prefixMatch = command.match(/^([^:]+):/);
153
+ const commandPrefix = prefixMatch ? prefixMatch[1] : '';
154
+ if (commandPrefix) {
155
+ const successor = DEPRECATED_COMPONENTS[commandPrefix];
156
+ if (successor) {
157
+ errors.push(`Node '${node.id}' uses deprecated component '${commandPrefix}'. ` +
158
+ `This component is no longer supported and may not work correctly in the UI. ` +
159
+ `Use '${successor}' instead. Call 'list_components' to find the replacement.`);
160
+ }
161
+ }
162
+ // Check for deprecated actions/triggers by action ID scoped to component
163
+ const actionMatch = command.match(/^[^:]+:([^@]+)@/);
164
+ if (actionMatch && commandPrefix) {
165
+ const actionId = actionMatch[1];
166
+ const replacement = getDeprecatedActionReplacement(commandPrefix, actionId);
167
+ if (replacement) {
168
+ errors.push(`Node '${node.id}' uses deprecated action '${actionId}'. ` +
169
+ `This action is marked as deprecated in the component descriptor and shows a "Deprecated" badge in the UI. ` +
170
+ `Use '${replacement}' instead. Call 'get_component' to verify available non-deprecated actions.`);
171
+ }
172
+ }
173
+ }
174
+ // Validate JSONata-mode node fields (e.g. Splitter's "expression") for bare references
175
+ // that should use $getPassthrough() paths
176
+ if (node.fields && node.component_id) {
177
+ for (const [fieldKey, fieldValue] of Object.entries(node.fields)) {
178
+ if (typeof fieldValue === 'string') {
179
+ const trimmedField = fieldValue.trim();
180
+ // Catch bare "body" or "body.something" that should be $getPassthrough().step_id.body
181
+ if (/^body(\.[a-zA-Z_]\w*)*$/.test(trimmedField)) {
182
+ errors.push(`Node '${node.id}' field '${fieldKey}' contains a bare reference "${trimmedField}" which will resolve to empty/undefined at runtime. ` +
183
+ `If this is a JSONata expression field (e.g. Splitter expression), you MUST use the passthrough path format: ` +
184
+ `"$getPassthrough().step_id.${trimmedField}" where step_id is the ID of the step that produces the data ` +
185
+ `(e.g., "$getPassthrough().step_1.${trimmedField}").`);
186
+ }
187
+ // Warn about deep $getPassthrough() paths in JSONata fields (e.g. Splitter expression).
188
+ const deepPathMatch = /^\$getPassthrough\(\)\.\w+\.body\.[a-zA-Z_]\w*(\.[a-zA-Z_]\w*)+$/.exec(trimmedField);
189
+ if (deepPathMatch) {
190
+ warnings.push(`Node '${node.id}' field '${fieldKey}' uses a deep JSONata path "${trimmedField}". ` +
191
+ `The UI form validator only accepts paths up to "$getPassthrough().step_X.body" — deeper paths ` +
192
+ `cause a red indicator and blocked Continue button in the UI, even though they work at runtime. ` +
193
+ `Consider using "$getPassthrough().step_X.body" and restructuring the incoming data to send the array ` +
194
+ `directly as the webhook/trigger body, or add a Code component before the Splitter to extract the nested array.`);
195
+ }
196
+ }
197
+ }
198
+ }
199
+ // Validate placeholder/dummy values in fields that use dynamic select models.
200
+ if (node.fields) {
201
+ for (const [fieldKey, fieldValue] of Object.entries(node.fields)) {
202
+ if (typeof fieldValue === 'string' && PLACEHOLDER_FIELD_VALUES.includes(fieldValue)) {
203
+ warnings.push(`Node '${node.id}' field '${fieldKey}' has a placeholder value "${fieldValue}" which likely means it was never ` +
204
+ `properly configured via a dynamic select model. Call 'init_dynamic_select_model' to retrieve the valid options ` +
205
+ `and select a real value.`);
206
+ }
207
+ }
208
+ }
209
+ // Validate REST API reader fields
210
+ if (node.fields?.reader && typeof node.fields.reader === 'object') {
211
+ validateRestApiReader(node.fields.reader, `Node '${node.id}'`, errors, warnings);
212
+ }
213
+ }
214
+ }
215
+ /** Validate all edges in a flow graph: references, mappers, router conditions. */
216
+ function validateEdges(edges, nodeMap, errors, warnings) {
217
+ // Identify router nodes for condition validation
218
+ const routerNodeIds = new Set();
219
+ for (const [id, node] of nodeMap) {
220
+ if (node.command && /router:route/.test(node.command)) {
221
+ routerNodeIds.add(id);
222
+ }
223
+ }
224
+ for (const edge of edges) {
225
+ const edgeId = edge.id || `${edge.source}:${edge.target}`;
226
+ if (!edge.source || !edge.target) {
227
+ errors.push(`Edge '${edgeId}' is missing 'source' or 'target' properties.`);
228
+ continue;
229
+ }
230
+ if (!nodeMap.has(edge.source)) {
231
+ errors.push(`Edge '${edgeId}' references source step '${edge.source}' which does not exist in the graph.`);
232
+ }
233
+ if (!nodeMap.has(edge.target)) {
234
+ errors.push(`Edge '${edgeId}' references target step '${edge.target}' which does not exist in the graph.`);
235
+ }
236
+ // Warn when edge has no mapper config at all for a non-first target step.
237
+ if (!edge.config?.mapper) {
238
+ const targetNode = nodeMap.get(edge.target);
239
+ if (targetNode && !targetNode.first) {
240
+ warnings.push(`Edge '${edgeId}' has no mapper configuration. Target step '${edge.target}' (${targetNode.name || 'unnamed'}) ` +
241
+ `will receive no mapped data from '${edge.source}', causing empty fields in the UI. ` +
242
+ `Add a 'config' with 'mapper_type: "jsonata"' and a 'mapper' object that maps the target step's input fields ` +
243
+ `using $getPassthrough() paths. For array-typed input fields, use JSONata array expressions starting with "[".`);
244
+ }
245
+ }
246
+ const mapper = edge.config?.mapper;
247
+ if (mapper) {
248
+ validateMapper(mapper, edgeId, errors);
249
+ }
250
+ // Validate router edge conditions for unsupported JSONata functions
251
+ if (edge.config?.condition && routerNodeIds.has(edge.source)) {
252
+ validateRouterCondition(edge.config.condition, edgeId, errors);
253
+ }
254
+ // Check for REST API reader fields misplaced in edge mapper.
255
+ if (mapper && typeof mapper === 'object' && !Array.isArray(mapper)) {
256
+ const targetNode = nodeMap.get(edge.target);
257
+ if (targetNode?.fields?.reader || targetNode?.command?.includes('rest-api-component') || targetNode?.command?.includes('REST-API')) {
258
+ const misplacedFields = REST_API_READER_FIELDS.filter(f => f in mapper);
259
+ if (misplacedFields.length > 0) {
260
+ errors.push(`Edge '${edgeId}' mapper contains REST API reader fields [${misplacedFields.join(', ')}] ` +
261
+ `that belong in the target node's 'fields.reader' object, NOT in the edge mapper. ` +
262
+ `Move these to node '${edge.target}' fields.reader and remove them from the edge mapper. ` +
263
+ `The edge mapper should only map input metadata fields.`);
264
+ }
265
+ }
266
+ }
267
+ // Check that router edges have conditions
268
+ if (routerNodeIds.has(edge.source) && !edge.config?.condition) {
269
+ errors.push(`Edge '${edgeId}' originates from router step '${edge.source}' but has no 'condition' in its config. ` +
270
+ `All outgoing edges from a router MUST have a 'condition' property with a JSONata boolean expression.`);
271
+ }
272
+ }
273
+ }
274
+ /** Validate mapper format and JSONata expressions within an edge mapper. */
275
+ function validateMapper(mapper, edgeId, errors) {
276
+ const mapperValues = [];
277
+ if (typeof mapper === 'string') {
278
+ const trimmed = mapper.trim();
279
+ // Allow root JSONata expressions: object constructors ({...}), blocks/parentheses ((...)), or array constructors ([...])
280
+ if (trimmed.startsWith('{') || trimmed.startsWith('(') || trimmed.startsWith('[')) {
281
+ mapperValues.push({ key: '_root', value: mapper });
282
+ }
283
+ else {
284
+ const hasOuterQuotes = (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
285
+ (trimmed.startsWith("'") && trimmed.endsWith("'"));
286
+ if (!hasOuterQuotes) {
287
+ errors.push(`Edge '${edgeId}' mapper is a string but does not start with '{', '(', or '['. ` +
288
+ `A root JSONata mapper expression must evaluate to an object or array ` +
289
+ `(e.g., "{\\n \\"fieldName\\": $getPassthrough().step_1.body.value\\n}").`);
290
+ }
291
+ mapperValues.push({ key: '_root', value: mapper });
292
+ }
293
+ }
294
+ else if (typeof mapper === 'object' && mapper !== null) {
295
+ for (const [key, val] of Object.entries(mapper)) {
296
+ if (Array.isArray(val)) {
297
+ for (let i = 0; i < val.length; i++) {
298
+ const item = val[i];
299
+ if (typeof item === 'string') {
300
+ mapperValues.push({ key: `${key}[${i}]`, value: item });
301
+ }
302
+ else if (item && typeof item === 'object') {
303
+ validateMapper(item, `${edgeId}.${key}[${i}]`, errors);
304
+ }
305
+ else if (typeof item === 'number' || typeof item === 'boolean') {
306
+ errors.push(`Edge '${edgeId}' mapper field '${key}[${i}]' must be a JSONata expression string.`);
307
+ }
308
+ }
309
+ }
310
+ else if (typeof val === 'number' || typeof val === 'boolean') {
311
+ errors.push(`Edge '${edgeId}' mapper field '${key}' must be a JSONata expression string.`);
312
+ }
313
+ else if (typeof val === 'string') {
314
+ mapperValues.push({ key, value: val });
315
+ }
316
+ else if (val && typeof val === 'object') {
317
+ validateMapper(val, `${edgeId}.${key}`, errors);
318
+ }
319
+ else {
320
+ errors.push(`Edge '${edgeId}' mapper field '${key}' must contain a JSONata expression.`);
321
+ }
322
+ }
323
+ }
324
+ for (const { key, value } of mapperValues) {
325
+ const fieldLabel = key === '_root' ? `mapper` : `mapper field '${key}'`;
326
+ const trimmed = value.trim();
327
+ const hasOuterQuotes = (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
328
+ (trimmed.startsWith("'") && trimmed.endsWith("'"));
329
+ if (hasOuterQuotes) {
330
+ const inner = trimmed.substring(1, trimmed.length - 1).trim();
331
+ if (inner.startsWith('{') || inner.startsWith('[') || inner.startsWith('$')) {
332
+ errors.push(`Edge '${edgeId}' ${fieldLabel} appears to be wrapped in outer double or single quotes: \`${value}\`. ` +
333
+ `This turns the JSONata expression into a static string literal, preventing variables and functions from resolving. ` +
334
+ `Make sure the JSONata expression value itself starts directly with the path, function, or object definition, without outer wrapping quotes.`);
335
+ }
336
+ }
337
+ const strippedValue = stripStringLiterals(trimmed);
338
+ for (const field of BARE_FIELD_NAMES) {
339
+ const fieldRegex = new RegExp(`(?<![$.])\\b${field}\\b(?!\\s*:)`, 'g');
340
+ if (fieldRegex.test(strippedValue)) {
341
+ errors.push(`Edge '${edgeId}' ${fieldLabel} uses direct variable reference \`${field}\` which does not resolve. ` +
342
+ `You MUST ALWAYS use the dynamic passthrough path format: \`$getPassthrough().step_id.body.${field}\`, ` +
343
+ `where \`step_id\` is the ID of the step that produced the data (e.g., \`$getPassthrough().step_1.body.${field}\`).`);
344
+ }
345
+ }
346
+ }
347
+ }
348
+ /** Validate a router edge condition for unsupported JSONata functions. */
349
+ function validateRouterCondition(condition, edgeId, errors) {
350
+ if (typeof condition !== 'string')
351
+ return;
352
+ const strippedCondition = stripStringLiterals(condition);
353
+ for (const fn of UNSUPPORTED_ROUTER_JSONATA_FUNCTIONS) {
354
+ // Escape the leading $ so it's matched literally, not as a regex anchor
355
+ const escapedFn = fn.replace(/\$/g, '\\$');
356
+ const fnRegex = new RegExp(`${escapedFn}\\s*\\(`, 'g');
357
+ if (fnRegex.test(strippedCondition)) {
358
+ errors.push(`Edge '${edgeId}' router condition uses '${fn}()' which is NOT available in the router condition context ` +
359
+ `and will cause "Attempted to invoke a non-function" error at runtime. ` +
360
+ `Use $substring() on date strings from passthrough data instead. ` +
361
+ `For example, to extract the hour from a fireTime: $substring($getPassthrough().step_1.body.fireTime, 11, 2) < "12".`);
362
+ }
363
+ }
364
+ }
365
+ /** Validate graph topology: exactly one root, DAG (no cycles), all nodes reachable. */
366
+ function validateTopology(nodes, edges, nodeMap, errors) {
367
+ if (nodes.length === 0)
368
+ return;
369
+ // Find root nodes (not a target of any edge)
370
+ const targets = new Set(edges.map(e => e.target));
371
+ const roots = nodes.filter(n => !targets.has(n.id));
372
+ if (roots.length === 0) {
373
+ errors.push('Flow graph has no root node (every node is a target of some edge — possible cycle).');
374
+ return;
375
+ }
376
+ // Exactly one node should have first:true
377
+ const firstNodes = nodes.filter(n => n.first);
378
+ if (firstNodes.length > 1) {
379
+ errors.push(`Multiple nodes are marked as 'first': ${firstNodes.map(n => n.id).join(', ')}. Only one trigger node should be first.`);
380
+ }
381
+ // Check for cycles using DFS
382
+ const adjacency = new Map();
383
+ for (const node of nodes)
384
+ adjacency.set(node.id, []);
385
+ for (const edge of edges)
386
+ adjacency.get(edge.source)?.push(edge.target);
387
+ const visited = new Set();
388
+ const inStack = new Set();
389
+ const hasCycle = (id) => {
390
+ if (inStack.has(id))
391
+ return true;
392
+ if (visited.has(id))
393
+ return false;
394
+ visited.add(id);
395
+ inStack.add(id);
396
+ for (const next of adjacency.get(id) || []) {
397
+ if (hasCycle(next))
398
+ return true;
399
+ }
400
+ inStack.delete(id);
401
+ return false;
402
+ };
403
+ for (const node of nodes) {
404
+ if (hasCycle(node.id)) {
405
+ errors.push('Flow graph contains a cycle, which is not allowed.');
406
+ return;
407
+ }
408
+ }
409
+ // Check all nodes are reachable from roots
410
+ const reachable = new Set();
411
+ const queue = roots.map(r => r.id);
412
+ while (queue.length > 0) {
413
+ const id = queue.shift();
414
+ if (reachable.has(id))
415
+ continue;
416
+ reachable.add(id);
417
+ for (const next of adjacency.get(id) || [])
418
+ queue.push(next);
419
+ }
420
+ const unreachable = nodes.filter(n => !reachable.has(n.id));
421
+ if (unreachable.length > 0) {
422
+ errors.push(`Nodes not reachable from any root: ${unreachable.map(n => n.id).join(', ')}.`);
423
+ }
424
+ }
425
+ export function validateFlowGraph(body) {
426
+ const errors = [];
427
+ const warnings = [];
428
+ const graph = body?.data?.attributes?.graph || body?.attributes?.graph || body?.graph;
429
+ if (!graph) {
430
+ return { errors, warnings };
431
+ }
432
+ const nodes = graph.nodes || [];
433
+ const edges = graph.edges || [];
434
+ if (!Array.isArray(nodes) || !Array.isArray(edges))
435
+ return { errors: ['graph.nodes and graph.edges must be arrays.'], warnings };
436
+ const nodeMap = new Map();
437
+ validateNodes(nodes, nodeMap, errors, warnings);
438
+ validateEdges(edges, nodeMap, errors, warnings);
439
+ validateTopology(nodes, edges, nodeMap, errors);
440
+ return { errors, warnings };
441
+ }
442
+ export function checkDiscoveryState(body, discoveryState, existingFlow) {
443
+ const errors = [];
444
+ const warnings = [];
445
+ const graph = body?.data?.attributes?.graph || body?.attributes?.graph || body?.graph;
446
+ if (!graph)
447
+ return { errors, warnings };
448
+ const nodes = graph.nodes || [];
449
+ const existingComponentIds = new Set();
450
+ const existingCredSecretIds = new Set();
451
+ if (existingFlow) {
452
+ const existingNodes = existingFlow?.graph?.nodes || [];
453
+ existingNodes.forEach((n) => {
454
+ if (n.component_id)
455
+ existingComponentIds.add(n.component_id);
456
+ const credId = resolveCredentialId(n.credentials_id);
457
+ if (credId)
458
+ existingCredSecretIds.add(credId);
459
+ const secId = resolveCredentialId(n.secret_id);
460
+ if (secId)
461
+ existingCredSecretIds.add(secId);
462
+ });
463
+ }
464
+ const OBJECT_ID_RE = /^[0-9a-f]{24}$/;
465
+ for (const node of nodes) {
466
+ // Format validation: component_id, credentials_id, secret_id must be valid 24-char hex ObjectIDs
467
+ if (node.component_id && !OBJECT_ID_RE.test(node.component_id)) {
468
+ errors.push(`Node '${node.id}' has invalid component_id '${node.component_id}' — must be a 24-character hex string (MongoDB ObjectID). ` +
469
+ `Do NOT use placeholder IDs from documentation examples. Call 'list_components' to discover real component IDs.`);
470
+ }
471
+ const rawCredId = resolveCredentialId(node.credentials_id);
472
+ if (rawCredId && !OBJECT_ID_RE.test(rawCredId)) {
473
+ errors.push(`Node '${node.id}' has invalid credentials_id '${rawCredId}' — must be a 24-character hex string. ` +
474
+ `Call 'list_credentials' to discover real credential IDs.`);
475
+ }
476
+ const rawSecId = resolveCredentialId(node.secret_id);
477
+ if (rawSecId && !OBJECT_ID_RE.test(rawSecId)) {
478
+ errors.push(`Node '${node.id}' has invalid secret_id '${rawSecId}' — must be a 24-character hex string. ` +
479
+ `Call 'list_secrets' to discover real secret IDs.`);
480
+ }
481
+ const oldNode = existingFlow?.graph?.nodes?.find((n) => n.id === node.id);
482
+ const isExistingNode = oldNode && oldNode.component_id === node.component_id && oldNode.command === node.command
483
+ && resolveCredentialId(oldNode.credentials_id) === rawCredId && resolveCredentialId(oldNode.secret_id) === rawSecId;
484
+ // Discovery checks only apply to new nodes
485
+ if (!isExistingNode) {
486
+ if (node.component_id && !existingComponentIds.has(node.component_id) && !discoveryState.components.has(node.component_id)) {
487
+ errors.push(`Node '${node.id}' references component ID '${node.component_id}', but you have not read its schema using 'get_component'. ` +
488
+ `You MUST call 'get_component' for this component first to retrieve its official actions, triggers, and fields schema.`);
489
+ }
490
+ const nodeCredId = resolveCredentialId(node.credentials_id);
491
+ if (nodeCredId && !existingCredSecretIds.has(nodeCredId) && !discoveryState.credentials.has(nodeCredId)) {
492
+ errors.push(`Node '${node.id}' references credential ID '${nodeCredId}', but you have not listed or discovered credentials for component '${node.component_id}' in this workspace. ` +
493
+ `You MUST search/list credentials via 'list_credentials' first before configuring them in the flow.`);
494
+ }
495
+ const nodeSecId = resolveCredentialId(node.secret_id);
496
+ if (nodeSecId && !existingCredSecretIds.has(nodeSecId) && !discoveryState.secrets.has(nodeSecId)) {
497
+ errors.push(`Node '${node.id}' references secret ID '${nodeSecId}', but you have not listed or discovered secrets for component '${node.component_id}' in this workspace. ` +
498
+ `You MUST search/list secrets via 'list_secrets' first before configuring them in the flow.`);
499
+ }
500
+ }
501
+ // Check secret state for ALL nodes (existing and new)
502
+ const secId = resolveCredentialId(node.secret_id);
503
+ if (secId && discoveryState.secrets.has(secId)) {
504
+ const secretState = discoveryState.secrets.get(secId);
505
+ if (secretState && secretState !== 'ready') {
506
+ errors.push(`Node '${node.id}' uses secret '${secId}' which is in '${secretState}' state. ` +
507
+ `Only secrets in 'ready' state can be used in flows. Please re-authenticate or choose a different secret.`);
508
+ }
509
+ }
510
+ // Check credential/secret verification results for ALL nodes
511
+ const credIdForVerify = resolveCredentialId(node.credentials_id) || resolveCredentialId(node.secret_id);
512
+ if (credIdForVerify && discoveryState.credentialVerifications.has(credIdForVerify)) {
513
+ const verification = discoveryState.credentialVerifications.get(credIdForVerify);
514
+ if (verification && !verification.ok) {
515
+ errors.push(`Node '${node.id}' uses credential/secret '${credIdForVerify}' which FAILED verification: ${verification.error}. ` +
516
+ `Fix or replace this credential/secret before using it in a flow.`);
517
+ }
518
+ }
519
+ // Auth check applies to ALL nodes (existing and new)
520
+ if (node.component_id && discoveryState.componentsRequiringAuth.has(node.component_id)
521
+ && !resolveCredentialId(node.credentials_id) && !resolveCredentialId(node.secret_id)) {
522
+ errors.push(`Node '${node.id}' uses component '${node.component_id}' which requires authentication, but no credentials_id or secret_id is set. ` +
523
+ `You MUST first find or create credentials/secrets using 'list_credentials'/'list_secrets' (or 'create_credentials'/'create_secret'), ` +
524
+ `then assign them to the node before creating or updating the flow.`);
525
+ }
526
+ // Warn when a Webhook step has no credentials — auth is optional but should always be proposed
527
+ if (node.command && typeof node.command === 'string' && /webhook.*:receive/.test(node.command)
528
+ && !resolveCredentialId(node.credentials_id) && !resolveCredentialId(node.secret_id)) {
529
+ warnings.push(`Node '${node.id}' is a Webhook trigger with no credentials attached. ` +
530
+ `Webhook authentication is optional but strongly recommended. Consider attaching credentials ` +
531
+ `to enable HMAC signature verification, API key validation, or basic auth. ` +
532
+ `Call 'list_credentials' for the Webhook component and ask the user which credential to attach.`);
533
+ }
534
+ }
535
+ return { errors, warnings };
536
+ }
537
+ export function checkMapperMetadataRetrieved(body, discoveryState, workspaceId = '') {
538
+ const graph = body?.data?.attributes?.graph || body?.attributes?.graph || body?.graph;
539
+ if (!graph)
540
+ return [];
541
+ const errors = [];
542
+ for (const edge of graph.edges || []) {
543
+ if (!edge.config?.mapper)
544
+ continue;
545
+ for (const id of [edge.source, edge.target]) {
546
+ const node = graph.nodes?.find((n) => n.id === id);
547
+ if (!node)
548
+ continue;
549
+ const op = operation(node);
550
+ const descriptor = descriptorFor(discoveryState, node);
551
+ const definition = descriptor?.actions?.[op.method] || descriptor?.triggers?.[op.method];
552
+ if (!definition)
553
+ errors.push(`Call get_component for ${op.componentId} at version ${op.version} before mapping step ${id}.`);
554
+ else if (definition.dynamicMetadata && evidence(discoveryState).metadata.get(nodeMetadataKey(workspaceId, node))?.status !== 'VERIFIED') {
555
+ errors.push(`Step ${id}: retrieve successful dynamic metadata for this exact version, connection and fields before mapping.`);
556
+ }
557
+ }
558
+ }
559
+ return [...new Set(errors)];
560
+ }
561
+ /**
562
+ * Runtime health check for a flow — validates credentials, required fields, URLs, and field values.
563
+ * Runs after create_flow/update_flow and before start_flow to catch config issues
564
+ * that would show as red indicators in the UI.
565
+ */
566
+ export async function healthCheckFlow(flow, client, discoveryState) {
567
+ const errors = [];
568
+ const warnings = [];
569
+ const stepStatuses = [];
570
+ const nodes = flow?.graph?.nodes || [];
571
+ if (nodes.length === 0) {
572
+ errors.push('Flow has no steps configured');
573
+ return { errors, warnings, stepStatuses };
574
+ }
575
+ for (const node of nodes) {
576
+ const stepLabel = `Step '${node.id}' (${node.name || 'unnamed'})`;
577
+ const errCountBefore = errors.length;
578
+ const warnCountBefore = warnings.length;
579
+ // 1. Verify credentials exist and are accessible
580
+ const healthCredId = resolveCredentialId(node.credentials_id);
581
+ if (healthCredId) {
582
+ try {
583
+ await client.get(`/credentials/${healthCredId}`);
584
+ }
585
+ catch {
586
+ errors.push(`${stepLabel}: credential '${healthCredId}' could not be fetched — it may have been deleted`);
587
+ }
588
+ }
589
+ const healthSecId = resolveCredentialId(node.secret_id);
590
+ if (healthSecId) {
591
+ try {
592
+ const workspaceId = flow?.workspace?.id || flow?.workspace?.data?.id;
593
+ if (workspaceId) {
594
+ const secretRes = await client.get(`/workspaces/${workspaceId}/secrets/${healthSecId}`);
595
+ const secret = secretRes.data?.data;
596
+ const state = secret?.attributes?.state;
597
+ if (state && state !== 'ready') {
598
+ errors.push(`${stepLabel}: secret '${healthSecId}' is in state '${state}', expected 'ready'`);
599
+ }
600
+ }
601
+ }
602
+ catch {
603
+ errors.push(`${stepLabel}: secret '${healthSecId}' could not be fetched — it may have been deleted`);
604
+ }
605
+ }
606
+ // 1b. Verify credentials actually work by calling verify-credential endpoint.
607
+ // A credential/secret can exist and be in "ready" state but still fail at runtime
608
+ // (e.g. OAuth token revoked, API key disabled, 403 Forbidden).
609
+ if (node.component_id) {
610
+ const credOrSecretId = healthCredId || healthSecId;
611
+ if (credOrSecretId) {
612
+ try {
613
+ const args = nodeCredentialArgs(flow?.workspace?.id || flow?.workspace?.data?.id, node);
614
+ const verifyResult = await verifyCredentials(client, args);
615
+ if (discoveryState)
616
+ evidence(discoveryState).credentials.set(credentialKey(args), verifyResult);
617
+ if (!verifyResult.ok) {
618
+ errors.push(`${stepLabel}: credential verification FAILED for '${credOrSecretId}'. ` +
619
+ `${verifyResult.error} ` +
620
+ `The credential may be expired, revoked, or the user no longer has access. ` +
621
+ `Re-authorize the credential in the UI or replace it with a working one.`);
622
+ }
623
+ }
624
+ catch (err) {
625
+ errors.push(`${stepLabel}: credential verification could not complete: ${err instanceof Error ? err.message : String(err)}`);
626
+ }
627
+ }
628
+ }
629
+ // 2. Check component and required fields
630
+ if (node.component_id) {
631
+ try {
632
+ // Use the descriptor endpoint to get actions/triggers definitions
633
+ const compRes = await client.get(`/components/${operation(node).componentId}/versions/${operation(node).version}/descriptor`);
634
+ const comp = deserializeJsonApi(compRes.data);
635
+ if (comp && discoveryState)
636
+ evidence(discoveryState).descriptors.set(`${operation(node).componentId}@${operation(node).version}`, comp);
637
+ if (!comp) {
638
+ errors.push(`${stepLabel}: component '${node.component_id}' returned empty data — it may be unavailable`);
639
+ }
640
+ else {
641
+ const actionOrTrigger = findActionOrTrigger(comp, node.command);
642
+ if (node.command && !actionOrTrigger) {
643
+ const commandMatch = node.command.match(/^[^:]+:([^@]+)@/);
644
+ const methodId = commandMatch ? commandMatch[1] : node.command;
645
+ const availableActions = Object.keys(comp.actions || {});
646
+ const availableTriggers = Object.keys(comp.triggers || {});
647
+ errors.push(`${stepLabel}: action/trigger '${methodId}' not found in component '${comp.name || node.component_id}'. ` +
648
+ `Available actions: [${availableActions.join(', ')}], triggers: [${availableTriggers.join(', ')}]`);
649
+ }
650
+ if (actionOrTrigger?.fields) {
651
+ for (const [fieldId, fieldDef] of Object.entries(actionOrTrigger.fields)) {
652
+ // Check required fields
653
+ if (fieldDef.required && (!node.fields || node.fields[fieldId] === undefined || node.fields[fieldId] === '')) {
654
+ errors.push(`${stepLabel}: required field '${fieldId}' (${fieldDef.label || fieldId}) is not set`);
655
+ }
656
+ // Check static select model values
657
+ if (fieldDef.model && typeof fieldDef.model === 'object' && node.fields?.[fieldId]) {
658
+ const allowedKeys = Object.keys(fieldDef.model);
659
+ if (!allowedKeys.includes(node.fields[fieldId])) {
660
+ warnings.push(`${stepLabel}: field '${fieldId}' has value '${node.fields[fieldId]}' which is not in the ` +
661
+ `allowed static values: ${allowedKeys.join(', ')}. This may be valid if the field uses a dynamic model.`);
662
+ }
663
+ }
664
+ }
665
+ }
666
+ // Check if component is deprecated
667
+ const compName = comp.name || '';
668
+ const compCommandPrefix = comp.team_name && comp.repo_name
669
+ ? `${comp.team_name}/${comp.repo_name}` : '';
670
+ const successor = DEPRECATED_COMPONENTS[compName] || DEPRECATED_COMPONENTS[compCommandPrefix];
671
+ if (successor) {
672
+ warnings.push(`${stepLabel}: component '${compName || compCommandPrefix}' is DEPRECATED. ` +
673
+ `Use '${successor}' instead.`);
674
+ }
675
+ if (actionOrTrigger?.deprecated) {
676
+ const commandMatch = node.command?.match(/^[^:]+:([^@]+)@/);
677
+ const methodId = commandMatch ? commandMatch[1] : (node.command || '');
678
+ const replacement = getDeprecatedActionReplacement(compCommandPrefix || compName, methodId);
679
+ const hint = replacement ? ` Use '${replacement}' instead.` : '';
680
+ warnings.push(`${stepLabel}: action/trigger '${methodId}' is marked as DEPRECATED in component '${comp.name || node.component_id}'.${hint}`);
681
+ }
682
+ // Check if component requires auth but none is set
683
+ const credFields = comp.credentials?.fields;
684
+ const hasCredFields = credFields && Object.keys(credFields).length > 0;
685
+ if (hasCredFields && !resolveCredentialId(node.credentials_id) && !resolveCredentialId(node.secret_id)) {
686
+ errors.push(`${stepLabel}: component requires authentication but no credentials or secret is configured`);
687
+ }
688
+ }
689
+ }
690
+ catch (err) {
691
+ errors.push(`${stepLabel}: component '${node.component_id}' could not be fetched — it may have been deleted or is not accessible`);
692
+ logger.warn(`[healthCheck] Could not fetch component '${node.component_id}': ${err instanceof Error ? err.message : String(err)}`);
693
+ }
694
+ }
695
+ // 3. Validate REST API reader format (headers, body for POST/PUT/PATCH)
696
+ if (node.fields?.reader && typeof node.fields.reader === 'object') {
697
+ validateRestApiReader(node.fields.reader, stepLabel, errors, warnings);
698
+ }
699
+ // 4. Validate URLs (REST API, webhooks)
700
+ if (node.fields?.reader?.url) {
701
+ const url = node.fields.reader.url;
702
+ // Check for plain URL in JSONata-mode field (reader fields use JSONata Mode)
703
+ if (/^https?:\/\//i.test(url.trim())) {
704
+ errors.push(`${stepLabel}: reader.url contains a plain URL "${url}" which is invalid JSONata syntax ` +
705
+ `(the ":" in the protocol causes a parse error). ` +
706
+ `This field uses JSONata Mode — wrap the URL in escaped double quotes: "\\\"${url.trim()}\\\"".`);
707
+ }
708
+ else {
709
+ // If it's a JSONata-quoted string, extract the actual URL for further validation
710
+ const quotedMatch = url.trim().match(/^"(.*)"$/);
711
+ const actualUrl = quotedMatch ? quotedMatch[1] : url;
712
+ try {
713
+ const parsed = new URL(actualUrl);
714
+ if (parsed.hostname === 'webhook.site' && parsed.pathname === '/') {
715
+ warnings.push(`${stepLabel}: URL '${actualUrl}' is the webhook.site homepage, not a unique endpoint. ` +
716
+ `Go to https://webhook.site to get a unique URL like https://webhook.site/<unique-id>`);
717
+ }
718
+ if (parsed.hostname === 'example.com') {
719
+ warnings.push(`${stepLabel}: URL '${actualUrl}' points to example.com which is a placeholder domain`);
720
+ }
721
+ }
722
+ catch {
723
+ // Not a valid URL — might be a JSONata expression, which is fine
724
+ }
725
+ }
726
+ }
727
+ // Collect per-step status
728
+ const stepErrors = errors.slice(errCountBefore);
729
+ const stepWarnings = warnings.slice(warnCountBefore);
730
+ const commandMatch = node.command?.match(/^([^:]+):([^@]+)@/);
731
+ const componentName = commandMatch ? commandMatch[1] : (node.component_id || 'unknown');
732
+ stepStatuses.push({
733
+ stepId: node.id,
734
+ name: node.name || 'unnamed',
735
+ component: componentName,
736
+ status: stepErrors.length > 0 ? 'ERROR' : stepWarnings.length > 0 ? 'WARNING' : 'OK',
737
+ issues: [...stepErrors, ...stepWarnings],
738
+ });
739
+ }
740
+ // 5. Check edges with missing mappers for non-first target steps.
741
+ // Missing mappers cause empty/red fields in the UI.
742
+ const edges = flow?.graph?.edges || [];
743
+ for (const edge of edges) {
744
+ const edgeId = edge.id || `${edge.source}:${edge.target}`;
745
+ if (!edge.config?.mapper) {
746
+ const targetNode = nodes.find((n) => n.id === edge.target);
747
+ if (targetNode && !targetNode.first) {
748
+ warnings.push(`Edge '${edgeId}': no mapper configured. Step '${edge.target}' (${targetNode.name || 'unnamed'}) ` +
749
+ `will receive no mapped input data, causing empty/red fields in the UI. ` +
750
+ `Add a mapper with $getPassthrough() paths for the step's input fields.`);
751
+ }
752
+ }
753
+ }
754
+ return { errors, warnings, stepStatuses };
755
+ }
756
+ /** Find action or trigger definition by parsing the command string */
757
+ function findActionOrTrigger(component, command) {
758
+ if (!command)
759
+ return null;
760
+ const match = /^[^:]+:([^@]+)@/.exec(command);
761
+ if (!match)
762
+ return null;
763
+ const methodId = match[1];
764
+ return component.actions?.[methodId] || component.triggers?.[methodId] || null;
765
+ }
766
+ /** Format health check results as a readable string to append to tool response */
767
+ export function formatHealthCheck(result) {
768
+ const parts = ['\n\n--- Flow Health Check ---'];
769
+ // Per-step status summary
770
+ if (result.stepStatuses && result.stepStatuses.length > 0) {
771
+ parts.push('\nStep Validation:');
772
+ for (const step of result.stepStatuses) {
773
+ const icon = step.status === 'OK' ? '[OK]' : step.status === 'WARNING' ? '[WARN]' : '[FAIL]';
774
+ parts.push(` ${icon} ${step.stepId} (${step.name}) — ${step.component}`);
775
+ for (const issue of step.issues) {
776
+ parts.push(` ${issue}`);
777
+ }
778
+ }
779
+ }
780
+ if (result.errors.length > 0) {
781
+ parts.push(`\nERRORS (${result.errors.length}):`);
782
+ for (const err of result.errors) {
783
+ parts.push(` - ${err}`);
784
+ }
785
+ }
786
+ if (result.warnings.length > 0) {
787
+ parts.push(`\nWARNINGS (${result.warnings.length}):`);
788
+ for (const warn of result.warnings) {
789
+ parts.push(` - ${warn}`);
790
+ }
791
+ }
792
+ if (result.errors.length === 0 && result.warnings.length === 0) {
793
+ parts.push('\nAll steps validated successfully.');
794
+ }
795
+ else if (result.errors.length > 0) {
796
+ parts.push('\nFix the errors above before starting the flow.');
797
+ }
798
+ return parts.join('\n');
799
+ }
800
+ //# sourceMappingURL=validation.js.map