@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
@@ -12,7 +12,7 @@
12
12
  * │ [🏠 Home] [💬 Chat] [✶+ Ask helper] │ Tab row
13
13
  * ├──────────────────────────────────────────────┤
14
14
  * │ HOME tab body: CHAT tab body: │
15
- * │ Dashboards Latest │
15
+ * │ Builder Latest │
16
16
  * │ Data Model 💬 Best Skills │
17
17
  * │ Management 💬 Casual greet │
18
18
  * │ Workspace Settings (… more threads) │
@@ -26,20 +26,21 @@
26
26
  *
27
27
  * Surface-specific slots (`dashboardsSlot`, `dataModelSlot`,
28
28
  * `managementSlot`, `settingsSlot`) let the page inject its own
29
- * Dashboards / Data Model / Management / Workspace Settings behaviour
29
+ * Builder / Management / Workspace Settings behaviour
30
30
  * while keeping the visual treatment identical across every page.
31
31
  */
32
32
 
33
33
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
34
34
  import { createPortal } from "react-dom";
35
35
  import Link from "next/link";
36
- import { usePathname, useRouter } from "next/navigation";
36
+ import { usePathname, useRouter, useSearchParams } from "next/navigation";
37
37
  import {
38
38
  Archive,
39
39
  ChevronDown,
40
40
  ChevronRight,
41
41
  Folder,
42
42
  FolderPlus,
43
+ GitBranch,
43
44
  Home,
44
45
  LayoutDashboard,
45
46
  MessageCircle,
@@ -64,6 +65,7 @@ import {
64
65
  nextNavFolderId,
65
66
  nextNavItemId,
66
67
  } from "@/lib/workspace-helper-apply";
68
+ import { listAvailableWorkflows } from "@/lib/nav-workflows";
67
69
  import { ICON_PICKER_SET, LucideIcon } from "./data-model/components/dm-shared.jsx";
68
70
 
69
71
  function textColorForAccent(accent) {
@@ -172,6 +174,7 @@ const NAV_FOLDER_STYLE_DEFAULT = { icon: "Folder", color: "#f97316", iconBg: "#f
172
174
  const NAV_ITEM_STYLE_DEFAULT = {
173
175
  dashboard: { icon: "LayoutDashboard", color: "#3b82f6", iconBg: "#eff6ff" },
174
176
  view: { icon: "Table", color: "#14b8a6", iconBg: "#f0fdfa" },
177
+ workflow: { icon: "GitBranch", color: "#8b5cf6", iconBg: "#f5f3ff" },
175
178
  };
176
179
 
177
180
  /** Default visible rows before scroll — keeps the rail from growing unbounded. */
@@ -394,6 +397,7 @@ function NavFoldersSection({
394
397
  onPatchNavFolders,
395
398
  }) {
396
399
  const router = useRouter();
400
+ const searchParams = useSearchParams();
397
401
  const [creating, setCreating] = useState(false);
398
402
  const [createDraft, setCreateDraft] = useState("");
399
403
  const [createDiscardWarn, setCreateDiscardWarn] = useState(false);
@@ -401,15 +405,16 @@ function NavFoldersSection({
401
405
  const [menuAnchor, setMenuAnchor] = useState(null);
402
406
  const [customizeTarget, setCustomizeTarget] = useState(null);
403
407
  const [discardWarn, setDiscardWarn] = useState(false);
404
- const [addPickerFor, setAddPickerFor] = useState(null); // { folderId, kind: "dashboard"|"view" }
408
+ const [addPickerFor, setAddPickerFor] = useState(null); // { folderId, kind: "dashboard"|"view"|"workflow" }
405
409
  const [filterQuery, setFilterQuery] = useState("");
406
- const [filterType, setFilterType] = useState("all"); // all | dashboard | view
410
+ const [filterType, setFilterType] = useState("all"); // all | dashboard | view | workflow
407
411
  const [filterMenuOpen, setFilterMenuOpen] = useState(false);
408
412
  const [sectionCollapsed, setSectionCollapsed] = useState(true);
409
413
 
410
414
  const rows = useMemo(() => getNavFolderRows(workspaceConfig), [workspaceConfig]);
411
415
  const dashboards = useMemo(() => listAvailableDashboards(workspaceConfig), [workspaceConfig]);
412
416
  const viewableObjects = useMemo(() => listAvailableObjectsForViews(workspaceConfig), [workspaceConfig]);
417
+ const workflows = useMemo(() => listAvailableWorkflows(workspaceConfig), [workspaceConfig]);
413
418
  const filteredEntries = useMemo(
414
419
  () => filterNavFolderRows(rows, filterQuery, filterType),
415
420
  [rows, filterQuery, filterType],
@@ -680,6 +685,28 @@ function NavFoldersSection({
680
685
  await writeRows(next);
681
686
  }, [rows, writeRows]);
682
687
 
688
+ const addWorkflowItem = useCallback(async (folderId, workflow) => {
689
+ setAddPickerFor(null);
690
+ setOpenMenuId(null);
691
+ const style = NAV_ITEM_STYLE_DEFAULT.workflow;
692
+ const item = {
693
+ id: nextNavItemId(),
694
+ type: "workflow",
695
+ objectId: workflow.objectId,
696
+ rowId: workflow.rowId,
697
+ fieldName: "orchestrationGraph",
698
+ label: workflow.label,
699
+ icon: style.icon,
700
+ color: style.color,
701
+ iconBg: style.iconBg,
702
+ };
703
+ const next = rows.map((row) => {
704
+ if (row.id !== folderId) return row;
705
+ return { ...row, items: [...(row.items || []), item] };
706
+ });
707
+ await writeRows(next);
708
+ }, [rows, writeRows]);
709
+
683
710
  // ── Drag-and-drop ────────────────────────────────────────────────────
684
711
  //
685
712
  // HTML5 DnD with a tiny in-ref state machine; mirrors Twenty's
@@ -768,9 +795,9 @@ function NavFoldersSection({
768
795
  };
769
796
 
770
797
  const openDashboardItem = (item) => {
771
- // Dashboards are top-level surfaces; the builder reads the active
798
+ // Builder items are top-level surfaces; the builder reads the active
772
799
  // dashboard from query params if present. Other surfaces simply
773
- // navigate home; the user lands on the dashboards list. This keeps
800
+ // navigate home; the user lands on the Builder list. This keeps
774
801
  // the rail itself agnostic of surface-specific routing.
775
802
  router.push(`/?dashboard=${encodeURIComponent(item.refId)}`);
776
803
  };
@@ -779,6 +806,13 @@ function NavFoldersSection({
779
806
  router.push(`/data-model?object=${encodeURIComponent(item.objectId || "")}`);
780
807
  };
781
808
 
809
+ const openWorkflowItem = (item) => {
810
+ const objectId = encodeURIComponent(item.objectId || "");
811
+ const row = encodeURIComponent(item.rowId || "");
812
+ const field = encodeURIComponent(item.fieldName || "orchestrationGraph");
813
+ router.push(`/workflows?object=${objectId}&row=${row}&field=${field}`);
814
+ };
815
+
782
816
  const renderItemMenu = (folder, item) => {
783
817
  const composedId = `${folder.id}::${item.id}`;
784
818
  const isMenuOpen = openMenuId === composedId;
@@ -862,9 +896,13 @@ function NavFoldersSection({
862
896
  const renderItemRow = (folder, item) => {
863
897
  const composedId = `${folder.id}::${item.id}`;
864
898
  const isMenuOpen = openMenuId === composedId;
865
- const isActive = item.type === "view" && pathname.startsWith(`/views/${encodeURIComponent(item.id)}`);
899
+ const isActive = item.type === "view" && pathname.startsWith(`/views/${encodeURIComponent(item.id)}`)
900
+ || (item.type === "workflow"
901
+ && pathname.startsWith("/workflows")
902
+ && searchParams.get("object") === item.objectId
903
+ && searchParams.get("row") === item.rowId);
866
904
  const style = navItemStyle(item);
867
- const typeHint = item.type === "dashboard" ? "Dashboard" : "View";
905
+ const typeHint = item.type === "dashboard" ? "Dashboard" : item.type === "workflow" ? "Workflow" : "View";
868
906
  return (
869
907
  <li
870
908
  key={item.id}
@@ -880,7 +918,11 @@ function NavFoldersSection({
880
918
  <button
881
919
  type="button"
882
920
  className="workspace-rail-nav-row-main"
883
- onClick={() => (item.type === "dashboard" ? openDashboardItem(item) : openViewItem(item))}
921
+ onClick={() => {
922
+ if (item.type === "dashboard") openDashboardItem(item);
923
+ else if (item.type === "workflow") openWorkflowItem(item);
924
+ else openViewItem(item);
925
+ }}
884
926
  title={`${item.label || item.refId || item.objectId} · ${typeHint}`}
885
927
  >
886
928
  <NavIconBadge icon={style.icon} color={style.color} iconBg={style.iconBg} />
@@ -984,6 +1026,19 @@ function NavFoldersSection({
984
1026
  >
985
1027
  <TableIcon size={13} aria-hidden="true" /> Add view
986
1028
  </button>
1029
+ <button
1030
+ type="button"
1031
+ role="menuitem"
1032
+ className="workspace-rail-thread-menu-item"
1033
+ disabled={workflows.length === 0}
1034
+ onClick={() => {
1035
+ setOpenMenuId(null);
1036
+ setMenuAnchor(null);
1037
+ setAddPickerFor({ folderId: folder.id, kind: "workflow" });
1038
+ }}
1039
+ >
1040
+ <GitBranch size={13} aria-hidden="true" /> Add workflow
1041
+ </button>
987
1042
  <button
988
1043
  type="button"
989
1044
  role="menuitem"
@@ -1060,7 +1115,13 @@ function NavFoldersSection({
1060
1115
  <NavFolderPickerOverlay onClose={() => setAddPickerFor(null)}>
1061
1116
  <div className="workspace-rail-folder-picker" onClick={(e) => e.stopPropagation()}>
1062
1117
  <div className="workspace-rail-folder-picker-head">
1063
- <strong>{addPickerFor.kind === "dashboard" ? "Add dashboard" : "Add view"}</strong>
1118
+ <strong>
1119
+ {addPickerFor.kind === "dashboard"
1120
+ ? "Add dashboard"
1121
+ : addPickerFor.kind === "workflow"
1122
+ ? "Add workflow"
1123
+ : "Add view"}
1124
+ </strong>
1064
1125
  <button
1065
1126
  type="button"
1066
1127
  className="workspace-rail-folder-picker-close"
@@ -1088,23 +1149,45 @@ function NavFoldersSection({
1088
1149
  </button>
1089
1150
  </li>
1090
1151
  ))
1091
- : viewableObjects.map((o) => (
1092
- <li key={o.id}>
1093
- <button
1094
- type="button"
1095
- className="workspace-rail-folder-picker-item"
1096
- onClick={() => addViewItem(addPickerFor.folderId, o)}
1097
- >
1098
- <NavIconBadge
1099
- icon={NAV_ITEM_STYLE_DEFAULT.view.icon}
1100
- color={NAV_ITEM_STYLE_DEFAULT.view.color}
1101
- iconBg={NAV_ITEM_STYLE_DEFAULT.view.iconBg}
1102
- />
1103
- <span>{o.label}</span>
1104
- <span className="workspace-rail-folder-picker-hint">{o.columns.length} field{o.columns.length === 1 ? "" : "s"}</span>
1105
- </button>
1106
- </li>
1107
- ))}
1152
+ : addPickerFor.kind === "workflow"
1153
+ ? workflows.map((w) => (
1154
+ <li key={`${w.objectId}:${w.rowId}`}>
1155
+ <button
1156
+ type="button"
1157
+ className="workspace-rail-folder-picker-item"
1158
+ onClick={() => addWorkflowItem(addPickerFor.folderId, w)}
1159
+ >
1160
+ <NavIconBadge
1161
+ icon={NAV_ITEM_STYLE_DEFAULT.workflow.icon}
1162
+ color={NAV_ITEM_STYLE_DEFAULT.workflow.color}
1163
+ iconBg={NAV_ITEM_STYLE_DEFAULT.workflow.iconBg}
1164
+ />
1165
+ <span className="workspace-rail-folder-picker-item-text">
1166
+ <span>{w.label}</span>
1167
+ <span className="workspace-rail-folder-picker-hint">
1168
+ {w.objectLabel} · {w.status} · {w.graphNodeCount} node{w.graphNodeCount === 1 ? "" : "s"}
1169
+ </span>
1170
+ </span>
1171
+ </button>
1172
+ </li>
1173
+ ))
1174
+ : viewableObjects.map((o) => (
1175
+ <li key={o.id}>
1176
+ <button
1177
+ type="button"
1178
+ className="workspace-rail-folder-picker-item"
1179
+ onClick={() => addViewItem(addPickerFor.folderId, o)}
1180
+ >
1181
+ <NavIconBadge
1182
+ icon={NAV_ITEM_STYLE_DEFAULT.view.icon}
1183
+ color={NAV_ITEM_STYLE_DEFAULT.view.color}
1184
+ iconBg={NAV_ITEM_STYLE_DEFAULT.view.iconBg}
1185
+ />
1186
+ <span>{o.label}</span>
1187
+ <span className="workspace-rail-folder-picker-hint">{o.columns.length} field{o.columns.length === 1 ? "" : "s"}</span>
1188
+ </button>
1189
+ </li>
1190
+ ))}
1108
1191
  </ul>
1109
1192
  </div>
1110
1193
  </NavFolderPickerOverlay>
@@ -1154,7 +1237,7 @@ function NavFoldersSection({
1154
1237
  <input
1155
1238
  type="search"
1156
1239
  className="workspace-rail-folders-search-input"
1157
- placeholder="Filter folders & views"
1240
+ placeholder="Filter folders & shortcuts"
1158
1241
  value={filterQuery}
1159
1242
  onChange={(e) => setFilterQuery(e.target.value)}
1160
1243
  aria-label="Filter folders and views by name"
@@ -1203,6 +1286,7 @@ function NavFoldersSection({
1203
1286
  { id: "all", label: "All" },
1204
1287
  { id: "dashboard", label: "Dashboards" },
1205
1288
  { id: "view", label: "Views" },
1289
+ { id: "workflow", label: "Workflows" },
1206
1290
  ].map((opt) => (
1207
1291
  <button
1208
1292
  key={opt.id}
@@ -1579,7 +1663,7 @@ export function WorkspaceRail({
1579
1663
  <nav className="workspace-nav" aria-label="Workspace pages">
1580
1664
  {dashboardsSlot ?? (
1581
1665
  <Link href="/" className={pathname === "/" ? "active" : undefined}>
1582
- Dashboards
1666
+ Builder
1583
1667
  </Link>
1584
1668
  )}
1585
1669
  {dataModelSlot ?? (
@@ -27,6 +27,7 @@ const SANDBOX_ENVIRONMENT_FIELDS = {
27
27
  },
28
28
  envRefs: { editor: "env-ref-multiselect" },
29
29
  lastResponse: { editor: "json-preview", readonly: true },
30
+ orchestrationGraph: { editor: "orchestration-graph", readonly: true },
30
31
  lastRunId: { editor: "readonly-text" },
31
32
  lastSourceId: { editor: "readonly-text" },
32
33
  resolverTemplateId: { editor: "readonly-text" },
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Folder workflow shortcuts — discover sandbox-environment rows for nav-folders.
3
+ * Shortcuts reference rows; they do not copy orchestrationGraph JSON.
4
+ */
5
+
6
+ import { parseOrchestrationGraph } from "./orchestration-graph.js";
7
+
8
+ const HIDDEN_SANDBOX_OBJECT_IDS = new Set(["workspace-helper-sandbox"]);
9
+
10
+ function sandboxRowId(row) {
11
+ return String(row?.Name || row?.name || row?.slug || row?.id || "").trim();
12
+ }
13
+
14
+ function listAvailableWorkflows(workspaceConfig) {
15
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
16
+ const out = [];
17
+ for (const object of objects) {
18
+ if (object?.objectType !== "sandbox-environment") continue;
19
+ if (HIDDEN_SANDBOX_OBJECT_IDS.has(String(object?.id || ""))) continue;
20
+ const objectId = String(object?.id || "").trim();
21
+ if (!objectId) continue;
22
+ const objectLabel = String(object?.label || "Sandbox Environment").trim();
23
+ const rows = Array.isArray(object?.rows) ? object.rows : [];
24
+ for (const row of rows) {
25
+ const rowId = sandboxRowId(row);
26
+ if (!rowId) continue;
27
+ const graph = parseOrchestrationGraph(row?.orchestrationGraph || row?.orchestrationConfig);
28
+ const graphNodeCount = Array.isArray(graph?.nodes) ? graph.nodes.length : 0;
29
+ out.push({
30
+ objectId,
31
+ rowId,
32
+ label: rowId,
33
+ status: String(row?.lifecycleStatus || row?.status || "draft").trim(),
34
+ version: String(row?.version || "1").trim(),
35
+ graphNodeCount,
36
+ objectLabel,
37
+ });
38
+ }
39
+ }
40
+ return out.sort((a, b) => a.label.localeCompare(b.label));
41
+ }
42
+
43
+ function findSandboxRowByWorkflowRef(workspaceConfig, objectId, rowId) {
44
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
45
+ const object = objects.find((o) => o?.id === objectId && o?.objectType === "sandbox-environment");
46
+ if (!object) return { object: null, row: null, rowIndex: -1 };
47
+ const wanted = String(rowId || "").trim();
48
+ const rows = Array.isArray(object.rows) ? object.rows : [];
49
+ const rowIndex = rows.findIndex((row) => sandboxRowId(row) === wanted);
50
+ if (rowIndex < 0) return { object, row: null, rowIndex: -1 };
51
+ return { object, row: rows[rowIndex], rowIndex };
52
+ }
53
+
54
+ export { listAvailableWorkflows, findSandboxRowByWorkflowRef, sandboxRowId };
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Server-side growthub-native orchestration graph execution for sandbox-run.
3
+ */
4
+
5
+ import {
6
+ applyFieldMap,
7
+ applyFilters,
8
+ extractApiRegistryCallNode,
9
+ extractInputNode,
10
+ extractTransformConfig,
11
+ normalizeJsonAtPath,
12
+ parseOrchestrationGraph,
13
+ redactSecretsFromText,
14
+ substituteVariables
15
+ } from "./orchestration-graph.js";
16
+
17
+ function normalizeMethod(value) {
18
+ const method = String(value || "GET").trim().toUpperCase();
19
+ return ["GET", "POST", "PUT", "PATCH", "DELETE"].includes(method) ? method : "GET";
20
+ }
21
+
22
+ function buildUrl(record, inputPayload) {
23
+ const baseUrl = String(record?.baseUrl || "").trim();
24
+ let endpoint = String(record?.endpoint || "").trim();
25
+ endpoint = substituteVariables(endpoint, inputPayload);
26
+ const raw = endpoint || baseUrl;
27
+ if (!raw) throw new Error("baseUrl or endpoint is required");
28
+ if (/^https?:\/\//i.test(endpoint)) return endpoint;
29
+ if (!baseUrl) throw new Error("baseUrl is required when endpoint is relative");
30
+ return `${baseUrl.replace(/\/+$/, "")}/${endpoint.replace(/^\/+/, "")}`;
31
+ }
32
+
33
+ function envKeyCandidates(ref) {
34
+ const token = String(ref || "")
35
+ .trim()
36
+ .replace(/[^a-z0-9]+/gi, "_")
37
+ .replace(/^_+|_+$/g, "")
38
+ .toUpperCase();
39
+ return Array.from(new Set([
40
+ token,
41
+ token ? `${token}_API_KEY` : "",
42
+ token ? `${token}_TOKEN` : ""
43
+ ].filter(Boolean)));
44
+ }
45
+
46
+ function readServerSecret(authRef) {
47
+ for (const key of envKeyCandidates(authRef)) {
48
+ if (process.env[key]) return { key, value: process.env[key] };
49
+ }
50
+ return null;
51
+ }
52
+
53
+ function buildAuthHeaders(record, secretValue) {
54
+ if (!secretValue) return {};
55
+ const meta = record?.requestHeadersMetadata || {};
56
+ const headerName = String(meta.authHeaderName || record?.authHeaderName || record?.authHeader || "x-api-key").trim();
57
+ if (!headerName) return {};
58
+ const prefix = String(meta.authPrefix || record?.authPrefix || "").trim();
59
+ return { [headerName]: prefix ? `${prefix} ${secretValue}` : secretValue };
60
+ }
61
+
62
+ function findRegistryRecord(workspaceConfig, registryId) {
63
+ const id = String(registryId || "").trim();
64
+ if (!id) return null;
65
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
66
+ for (const objectItem of objects) {
67
+ if (objectItem?.objectType !== "api-registry") continue;
68
+ const rows = Array.isArray(objectItem.rows) ? objectItem.rows : [];
69
+ const match = rows.find(
70
+ (r) => String(r?.integrationId || "").trim() === id
71
+ || String(r?.id || "").trim() === id
72
+ || String(r?.Name || "").trim() === id
73
+ );
74
+ if (match) return match;
75
+ }
76
+ return null;
77
+ }
78
+
79
+ function parseInputPayload(inputNode) {
80
+ const config = inputNode?.config || {};
81
+ const mode = String(config.inputMode || "manual").trim();
82
+ if (mode === "manual") {
83
+ const sample = config.samplePayload;
84
+ if (sample && typeof sample === "object") return sample;
85
+ if (typeof sample === "string" && sample.trim()) {
86
+ try {
87
+ return JSON.parse(sample);
88
+ } catch {
89
+ return {};
90
+ }
91
+ }
92
+ return {};
93
+ }
94
+ return {};
95
+ }
96
+
97
+ function transformProviderPayload(rawPayload, transformConfig) {
98
+ const config = transformConfig || {};
99
+ const rootPath = String(config.rootPath || "").trim();
100
+ let cursor = rootPath ? getValueAtPath(rawPayload, rootPath) : rawPayload;
101
+ if (cursor === undefined) cursor = rawPayload;
102
+
103
+ const fieldMap = config.fieldMap && typeof config.fieldMap === "object" ? config.fieldMap : {};
104
+ if (Object.keys(fieldMap).length) {
105
+ if (Array.isArray(cursor)) {
106
+ cursor = cursor.map((row) => applyFieldMap(row, fieldMap));
107
+ } else if (cursor && typeof cursor === "object") {
108
+ cursor = applyFieldMap(cursor, fieldMap);
109
+ }
110
+ }
111
+
112
+ if (Array.isArray(cursor)) {
113
+ const filtered = applyFilters(cursor, config.filters, config.filterMode);
114
+ const maxRows = Number(config.maxRows);
115
+ if (Number.isFinite(maxRows) && maxRows > 0) {
116
+ return filtered.slice(0, maxRows);
117
+ }
118
+ return filtered;
119
+ }
120
+
121
+ return cursor;
122
+ }
123
+
124
+ function getValueAtPath(obj, path) {
125
+ if (!path) return obj;
126
+ const parts = String(path).split(".").filter(Boolean);
127
+ let cursor = obj;
128
+ for (const part of parts) {
129
+ if (cursor == null || typeof cursor !== "object") return undefined;
130
+ cursor = cursor[part];
131
+ }
132
+ return cursor;
133
+ }
134
+
135
+ async function executeApiRegistryCall(workspaceConfig, nodeConfig, inputPayload, timeoutMs) {
136
+ const registryId = String(nodeConfig?.registryId || nodeConfig?.integrationId || "").trim();
137
+ const registryRecord = findRegistryRecord(workspaceConfig, registryId);
138
+ if (!registryRecord) {
139
+ return {
140
+ ok: false,
141
+ exitCode: 1,
142
+ durationMs: 0,
143
+ stdout: "",
144
+ stderr: "",
145
+ error: `no API Registry row for integrationId ${registryId}`,
146
+ adapterMeta: { mode: "orchestration-graph", registryId }
147
+ };
148
+ }
149
+
150
+ const merged = {
151
+ ...registryRecord,
152
+ method: nodeConfig?.method || registryRecord.method,
153
+ endpoint: nodeConfig?.endpoint || registryRecord.endpoint,
154
+ baseUrl: nodeConfig?.baseUrl || registryRecord.baseUrl,
155
+ authRef: nodeConfig?.authRef || registryRecord.authRef || registryId,
156
+ requestHeadersMetadata: {
157
+ ...(registryRecord.requestHeadersMetadata || {}),
158
+ ...(nodeConfig?.requestHeadersMetadata || {})
159
+ },
160
+ authHeaderName: nodeConfig?.requestHeadersMetadata?.authHeaderName
161
+ || registryRecord.authHeaderName
162
+ || registryRecord.authHeader,
163
+ authPrefix: nodeConfig?.requestHeadersMetadata?.authPrefix || registryRecord.authPrefix
164
+ };
165
+
166
+ let url;
167
+ try {
168
+ url = buildUrl(merged, inputPayload);
169
+ } catch (err) {
170
+ return {
171
+ ok: false,
172
+ exitCode: 1,
173
+ durationMs: 0,
174
+ stdout: "",
175
+ stderr: "",
176
+ error: err.message || "invalid URL",
177
+ adapterMeta: { mode: "orchestration-graph", registryId }
178
+ };
179
+ }
180
+
181
+ const method = normalizeMethod(merged.method);
182
+ const authRef = merged.authRef || registryId;
183
+ const secretEntry = readServerSecret(authRef);
184
+ const secret = secretEntry?.value || "";
185
+ const outboundTimeout = Math.min(Math.max(timeoutMs, 1000), 120000);
186
+ const startedAt = Date.now();
187
+ const controller = new AbortController();
188
+ const timer = setTimeout(() => controller.abort(), outboundTimeout);
189
+
190
+ const meta = nodeConfig?.requestHeadersMetadata || {};
191
+ const contentType = String(meta.contentType || "").trim() || (method === "GET" ? "" : "application/json");
192
+
193
+ let body;
194
+ const bodyTemplate = substituteVariables(String(nodeConfig?.bodyTemplate || ""), inputPayload);
195
+ if (method !== "GET" && bodyTemplate) {
196
+ try {
197
+ body = JSON.parse(bodyTemplate);
198
+ } catch {
199
+ body = bodyTemplate;
200
+ }
201
+ }
202
+
203
+ try {
204
+ const response = await fetch(url, {
205
+ method,
206
+ headers: {
207
+ accept: "application/json, text/plain;q=0.9,*/*;q=0.8",
208
+ ...(contentType ? { "content-type": contentType } : {}),
209
+ ...buildAuthHeaders(merged, secret)
210
+ },
211
+ ...(body !== undefined ? { body: typeof body === "string" ? body : JSON.stringify(body) } : {}),
212
+ signal: controller.signal
213
+ });
214
+ const durationMs = Date.now() - startedAt;
215
+ const responseContentType = response.headers.get("content-type") || "";
216
+ const payload = responseContentType.includes("application/json") ? await response.json() : await response.text();
217
+
218
+ return {
219
+ ok: response.ok,
220
+ exitCode: response.ok ? 0 : 1,
221
+ durationMs,
222
+ stdout: typeof payload === "string" ? payload : JSON.stringify(payload, null, 2),
223
+ stderr: "",
224
+ error: response.ok ? undefined : `HTTP ${response.status}`,
225
+ rawPayload: payload,
226
+ httpStatus: response.status,
227
+ adapterMeta: {
228
+ mode: "orchestration-graph",
229
+ registryId,
230
+ url,
231
+ httpStatus: response.status,
232
+ method,
233
+ authRefSlug: authRef
234
+ }
235
+ };
236
+ } catch (error) {
237
+ const durationMs = Date.now() - startedAt;
238
+ const safeError = redactSecretsFromText(
239
+ error.name === "AbortError" ? `request timed out after ${outboundTimeout}ms` : (error.message || "fetch failed")
240
+ );
241
+ return {
242
+ ok: false,
243
+ exitCode: null,
244
+ durationMs,
245
+ stdout: "",
246
+ stderr: "",
247
+ error: safeError,
248
+ adapterMeta: { mode: "orchestration-graph", registryId, url, aborted: error.name === "AbortError" }
249
+ };
250
+ } finally {
251
+ clearTimeout(timer);
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Run a growthub-native orchestration graph when present on the sandbox row.
257
+ * Returns null when the row has no executable graph (caller falls back to adapter path).
258
+ */
259
+ async function runOrchestrationGraphIfPresent({ workspaceConfig, row, timeoutMs }) {
260
+ const graph = parseOrchestrationGraph(row?.orchestrationGraph || row?.orchestrationConfig);
261
+ if (!graph || String(graph.provider || "").trim() !== "growthub-native") return null;
262
+
263
+ const apiNode = extractApiRegistryCallNode(graph);
264
+ if (!apiNode?.config) {
265
+ return {
266
+ ok: false,
267
+ exitCode: 1,
268
+ durationMs: 0,
269
+ stdout: "",
270
+ stderr: "",
271
+ error: "orchestrationGraph is missing an api-registry-call node",
272
+ adapterMeta: { mode: "orchestration-graph", provider: graph.provider }
273
+ };
274
+ }
275
+
276
+ const inputNode = extractInputNode(graph);
277
+ const inputPayload = parseInputPayload(inputNode);
278
+ const transformConfig = extractTransformConfig(graph);
279
+ const resultNode = graph.nodes?.find((n) => n?.type === "tool-result");
280
+ const successCodes = Array.isArray(resultNode?.config?.successStatusCodes)
281
+ ? resultNode.config.successStatusCodes.map(Number).filter(Number.isFinite)
282
+ : [200];
283
+
284
+ const raw = await executeApiRegistryCall(
285
+ workspaceConfig,
286
+ apiNode.config,
287
+ inputPayload,
288
+ Number(apiNode.config?.timeoutMs) || timeoutMs
289
+ );
290
+
291
+ if (raw.ok && raw.rawPayload !== undefined) {
292
+ const httpStatus = Number(raw.httpStatus);
293
+ if (successCodes.length && !successCodes.includes(httpStatus)) {
294
+ raw.ok = false;
295
+ raw.exitCode = 1;
296
+ raw.error = `HTTP ${httpStatus} is not in successStatusCodes`;
297
+ }
298
+ const transformed = transformProviderPayload(raw.rawPayload, transformConfig);
299
+ raw.stdout = typeof transformed === "string"
300
+ ? transformed
301
+ : normalizeJsonAtPath(transformed, "");
302
+ delete raw.rawPayload;
303
+ delete raw.httpStatus;
304
+ } else if (raw.error) {
305
+ raw.error = redactSecretsFromText(raw.error);
306
+ }
307
+
308
+ if (raw.stdout) {
309
+ raw.stdout = redactSecretsFromText(raw.stdout);
310
+ }
311
+
312
+ return {
313
+ ...raw,
314
+ adapterMeta: {
315
+ ...(raw.adapterMeta || {}),
316
+ orchestrationProvider: graph.provider,
317
+ orchestrationVersion: graph.version
318
+ }
319
+ };
320
+ }
321
+
322
+ export { runOrchestrationGraphIfPresent };