@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.
Files changed (52) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/QUICKSTART.md +19 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/.env.example +8 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +4 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/action/execute/route.js +60 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/actions/route.js +50 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/connect-session/route.js +68 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/connection-status/route.js +56 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/proxy/route.js +67 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/status/route.js +50 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +184 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +25 -2
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +326 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +161 -50
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/NangoConnectionPanel.jsx +496 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +6 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +88 -1
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +41 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/WorkspaceGraphInspectorPanel.jsx +226 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +120 -17
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/nango/page.jsx +167 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +1 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +67 -11
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +31 -10
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +16 -14
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/env.js +7 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/index.js +38 -0
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-adapter.js +552 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-config-loader.js +202 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-schema.js +303 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +49 -10
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/source-resolver-registry.js +1 -1
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/nango.js +49 -0
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/template-registry.js +4 -2
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +923 -0
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +14 -1
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +218 -7
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +28 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-inputs.js +43 -0
  39. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +3 -1
  40. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +36 -0
  41. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-chart-values.js +53 -0
  42. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +2 -1
  43. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-graph.js +646 -0
  44. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-selectors.js +249 -0
  45. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +1186 -0
  46. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +102 -3
  47. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
  48. package/assets/worker-kits/growthub-custom-workspace-starter-v1/bundles/growthub-custom-workspace-starter-v1.json +1 -0
  49. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +7 -0
  50. package/assets/worker-kits/growthub-custom-workspace-starter-v1/templates/seeded-configs/project-management.config.json +276 -0
  51. package/dist/index.js +127 -44
  52. 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
+ };