@growthub/cli 0.13.5 → 0.13.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/QUICKSTART.md +19 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/.env.example +8 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +4 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/action/execute/route.js +60 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/actions/route.js +50 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/connect-session/route.js +68 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/connection-status/route.js +56 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/proxy/route.js +67 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/status/route.js +50 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +172 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +161 -50
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/NangoConnectionPanel.jsx +531 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +274 -18
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/nango/page.jsx +167 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +62 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +554 -48
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +24 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/env.js +7 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/index.js +38 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-adapter.js +552 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-config-loader.js +202 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-schema.js +303 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +49 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/source-resolver-registry.js +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/nango.js +49 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/template-registry.js +4 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +534 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +3 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +82 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +102 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/bundles/growthub-custom-workspace-starter-v1.json +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/templates/seeded-configs/project-management.config.json +276 -0
- package/dist/index.js +127 -44
- package/package.json +1 -1
|
@@ -10,6 +10,25 @@ This kit is the canonical v1 starter primitive. Run `growthub starter init --nam
|
|
|
10
10
|
|
|
11
11
|
After that, every customization you make stays yours — the Self-Healing Fork Sync Agent will propose upstream additions non-destructively, honouring your policy.
|
|
12
12
|
|
|
13
|
+
## Official workspace paths
|
|
14
|
+
|
|
15
|
+
Workspace 1 stays the blank governed workspace:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
growthub starter init --name "My Workspace" --out ./my-workspace
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Workspace 2 is the opinionated project-management template path. It builds on the same app and adapter surface, but seeds API Registry, Data Source, Sandbox Environment workflow, and dashboard placeholders for project task deltas. It stores no provider secrets, OAuth connection ids, or provider task data.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
growthub starter init \
|
|
25
|
+
--name "Project Management Workspace" \
|
|
26
|
+
--out ./project-management-workspace \
|
|
27
|
+
--seed-config project-management
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
After export, set `NANGO_SECRET_KEY` in the workspace runtime, complete provider OAuth, then fill `providerConfigKey`, `connectionIds`, and project identifiers in the Data Model before running the workflow.
|
|
31
|
+
|
|
13
32
|
## Run the bundled Vite UI shell
|
|
14
33
|
|
|
15
34
|
```bash
|
|
@@ -10,6 +10,14 @@ GROWTHUB_WORKSPACE_PAYMENT_ADAPTER=none
|
|
|
10
10
|
# - static: local starter catalog only
|
|
11
11
|
GROWTHUB_WORKSPACE_INTEGRATION_ADAPTER=growthub-bridge
|
|
12
12
|
|
|
13
|
+
# Nango thin adapter — row-scoped, not a workspace-wide adapter.
|
|
14
|
+
# Set NANGO_SECRET_KEY when any api-registry row has connectorKind: "nango".
|
|
15
|
+
# Cloud mode reaches Nango Cloud; self-hosted requires NANGO_HOST_URL.
|
|
16
|
+
NANGO_MODE=cloud
|
|
17
|
+
NANGO_HOST_URL=
|
|
18
|
+
NANGO_ENVIRONMENT=dev
|
|
19
|
+
NANGO_SECRET_KEY=
|
|
20
|
+
|
|
13
21
|
# Hosted bridge authority
|
|
14
22
|
GROWTHUB_BRIDGE_BASE_URL=https://www.growthub.ai
|
|
15
23
|
GROWTHUB_BRIDGE_INTEGRATIONS_PATH=/api/mcp/accounts
|
|
@@ -13,6 +13,10 @@ It intentionally depends on adapter contracts:
|
|
|
13
13
|
- `GROWTHUB_BRIDGE_ACCESS_TOKEN`
|
|
14
14
|
- `GROWTHUB_BRIDGE_USER_ID`
|
|
15
15
|
- `GROWTHUB_WORKSPACE_BYO_CONNECTIONS_JSON`
|
|
16
|
+
- `NANGO_SECRET_KEY` (required when any api-registry row uses `connectorKind: "nango"`)
|
|
17
|
+
- `NANGO_HOST_URL` (self-hosted Nango only)
|
|
18
|
+
- `NANGO_ENVIRONMENT` (default `dev`)
|
|
19
|
+
- `NANGO_MODE` (`cloud` | `self-hosted`, default `cloud`)
|
|
16
20
|
|
|
17
21
|
The Growthub local-first operator shell remains at `../../studio`.
|
|
18
22
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/workspace/integrations/nango/action/execute
|
|
3
|
+
*
|
|
4
|
+
* Executes a declared Nango action function against the connection
|
|
5
|
+
* identified by `providerConfigKey` + `connectionId`. Used by workspace
|
|
6
|
+
* agents as a tool-call surface.
|
|
7
|
+
*
|
|
8
|
+
* Request body:
|
|
9
|
+
* {
|
|
10
|
+
* providerConfigKey: string,
|
|
11
|
+
* connectionId: string,
|
|
12
|
+
* action: string, // action function name from nango.yaml
|
|
13
|
+
* input?: object | array // arguments to the action
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* Response — success: 200 { ok: true, action, providerConfigKey, connectionId, result, environment }
|
|
17
|
+
* Response — validation failure: 400 { ok: false, error, details }
|
|
18
|
+
* Response — Nango not configured: 503 { ok: false, error, code }
|
|
19
|
+
* Response — upstream failure: 502 { ok: false, error }
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { NextResponse } from "next/server";
|
|
23
|
+
import { executeAction, validateActionExecuteRequest } from "@/lib/adapters/integrations/nango";
|
|
24
|
+
|
|
25
|
+
async function POST(request) {
|
|
26
|
+
let body;
|
|
27
|
+
try {
|
|
28
|
+
body = await request.json();
|
|
29
|
+
} catch {
|
|
30
|
+
return NextResponse.json({ ok: false, error: "invalid JSON body" }, { status: 400 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let validated;
|
|
34
|
+
try {
|
|
35
|
+
validated = validateActionExecuteRequest(body);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return NextResponse.json(
|
|
38
|
+
{ ok: false, error: error.message, details: error.details || null },
|
|
39
|
+
{ status: 400 }
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const result = await executeAction(validated);
|
|
45
|
+
return NextResponse.json({ ok: true, ...result });
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error.code === "NANGO_NOT_CONFIGURED" || error.code === "NANGO_SDK_UNAVAILABLE" || error.code === "NANGO_SDK_SHAPE") {
|
|
48
|
+
return NextResponse.json(
|
|
49
|
+
{ ok: false, error: error.message, code: error.code },
|
|
50
|
+
{ status: 503 }
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return NextResponse.json(
|
|
54
|
+
{ ok: false, error: error?.message || "nango action execution failed" },
|
|
55
|
+
{ status: 502 }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { POST };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/workspace/integrations/nango/actions
|
|
3
|
+
*
|
|
4
|
+
* Lists the actions declared in `nango.yaml` for a given provider config key
|
|
5
|
+
* (or, when no key is provided, returns the full discoverable set). The
|
|
6
|
+
* response shape is MCP-compatible: each action exposes a name and (when
|
|
7
|
+
* provided by Nango) an input/output schema.
|
|
8
|
+
*
|
|
9
|
+
* Query parameters (optional):
|
|
10
|
+
* - providerConfigKey filter to actions for a single provider
|
|
11
|
+
*
|
|
12
|
+
* Response shape:
|
|
13
|
+
* { ok: true, providerConfigKey, probedShape, actions: Array, hint: string|null }
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { NextResponse } from "next/server";
|
|
17
|
+
import { listActions, validateActionsListInput } from "@/lib/adapters/integrations/nango";
|
|
18
|
+
|
|
19
|
+
async function GET(request) {
|
|
20
|
+
const url = new URL(request.url);
|
|
21
|
+
const raw = {
|
|
22
|
+
providerConfigKey: url.searchParams.get("providerConfigKey") || undefined
|
|
23
|
+
};
|
|
24
|
+
let input;
|
|
25
|
+
try {
|
|
26
|
+
input = validateActionsListInput(raw);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return NextResponse.json(
|
|
29
|
+
{ ok: false, error: error.message, details: error.details || null },
|
|
30
|
+
{ status: 400 }
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const result = await listActions(input);
|
|
35
|
+
return NextResponse.json({ ok: true, ...result });
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error.code === "NANGO_NOT_CONFIGURED" || error.code === "NANGO_SDK_UNAVAILABLE") {
|
|
38
|
+
return NextResponse.json(
|
|
39
|
+
{ ok: false, error: error.message, code: error.code },
|
|
40
|
+
{ status: 503 }
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return NextResponse.json(
|
|
44
|
+
{ ok: false, error: error?.message || "nango actions list failed" },
|
|
45
|
+
{ status: 502 }
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { GET };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/workspace/integrations/nango/connect-session
|
|
3
|
+
*
|
|
4
|
+
* Creates a Nango Connect Session for the given providerConfigKey and
|
|
5
|
+
* returns a short-lived connect_link the browser opens to start OAuth.
|
|
6
|
+
* The Nango Connect UI mints tokens directly against Nango — the workspace
|
|
7
|
+
* never sees the resulting OAuth credentials. The token returned here is a
|
|
8
|
+
* Connect Session token (handoff only), not a provider credential.
|
|
9
|
+
*
|
|
10
|
+
* Lifecycle: Nango creates the connectionId AFTER the user completes OAuth
|
|
11
|
+
* and delivers it through the auth webhook. A normal Connect Session
|
|
12
|
+
* therefore does NOT need a connectionId as input — only the explicit
|
|
13
|
+
* Reconnect path (`reconnect: true`) operates on an existing connectionId.
|
|
14
|
+
*
|
|
15
|
+
* Request body:
|
|
16
|
+
* {
|
|
17
|
+
* providerConfigKey: string, // required, alphanumeric (+ _.-) <= 64 chars
|
|
18
|
+
* reconnect?: boolean, // true → use the SDK's reconnect-session path
|
|
19
|
+
* connectionId?: string, // REQUIRED when reconnect=true; ignored for a normal Create Connect Session
|
|
20
|
+
* endUser?: { id?: string, email?: string },
|
|
21
|
+
* tags?: Record<string, string> // echoed back in the Nango auth webhook so the workspace can map the eventual connectionId to the originating row
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* Response — success: 200 { ok: true, providerConfigKey, environment, mode, token, connectLink, sdkMethod, tagsEchoed }
|
|
25
|
+
* Response — validation failure: 400 { ok: false, error, details }
|
|
26
|
+
* Response — Nango not configured / SDK missing: 503 { ok: false, error, code }
|
|
27
|
+
* Response — upstream failure: 502 { ok: false, error }
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { NextResponse } from "next/server";
|
|
31
|
+
import { createConnectSession, validateConnectSessionRequest } from "@/lib/adapters/integrations/nango";
|
|
32
|
+
|
|
33
|
+
async function POST(request) {
|
|
34
|
+
let body;
|
|
35
|
+
try {
|
|
36
|
+
body = await request.json();
|
|
37
|
+
} catch {
|
|
38
|
+
return NextResponse.json({ ok: false, error: "invalid JSON body" }, { status: 400 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let validated;
|
|
42
|
+
try {
|
|
43
|
+
validated = validateConnectSessionRequest(body);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return NextResponse.json(
|
|
46
|
+
{ ok: false, error: error.message, details: error.details || null },
|
|
47
|
+
{ status: 400 }
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const result = await createConnectSession(validated);
|
|
53
|
+
return NextResponse.json({ ok: true, ...result });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error.code === "NANGO_NOT_CONFIGURED" || error.code === "NANGO_SDK_UNAVAILABLE" || error.code === "NANGO_SDK_SHAPE") {
|
|
56
|
+
return NextResponse.json(
|
|
57
|
+
{ ok: false, error: error.message, code: error.code },
|
|
58
|
+
{ status: 503 }
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return NextResponse.json(
|
|
62
|
+
{ ok: false, error: error?.message || "nango connect session failed" },
|
|
63
|
+
{ status: 502 }
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { POST };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/workspace/integrations/nango/connection-status
|
|
3
|
+
*
|
|
4
|
+
* Returns the safe (non-credential) status summary for a specific Nango
|
|
5
|
+
* connection. Used by the NangoConnectionPanel sidecar to verify a
|
|
6
|
+
* per-row connection without exposing OAuth tokens to the browser.
|
|
7
|
+
*
|
|
8
|
+
* Request body:
|
|
9
|
+
* { providerConfigKey: string, connectionId: string }
|
|
10
|
+
*
|
|
11
|
+
* Response — connected: 200 { ok: true, status: "connected", providerConfigKey, connectionId, environment, connection: { ...safe fields... } }
|
|
12
|
+
* Response — not yet connected: 200 { ok: true, status: "not-connected", providerConfigKey, connectionId, environment, reason }
|
|
13
|
+
* Response — validation failure: 400 { ok: false, error, details }
|
|
14
|
+
* Response — Nango not configured: 503 { ok: false, error, code }
|
|
15
|
+
* Response — upstream failure: 502 { ok: false, error }
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { NextResponse } from "next/server";
|
|
19
|
+
import { getConnectionSummary, validateConnectionSummaryRequest } from "@/lib/adapters/integrations/nango";
|
|
20
|
+
|
|
21
|
+
async function POST(request) {
|
|
22
|
+
let body;
|
|
23
|
+
try {
|
|
24
|
+
body = await request.json();
|
|
25
|
+
} catch {
|
|
26
|
+
return NextResponse.json({ ok: false, error: "invalid JSON body" }, { status: 400 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let validated;
|
|
30
|
+
try {
|
|
31
|
+
validated = validateConnectionSummaryRequest(body);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return NextResponse.json(
|
|
34
|
+
{ ok: false, error: error.message, details: error.details || null },
|
|
35
|
+
{ status: 400 }
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const result = await getConnectionSummary(validated);
|
|
41
|
+
return NextResponse.json({ ok: true, ...result });
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error.code === "NANGO_NOT_CONFIGURED" || error.code === "NANGO_SDK_UNAVAILABLE" || error.code === "NANGO_SDK_SHAPE") {
|
|
44
|
+
return NextResponse.json(
|
|
45
|
+
{ ok: false, error: error.message, code: error.code },
|
|
46
|
+
{ status: 503 }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return NextResponse.json(
|
|
50
|
+
{ ok: false, error: error?.message || "nango connection status failed" },
|
|
51
|
+
{ status: 502 }
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { POST };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/workspace/integrations/nango/proxy
|
|
3
|
+
*
|
|
4
|
+
* Proxies an API request through Nango using the connection identified by
|
|
5
|
+
* `providerConfigKey` + `connectionId`. The Nango SDK injects credentials
|
|
6
|
+
* server-side; the browser never holds the upstream provider's auth.
|
|
7
|
+
*
|
|
8
|
+
* Request body:
|
|
9
|
+
* {
|
|
10
|
+
* providerConfigKey: string, // Nango integration config key
|
|
11
|
+
* connectionId: string, // Nango connection id
|
|
12
|
+
* method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
|
|
13
|
+
* endpoint: string, // path or absolute URL
|
|
14
|
+
* headers?: object, // forbidden: Authorization, X-API-Key, etc.
|
|
15
|
+
* params?: object,
|
|
16
|
+
* data?: any,
|
|
17
|
+
* retries?: number, // 0..10
|
|
18
|
+
* timeoutMs?: number // 0..60000
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* Response — success:
|
|
22
|
+
* { ok: true, status: number, data: any, environment: string }
|
|
23
|
+
*
|
|
24
|
+
* Response — validation failure: 400 { ok: false, error, details }
|
|
25
|
+
* Response — Nango not configured: 503 { ok: false, error, code }
|
|
26
|
+
* Response — upstream failure: 502 { ok: false, error }
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { NextResponse } from "next/server";
|
|
30
|
+
import { proxyRequest, validateProxyRequest } from "@/lib/adapters/integrations/nango";
|
|
31
|
+
|
|
32
|
+
async function POST(request) {
|
|
33
|
+
let body;
|
|
34
|
+
try {
|
|
35
|
+
body = await request.json();
|
|
36
|
+
} catch {
|
|
37
|
+
return NextResponse.json({ ok: false, error: "invalid JSON body" }, { status: 400 });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let validated;
|
|
41
|
+
try {
|
|
42
|
+
validated = validateProxyRequest(body);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return NextResponse.json(
|
|
45
|
+
{ ok: false, error: error.message, details: error.details || null },
|
|
46
|
+
{ status: 400 }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const result = await proxyRequest(validated);
|
|
52
|
+
return NextResponse.json({ ok: true, ...result });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error.code === "NANGO_NOT_CONFIGURED" || error.code === "NANGO_SDK_UNAVAILABLE" || error.code === "NANGO_SDK_SHAPE") {
|
|
55
|
+
return NextResponse.json(
|
|
56
|
+
{ ok: false, error: error.message, code: error.code },
|
|
57
|
+
{ status: 503 }
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return NextResponse.json(
|
|
61
|
+
{ ok: false, error: error?.message || "nango proxy failed" },
|
|
62
|
+
{ status: 502 }
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { POST };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/workspace/integrations/nango/status
|
|
3
|
+
*
|
|
4
|
+
* Returns Nango connection health for the no-code Integrations panel.
|
|
5
|
+
*
|
|
6
|
+
* Query parameters (all optional):
|
|
7
|
+
* - providerConfigKey probe a specific provider connection
|
|
8
|
+
* - connectionId probe a specific connection (paired with providerConfigKey)
|
|
9
|
+
* - mode override mode for the probe ("cloud" | "self-hosted")
|
|
10
|
+
* - hostUrl override host URL for self-hosted probes
|
|
11
|
+
*
|
|
12
|
+
* Response shape:
|
|
13
|
+
* { status: "connected" | "disconnected" | "unconfigured", ... }
|
|
14
|
+
*
|
|
15
|
+
* Authority contract: never returns the Nango secret key. Never echoes
|
|
16
|
+
* credential-shaped fields from upstream SDK responses.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { NextResponse } from "next/server";
|
|
20
|
+
import { getStatus, validateConnectionStatusRequest } from "@/lib/adapters/integrations/nango";
|
|
21
|
+
|
|
22
|
+
async function GET(request) {
|
|
23
|
+
const url = new URL(request.url);
|
|
24
|
+
const raw = {
|
|
25
|
+
providerConfigKey: url.searchParams.get("providerConfigKey") || undefined,
|
|
26
|
+
connectionId: url.searchParams.get("connectionId") || undefined,
|
|
27
|
+
mode: url.searchParams.get("mode") || undefined,
|
|
28
|
+
hostUrl: url.searchParams.get("hostUrl") || undefined
|
|
29
|
+
};
|
|
30
|
+
let input;
|
|
31
|
+
try {
|
|
32
|
+
input = validateConnectionStatusRequest(raw);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return NextResponse.json(
|
|
35
|
+
{ error: error.message, details: error.details || null },
|
|
36
|
+
{ status: 400 }
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const result = await getStatus(input);
|
|
41
|
+
return NextResponse.json(result);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return NextResponse.json(
|
|
44
|
+
{ error: error?.message || "nango status probe failed" },
|
|
45
|
+
{ status: 500 }
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { GET };
|
|
@@ -152,6 +152,7 @@ async function GET(request) {
|
|
|
152
152
|
runs: metadataStore.runs,
|
|
153
153
|
outputArtifacts: metadataStore.outputArtifacts,
|
|
154
154
|
workerKits: metadataStore.workerKits,
|
|
155
|
+
provenance: metadataStore.provenance,
|
|
155
156
|
pipelineHealth: metadataStore.pipelineHealth
|
|
156
157
|
},
|
|
157
158
|
graph: {
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WorkspaceActivationPanel — customer activation checklist.
|
|
5
|
+
*
|
|
6
|
+
* Renders the derived activation state from
|
|
7
|
+
* `lib/workspace-activation.js` as a goal-first checklist with deep links
|
|
8
|
+
* into the existing surfaces. The panel is read-only; every step routes
|
|
9
|
+
* through routes the workspace already owns.
|
|
10
|
+
*
|
|
11
|
+
* Props:
|
|
12
|
+
* workspaceConfig — parsed growthub.config.json (governed)
|
|
13
|
+
* workspaceSourceRecords — parsed growthub.source-records.json (sidecar)
|
|
14
|
+
* metadataGraph — optional metadata graph envelope
|
|
15
|
+
* onOpenHelper — optional helper-thread CTA handler
|
|
16
|
+
* compact — render the rail-friendly compact variant
|
|
17
|
+
*
|
|
18
|
+
* Invariants:
|
|
19
|
+
* - No fetch. No mutation. No secret reading.
|
|
20
|
+
* - Reads only safe data (booleans, status strings, deep-link routes).
|
|
21
|
+
* - Falls back to a sensible default state if config is missing.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import Link from "next/link";
|
|
25
|
+
import {
|
|
26
|
+
ArrowRight,
|
|
27
|
+
Check,
|
|
28
|
+
CircleDot,
|
|
29
|
+
HelpCircle,
|
|
30
|
+
Lock,
|
|
31
|
+
Sparkles,
|
|
32
|
+
} from "lucide-react";
|
|
33
|
+
import { deriveWorkspaceActivationState } from "@/lib/workspace-activation";
|
|
34
|
+
|
|
35
|
+
function StatusIcon({ status }) {
|
|
36
|
+
if (status === "complete") return <Check size={14} aria-hidden="true" />;
|
|
37
|
+
if (status === "blocked") return <Lock size={14} aria-hidden="true" />;
|
|
38
|
+
if (status === "optional") return <Sparkles size={14} aria-hidden="true" />;
|
|
39
|
+
return <CircleDot size={14} aria-hidden="true" />;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function StatusLabel({ status }) {
|
|
43
|
+
if (status === "complete") return "Done";
|
|
44
|
+
if (status === "blocked") return "Blocked";
|
|
45
|
+
if (status === "optional") return "Optional";
|
|
46
|
+
return "Next";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function WorkspaceActivationPanel({
|
|
50
|
+
workspaceConfig,
|
|
51
|
+
workspaceSourceRecords,
|
|
52
|
+
metadataGraph,
|
|
53
|
+
onOpenHelper,
|
|
54
|
+
compact = false,
|
|
55
|
+
}) {
|
|
56
|
+
const state = deriveWorkspaceActivationState({
|
|
57
|
+
workspaceConfig,
|
|
58
|
+
workspaceSourceRecords,
|
|
59
|
+
metadataGraph,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const stepsToRender = compact
|
|
63
|
+
? state.steps.filter((step) => step.status !== "optional")
|
|
64
|
+
: state.steps;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<section
|
|
68
|
+
className={
|
|
69
|
+
"workspace-activation-panel"
|
|
70
|
+
+ (compact ? " is-compact" : "")
|
|
71
|
+
+ (state.complete ? " is-complete" : "")
|
|
72
|
+
}
|
|
73
|
+
aria-label="Workspace activation"
|
|
74
|
+
data-template={state.template}
|
|
75
|
+
>
|
|
76
|
+
<header className="workspace-activation-head">
|
|
77
|
+
<div className="workspace-activation-head-text">
|
|
78
|
+
<p className="workspace-activation-eyebrow">{state.templateName}</p>
|
|
79
|
+
<h2 className="workspace-activation-headline">{state.headline}</h2>
|
|
80
|
+
{state.subheadline ? (
|
|
81
|
+
<p className="workspace-activation-subheadline">{state.subheadline}</p>
|
|
82
|
+
) : null}
|
|
83
|
+
</div>
|
|
84
|
+
<div
|
|
85
|
+
className="workspace-activation-progress"
|
|
86
|
+
aria-label={`${state.completedCount} of ${state.totalCount} setup steps complete`}
|
|
87
|
+
>
|
|
88
|
+
<span className="workspace-activation-progress-value">
|
|
89
|
+
{state.completedCount}/{state.totalCount}
|
|
90
|
+
</span>
|
|
91
|
+
<span className="workspace-activation-progress-bar" aria-hidden="true">
|
|
92
|
+
<span
|
|
93
|
+
className="workspace-activation-progress-fill"
|
|
94
|
+
style={{
|
|
95
|
+
width: state.totalCount > 0
|
|
96
|
+
? `${Math.min(100, Math.round((state.completedCount / state.totalCount) * 100))}%`
|
|
97
|
+
: "0%",
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
</header>
|
|
103
|
+
|
|
104
|
+
<ol className="workspace-activation-steps" role="list">
|
|
105
|
+
{stepsToRender.map((step) => {
|
|
106
|
+
const isNext = state.nextStepId === step.id;
|
|
107
|
+
return (
|
|
108
|
+
<li
|
|
109
|
+
key={step.id}
|
|
110
|
+
className={
|
|
111
|
+
"workspace-activation-step"
|
|
112
|
+
+ ` is-${step.status}`
|
|
113
|
+
+ (isNext ? " is-next" : "")
|
|
114
|
+
}
|
|
115
|
+
>
|
|
116
|
+
<span className="workspace-activation-step-status" aria-hidden="true">
|
|
117
|
+
<StatusIcon status={step.status} />
|
|
118
|
+
</span>
|
|
119
|
+
<div className="workspace-activation-step-body">
|
|
120
|
+
<div className="workspace-activation-step-titlebar">
|
|
121
|
+
<h3 className="workspace-activation-step-title">{step.label}</h3>
|
|
122
|
+
<span className={`workspace-activation-step-badge is-${step.status}`}>
|
|
123
|
+
<StatusLabel status={step.status} />
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
<p className="workspace-activation-step-description">{step.description}</p>
|
|
127
|
+
{step.hint ? (
|
|
128
|
+
<p className="workspace-activation-step-hint">
|
|
129
|
+
<HelpCircle size={12} aria-hidden="true" />
|
|
130
|
+
<span>{step.hint}</span>
|
|
131
|
+
</p>
|
|
132
|
+
) : null}
|
|
133
|
+
{step.href ? (
|
|
134
|
+
<Link
|
|
135
|
+
href={step.href}
|
|
136
|
+
className={
|
|
137
|
+
"workspace-activation-step-cta"
|
|
138
|
+
+ (isNext ? " is-primary" : "")
|
|
139
|
+
}
|
|
140
|
+
>
|
|
141
|
+
<span>{step.cta || (step.status === "complete" ? "Review" : "Open")}</span>
|
|
142
|
+
<ArrowRight size={12} aria-hidden="true" />
|
|
143
|
+
</Link>
|
|
144
|
+
) : null}
|
|
145
|
+
</div>
|
|
146
|
+
</li>
|
|
147
|
+
);
|
|
148
|
+
})}
|
|
149
|
+
</ol>
|
|
150
|
+
|
|
151
|
+
{onOpenHelper ? (
|
|
152
|
+
<div className="workspace-activation-helper-cta">
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
className="workspace-activation-helper-btn"
|
|
156
|
+
onClick={() => onOpenHelper({
|
|
157
|
+
template: state.template,
|
|
158
|
+
nextStepId: state.nextStepId,
|
|
159
|
+
})}
|
|
160
|
+
>
|
|
161
|
+
<Sparkles size={13} aria-hidden="true" />
|
|
162
|
+
<span>
|
|
163
|
+
{state.complete
|
|
164
|
+
? "Ask helper to customize this workspace"
|
|
165
|
+
: `Ask helper to finish ${state.templateName}`}
|
|
166
|
+
</span>
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
) : null}
|
|
170
|
+
</section>
|
|
171
|
+
);
|
|
172
|
+
}
|