@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,202 @@
1
+ /**
2
+ * Nango config-driven resolver registration.
3
+ *
4
+ * Scans `dataModel.objects[]` for `objectType: "api-registry"` rows with
5
+ * `connectorKind: "nango"` and registers a source resolver for each one.
6
+ * The resolver key is the row's `integrationId`, matching the rest of the
7
+ * resolver registry contract — `getSourceResolver(integrationId)` returns
8
+ * a resolver that fans out via Nango Proxy.
9
+ *
10
+ * Invariants:
11
+ * - No file authoring is required. Operators add api-registry rows; the
12
+ * loader picks them up at the next route invocation.
13
+ * - The Nango secret is resolved from env at proxy time, never read here.
14
+ * - This loader is idempotent: re-registration with the same integrationId
15
+ * replaces the previous resolver (matching the existing registry behavior).
16
+ *
17
+ * This module is server-only. Browser code must not import it.
18
+ */
19
+
20
+ import { registerSourceResolver } from "../source-resolver-registry.js";
21
+ import {
22
+ executeAction as nangoExecuteAction,
23
+ projectNangoBinding,
24
+ proxyRequest as nangoProxyRequest
25
+ } from "./nango-adapter.js";
26
+
27
+ function isPlainObject(value) {
28
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
29
+ }
30
+
31
+ function pickArray(value) {
32
+ return Array.isArray(value)
33
+ ? value.filter((v) => typeof v === "string" && v.trim()).map((v) => v.trim())
34
+ : typeof value === "string"
35
+ ? value.split(",").map((v) => v.trim()).filter(Boolean)
36
+ : [];
37
+ }
38
+
39
+ /**
40
+ * Build the resolver object for a single Nango-backed api-registry row.
41
+ *
42
+ * The resolver:
43
+ * - fetchRecords(binding) — proxies one request per connectionId; records
44
+ * are flattened (with `_nangoConnectionId` stamped) and returned. The
45
+ * binding can override `endpoint`, `method`, `params`, and `data`.
46
+ * - listEntities() — returns the configured connection IDs as
47
+ * NormalizedIntegrationEntity records so the no-code reference UI can
48
+ * pick one to bind against.
49
+ * - runAction(actionName, input) — runs a Nango action function for the
50
+ * binding's `connectionId` (or the first one when the binding omits it).
51
+ */
52
+ function buildNangoResolver(row) {
53
+ const binding = projectNangoBinding(row);
54
+ if (!binding) return null;
55
+ const integrationId = binding.integrationId || binding.providerConfigKey;
56
+ if (!integrationId) return null;
57
+
58
+ async function fetchRecords(_config, _connection, callBinding = {}) {
59
+ const connectionIds = Array.isArray(callBinding?.connectionIds) && callBinding.connectionIds.length
60
+ ? pickArray(callBinding.connectionIds)
61
+ : binding.connectionIds;
62
+ const targets = connectionIds.length
63
+ ? connectionIds
64
+ : (typeof callBinding?.connectionId === "string" && callBinding.connectionId.trim()
65
+ ? [callBinding.connectionId.trim()]
66
+ : []);
67
+ if (!targets.length) {
68
+ const error = new Error("No Nango connectionId provided. Set connectionIds on the api-registry row, or pass connectionId in the binding.");
69
+ error.code = "NANGO_NO_CONNECTION";
70
+ throw error;
71
+ }
72
+ const endpoint = (callBinding?.endpoint || binding.endpoint || "").trim();
73
+ if (!endpoint) {
74
+ const error = new Error("No Nango proxy endpoint configured. Set endpoint on the api-registry row, or pass endpoint in the binding.");
75
+ error.code = "NANGO_NO_ENDPOINT";
76
+ throw error;
77
+ }
78
+ const method = String(callBinding?.method || binding.method || "GET").toUpperCase();
79
+ const params = isPlainObject(callBinding?.params) ? callBinding.params : undefined;
80
+ const data = callBinding?.data;
81
+ const aggregated = [];
82
+ for (const connectionId of targets) {
83
+ const result = await nangoProxyRequest({
84
+ providerConfigKey: binding.providerConfigKey,
85
+ connectionId,
86
+ method,
87
+ endpoint,
88
+ params,
89
+ data,
90
+ secretEnvName: binding.secretEnvName
91
+ });
92
+ const payload = result?.data;
93
+ if (Array.isArray(payload)) {
94
+ for (const record of payload) {
95
+ aggregated.push(isPlainObject(record)
96
+ ? { ...record, _nangoConnectionId: connectionId }
97
+ : { value: record, _nangoConnectionId: connectionId });
98
+ }
99
+ } else if (isPlainObject(payload)) {
100
+ for (const key of ["records", "results", "data", "items", "rows"]) {
101
+ if (Array.isArray(payload[key])) {
102
+ for (const record of payload[key]) {
103
+ aggregated.push(isPlainObject(record)
104
+ ? { ...record, _nangoConnectionId: connectionId }
105
+ : { value: record, _nangoConnectionId: connectionId });
106
+ }
107
+ break;
108
+ }
109
+ }
110
+ if (aggregated.length === 0) {
111
+ aggregated.push({ ...payload, _nangoConnectionId: connectionId });
112
+ }
113
+ }
114
+ }
115
+ return aggregated;
116
+ }
117
+
118
+ async function listEntities(_config, _connection) {
119
+ return binding.connectionIds.map((connectionId) => ({
120
+ id: connectionId,
121
+ label: connectionId,
122
+ secondaryLabel: binding.providerConfigKey,
123
+ entityType: "nango.connection",
124
+ provider: integrationId,
125
+ lane: "workspace-integration",
126
+ status: "configured",
127
+ metadata: {
128
+ providerConfigKey: binding.providerConfigKey,
129
+ environment: binding.environment || null
130
+ }
131
+ }));
132
+ }
133
+
134
+ async function runAction(_config, _connection, callBinding = {}) {
135
+ const actionName = String(callBinding?.action || "").trim();
136
+ if (!actionName) {
137
+ const error = new Error("action name is required");
138
+ error.code = "NANGO_NO_ACTION";
139
+ throw error;
140
+ }
141
+ if (binding.enabledActions.length && !binding.enabledActions.includes(actionName)) {
142
+ const error = new Error(`action "${actionName}" is not in the api-registry row's enabledActions allowlist`);
143
+ error.code = "NANGO_ACTION_NOT_ALLOWED";
144
+ throw error;
145
+ }
146
+ const connectionId = String(callBinding?.connectionId || binding.connectionIds[0] || "").trim();
147
+ if (!connectionId) {
148
+ const error = new Error("No Nango connectionId provided for the action.");
149
+ error.code = "NANGO_NO_CONNECTION";
150
+ throw error;
151
+ }
152
+ return nangoExecuteAction({
153
+ providerConfigKey: binding.providerConfigKey,
154
+ connectionId,
155
+ action: actionName,
156
+ input: callBinding?.input,
157
+ secretEnvName: binding.secretEnvName
158
+ });
159
+ }
160
+
161
+ return {
162
+ integrationId,
163
+ entityTypes: ["nango.connection"],
164
+ connectorKind: "nango",
165
+ templateId: "nango",
166
+ capabilities: binding.enabledActions.length ? ["listEntities", "fetchRecords", "runAction"] : ["listEntities", "fetchRecords"],
167
+ referenceSchema: {
168
+ valueField: "id",
169
+ labelField: "label",
170
+ secondaryLabelField: "secondaryLabel"
171
+ },
172
+ listEntities,
173
+ fetchRecords,
174
+ runAction
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Register a Nango source resolver for every api-registry row in the given
180
+ * workspace config that declares `connectorKind: "nango"`. Returns the list
181
+ * of integrationIds registered (useful for diagnostics).
182
+ */
183
+ function registerNangoResolversFromConfig(workspaceConfig) {
184
+ if (!isPlainObject(workspaceConfig)) return [];
185
+ const objects = workspaceConfig?.dataModel?.objects;
186
+ if (!Array.isArray(objects)) return [];
187
+ const registered = [];
188
+ for (const object of objects) {
189
+ if (!isPlainObject(object) || object.objectType !== "api-registry") continue;
190
+ const rows = Array.isArray(object.rows) ? object.rows : [];
191
+ for (const row of rows) {
192
+ if (!isPlainObject(row) || row.connectorKind !== "nango") continue;
193
+ const resolver = buildNangoResolver(row);
194
+ if (!resolver) continue;
195
+ registerSourceResolver(resolver);
196
+ registered.push(resolver.integrationId);
197
+ }
198
+ }
199
+ return registered;
200
+ }
201
+
202
+ export { buildNangoResolver, registerNangoResolversFromConfig };
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Nango adapter — input validation.
3
+ *
4
+ * Pure server-side input validation for the Nango API routes. Mirrors the
5
+ * `api-registry` row contract owned by `lib/workspace-schema.js`. No
6
+ * @nangohq/node import here — this module only validates shapes.
7
+ *
8
+ * Authority contract: every value passed to a Nango SDK call must flow
9
+ * through these validators first. Credentials never appear in inputs; the
10
+ * server resolves the Nango secret key from env (`NANGO_SECRET_KEY`).
11
+ */
12
+
13
+ import {
14
+ KNOWN_NANGO_MODES,
15
+ NANGO_PROVIDER_CONFIG_KEY_MAX,
16
+ NANGO_PROVIDER_CONFIG_KEY_PATTERN
17
+ } from "../../../workspace-schema.js";
18
+
19
+ const KNOWN_HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
20
+
21
+ function isPlainObject(value) {
22
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
23
+ }
24
+
25
+ function makeInvalidInputError(message, details) {
26
+ const error = new Error(message);
27
+ error.code = "NANGO_INVALID_INPUT";
28
+ if (Array.isArray(details) && details.length) {
29
+ error.details = details;
30
+ }
31
+ return error;
32
+ }
33
+
34
+ function validateProviderConfigKey(value, fieldPath = "providerConfigKey") {
35
+ const errors = [];
36
+ if (typeof value !== "string" || !value.trim()) {
37
+ errors.push(`${fieldPath} must be a non-empty string`);
38
+ } else if (
39
+ value.length > NANGO_PROVIDER_CONFIG_KEY_MAX
40
+ || !NANGO_PROVIDER_CONFIG_KEY_PATTERN.test(value)
41
+ ) {
42
+ errors.push(`${fieldPath} must be alphanumeric (with _.- separators), starting alphanumeric, and <= ${NANGO_PROVIDER_CONFIG_KEY_MAX} chars`);
43
+ }
44
+ return errors;
45
+ }
46
+
47
+ function validateConnectionId(value, fieldPath = "connectionId") {
48
+ const errors = [];
49
+ if (typeof value !== "string" || !value.trim()) {
50
+ errors.push(`${fieldPath} must be a non-empty string`);
51
+ } else if (value.length > 256) {
52
+ errors.push(`${fieldPath} must be <= 256 chars`);
53
+ }
54
+ return errors;
55
+ }
56
+
57
+ function validateNangoMode(value, fieldPath = "mode") {
58
+ if (value === undefined || value === null || value === "") return [];
59
+ if (!KNOWN_NANGO_MODES.includes(value)) {
60
+ return [`${fieldPath} must be one of ${KNOWN_NANGO_MODES.join(", ")}`];
61
+ }
62
+ return [];
63
+ }
64
+
65
+ function validateHostUrl(value, fieldPath = "hostUrl") {
66
+ if (value === undefined || value === null || value === "") return [];
67
+ if (typeof value !== "string") {
68
+ return [`${fieldPath} must be a string when present`];
69
+ }
70
+ let url;
71
+ try {
72
+ url = new URL(value);
73
+ } catch {
74
+ return [`${fieldPath} must be a valid URL`];
75
+ }
76
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
77
+ return [`${fieldPath} must use http:// or https://`];
78
+ }
79
+ return [];
80
+ }
81
+
82
+ /**
83
+ * Proxy request shape used by POST /api/workspace/integrations/nango/proxy.
84
+ * Mirrors the upstream `@nangohq/node` proxy contract (method, endpoint,
85
+ * optional headers/params/data) while keeping credentials out of band.
86
+ */
87
+ function validateProxyRequest(input) {
88
+ if (!isPlainObject(input)) {
89
+ throw makeInvalidInputError("proxy request body must be a plain object");
90
+ }
91
+ const errors = [];
92
+ errors.push(...validateProviderConfigKey(input.providerConfigKey));
93
+ errors.push(...validateConnectionId(input.connectionId));
94
+
95
+ const method = String(input.method || "GET").toUpperCase();
96
+ if (!KNOWN_HTTP_METHODS.includes(method)) {
97
+ errors.push(`method must be one of ${KNOWN_HTTP_METHODS.join(", ")}`);
98
+ }
99
+ if (typeof input.endpoint !== "string" || !input.endpoint.trim()) {
100
+ errors.push("endpoint must be a non-empty string (path or absolute URL)");
101
+ } else if (input.endpoint.length > 2048) {
102
+ errors.push("endpoint must be <= 2048 chars");
103
+ }
104
+ if (input.headers !== undefined && !isPlainObject(input.headers)) {
105
+ errors.push("headers must be a plain object when present");
106
+ }
107
+ if (input.params !== undefined && !isPlainObject(input.params)) {
108
+ errors.push("params must be a plain object when present");
109
+ }
110
+ if (input.retries !== undefined) {
111
+ const r = Number(input.retries);
112
+ if (!Number.isFinite(r) || r < 0 || r > 10) {
113
+ errors.push("retries must be a finite number between 0 and 10");
114
+ }
115
+ }
116
+ if (input.timeoutMs !== undefined) {
117
+ const ms = Number(input.timeoutMs);
118
+ if (!Number.isFinite(ms) || ms < 0 || ms > 60000) {
119
+ errors.push("timeoutMs must be between 0 and 60000");
120
+ }
121
+ }
122
+ // Reject any header that looks like an auth-credential carrier. The Nango
123
+ // SDK injects credentials server-side from the connection — callers MUST
124
+ // NOT forward Authorization headers from the browser.
125
+ if (isPlainObject(input.headers)) {
126
+ const forbiddenHeaders = ["authorization", "x-api-key", "x-auth-token", "cookie"];
127
+ for (const key of Object.keys(input.headers)) {
128
+ if (forbiddenHeaders.includes(key.toLowerCase())) {
129
+ errors.push(`headers.${key} is not allowed — Nango injects credentials from the connection`);
130
+ }
131
+ }
132
+ }
133
+
134
+ if (errors.length) {
135
+ throw makeInvalidInputError("invalid proxy request", errors);
136
+ }
137
+
138
+ return {
139
+ providerConfigKey: input.providerConfigKey.trim(),
140
+ connectionId: input.connectionId.trim(),
141
+ method,
142
+ endpoint: input.endpoint.trim(),
143
+ headers: isPlainObject(input.headers) ? { ...input.headers } : undefined,
144
+ params: isPlainObject(input.params) ? { ...input.params } : undefined,
145
+ data: input.data,
146
+ retries: input.retries !== undefined ? Number(input.retries) : undefined,
147
+ timeoutMs: input.timeoutMs !== undefined ? Number(input.timeoutMs) : undefined
148
+ };
149
+ }
150
+
151
+ function validateActionsListInput(input) {
152
+ if (input === null || input === undefined) {
153
+ return { providerConfigKey: undefined };
154
+ }
155
+ if (!isPlainObject(input)) {
156
+ throw makeInvalidInputError("actions list input must be a plain object");
157
+ }
158
+ if (input.providerConfigKey === undefined || input.providerConfigKey === null || input.providerConfigKey === "") {
159
+ return { providerConfigKey: undefined };
160
+ }
161
+ const errors = validateProviderConfigKey(input.providerConfigKey);
162
+ if (errors.length) throw makeInvalidInputError("invalid actions list input", errors);
163
+ return { providerConfigKey: input.providerConfigKey.trim() };
164
+ }
165
+
166
+ function validateActionExecuteRequest(input) {
167
+ if (!isPlainObject(input)) {
168
+ throw makeInvalidInputError("action execute body must be a plain object");
169
+ }
170
+ const errors = [];
171
+ errors.push(...validateProviderConfigKey(input.providerConfigKey));
172
+ errors.push(...validateConnectionId(input.connectionId));
173
+ if (typeof input.action !== "string" || !input.action.trim()) {
174
+ errors.push("action must be a non-empty string");
175
+ } else if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(input.action) || input.action.length > 128) {
176
+ errors.push("action must be alphanumeric (with _.- separators), <= 128 chars");
177
+ }
178
+ if (input.input !== undefined && !isPlainObject(input.input) && !Array.isArray(input.input)) {
179
+ errors.push("input must be a plain object or array when present");
180
+ }
181
+ if (errors.length) throw makeInvalidInputError("invalid action execute request", errors);
182
+ return {
183
+ providerConfigKey: input.providerConfigKey.trim(),
184
+ connectionId: input.connectionId.trim(),
185
+ action: input.action.trim(),
186
+ input: input.input
187
+ };
188
+ }
189
+
190
+ function validateConnectSessionRequest(input) {
191
+ if (!isPlainObject(input)) {
192
+ throw makeInvalidInputError("connect session body must be a plain object");
193
+ }
194
+ const errors = [];
195
+ errors.push(...validateProviderConfigKey(input.providerConfigKey));
196
+ // connectionId is OPTIONAL on Create Connect Session — Nango mints the
197
+ // connection during OAuth and delivers the id via the auth webhook. The
198
+ // field is only meaningful for explicit Reconnect flows against a known
199
+ // existing connection.
200
+ if (input.connectionId !== undefined && input.connectionId !== null && input.connectionId !== "") {
201
+ errors.push(...validateConnectionId(input.connectionId));
202
+ }
203
+ if (input.reconnect !== undefined && typeof input.reconnect !== "boolean") {
204
+ errors.push("reconnect must be a boolean when present");
205
+ }
206
+ if (input.reconnect === true && (typeof input.connectionId !== "string" || !input.connectionId.trim())) {
207
+ errors.push("connectionId is required when reconnect=true");
208
+ }
209
+ if (input.endUser !== undefined && input.endUser !== null) {
210
+ if (!isPlainObject(input.endUser)) {
211
+ errors.push("endUser must be a plain object when present");
212
+ } else {
213
+ if (input.endUser.id !== undefined && typeof input.endUser.id !== "string") {
214
+ errors.push("endUser.id must be a string when present");
215
+ }
216
+ if (input.endUser.email !== undefined && typeof input.endUser.email !== "string") {
217
+ errors.push("endUser.email must be a string when present");
218
+ }
219
+ }
220
+ }
221
+ // Tags: arbitrary string→string map that Nango echoes back in the auth
222
+ // webhook so the workspace can correlate "which row asked for OAuth" once
223
+ // a connectionId is minted. Keys constrained to identifier-shape and
224
+ // values constrained to short strings — no secret payloads.
225
+ if (input.tags !== undefined && input.tags !== null) {
226
+ if (!isPlainObject(input.tags)) {
227
+ errors.push("tags must be a plain object when present");
228
+ } else {
229
+ for (const [key, value] of Object.entries(input.tags)) {
230
+ if (!/^[A-Za-z0-9_.-]{1,64}$/.test(key)) {
231
+ errors.push(`tags.${key} must match [A-Za-z0-9_.-]{1,64}`);
232
+ continue;
233
+ }
234
+ if (typeof value !== "string" || value.length > 256) {
235
+ errors.push(`tags.${key} must be a string (<= 256 chars)`);
236
+ }
237
+ }
238
+ }
239
+ }
240
+ if (errors.length) throw makeInvalidInputError("invalid connect session request", errors);
241
+ return {
242
+ providerConfigKey: input.providerConfigKey.trim(),
243
+ connectionId: input.connectionId ? input.connectionId.trim() : undefined,
244
+ reconnect: input.reconnect === true,
245
+ endUser: isPlainObject(input.endUser) ? { ...input.endUser } : undefined,
246
+ tags: isPlainObject(input.tags) ? { ...input.tags } : undefined
247
+ };
248
+ }
249
+
250
+ function validateConnectionSummaryRequest(input) {
251
+ if (!isPlainObject(input)) {
252
+ throw makeInvalidInputError("connection status body must be a plain object");
253
+ }
254
+ const errors = [];
255
+ errors.push(...validateProviderConfigKey(input.providerConfigKey));
256
+ errors.push(...validateConnectionId(input.connectionId));
257
+ if (errors.length) throw makeInvalidInputError("invalid connection status request", errors);
258
+ return {
259
+ providerConfigKey: input.providerConfigKey.trim(),
260
+ connectionId: input.connectionId.trim()
261
+ };
262
+ }
263
+
264
+ function validateConnectionStatusRequest(input) {
265
+ if (input === null || input === undefined) return {};
266
+ if (!isPlainObject(input)) {
267
+ throw makeInvalidInputError("status request must be a plain object");
268
+ }
269
+ const errors = [];
270
+ if (input.providerConfigKey !== undefined) {
271
+ errors.push(...validateProviderConfigKey(input.providerConfigKey));
272
+ }
273
+ if (input.connectionId !== undefined) {
274
+ errors.push(...validateConnectionId(input.connectionId));
275
+ }
276
+ if (input.mode !== undefined) {
277
+ errors.push(...validateNangoMode(input.mode));
278
+ }
279
+ if (input.hostUrl !== undefined) {
280
+ errors.push(...validateHostUrl(input.hostUrl));
281
+ }
282
+ if (errors.length) throw makeInvalidInputError("invalid status request", errors);
283
+ return {
284
+ providerConfigKey: input.providerConfigKey ? input.providerConfigKey.trim() : undefined,
285
+ connectionId: input.connectionId ? input.connectionId.trim() : undefined,
286
+ mode: input.mode || undefined,
287
+ hostUrl: input.hostUrl ? input.hostUrl.trim() : undefined
288
+ };
289
+ }
290
+
291
+ export {
292
+ KNOWN_HTTP_METHODS,
293
+ validateActionExecuteRequest,
294
+ validateActionsListInput,
295
+ validateConnectSessionRequest,
296
+ validateConnectionId,
297
+ validateConnectionStatusRequest,
298
+ validateConnectionSummaryRequest,
299
+ validateHostUrl,
300
+ validateNangoMode,
301
+ validateProviderConfigKey,
302
+ validateProxyRequest
303
+ };
@@ -9,31 +9,41 @@
9
9
  * stays empty — the refresh button and test route gracefully skip unknown
10
10
  * integrationIds and surface them in the `skipped` array.
11
11
  *
12
- * Called once per route handler invocation (Next.js module cache means it
13
- * will only do real I/O on the first call in each worker process).
12
+ * Two cadences:
13
+ * - `loadStaticResolversOnce()` imports `.js` files in the resolvers/
14
+ * directory exactly once per worker process. Static resolvers register
15
+ * themselves at module-load time; re-importing would be a no-op anyway.
16
+ * - `refreshConfigDrivenResolvers()` — re-scans growthub.config.json on
17
+ * EVERY call. New Nango-backed api-registry rows are picked up without
18
+ * a server restart. The source-resolver registry's `registerSourceResolver`
19
+ * contract is "calling again with the same integrationId replaces the
20
+ * existing resolver", so repeated registration is safe.
21
+ *
22
+ * `loadAllResolvers()` calls both and remains the single entry point that
23
+ * existing routes (test-source, refresh-source) consume.
14
24
  */
15
25
 
16
26
  import { promises as fs } from "node:fs";
17
27
  import path from "node:path";
18
28
  import { pathToFileURL } from "node:url";
19
29
 
20
- const loaded = new Set();
21
- let loadAttempted = false;
30
+ const staticLoaded = new Set();
31
+ let staticLoadDone = false;
22
32
 
23
- async function loadAllResolvers() {
24
- if (loadAttempted) return;
25
- loadAttempted = true;
33
+ async function loadStaticResolversOnce() {
34
+ if (staticLoadDone) return;
35
+ staticLoadDone = true;
26
36
  const resolversDir = path.resolve(/*turbopackIgnore: true*/ process.cwd(), "lib/adapters/integrations/resolvers");
27
37
  try {
28
38
  const entries = await fs.readdir(resolversDir);
29
39
  const jsFiles = entries.filter((f) => f.endsWith(".js") && !f.startsWith("_") && !f.startsWith("."));
30
40
  await Promise.all(
31
41
  jsFiles.map(async (file) => {
32
- if (loaded.has(file)) return;
42
+ if (staticLoaded.has(file)) return;
33
43
  try {
34
44
  const absolutePath = path.join(resolversDir, file);
35
45
  await import(/*turbopackIgnore: true*/ pathToFileURL(absolutePath).href);
36
- loaded.add(file);
46
+ staticLoaded.add(file);
37
47
  } catch {
38
48
  // Malformed resolver — skip silently; operator needs to fix the file
39
49
  }
@@ -44,6 +54,30 @@ async function loadAllResolvers() {
44
54
  }
45
55
  }
46
56
 
57
+ /**
58
+ * Re-scan growthub.config.json for Nango-backed api-registry rows and
59
+ * register one source resolver per row. Runs on every invocation so newly
60
+ * added rows are picked up between requests without a server restart.
61
+ *
62
+ * Returns the list of integrationIds registered on this call (or `[]` on
63
+ * any non-fatal error — the static resolvers still work).
64
+ */
65
+ async function refreshConfigDrivenResolvers() {
66
+ try {
67
+ const { readWorkspaceConfig } = await import("../../workspace-config.js");
68
+ const { registerNangoResolversFromConfig } = await import("./nango/index.js");
69
+ const workspaceConfig = await readWorkspaceConfig();
70
+ return registerNangoResolversFromConfig(workspaceConfig) || [];
71
+ } catch {
72
+ return [];
73
+ }
74
+ }
75
+
76
+ async function loadAllResolvers() {
77
+ await loadStaticResolversOnce();
78
+ await refreshConfigDrivenResolvers();
79
+ }
80
+
47
81
  async function listResolverFiles() {
48
82
  const resolversDir = path.resolve(/*turbopackIgnore: true*/ process.cwd(), "lib/adapters/integrations/resolvers");
49
83
  try {
@@ -54,4 +88,9 @@ async function listResolverFiles() {
54
88
  }
55
89
  }
56
90
 
57
- export { loadAllResolvers, listResolverFiles };
91
+ export {
92
+ loadAllResolvers,
93
+ loadStaticResolversOnce,
94
+ listResolverFiles,
95
+ refreshConfigDrivenResolvers
96
+ };
@@ -12,7 +12,7 @@
12
12
  * entityTypes: string[], // e.g. ["project.tasks", "workspace.users"]
13
13
  * listEntities: async (config, connection) => NormalizedEntity[],
14
14
  * fetchRecords: async (config, connection, binding) => Record[]
15
- * connectorKind?: string, // http | mcp | chrome | tool | custom
15
+ * connectorKind?: string, // http | mcp | chrome | tool | custom | nango
16
16
  * templateId?: string,
17
17
  * capabilities?: string[],
18
18
  * configSchema?: SchemaField[],
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Nango — resolver template.
3
+ *
4
+ * Seeds API Registry rows whose proxy + auth flow is delegated to Nango.
5
+ * Each row carries the Nango `providerConfigKey` (defaults to the
6
+ * `integrationId` when not set explicitly), the connection ID set, and the
7
+ * list of enabled action functions. The Nango secret lives in env (default
8
+ * env-ref name: `NANGO_SECRET_KEY`) and is resolved server-side at proxy
9
+ * time. The browser never holds the secret.
10
+ *
11
+ * `connectorKind: "nango"` is recognized by the resolver registry and the
12
+ * config-driven loader at `lib/adapters/integrations/nango/index.js`, which
13
+ * registers a resolver for each api-registry row tagged with this kind.
14
+ */
15
+
16
+ const template = {
17
+ schemaVersion: "growthub-resolver-template-v1",
18
+ templateId: "nango",
19
+ label: "Nango integration backbone",
20
+ connectorKind: "nango",
21
+ capabilities: ["listEntities", "fetchRecords", "runAction"],
22
+ apiRegistryDefaults: {
23
+ integrationId: "nango-provider",
24
+ authRef: "NANGO_SECRET_KEY",
25
+ method: "GET",
26
+ connectorKind: "nango",
27
+ resolverTemplateId: "nango",
28
+ schemaVersion: "growthub-resolver-template-v1"
29
+ },
30
+ dataSourceDefaults: {
31
+ objectType: "data-source",
32
+ binding: { sourceStorage: "workspace-source-records" }
33
+ },
34
+ configSchema: [
35
+ { name: "integrationId", label: "Integration ID", type: "text", required: true, description: "Stable provider slug. Used as the resolver key and (when no override is set) as the Nango providerConfigKey." },
36
+ { name: "providerConfigKey", label: "Nango provider config key", type: "text", required: false, description: "Overrides integrationId when the Nango key differs from the workspace slug." },
37
+ { name: "endpoint", label: "Proxy endpoint", type: "text", required: false, description: "Path or absolute URL forwarded through Nango Proxy. Leave blank when the resolver derives the endpoint from listEntities." },
38
+ { name: "method", label: "HTTP method", type: "text", required: false, description: "GET (default) | POST | PUT | PATCH | DELETE." },
39
+ { name: "authRef", label: "Nango secret env ref", type: "secretRef", required: false, description: "Defaults to NANGO_SECRET_KEY. The env value is the Nango secret key; the workspace never sees it in the browser." },
40
+ { name: "connectionIds", label: "Connection IDs", type: "text", required: false, description: "Comma-separated Nango connection IDs (one per tenant). The resolver fans out across them when more than one is set." },
41
+ { name: "enabledActions", label: "Enabled actions", type: "text", required: false, description: "Comma-separated names of Nango action functions exposed as agent tools." },
42
+ { name: "nangoMode", label: "Nango mode", type: "text", required: false, description: "cloud (default) or self-hosted. Self-hosted requires nangoHostUrl." },
43
+ { name: "nangoHostUrl", label: "Nango host URL", type: "url", required: false, description: "Required when nangoMode = self-hosted." },
44
+ { name: "nangoEnvironment", label: "Nango environment", type: "text", required: false, description: "dev (default) or prod. Maps to the Nango environment header." }
45
+ ],
46
+ supportedLanes: ["data-source", "sandbox-local", "sandbox-serverless"]
47
+ };
48
+
49
+ export default template;
@@ -5,7 +5,7 @@
5
5
  * @property {"growthub-resolver-template-v1"} schemaVersion
6
6
  * @property {string} templateId
7
7
  * @property {string} label
8
- * @property {"http"|"mcp"|"chrome"|"tool"|"custom"} connectorKind
8
+ * @property {"http"|"mcp"|"chrome"|"tool"|"custom"|"nango"} connectorKind
9
9
  * @property {Array<"listEntities"|"fetchRecords"|"runAction">} capabilities
10
10
  * @property {Object} apiRegistryDefaults
11
11
  * @property {string} apiRegistryDefaults.integrationId
@@ -26,6 +26,7 @@ import genericCrm from "./generic-crm.js";
26
26
  import genericSpreadsheet from "./generic-spreadsheet.js";
27
27
  import genericProjectManagement from "./generic-project-management.js";
28
28
  import genericCommerce from "./generic-commerce.js";
29
+ import nango from "./nango.js";
29
30
 
30
31
  const ALL = [
31
32
  customHttp,
@@ -35,7 +36,8 @@ const ALL = [
35
36
  genericCrm,
36
37
  genericSpreadsheet,
37
38
  genericProjectManagement,
38
- genericCommerce
39
+ genericCommerce,
40
+ nango
39
41
  ];
40
42
 
41
43
  function listResolverTemplates() {