@growthub/cli 0.13.4 → 0.13.6
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 +184 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +25 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +326 -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 +496 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +6 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +88 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +41 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/WorkspaceGraphInspectorPanel.jsx +226 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +120 -17
- 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 +67 -11
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +31 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +16 -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-agent-swarm.js +923 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +14 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +218 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +28 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-inputs.js +43 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +3 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +36 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-chart-values.js +53 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +2 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-graph.js +646 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-selectors.js +249 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +1186 -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 +7 -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
|
@@ -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
|
+
};
|