@growthub/cli 0.13.0 → 0.13.2

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 (27) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +50 -25
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +141 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryReviewModal.jsx +38 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +522 -35
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +242 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +52 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +1203 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +163 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxOrchestrationEditorPanel.jsx +190 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolConfirmModal.jsx +64 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolDraftPanel.jsx +376 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +6 -1
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +1062 -2
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +10 -7
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +906 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/page.jsx +12 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +492 -28
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +114 -30
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/nav-workflows.js +54 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +322 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +734 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +73 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-sidecar-routing.js +24 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +2 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +21 -1
  27. package/package.json +1 -1
@@ -0,0 +1,734 @@
1
+ /**
2
+ * Governed orchestrationGraph field contract (sandbox-environment row).
3
+ * V1: growthub-native declarative run plan. Execution: POST /api/workspace/sandbox-run only.
4
+ */
5
+
6
+ const TRUSTED_API_STATUSES = ["connected", "approved", "ok", "success"];
7
+ const SUPPORTED_PROVIDERS_V1 = new Set(["growthub-native", "custom-webhook"]);
8
+
9
+ const FILTER_OPERATORS = ["eq", "ne", "contains", "gt", "lt", "isEmpty", "isNotEmpty"];
10
+ const FILTER_CONJUNCTIONS = ["and", "or"];
11
+
12
+ const KNOWN_NODE_TYPES = new Set([
13
+ "input",
14
+ "api-registry-call",
15
+ "transform-filter",
16
+ "normalize-output",
17
+ "tool-result",
18
+ "sandbox-adapter",
19
+ "custom-webhook",
20
+ "thinAdapter",
21
+ "data-trigger",
22
+ "data-action",
23
+ "ai-agent",
24
+ "flow-control",
25
+ "core-action",
26
+ "human-input"
27
+ ]);
28
+
29
+ const API_REGISTRY_SETUP_FIELDS = ["integrationId", "baseUrl", "endpoint", "method", "authRef"];
30
+
31
+ const CANONICAL_NODE_ORDER = ["input", "api-request", "transform", "result"];
32
+
33
+ function slugifyName(value) {
34
+ return String(value || "")
35
+ .trim()
36
+ .toLowerCase()
37
+ .replace(/[^a-z0-9]+/g, "-")
38
+ .replace(/^-+|-+$/g, "");
39
+ }
40
+
41
+ function parseOrchestrationGraph(value) {
42
+ if (!value) return null;
43
+ if (typeof value === "object" && !Array.isArray(value)) return normalizeOrchestrationGraphShape(value);
44
+ if (typeof value !== "string") return null;
45
+ const text = value.trim();
46
+ if (!text) return null;
47
+ try {
48
+ const parsed = JSON.parse(text);
49
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
50
+ ? normalizeOrchestrationGraphShape(parsed)
51
+ : null;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ function normalizeOrchestrationGraphShape(graph) {
58
+ if (!graph || typeof graph !== "object" || Array.isArray(graph)) return null;
59
+ if (graph.graph && typeof graph.graph === "object" && !Array.isArray(graph.graph)) {
60
+ return {
61
+ ...graph.graph,
62
+ version: graph.graph.version || graph.version || 1,
63
+ provider: graph.graph.provider || graph.provider || "growthub-native"
64
+ };
65
+ }
66
+ return graph;
67
+ }
68
+
69
+ function serializeOrchestrationGraph(graph) {
70
+ if (!graph || typeof graph !== "object") return "";
71
+ return JSON.stringify(graph, null, 2);
72
+ }
73
+
74
+ function isApiRegistryTestSuccessful(row) {
75
+ const status = String(row?.status || "").trim().toLowerCase();
76
+ return TRUSTED_API_STATUSES.includes(status);
77
+ }
78
+
79
+ function getApiRegistrySetupChecklist(registryRow) {
80
+ const row = registryRow || {};
81
+ return API_REGISTRY_SETUP_FIELDS.map((field) => {
82
+ const value = String(row[field] ?? "").trim();
83
+ const ok = field === "baseUrl" || field === "endpoint"
84
+ ? Boolean(String(row.baseUrl || "").trim() || String(row.endpoint || "").trim())
85
+ : Boolean(value);
86
+ return { field, ok, value: field === "method" ? (value || "GET") : value };
87
+ });
88
+ }
89
+
90
+ function isApiRegistrySetupComplete(registryRow) {
91
+ return getApiRegistrySetupChecklist(registryRow).every((item) => item.ok);
92
+ }
93
+
94
+ /**
95
+ * Sidecar action state for API Registry → sandbox tool bridge (UI only).
96
+ */
97
+ function getApiRegistrySandboxToolState(registryRow, workspaceConfig) {
98
+ if (!isApiRegistrySetupComplete(registryRow)) {
99
+ return { kind: "incomplete", checklist: getApiRegistrySetupChecklist(registryRow) };
100
+ }
101
+ if (!isApiRegistryTestSuccessful(registryRow)) {
102
+ const status = String(registryRow?.status || "").trim().toLowerCase();
103
+ if (status === "failed") {
104
+ return {
105
+ kind: "failed",
106
+ message: "Connection test failed. Fix the endpoint or auth reference, then test again."
107
+ };
108
+ }
109
+ return {
110
+ kind: "untested",
111
+ message: "Test connection first. Sandbox tool creation unlocks after a successful test."
112
+ };
113
+ }
114
+ const integrationId = String(registryRow?.integrationId || "").trim();
115
+ const existing = findSandboxRowsForRegistry(workspaceConfig, integrationId);
116
+ if (existing.length > 0) {
117
+ return { kind: "existing", row: existing[0] };
118
+ }
119
+ return { kind: "create" };
120
+ }
121
+
122
+ function validateOrchestrationGraph(graph) {
123
+ const errors = [];
124
+ if (!graph || typeof graph !== "object") {
125
+ return { ok: false, errors: ["orchestrationGraph must be an object"] };
126
+ }
127
+ const version = Number(graph.version);
128
+ if (!Number.isFinite(version) || version < 1) {
129
+ errors.push("orchestrationGraph.version must be a positive number");
130
+ }
131
+ const provider = String(graph.provider || "").trim();
132
+ if (!provider) {
133
+ errors.push("orchestrationGraph.provider is required");
134
+ }
135
+ if (!Array.isArray(graph.nodes) || !graph.nodes.length) {
136
+ errors.push("orchestrationGraph.nodes must be a non-empty array");
137
+ } else {
138
+ const ids = new Set();
139
+ graph.nodes.forEach((node, index) => {
140
+ const prefix = `nodes[${index}]`;
141
+ if (!node || typeof node !== "object") {
142
+ errors.push(`${prefix} must be an object`);
143
+ return;
144
+ }
145
+ const id = String(node.id || "").trim();
146
+ if (!id) errors.push(`${prefix}.id is required`);
147
+ else if (ids.has(id)) errors.push(`${prefix}.id duplicates "${id}"`);
148
+ else ids.add(id);
149
+ const type = String(node.type || "").trim();
150
+ if (!KNOWN_NODE_TYPES.has(type)) {
151
+ errors.push(`${prefix}.type "${type}" is not a known node type`);
152
+ }
153
+ });
154
+ const hasThinAdapter = graph.nodes.some((n) => n?.type === "thinAdapter");
155
+ const hasApi = graph.nodes.some((n) => n?.type === "api-registry-call");
156
+ const hasResult = graph.nodes.some((n) => n?.type === "tool-result");
157
+ if (!hasThinAdapter && !hasApi) errors.push("orchestrationGraph requires an api-registry-call node");
158
+ if (!hasThinAdapter && !hasResult) errors.push("orchestrationGraph requires a tool-result node");
159
+ }
160
+ if (!Array.isArray(graph.edges)) {
161
+ errors.push("orchestrationGraph.edges must be an array");
162
+ } else {
163
+ graph.edges.forEach((edge, index) => {
164
+ if (!edge || typeof edge !== "object") {
165
+ errors.push(`edges[${index}] must be an object`);
166
+ return;
167
+ }
168
+ if (!String(edge.from || "").trim() || !String(edge.to || "").trim()) {
169
+ errors.push(`edges[${index}] requires from and to`);
170
+ }
171
+ });
172
+ }
173
+ return { ok: errors.length === 0, errors };
174
+ }
175
+
176
+ function summarizeOrchestrationGraph(graph) {
177
+ const parsed = parseOrchestrationGraph(graph) || graph;
178
+ if (!parsed?.nodes?.length) return "No orchestration graph";
179
+ const ordered = orderedGraphNodes(parsed);
180
+ return ordered.map((n) => String(n.label || n.id || "").trim()).filter(Boolean).join(" → ");
181
+ }
182
+
183
+ function buildDefaultOrchestrationGraphFromRegistry(registryRow, options = {}) {
184
+ const integrationId = String(registryRow?.integrationId || registryRow?.Name || "").trim();
185
+ const label = String(options.label || registryRow?.Name || integrationId || "API").trim();
186
+ const method = String(options.method || registryRow?.method || "GET").trim().toUpperCase();
187
+ const endpoint = String(options.endpoint || registryRow?.endpoint || "").trim();
188
+ const baseUrl = String(registryRow?.baseUrl || "").trim();
189
+ const authRef = String(options.authRef || registryRow?.authRef || integrationId).trim();
190
+ const rootPath = String(options.rootPath || "data").trim();
191
+
192
+ return {
193
+ version: 1,
194
+ provider: "growthub-native",
195
+ nodes: [
196
+ {
197
+ id: "input",
198
+ type: "input",
199
+ label: "Input",
200
+ subtitle: "Manual or source payload",
201
+ config: {
202
+ inputMode: "manual",
203
+ samplePayload: {},
204
+ sourceType: "",
205
+ sourceId: "",
206
+ entityId: "",
207
+ filterMode: "and",
208
+ filters: []
209
+ }
210
+ },
211
+ {
212
+ id: "api-request",
213
+ type: "api-registry-call",
214
+ label: "API Registry",
215
+ subtitle: `${integrationId} · ${method} ${endpoint}`,
216
+ config: {
217
+ registryId: integrationId,
218
+ integrationId,
219
+ baseUrl,
220
+ endpoint,
221
+ method,
222
+ authRef,
223
+ queryParams: {},
224
+ bodyTemplate: "",
225
+ requestHeadersMetadata: {
226
+ authHeaderName: String(registryRow?.authHeaderName || registryRow?.authHeader || "x-api-key").trim(),
227
+ authPrefix: String(registryRow?.authPrefix || "").trim(),
228
+ contentType: method === "GET" ? "" : "application/json"
229
+ },
230
+ timeoutMs: Number(options.timeoutMs) || 30000
231
+ }
232
+ },
233
+ {
234
+ id: "transform",
235
+ type: "transform-filter",
236
+ label: "Transform",
237
+ subtitle: "Map fields and apply filters",
238
+ config: {
239
+ rootPath,
240
+ mode: "json",
241
+ fieldMap: {},
242
+ includeFields: [],
243
+ excludeFields: [],
244
+ computedFields: {},
245
+ filters: [],
246
+ filterMode: "and",
247
+ maxRows: 0
248
+ }
249
+ },
250
+ {
251
+ id: "result",
252
+ type: "tool-result",
253
+ label: "Result",
254
+ subtitle: "Save status and response",
255
+ config: {
256
+ successStatusCodes: [200],
257
+ writeLastResponse: true,
258
+ writeSourceRecord: true,
259
+ sourceRecordId: "",
260
+ outputMode: "normalized-json",
261
+ previewFields: [],
262
+ statusField: "status",
263
+ lastTestedField: "lastTested"
264
+ }
265
+ }
266
+ ],
267
+ edges: [
268
+ { from: "input", to: "api-request", passes: "payload, filters, variables" },
269
+ { from: "api-request", to: "transform", passes: "provider-response" },
270
+ { from: "transform", to: "result", passes: "normalized-output" }
271
+ ]
272
+ };
273
+ }
274
+
275
+ function updateGraphNode(graph, nodeId, configPatch) {
276
+ const parsed = parseOrchestrationGraph(graph) || graph;
277
+ if (!parsed?.nodes) return parsed;
278
+ const id = String(nodeId || "").trim();
279
+ return {
280
+ ...parsed,
281
+ nodes: parsed.nodes.map((node) => {
282
+ if (String(node.id) !== id) return node;
283
+ return {
284
+ ...node,
285
+ config: { ...(node.config || {}), ...(configPatch || {}) }
286
+ };
287
+ })
288
+ };
289
+ }
290
+
291
+ function findSandboxObject(workspaceConfig) {
292
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
293
+ return objects.find((o) => o?.objectType === "sandbox-environment") || null;
294
+ }
295
+
296
+ function findSandboxRowsForRegistry(workspaceConfig, integrationId) {
297
+ const id = String(integrationId || "").trim();
298
+ if (!id) return [];
299
+ const sandboxObject = findSandboxObject(workspaceConfig);
300
+ if (!sandboxObject) return [];
301
+ const rows = Array.isArray(sandboxObject.rows) ? sandboxObject.rows : [];
302
+ return rows.filter((row) => {
303
+ const graph = parseOrchestrationGraph(row?.orchestrationGraph);
304
+ if (!graph?.nodes) return String(row?.schedulerRegistryId || "").trim() === id;
305
+ return graph.nodes.some(
306
+ (node) => node?.type === "api-registry-call"
307
+ && String(node?.config?.registryId || node?.config?.integrationId || "").trim() === id
308
+ );
309
+ });
310
+ }
311
+
312
+ function buildSandboxRowFromApiRegistry(workspaceConfig, registryRow, options = {}) {
313
+ const integrationId = String(registryRow?.integrationId || "").trim();
314
+ const baseName = String(options.name || registryRow?.Name || integrationId || "Sandbox Tool").trim();
315
+ const name = baseName.endsWith(" Tool") ? baseName : `${baseName} Tool`;
316
+ const runLocality = String(options.runLocality || "local").trim() === "serverless" ? "serverless" : "local";
317
+ const adapter = String(options.adapter || (runLocality === "serverless" ? "serverless" : "local-process")).trim();
318
+ const graph = options.orchestrationGraph
319
+ ? (typeof options.orchestrationGraph === "string"
320
+ ? parseOrchestrationGraph(options.orchestrationGraph)
321
+ : options.orchestrationGraph)
322
+ : buildDefaultOrchestrationGraphFromRegistry(registryRow, options);
323
+
324
+ const apiNode = graph?.nodes?.find((n) => n?.type === "api-registry-call");
325
+ const authRef = String(options.authRef || apiNode?.config?.authRef || registryRow?.authRef || integrationId).trim();
326
+ const transformNode = graph?.nodes?.find((n) => n?.type === "transform-filter" || n?.type === "normalize-output");
327
+ const rootPath = transformNode?.config?.rootPath || "data";
328
+ const method = String(registryRow?.method || apiNode?.config?.method || "GET").trim().toUpperCase();
329
+ const endpoint = String(registryRow?.endpoint || apiNode?.config?.endpoint || "").trim();
330
+ const baseUrl = String(registryRow?.baseUrl || apiNode?.config?.baseUrl || "").trim();
331
+
332
+ return {
333
+ Name: name,
334
+ slug: options.slug || slugifyName(name) || slugifyName(integrationId),
335
+ objectType: "sandbox-environment",
336
+ lifecycleStatus: "draft",
337
+ version: "1",
338
+ runLocality,
339
+ schedulerRegistryId: runLocality === "serverless" ? integrationId : "",
340
+ runtime: String(options.runtime || "node").trim(),
341
+ adapter,
342
+ agentHost: String(options.agentHost || "").trim(),
343
+ envRefs: Array.isArray(options.envRefs) ? options.envRefs.join(",") : String(options.envRefs || "").trim(),
344
+ networkAllow: options.networkAllow === true ? "true" : "",
345
+ allowList: String(options.allowList || "").trim(),
346
+ instructions: String(
347
+ options.instructions
348
+ || `Governed sandbox tool for ${integrationId}. Calls ${method} ${endpoint || baseUrl} and normalizes at "${rootPath}". authRef ${authRef} only — secrets resolve server-side.`
349
+ ).trim(),
350
+ command: String(options.command || "").trim(),
351
+ timeoutMs: String(options.timeoutMs || "30000").trim(),
352
+ status: "untested",
353
+ lastTested: "",
354
+ lastRunId: "",
355
+ lastSourceId: "",
356
+ lastResponse: "",
357
+ orchestrationGraph: serializeOrchestrationGraph(graph),
358
+ description: String(options.description || registryRow?.description || "").trim()
359
+ };
360
+ }
361
+
362
+ function extractNodeByType(graph, type) {
363
+ const parsed = parseOrchestrationGraph(graph) || graph;
364
+ if (!parsed?.nodes) return null;
365
+ return parsed.nodes.find((n) => n?.type === type) || null;
366
+ }
367
+
368
+ function extractApiRegistryCallNode(graph) {
369
+ const parsed = parseOrchestrationGraph(graph) || graph;
370
+ if (!parsed?.nodes) return null;
371
+ return parsed.nodes.find((n) => n?.type === "api-registry-call") || null;
372
+ }
373
+
374
+ function extractInputNode(graph) {
375
+ return extractNodeByType(graph, "input");
376
+ }
377
+
378
+ function extractTransformConfig(graph) {
379
+ const parsed = parseOrchestrationGraph(graph) || graph;
380
+ const node = parsed?.nodes?.find((n) => n?.type === "transform-filter" || n?.type === "normalize-output");
381
+ const config = node?.config || { mode: "json", rootPath: "data", fieldMap: {}, filters: [] };
382
+ const mode = config.responseMode || config.mode || "json";
383
+ return { ...config, mode, responseMode: mode };
384
+ }
385
+
386
+ /** @deprecated use extractTransformConfig */
387
+ function extractNormalizeConfig(graph) {
388
+ return extractTransformConfig(graph);
389
+ }
390
+
391
+ function getValueAtPath(obj, path) {
392
+ if (!path) return obj;
393
+ const parts = String(path).split(".").filter(Boolean);
394
+ let cursor = obj;
395
+ for (const part of parts) {
396
+ if (cursor == null || typeof cursor !== "object") return undefined;
397
+ cursor = cursor[part];
398
+ }
399
+ return cursor;
400
+ }
401
+
402
+ function normalizeJsonAtPath(payload, rootPath) {
403
+ const cursor = rootPath ? getValueAtPath(payload, rootPath) : payload;
404
+ if (cursor == null) {
405
+ return typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
406
+ }
407
+ return typeof cursor === "string" ? cursor : JSON.stringify(cursor, null, 2);
408
+ }
409
+
410
+ function applyFieldMap(payload, fieldMap) {
411
+ if (!fieldMap || typeof fieldMap !== "object" || !Object.keys(fieldMap).length) {
412
+ return payload;
413
+ }
414
+ const source = payload && typeof payload === "object" ? payload : {};
415
+ const out = {};
416
+ for (const [target, sourcePath] of Object.entries(fieldMap)) {
417
+ out[target] = getValueAtPath(source, String(sourcePath || ""));
418
+ }
419
+ return out;
420
+ }
421
+
422
+ function rowMatchesFilter(row, clause) {
423
+ const fieldId = String(clause?.fieldId || "").trim();
424
+ const op = String(clause?.operator || "eq").trim();
425
+ const expected = clause?.value;
426
+ const raw = row && typeof row === "object" ? row[fieldId] : undefined;
427
+ const value = raw == null ? "" : String(raw);
428
+ switch (op) {
429
+ case "eq":
430
+ return value === String(expected ?? "");
431
+ case "ne":
432
+ return value !== String(expected ?? "");
433
+ case "contains":
434
+ return value.toLowerCase().includes(String(expected ?? "").toLowerCase());
435
+ case "gt":
436
+ return Number(value) > Number(expected);
437
+ case "lt":
438
+ return Number(value) < Number(expected);
439
+ case "isEmpty":
440
+ return value === "";
441
+ case "isNotEmpty":
442
+ return value !== "";
443
+ default:
444
+ return true;
445
+ }
446
+ }
447
+
448
+ function applyFilters(rows, filters, filterMode = "and") {
449
+ if (!Array.isArray(rows) || !Array.isArray(filters) || !filters.length) return rows;
450
+ const mode = String(filterMode || "and").toLowerCase() === "or" ? "or" : "and";
451
+ return rows.filter((row) => {
452
+ const results = filters.map((clause) => rowMatchesFilter(row, clause));
453
+ return mode === "or" ? results.some(Boolean) : results.every(Boolean);
454
+ });
455
+ }
456
+
457
+ function substituteVariables(template, inputPayload) {
458
+ const text = String(template || "");
459
+ if (!text.includes("{{")) return text;
460
+ const input = inputPayload && typeof inputPayload === "object" ? inputPayload : {};
461
+ return text.replace(/\{\{input\.([a-zA-Z0-9_.]+)\}\}/g, (_, key) => {
462
+ const val = getValueAtPath(input, key);
463
+ return val == null ? "" : String(val);
464
+ });
465
+ }
466
+
467
+ function orderedGraphNodes(graph) {
468
+ const parsed = parseOrchestrationGraph(graph) || graph;
469
+ if (!parsed?.nodes?.length) return [];
470
+ const byId = new Map(parsed.nodes.map((n) => [String(n.id), n]));
471
+ const ordered = [];
472
+ for (const id of CANONICAL_NODE_ORDER) {
473
+ if (byId.has(id)) ordered.push(byId.get(id));
474
+ }
475
+ const edges = Array.isArray(parsed.edges) ? parsed.edges : [];
476
+ const incoming = new Map();
477
+ parsed.nodes.forEach((n) => incoming.set(String(n.id), 0));
478
+ edges.forEach((e) => {
479
+ const to = String(e.to || "");
480
+ if (incoming.has(to)) incoming.set(to, (incoming.get(to) || 0) + 1);
481
+ });
482
+ const seen = new Set(ordered.map((n) => String(n.id)));
483
+ function walk(node) {
484
+ const id = String(node?.id || "");
485
+ if (!id || seen.has(id)) return;
486
+ seen.add(id);
487
+ ordered.push(node);
488
+ edges.filter((e) => String(e.from) === id).forEach((e) => {
489
+ const next = byId.get(String(e.to));
490
+ if (next) walk(next);
491
+ });
492
+ }
493
+ parsed.nodes.filter((n) => !incoming.get(String(n.id))).forEach(walk);
494
+ parsed.nodes.forEach((n) => {
495
+ if (!seen.has(String(n.id))) ordered.push(n);
496
+ });
497
+ return ordered;
498
+ }
499
+
500
+ function collectFieldIdsFromValue(value, prefix = "", out = new Set(), depth = 0) {
501
+ if (depth > 4 || value == null) return out;
502
+ if (Array.isArray(value)) {
503
+ value.slice(0, 5).forEach((item, index) => {
504
+ collectFieldIdsFromValue(item, prefix ? `${prefix}.${index}` : String(index), out, depth + 1);
505
+ });
506
+ return out;
507
+ }
508
+ if (typeof value === "object") {
509
+ Object.keys(value).forEach((key) => {
510
+ const path = prefix ? `${prefix}.${key}` : key;
511
+ out.add(path);
512
+ collectFieldIdsFromValue(value[key], path, out, depth + 1);
513
+ });
514
+ }
515
+ return out;
516
+ }
517
+
518
+ function detectFieldIdsFromLastResponse(lastResponse) {
519
+ const text = String(lastResponse || "").trim();
520
+ if (!text) return [];
521
+ try {
522
+ const parsed = JSON.parse(text);
523
+ const payload = parsed?.response ?? parsed?.data ?? parsed;
524
+ return Array.from(collectFieldIdsFromValue(payload)).sort();
525
+ } catch {
526
+ return [];
527
+ }
528
+ }
529
+
530
+ function isOrchestrationGraphEmpty(value) {
531
+ const graph = typeof value === "string" ? parseOrchestrationGraph(value) : value;
532
+ if (!graph || typeof graph !== "object") return true;
533
+ if (!Array.isArray(graph.nodes) || graph.nodes.length === 0) return true;
534
+ return false;
535
+ }
536
+
537
+ /** unset = no graph yet; blank-shell = valid shell with zero nodes; populated = has nodes */
538
+ function getOrchestrationGraphUiState(value) {
539
+ const text = typeof value === "string" ? String(value || "").trim() : "";
540
+ const graph = typeof value === "string" ? parseOrchestrationGraph(value) : value;
541
+ if (!graph || typeof graph !== "object") {
542
+ return text ? "unset" : "unset";
543
+ }
544
+ if (!Array.isArray(graph.nodes) || graph.nodes.length === 0) return "blank-shell";
545
+ return "populated";
546
+ }
547
+
548
+ function buildBlankOrchestrationGraphShell() {
549
+ return {
550
+ version: 1,
551
+ provider: "growthub-native",
552
+ nodes: [],
553
+ edges: []
554
+ };
555
+ }
556
+
557
+ function buildCanonicalNode(nodeId, registryRow = {}, options = {}) {
558
+ const integrationId = String(registryRow?.integrationId || registryRow?.Name || "").trim();
559
+ const method = String(registryRow?.method || "GET").trim().toUpperCase();
560
+ const endpoint = String(registryRow?.endpoint || "").trim();
561
+ const baseUrl = String(registryRow?.baseUrl || "").trim();
562
+ const authRef = String(options.authRef || registryRow?.authRef || integrationId).trim();
563
+ const rootPath = String(options.rootPath || "data").trim();
564
+
565
+ switch (nodeId) {
566
+ case "input":
567
+ return {
568
+ id: "input",
569
+ type: "input",
570
+ label: "Input",
571
+ subtitle: "Manual or source payload",
572
+ config: {
573
+ inputMode: "manual",
574
+ samplePayload: {},
575
+ sourceType: "",
576
+ sourceId: "",
577
+ entityId: "",
578
+ filterMode: "and",
579
+ filters: []
580
+ }
581
+ };
582
+ case "api-request":
583
+ return {
584
+ id: "api-request",
585
+ type: "api-registry-call",
586
+ label: "API Registry",
587
+ subtitle: `${integrationId} · ${method} ${endpoint}`,
588
+ config: {
589
+ registryId: integrationId,
590
+ integrationId,
591
+ baseUrl,
592
+ endpoint,
593
+ method,
594
+ authRef,
595
+ queryParams: {},
596
+ bodyTemplate: "",
597
+ requestHeadersMetadata: {
598
+ authHeaderName: String(registryRow?.authHeaderName || registryRow?.authHeader || "x-api-key").trim(),
599
+ authPrefix: String(registryRow?.authPrefix || "").trim(),
600
+ contentType: method === "GET" ? "" : "application/json"
601
+ },
602
+ timeoutMs: 30000
603
+ }
604
+ };
605
+ case "transform":
606
+ return {
607
+ id: "transform",
608
+ type: "transform-filter",
609
+ label: "Transform",
610
+ subtitle: "Map fields and filter rows",
611
+ config: {
612
+ rootPath,
613
+ mode: "json",
614
+ responseMode: "json",
615
+ fieldMap: {},
616
+ includeFields: [],
617
+ excludeFields: [],
618
+ computedFields: {},
619
+ filters: [],
620
+ filterMode: "and",
621
+ maxRows: 0
622
+ }
623
+ };
624
+ case "result":
625
+ return {
626
+ id: "result",
627
+ type: "tool-result",
628
+ label: "Result",
629
+ subtitle: "Save status and response",
630
+ config: {
631
+ successStatusCodes: [200],
632
+ writeLastResponse: true,
633
+ writeSourceRecord: true,
634
+ sourceRecordId: "",
635
+ outputMode: "normalized-json",
636
+ previewFields: [],
637
+ statusField: "status",
638
+ lastTestedField: "lastTested"
639
+ }
640
+ };
641
+ default:
642
+ return null;
643
+ }
644
+ }
645
+
646
+ function getNextCanonicalNodeId(graph) {
647
+ const parsed = parseOrchestrationGraph(graph) || graph;
648
+ if ((parsed?.nodes || []).some((n) => n?.type === "thinAdapter")) return null;
649
+ const ids = new Set((parsed?.nodes || []).map((n) => String(n.id)));
650
+ for (const id of CANONICAL_NODE_ORDER) {
651
+ if (!ids.has(id)) return id;
652
+ }
653
+ return null;
654
+ }
655
+
656
+ function addCanonicalNodeToGraph(graph, nodeId, registryRow, options = {}) {
657
+ const parsed = parseOrchestrationGraph(graph) || graph || buildBlankOrchestrationGraphShell();
658
+ const id = String(nodeId || "").trim();
659
+ const nextExpected = getNextCanonicalNodeId(parsed);
660
+ if (!id || id !== nextExpected) return parsed;
661
+ const node = buildCanonicalNode(id, registryRow, options);
662
+ if (!node) return parsed;
663
+ const nodes = [...(parsed.nodes || []), node];
664
+ const edges = [...(parsed.edges || [])];
665
+ const order = CANONICAL_NODE_ORDER;
666
+ const idx = order.indexOf(id);
667
+ if (idx > 0) {
668
+ const prev = order[idx - 1];
669
+ if (nodes.some((n) => n.id === prev) && !edges.some((e) => e.from === prev && e.to === id)) {
670
+ const passes = id === "api-request"
671
+ ? "payload, filters, variables"
672
+ : id === "transform"
673
+ ? "provider-response"
674
+ : id === "result"
675
+ ? "normalized-output"
676
+ : "";
677
+ edges.push({ from: prev, to: id, passes });
678
+ }
679
+ }
680
+ return { ...parsed, nodes, edges };
681
+ }
682
+
683
+ function redactSecretsFromText(text) {
684
+ let out = String(text || "");
685
+ for (const pattern of [
686
+ /(Bearer\s+)[^\s"']+/gi,
687
+ /(api[_-]?key["']?\s*[:=]\s*)["']?[^\s"',}]+/gi,
688
+ /(token["']?\s*[:=]\s*)["']?[^\s"',}]+/gi
689
+ ]) {
690
+ out = out.replace(pattern, "$1[redacted]");
691
+ }
692
+ return out;
693
+ }
694
+
695
+ export {
696
+ API_REGISTRY_SETUP_FIELDS,
697
+ TRUSTED_API_STATUSES,
698
+ SUPPORTED_PROVIDERS_V1,
699
+ FILTER_OPERATORS,
700
+ FILTER_CONJUNCTIONS,
701
+ CANONICAL_NODE_ORDER,
702
+ buildBlankOrchestrationGraphShell,
703
+ buildDefaultOrchestrationGraphFromRegistry,
704
+ buildCanonicalNode,
705
+ isOrchestrationGraphEmpty,
706
+ getOrchestrationGraphUiState,
707
+ getNextCanonicalNodeId,
708
+ addCanonicalNodeToGraph,
709
+ buildSandboxRowFromApiRegistry,
710
+ extractApiRegistryCallNode,
711
+ extractInputNode,
712
+ extractTransformConfig,
713
+ extractNormalizeConfig,
714
+ findSandboxObject,
715
+ findSandboxRowsForRegistry,
716
+ getApiRegistrySetupChecklist,
717
+ getApiRegistrySandboxToolState,
718
+ isApiRegistrySetupComplete,
719
+ isApiRegistryTestSuccessful,
720
+ normalizeJsonAtPath,
721
+ applyFieldMap,
722
+ applyFilters,
723
+ substituteVariables,
724
+ orderedGraphNodes,
725
+ parseOrchestrationGraph,
726
+ serializeOrchestrationGraph,
727
+ slugifyName,
728
+ summarizeOrchestrationGraph,
729
+ updateGraphNode,
730
+ validateOrchestrationGraph,
731
+ redactSecretsFromText,
732
+ detectFieldIdsFromLastResponse,
733
+ collectFieldIdsFromValue
734
+ };