@growthub/cli 0.13.5 → 0.13.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/QUICKSTART.md +19 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/.env.example +8 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +4 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/action/execute/route.js +60 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/actions/route.js +50 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/connect-session/route.js +68 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/connection-status/route.js +56 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/proxy/route.js +67 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/status/route.js +50 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +172 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +161 -50
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/NangoConnectionPanel.jsx +531 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +274 -18
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/nango/page.jsx +167 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +62 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +554 -48
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +24 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/env.js +7 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/index.js +38 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-adapter.js +552 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-config-loader.js +202 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-schema.js +303 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +49 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/source-resolver-registry.js +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/nango.js +49 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/template-registry.js +4 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +534 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +3 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +82 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +102 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/bundles/growthub-custom-workspace-starter-v1.json +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/templates/seeded-configs/project-management.config.json +276 -0
- package/dist/index.js +127 -44
- package/package.json +1 -1
|
@@ -1488,11 +1488,17 @@ export function WorkspaceRail({
|
|
|
1488
1488
|
onConfigChange,
|
|
1489
1489
|
dashboardsSlot,
|
|
1490
1490
|
dataModelSlot,
|
|
1491
|
+
defaultCollapsed = false,
|
|
1491
1492
|
// `managementSlot` retained as accepted-but-ignored prop for backward
|
|
1492
1493
|
// compatibility with callers that still pass it. The Management item
|
|
1493
1494
|
// moved to the Workspace Settings → Ownership tab.
|
|
1494
1495
|
managementSlot: _managementSlotDeprecated,
|
|
1495
1496
|
settingsSlot,
|
|
1497
|
+
// Customer Activation Layer V1 — optional rail-friendly slot rendered
|
|
1498
|
+
// above the Home tab nav items. The parent page owns the content
|
|
1499
|
+
// (typically a compact WorkspaceActivationPanel) so the rail stays
|
|
1500
|
+
// surface-agnostic.
|
|
1501
|
+
activationSlot,
|
|
1496
1502
|
}) {
|
|
1497
1503
|
const branding = workspaceConfig?.branding || {};
|
|
1498
1504
|
const workspaceName = branding.name || workspaceConfig?.name || "Growthub Workspace";
|
|
@@ -1500,7 +1506,7 @@ export function WorkspaceRail({
|
|
|
1500
1506
|
const router = useRouter();
|
|
1501
1507
|
|
|
1502
1508
|
const [activeTab, setActiveTab] = useState("home");
|
|
1503
|
-
const [railCollapsed, setRailCollapsed] = useState(
|
|
1509
|
+
const [railCollapsed, setRailCollapsed] = useState(Boolean(defaultCollapsed));
|
|
1504
1510
|
const [openMenuId, setOpenMenuId] = useState(null);
|
|
1505
1511
|
const [renamingId, setRenamingId] = useState(null);
|
|
1506
1512
|
const [renameDraft, setRenameDraft] = useState("");
|
|
@@ -1521,6 +1527,10 @@ export function WorkspaceRail({
|
|
|
1521
1527
|
|
|
1522
1528
|
const threads = useMemo(() => getHelperThreadRows(workspaceConfig), [workspaceConfig]);
|
|
1523
1529
|
|
|
1530
|
+
useEffect(() => {
|
|
1531
|
+
setRailCollapsed(Boolean(defaultCollapsed));
|
|
1532
|
+
}, [defaultCollapsed]);
|
|
1533
|
+
|
|
1524
1534
|
useEffect(() => {
|
|
1525
1535
|
if (typeof document === "undefined") return undefined;
|
|
1526
1536
|
document.body.classList.toggle("workspace-rail-collapsed", railCollapsed);
|
|
@@ -1640,6 +1650,16 @@ export function WorkspaceRail({
|
|
|
1640
1650
|
<ChevronDown size={13} className="workspace-brand-caret" aria-hidden="true" />
|
|
1641
1651
|
</button>
|
|
1642
1652
|
<div className="workspace-rail-topbar-actions">
|
|
1653
|
+
<button
|
|
1654
|
+
type="button"
|
|
1655
|
+
className="workspace-rail-icon-btn"
|
|
1656
|
+
aria-label={railCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
1657
|
+
title={railCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
1658
|
+
aria-pressed={railCollapsed}
|
|
1659
|
+
onClick={() => setRailCollapsed((v) => !v)}
|
|
1660
|
+
>
|
|
1661
|
+
<PanelLeftClose size={13} />
|
|
1662
|
+
</button>
|
|
1643
1663
|
<button
|
|
1644
1664
|
type="button"
|
|
1645
1665
|
className="workspace-rail-icon-btn"
|
|
@@ -1647,9 +1667,6 @@ export function WorkspaceRail({
|
|
|
1647
1667
|
title="Search (⌘K)"
|
|
1648
1668
|
data-rail-search=""
|
|
1649
1669
|
onClick={() => {
|
|
1650
|
-
// Surfaces with a command palette (DataModelShell) listen
|
|
1651
|
-
// for this event and open the palette in place. Other
|
|
1652
|
-
// surfaces are free to ignore it.
|
|
1653
1670
|
if (typeof window !== "undefined") {
|
|
1654
1671
|
window.dispatchEvent(new CustomEvent("growthub:open-command-palette"));
|
|
1655
1672
|
}
|
|
@@ -1657,16 +1674,6 @@ export function WorkspaceRail({
|
|
|
1657
1674
|
>
|
|
1658
1675
|
<Search size={13} />
|
|
1659
1676
|
</button>
|
|
1660
|
-
<button
|
|
1661
|
-
type="button"
|
|
1662
|
-
className="workspace-rail-icon-btn"
|
|
1663
|
-
aria-label={railCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
1664
|
-
title={railCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
1665
|
-
aria-pressed={railCollapsed}
|
|
1666
|
-
onClick={() => setRailCollapsed((v) => !v)}
|
|
1667
|
-
>
|
|
1668
|
-
<PanelLeftClose size={13} />
|
|
1669
|
-
</button>
|
|
1670
1677
|
<button
|
|
1671
1678
|
type="button"
|
|
1672
1679
|
className="workspace-rail-icon-btn"
|
|
@@ -1738,6 +1745,9 @@ export function WorkspaceRail({
|
|
|
1738
1745
|
model surface IS the user-facing object/list management. */}
|
|
1739
1746
|
{activeTab === "home" ? (
|
|
1740
1747
|
<nav className="workspace-nav" aria-label="Workspace pages">
|
|
1748
|
+
{activationSlot ? (
|
|
1749
|
+
<div className="workspace-rail-activation-slot">{activationSlot}</div>
|
|
1750
|
+
) : null}
|
|
1741
1751
|
{dashboardsSlot ?? (
|
|
1742
1752
|
<Link href="/" className={pathname === "/" ? "active" : undefined}>
|
|
1743
1753
|
Builder
|
package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/env.js
CHANGED
|
@@ -12,6 +12,13 @@ function readAdapterConfig() {
|
|
|
12
12
|
userId: process.env.GROWTHUB_BRIDGE_USER_ID || void 0,
|
|
13
13
|
hasAccessToken: Boolean(process.env.GROWTHUB_BRIDGE_ACCESS_TOKEN)
|
|
14
14
|
},
|
|
15
|
+
nango: {
|
|
16
|
+
mode: readEnum(["NANGO_MODE"], ["cloud", "self-hosted"], "cloud"),
|
|
17
|
+
hostUrl: process.env.NANGO_HOST_URL || void 0,
|
|
18
|
+
environment: process.env.NANGO_ENVIRONMENT || "dev",
|
|
19
|
+
secretEnvName: "NANGO_SECRET_KEY",
|
|
20
|
+
hasSecretKey: Boolean(process.env.NANGO_SECRET_KEY)
|
|
21
|
+
},
|
|
15
22
|
dataSources: {
|
|
16
23
|
hasWindsorApiKey: Boolean(process.env.WINDSOR_API_KEY)
|
|
17
24
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nango adapter barrel — server-side imports only.
|
|
3
|
+
*
|
|
4
|
+
* The browser must never import any module under this path. Routes consume
|
|
5
|
+
* this barrel; UI components hit the routes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
DEFAULT_NANGO_SECRET_ENV,
|
|
10
|
+
createConnectSession,
|
|
11
|
+
describeNangoAdapter,
|
|
12
|
+
executeAction,
|
|
13
|
+
getConnectionSummary,
|
|
14
|
+
getStatus,
|
|
15
|
+
listActions,
|
|
16
|
+
pickSafeConnectionFields,
|
|
17
|
+
projectNangoBinding,
|
|
18
|
+
proxyRequest,
|
|
19
|
+
resolveNangoEnv
|
|
20
|
+
} from "./nango-adapter.js";
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
buildNangoResolver,
|
|
24
|
+
registerNangoResolversFromConfig
|
|
25
|
+
} from "./nango-config-loader.js";
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
validateActionExecuteRequest,
|
|
29
|
+
validateActionsListInput,
|
|
30
|
+
validateConnectSessionRequest,
|
|
31
|
+
validateConnectionId,
|
|
32
|
+
validateConnectionStatusRequest,
|
|
33
|
+
validateConnectionSummaryRequest,
|
|
34
|
+
validateHostUrl,
|
|
35
|
+
validateNangoMode,
|
|
36
|
+
validateProviderConfigKey,
|
|
37
|
+
validateProxyRequest
|
|
38
|
+
} from "./nango-schema.js";
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nango thin adapter — server-side only.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `@nangohq/node` and exposes four operations consumed by the workspace:
|
|
5
|
+
* - getStatus() connection health (secret + reachability check)
|
|
6
|
+
* - proxyRequest() proxy an API call through Nango
|
|
7
|
+
* - listActions() enumerate enabled action functions for a provider
|
|
8
|
+
* - executeAction() execute a Nango action function
|
|
9
|
+
*
|
|
10
|
+
* The adapter operates on the EXISTING `objectType: "api-registry"` row
|
|
11
|
+
* shape owned by `lib/workspace-data-model.js`. Nango-backed rows declare
|
|
12
|
+
* `connectorKind: "nango"`. Their `integrationId` is the resolver key, and
|
|
13
|
+
* (when no `providerConfigKey` is set explicitly) the Nango providerConfigKey
|
|
14
|
+
* defaults to that same `integrationId`. The `authRef` column names the env
|
|
15
|
+
* var that holds the Nango secret (defaults to `NANGO_SECRET_KEY`).
|
|
16
|
+
*
|
|
17
|
+
* Authority invariants (do not violate):
|
|
18
|
+
* 1. Nango secret key is resolved from env on every call. It is NEVER
|
|
19
|
+
* read from request bodies, config files, or browser state.
|
|
20
|
+
* 2. The Nango SDK is loaded via dynamic import. When the package is not
|
|
21
|
+
* installed, `getStatus()` reports `status: "disconnected"` with a
|
|
22
|
+
* diagnostic reason so the rest of the workspace keeps building.
|
|
23
|
+
* 3. Every public method takes already-validated input from `nango-schema`.
|
|
24
|
+
* This module does not re-validate; it dispatches.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { readAdapterConfig } from "../../env.js";
|
|
28
|
+
|
|
29
|
+
const DEFAULT_NANGO_SECRET_ENV = "NANGO_SECRET_KEY";
|
|
30
|
+
|
|
31
|
+
let cachedNangoModule = null;
|
|
32
|
+
let nangoModuleLoadError = null;
|
|
33
|
+
|
|
34
|
+
async function loadNangoModule() {
|
|
35
|
+
if (cachedNangoModule) return cachedNangoModule;
|
|
36
|
+
if (nangoModuleLoadError) return null;
|
|
37
|
+
try {
|
|
38
|
+
// eslint-disable-next-line import/no-unresolved
|
|
39
|
+
const mod = await import("@nangohq/node");
|
|
40
|
+
cachedNangoModule = mod;
|
|
41
|
+
return mod;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
nangoModuleLoadError = error;
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve a Nango env profile. `override` lets callers (e.g. a per-row
|
|
50
|
+
* api-registry record) pin a different mode / host / environment without
|
|
51
|
+
* mutating process.env.
|
|
52
|
+
*/
|
|
53
|
+
function resolveNangoEnv(override = {}) {
|
|
54
|
+
const env = readAdapterConfig().nango || {};
|
|
55
|
+
const secretEnvName = String(override.secretEnvName || DEFAULT_NANGO_SECRET_ENV).trim() || DEFAULT_NANGO_SECRET_ENV;
|
|
56
|
+
const secretKey = process.env[secretEnvName] || null;
|
|
57
|
+
const mode = override.mode || env.mode || "cloud";
|
|
58
|
+
const hostUrl = override.hostUrl || env.hostUrl || null;
|
|
59
|
+
const environment = override.environment || env.environment || "dev";
|
|
60
|
+
return {
|
|
61
|
+
mode,
|
|
62
|
+
hostUrl,
|
|
63
|
+
environment,
|
|
64
|
+
secretEnvName,
|
|
65
|
+
hasSecretKey: Boolean(secretKey),
|
|
66
|
+
// Internal-only; never returned to callers.
|
|
67
|
+
_secretKey: secretKey
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildClientOptions(env) {
|
|
72
|
+
const opts = { secretKey: env._secretKey };
|
|
73
|
+
if (env.mode === "self-hosted" && env.hostUrl) {
|
|
74
|
+
opts.host = env.hostUrl;
|
|
75
|
+
}
|
|
76
|
+
return opts;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function getNangoClient(envOverride) {
|
|
80
|
+
const env = resolveNangoEnv(envOverride);
|
|
81
|
+
if (!env.hasSecretKey) {
|
|
82
|
+
return { client: null, env, reason: "missing-secret" };
|
|
83
|
+
}
|
|
84
|
+
const mod = await loadNangoModule();
|
|
85
|
+
if (!mod) {
|
|
86
|
+
return { client: null, env, reason: "sdk-not-installed" };
|
|
87
|
+
}
|
|
88
|
+
const Ctor = mod.Nango || mod.default;
|
|
89
|
+
if (typeof Ctor !== "function") {
|
|
90
|
+
return { client: null, env, reason: "sdk-shape-unrecognized" };
|
|
91
|
+
}
|
|
92
|
+
const client = new Ctor(buildClientOptions(env));
|
|
93
|
+
return { client, env, reason: null };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function stripSecrets(value) {
|
|
97
|
+
if (!value || typeof value !== "object") return value;
|
|
98
|
+
if (Array.isArray(value)) return value.map(stripSecrets);
|
|
99
|
+
const REDACTED_KEYS = [
|
|
100
|
+
"secret",
|
|
101
|
+
"secretKey",
|
|
102
|
+
"secret_key",
|
|
103
|
+
"apiKey",
|
|
104
|
+
"api_key",
|
|
105
|
+
"access_token",
|
|
106
|
+
"accessToken",
|
|
107
|
+
"refresh_token",
|
|
108
|
+
"refreshToken",
|
|
109
|
+
"client_secret",
|
|
110
|
+
"clientSecret"
|
|
111
|
+
];
|
|
112
|
+
const out = {};
|
|
113
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
114
|
+
if (REDACTED_KEYS.includes(key)) continue;
|
|
115
|
+
out[key] = stripSecrets(raw);
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Project a row from an `objectType: "api-registry"` object into the
|
|
122
|
+
* Nango-specific binding shape used by this adapter. Returns null when the
|
|
123
|
+
* row is not Nango-backed (so callers can early-out without throwing).
|
|
124
|
+
*/
|
|
125
|
+
function projectNangoBinding(row) {
|
|
126
|
+
if (!row || typeof row !== "object") return null;
|
|
127
|
+
if (row.connectorKind !== "nango") return null;
|
|
128
|
+
const integrationId = String(row.integrationId || "").trim();
|
|
129
|
+
const providerConfigKey = String(row.providerConfigKey || integrationId || "").trim();
|
|
130
|
+
if (!providerConfigKey) return null;
|
|
131
|
+
const connectionIds = Array.isArray(row.connectionIds)
|
|
132
|
+
? row.connectionIds.filter((c) => typeof c === "string" && c.trim()).map((c) => c.trim())
|
|
133
|
+
: typeof row.connectionIds === "string"
|
|
134
|
+
? row.connectionIds.split(",").map((c) => c.trim()).filter(Boolean)
|
|
135
|
+
: [];
|
|
136
|
+
const enabledActions = Array.isArray(row.enabledActions)
|
|
137
|
+
? row.enabledActions.filter((a) => typeof a === "string" && a.trim()).map((a) => a.trim())
|
|
138
|
+
: typeof row.enabledActions === "string"
|
|
139
|
+
? row.enabledActions.split(",").map((a) => a.trim()).filter(Boolean)
|
|
140
|
+
: [];
|
|
141
|
+
return {
|
|
142
|
+
integrationId,
|
|
143
|
+
providerConfigKey,
|
|
144
|
+
connectionIds,
|
|
145
|
+
enabledActions,
|
|
146
|
+
endpoint: typeof row.endpoint === "string" ? row.endpoint.trim() : "",
|
|
147
|
+
method: typeof row.method === "string" && row.method.trim() ? row.method.trim().toUpperCase() : "GET",
|
|
148
|
+
secretEnvName: typeof row.authRef === "string" && row.authRef.trim() ? row.authRef.trim() : DEFAULT_NANGO_SECRET_ENV,
|
|
149
|
+
mode: row.nangoMode || undefined,
|
|
150
|
+
hostUrl: row.nangoHostUrl || undefined,
|
|
151
|
+
environment: row.nangoEnvironment || undefined
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Describe the Nango configuration state. Used by GET /status. Never throws:
|
|
157
|
+
* returns one of `connected | disconnected | unconfigured` so the no-code UI
|
|
158
|
+
* can render a status badge without try/catch.
|
|
159
|
+
*/
|
|
160
|
+
async function getStatus(input = {}) {
|
|
161
|
+
const env = resolveNangoEnv(input);
|
|
162
|
+
if (!env.hasSecretKey) {
|
|
163
|
+
return {
|
|
164
|
+
status: "unconfigured",
|
|
165
|
+
mode: env.mode,
|
|
166
|
+
environment: env.environment,
|
|
167
|
+
hostUrl: env.hostUrl,
|
|
168
|
+
secretEnvName: env.secretEnvName,
|
|
169
|
+
reason: `Set ${env.secretEnvName} in this runtime's environment to enable Nango.`,
|
|
170
|
+
sdkAvailable: cachedNangoModule != null
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const { client, reason } = await getNangoClient(input);
|
|
174
|
+
if (!client) {
|
|
175
|
+
return {
|
|
176
|
+
status: "disconnected",
|
|
177
|
+
mode: env.mode,
|
|
178
|
+
environment: env.environment,
|
|
179
|
+
hostUrl: env.hostUrl,
|
|
180
|
+
secretEnvName: env.secretEnvName,
|
|
181
|
+
reason: reason === "sdk-not-installed"
|
|
182
|
+
? "Install `@nangohq/node` in apps/workspace to enable Nango operations."
|
|
183
|
+
: `Nango client unavailable: ${reason}`,
|
|
184
|
+
sdkAvailable: false
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
let connectionCount = null;
|
|
188
|
+
let providerCount = null;
|
|
189
|
+
try {
|
|
190
|
+
if (input.providerConfigKey && input.connectionId && typeof client.getConnection === "function") {
|
|
191
|
+
const conn = await client.getConnection(input.providerConfigKey, input.connectionId);
|
|
192
|
+
return {
|
|
193
|
+
status: "connected",
|
|
194
|
+
mode: env.mode,
|
|
195
|
+
environment: env.environment,
|
|
196
|
+
hostUrl: env.hostUrl,
|
|
197
|
+
secretEnvName: env.secretEnvName,
|
|
198
|
+
sdkAvailable: true,
|
|
199
|
+
probedProvider: input.providerConfigKey,
|
|
200
|
+
probedConnection: input.connectionId,
|
|
201
|
+
connection: stripSecrets(conn)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (typeof client.listConnections === "function") {
|
|
205
|
+
const list = await client.listConnections();
|
|
206
|
+
if (Array.isArray(list?.connections)) connectionCount = list.connections.length;
|
|
207
|
+
else if (Array.isArray(list)) connectionCount = list.length;
|
|
208
|
+
}
|
|
209
|
+
if (typeof client.listIntegrations === "function") {
|
|
210
|
+
const list = await client.listIntegrations();
|
|
211
|
+
if (Array.isArray(list?.configs)) providerCount = list.configs.length;
|
|
212
|
+
else if (Array.isArray(list)) providerCount = list.length;
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
return {
|
|
216
|
+
status: "disconnected",
|
|
217
|
+
mode: env.mode,
|
|
218
|
+
environment: env.environment,
|
|
219
|
+
hostUrl: env.hostUrl,
|
|
220
|
+
secretEnvName: env.secretEnvName,
|
|
221
|
+
sdkAvailable: true,
|
|
222
|
+
reason: error?.message || "nango reachability probe failed"
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
status: "connected",
|
|
227
|
+
mode: env.mode,
|
|
228
|
+
environment: env.environment,
|
|
229
|
+
hostUrl: env.hostUrl,
|
|
230
|
+
secretEnvName: env.secretEnvName,
|
|
231
|
+
sdkAvailable: true,
|
|
232
|
+
connectionCount,
|
|
233
|
+
providerCount
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function proxyRequest(request) {
|
|
238
|
+
const envOverride = request.secretEnvName ? { secretEnvName: request.secretEnvName } : undefined;
|
|
239
|
+
const { client, env, reason } = await getNangoClient(envOverride);
|
|
240
|
+
if (!client) {
|
|
241
|
+
const error = new Error(reason === "missing-secret"
|
|
242
|
+
? `Nango secret is missing (set ${env.secretEnvName})`
|
|
243
|
+
: reason === "sdk-not-installed"
|
|
244
|
+
? "Nango SDK (@nangohq/node) is not installed in apps/workspace"
|
|
245
|
+
: `Nango client unavailable: ${reason}`);
|
|
246
|
+
error.code = reason === "missing-secret" ? "NANGO_NOT_CONFIGURED" : "NANGO_SDK_UNAVAILABLE";
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
if (typeof client.proxy !== "function") {
|
|
250
|
+
const error = new Error("@nangohq/node does not expose a proxy method in this version");
|
|
251
|
+
error.code = "NANGO_SDK_SHAPE";
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
const sdkRequest = {
|
|
255
|
+
providerConfigKey: request.providerConfigKey,
|
|
256
|
+
connectionId: request.connectionId,
|
|
257
|
+
method: request.method,
|
|
258
|
+
endpoint: request.endpoint,
|
|
259
|
+
headers: request.headers,
|
|
260
|
+
params: request.params,
|
|
261
|
+
data: request.data,
|
|
262
|
+
retries: request.retries,
|
|
263
|
+
timeoutMs: request.timeoutMs
|
|
264
|
+
};
|
|
265
|
+
const result = await client.proxy(sdkRequest);
|
|
266
|
+
const responseStatus = typeof result?.status === "number" ? result.status : null;
|
|
267
|
+
const data = result?.data !== undefined ? result.data : result;
|
|
268
|
+
return {
|
|
269
|
+
status: responseStatus,
|
|
270
|
+
data: stripSecrets(data),
|
|
271
|
+
environment: env.environment
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function listActions(input = {}) {
|
|
276
|
+
const { client, reason } = await getNangoClient();
|
|
277
|
+
if (!client) {
|
|
278
|
+
const error = new Error(reason === "missing-secret"
|
|
279
|
+
? `Nango secret is missing (set ${DEFAULT_NANGO_SECRET_ENV})`
|
|
280
|
+
: `Nango client unavailable: ${reason}`);
|
|
281
|
+
error.code = reason === "missing-secret" ? "NANGO_NOT_CONFIGURED" : "NANGO_SDK_UNAVAILABLE";
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
let actions = [];
|
|
285
|
+
let probedShape = null;
|
|
286
|
+
for (const methodName of ["listActions", "getActions", "listScripts"]) {
|
|
287
|
+
if (typeof client[methodName] === "function") {
|
|
288
|
+
try {
|
|
289
|
+
const raw = await client[methodName](input.providerConfigKey);
|
|
290
|
+
probedShape = methodName;
|
|
291
|
+
if (Array.isArray(raw)) {
|
|
292
|
+
actions = raw;
|
|
293
|
+
} else if (Array.isArray(raw?.actions)) {
|
|
294
|
+
actions = raw.actions;
|
|
295
|
+
} else if (Array.isArray(raw?.scripts)) {
|
|
296
|
+
actions = raw.scripts;
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
} catch {
|
|
300
|
+
// try next shape
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
providerConfigKey: input.providerConfigKey || null,
|
|
306
|
+
probedShape,
|
|
307
|
+
actions: actions.map(stripSecrets),
|
|
308
|
+
hint: probedShape
|
|
309
|
+
? null
|
|
310
|
+
: "This @nangohq/node version does not expose an actions listing method; declare actions in nango.yaml and call /action/execute by name."
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function executeAction(request) {
|
|
315
|
+
const envOverride = request.secretEnvName ? { secretEnvName: request.secretEnvName } : undefined;
|
|
316
|
+
const { client, env, reason } = await getNangoClient(envOverride);
|
|
317
|
+
if (!client) {
|
|
318
|
+
const error = new Error(reason === "missing-secret"
|
|
319
|
+
? `Nango secret is missing (set ${env.secretEnvName})`
|
|
320
|
+
: `Nango client unavailable: ${reason}`);
|
|
321
|
+
error.code = reason === "missing-secret" ? "NANGO_NOT_CONFIGURED" : "NANGO_SDK_UNAVAILABLE";
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
if (typeof client.triggerAction !== "function") {
|
|
325
|
+
const error = new Error("@nangohq/node does not expose triggerAction in this version");
|
|
326
|
+
error.code = "NANGO_SDK_SHAPE";
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
const raw = await client.triggerAction(
|
|
330
|
+
request.providerConfigKey,
|
|
331
|
+
request.connectionId,
|
|
332
|
+
request.action,
|
|
333
|
+
request.input
|
|
334
|
+
);
|
|
335
|
+
return {
|
|
336
|
+
action: request.action,
|
|
337
|
+
providerConfigKey: request.providerConfigKey,
|
|
338
|
+
connectionId: request.connectionId,
|
|
339
|
+
environment: env.environment,
|
|
340
|
+
result: stripSecrets(raw)
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Project a Nango connection object down to the no-credential fields the
|
|
346
|
+
* browser is allowed to see. Anything provider-specific (raw OAuth payload,
|
|
347
|
+
* access/refresh tokens, client secrets) is dropped — only the fields a
|
|
348
|
+
* status badge or sidecar UI legitimately needs are kept.
|
|
349
|
+
*/
|
|
350
|
+
function pickSafeConnectionFields(connection) {
|
|
351
|
+
if (!connection || typeof connection !== "object") return null;
|
|
352
|
+
const safe = {
|
|
353
|
+
providerConfigKey: connection.providerConfigKey || connection.provider_config_key || null,
|
|
354
|
+
provider: connection.provider || null,
|
|
355
|
+
connectionId: connection.connectionId || connection.connection_id || null,
|
|
356
|
+
environment: connection.environment || null,
|
|
357
|
+
created: connection.created_at || connection.createdAt || null,
|
|
358
|
+
updated: connection.updated_at || connection.updatedAt || null,
|
|
359
|
+
lastFetchedAt: connection.last_fetched_at || connection.lastFetchedAt || null,
|
|
360
|
+
expiresAt: connection.credentials?.expires_at
|
|
361
|
+
|| connection.credentials?.expiresAt
|
|
362
|
+
|| connection.expires_at
|
|
363
|
+
|| connection.expiresAt
|
|
364
|
+
|| null,
|
|
365
|
+
credentialsType: connection.credentials?.type || null
|
|
366
|
+
};
|
|
367
|
+
// Drop any null-only fields so the response stays compact.
|
|
368
|
+
return Object.fromEntries(Object.entries(safe).filter(([, v]) => v !== null));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Create a Nango Connect session and return the OAuth handoff link (no
|
|
373
|
+
* secret). The Nango Connect UI opens with this link in a new window or
|
|
374
|
+
* redirect; tokens are minted by Nango directly — the workspace never sees
|
|
375
|
+
* raw OAuth credentials. The `connectionId` is generated by Nango during
|
|
376
|
+
* OAuth and delivered via the auth webhook; it is NOT required to create
|
|
377
|
+
* a normal session. Only the explicit Reconnect path (`reconnect: true`)
|
|
378
|
+
* needs a known connectionId.
|
|
379
|
+
*
|
|
380
|
+
* `input` shape:
|
|
381
|
+
* {
|
|
382
|
+
* providerConfigKey: string,
|
|
383
|
+
* connectionId?: string, // only for reconnect
|
|
384
|
+
* reconnect?: boolean,
|
|
385
|
+
* endUser?: { id, email },
|
|
386
|
+
* tags?: { [key]: string } // echoed back in auth webhook
|
|
387
|
+
* }
|
|
388
|
+
*/
|
|
389
|
+
async function createConnectSession(input) {
|
|
390
|
+
const { client, env, reason } = await getNangoClient(
|
|
391
|
+
input?.secretEnvName ? { secretEnvName: input.secretEnvName } : undefined
|
|
392
|
+
);
|
|
393
|
+
if (!client) {
|
|
394
|
+
const error = new Error(reason === "missing-secret"
|
|
395
|
+
? `Nango secret is missing (set ${env.secretEnvName})`
|
|
396
|
+
: reason === "sdk-not-installed"
|
|
397
|
+
? "Nango SDK (@nangohq/node) is not installed in apps/workspace"
|
|
398
|
+
: `Nango client unavailable: ${reason}`);
|
|
399
|
+
error.code = reason === "missing-secret" ? "NANGO_NOT_CONFIGURED" : "NANGO_SDK_UNAVAILABLE";
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
// Try the canonical SDK shape first, then alternates that have shipped
|
|
403
|
+
// across Nango versions. Each candidate is wrapped so a missing method
|
|
404
|
+
// does not crash — we throw a clear NANGO_SDK_SHAPE error if none match.
|
|
405
|
+
// The reconnect path uses a dedicated SDK method when available; falls
|
|
406
|
+
// back to the standard payload with `connection_id` for older SDKs.
|
|
407
|
+
const baseTags = {
|
|
408
|
+
growthub_workspace: "growthub-custom-workspace-starter-v1",
|
|
409
|
+
...(input.tags || {})
|
|
410
|
+
};
|
|
411
|
+
const payload = {
|
|
412
|
+
allowed_integrations: [input.providerConfigKey],
|
|
413
|
+
end_user: input.endUser && typeof input.endUser === "object" ? input.endUser : undefined,
|
|
414
|
+
tags: baseTags
|
|
415
|
+
};
|
|
416
|
+
const reconnectPayload = {
|
|
417
|
+
...payload,
|
|
418
|
+
connection_id: input.connectionId
|
|
419
|
+
};
|
|
420
|
+
const candidates = input.reconnect
|
|
421
|
+
? [
|
|
422
|
+
["createReconnectSession", reconnectPayload],
|
|
423
|
+
["reconnectSession", reconnectPayload],
|
|
424
|
+
["createConnectSession", reconnectPayload]
|
|
425
|
+
]
|
|
426
|
+
: [
|
|
427
|
+
["createConnectSession", payload],
|
|
428
|
+
["createSession", payload],
|
|
429
|
+
["connectSession", { providerConfigKey: input.providerConfigKey, tags: baseTags }]
|
|
430
|
+
];
|
|
431
|
+
let raw = null;
|
|
432
|
+
let usedMethod = null;
|
|
433
|
+
let lastError = null;
|
|
434
|
+
for (const [methodName, args] of candidates) {
|
|
435
|
+
if (typeof client[methodName] !== "function") continue;
|
|
436
|
+
try {
|
|
437
|
+
raw = await client[methodName](args);
|
|
438
|
+
usedMethod = methodName;
|
|
439
|
+
break;
|
|
440
|
+
} catch (error) {
|
|
441
|
+
lastError = error;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (!raw) {
|
|
445
|
+
if (lastError) {
|
|
446
|
+
const error = new Error(lastError?.message || "nango connect-session failed");
|
|
447
|
+
error.code = "NANGO_CONNECT_SESSION_FAILED";
|
|
448
|
+
throw error;
|
|
449
|
+
}
|
|
450
|
+
const error = new Error("@nangohq/node does not expose a connect-session method in this version");
|
|
451
|
+
error.code = "NANGO_SDK_SHAPE";
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
// The Nango response carries a `token` (Connect Session token) and a
|
|
455
|
+
// `connect_link` (URL the user opens). Both are surface-level pointers —
|
|
456
|
+
// the token cannot be used to mint provider credentials; only the Connect
|
|
457
|
+
// UI can. Still, we redact everything else.
|
|
458
|
+
const token = raw?.data?.token || raw?.token || null;
|
|
459
|
+
const connectLink = raw?.data?.connect_link || raw?.connect_link || raw?.url || null;
|
|
460
|
+
return {
|
|
461
|
+
providerConfigKey: input.providerConfigKey,
|
|
462
|
+
environment: env.environment,
|
|
463
|
+
mode: input.reconnect ? "reconnect" : "connect",
|
|
464
|
+
token,
|
|
465
|
+
connectLink,
|
|
466
|
+
sdkMethod: usedMethod,
|
|
467
|
+
tagsEchoed: baseTags
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Fetch a Nango connection and return only the safe (non-credential) summary.
|
|
473
|
+
* Used by POST /integrations/nango/connection-status to verify a per-row
|
|
474
|
+
* connection from the no-code UI.
|
|
475
|
+
*
|
|
476
|
+
* `input` shape:
|
|
477
|
+
* { providerConfigKey: string, connectionId: string, secretEnvName?: string }
|
|
478
|
+
*/
|
|
479
|
+
async function getConnectionSummary(input) {
|
|
480
|
+
const { client, env, reason } = await getNangoClient(
|
|
481
|
+
input?.secretEnvName ? { secretEnvName: input.secretEnvName } : undefined
|
|
482
|
+
);
|
|
483
|
+
if (!client) {
|
|
484
|
+
const error = new Error(reason === "missing-secret"
|
|
485
|
+
? `Nango secret is missing (set ${env.secretEnvName})`
|
|
486
|
+
: `Nango client unavailable: ${reason}`);
|
|
487
|
+
error.code = reason === "missing-secret" ? "NANGO_NOT_CONFIGURED" : "NANGO_SDK_UNAVAILABLE";
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
if (typeof client.getConnection !== "function") {
|
|
491
|
+
const error = new Error("@nangohq/node does not expose getConnection in this version");
|
|
492
|
+
error.code = "NANGO_SDK_SHAPE";
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
let raw;
|
|
496
|
+
try {
|
|
497
|
+
raw = await client.getConnection(input.providerConfigKey, input.connectionId);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
// Nango returns 404 for unknown connection — surface that explicitly so
|
|
500
|
+
// the UI can render a "not yet connected" badge instead of a hard error.
|
|
501
|
+
const status = error?.response?.status || error?.status;
|
|
502
|
+
if (status === 404) {
|
|
503
|
+
return {
|
|
504
|
+
status: "not-connected",
|
|
505
|
+
providerConfigKey: input.providerConfigKey,
|
|
506
|
+
connectionId: input.connectionId,
|
|
507
|
+
environment: env.environment,
|
|
508
|
+
reason: "Nango has no record of this providerConfigKey + connectionId pair yet."
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
const out = new Error(error?.message || "nango getConnection failed");
|
|
512
|
+
out.code = "NANGO_GET_CONNECTION_FAILED";
|
|
513
|
+
throw out;
|
|
514
|
+
}
|
|
515
|
+
const summary = pickSafeConnectionFields(raw);
|
|
516
|
+
return {
|
|
517
|
+
status: summary ? "connected" : "unknown",
|
|
518
|
+
providerConfigKey: input.providerConfigKey,
|
|
519
|
+
connectionId: input.connectionId,
|
|
520
|
+
environment: env.environment,
|
|
521
|
+
connection: summary
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function describeNangoAdapter() {
|
|
526
|
+
const env = resolveNangoEnv();
|
|
527
|
+
return {
|
|
528
|
+
id: "nango",
|
|
529
|
+
label: "Nango integration backbone",
|
|
530
|
+
requiredEnv: [DEFAULT_NANGO_SECRET_ENV],
|
|
531
|
+
authority: env.mode === "self-hosted" ? "nango-self-hosted" : "nango-cloud",
|
|
532
|
+
mode: env.mode,
|
|
533
|
+
environment: env.environment,
|
|
534
|
+
hostUrl: env.hostUrl,
|
|
535
|
+
hasSecretKey: env.hasSecretKey,
|
|
536
|
+
secretEnvName: env.secretEnvName
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export {
|
|
541
|
+
DEFAULT_NANGO_SECRET_ENV,
|
|
542
|
+
createConnectSession,
|
|
543
|
+
describeNangoAdapter,
|
|
544
|
+
executeAction,
|
|
545
|
+
getConnectionSummary,
|
|
546
|
+
getStatus,
|
|
547
|
+
listActions,
|
|
548
|
+
pickSafeConnectionFields,
|
|
549
|
+
projectNangoBinding,
|
|
550
|
+
proxyRequest,
|
|
551
|
+
resolveNangoEnv
|
|
552
|
+
};
|