@growthub/cli 0.14.9 → 0.14.11
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/apps/workspace/app/api/workspace/add-ons/[providerId]/callback/route.js +35 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/failure/route.js +35 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/schedule/route.js +423 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/connect/route.js +78 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/credentials/route.js +276 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/products/[productId]/resources/route.js +173 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/products/sync/route.js +347 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/sync/route.js +293 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/provider/connect/route.js +7 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/provider/sync/route.js +7 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/sync/route.js +197 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/apps/route.js +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/patch/preflight/route.js +38 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +3 -20
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-api-record/route.js +3 -20
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflow/publish/route.js +407 -290
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflows/[providerId]/route.js +209 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceAddOnsMarketplace.jsx +806 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +141 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/CeoCockpit.jsx +15 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +42 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +5 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +86 -20
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ScheduleCockpit.jsx +363 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +8 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +322 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/add-ons/add-ons-client.jsx +197 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/add-ons/page.jsx +23 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/settings-shell.jsx +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +734 -61
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +15 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +2 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +29 -19
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +8 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/schedule-cockpit-console.js +287 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/scheduler-orchestration.js +449 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-secrets.js +77 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-readiness.js +583 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-on-callback.js +63 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-on-scheduler.js +519 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-ons.js +957 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-app-readiness.js +212 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +607 -63
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-contract-compliance.js +168 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +21 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-operator-auth.js +32 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-patch-impact.js +133 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-provenance-lineage.js +214 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-stale-surfaces.js +217 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-workflow-impact.js +170 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/provider.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/qstash.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/redis.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/search.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/vector.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/scripts/scheduler-ingress-smoke.mjs +26 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +6 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/skills/governed-workspace-mutation/SKILL.md +3 -1
- package/dist/index.js +3024 -4191
- package/package.json +1 -1
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
import { readWorkspaceConfig, writeWorkspaceConfig } from "@/lib/workspace-config";
|
|
5
|
+
import {
|
|
6
|
+
getMarketplaceProvider,
|
|
7
|
+
withMarketplaceProviderRegistry,
|
|
8
|
+
} from "@/lib/workspace-add-ons";
|
|
9
|
+
import { appendOutcomeReceipt } from "@/lib/workspace-outcome-receipts";
|
|
10
|
+
import { requireWorkspaceOperator } from "@/lib/workspace-operator-auth";
|
|
11
|
+
|
|
12
|
+
const PROBE_TIMEOUT_MS = 8000;
|
|
13
|
+
|
|
14
|
+
function clean(value) {
|
|
15
|
+
return String(value == null ? "" : value).trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function jsonError(message, status = 400, extra = {}) {
|
|
19
|
+
return NextResponse.json({ error: message, ...extra }, { status });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function fetchWithTimeout(url, init = {}) {
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
25
|
+
try {
|
|
26
|
+
return await fetch(url, { ...init, signal: controller.signal, cache: "no-store" });
|
|
27
|
+
} finally {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function readJsonSafe(response) {
|
|
33
|
+
try {
|
|
34
|
+
return await response.json();
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function safeUrl(baseUrl, pathName) {
|
|
41
|
+
const base = clean(baseUrl).replace(/\/+$/, "");
|
|
42
|
+
const suffix = clean(pathName).startsWith("/") ? clean(pathName) : `/${clean(pathName)}`;
|
|
43
|
+
return `${base}${suffix}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function compactAccountOptions(payload, source, fallbackEmail) {
|
|
47
|
+
const rawItems = Array.isArray(payload)
|
|
48
|
+
? payload
|
|
49
|
+
: Array.isArray(payload?.teams)
|
|
50
|
+
? payload.teams
|
|
51
|
+
: Array.isArray(payload?.accounts)
|
|
52
|
+
? payload.accounts
|
|
53
|
+
: Array.isArray(payload?.data)
|
|
54
|
+
? payload.data
|
|
55
|
+
: [];
|
|
56
|
+
const options = rawItems
|
|
57
|
+
.map((item, index) => {
|
|
58
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
59
|
+
const id = clean(item.id || item.team_id || item.teamId || item.account_id || item.accountId || item.slug || item.name || `account-${index + 1}`);
|
|
60
|
+
const label = clean(item.name || item.team_name || item.teamName || item.email || item.slug || id);
|
|
61
|
+
if (!id || !label) return null;
|
|
62
|
+
return {
|
|
63
|
+
id,
|
|
64
|
+
label,
|
|
65
|
+
source,
|
|
66
|
+
role: clean(item.role || item.user_role || item.userRole || ""),
|
|
67
|
+
plan: clean(item.plan || item.tier || ""),
|
|
68
|
+
};
|
|
69
|
+
})
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
if (options.length) return options;
|
|
72
|
+
return fallbackEmail ? [{ id: fallbackEmail, label: fallbackEmail, source }] : [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getProviderSetupFields(provider) {
|
|
76
|
+
const fields = Array.isArray(provider.accountSetupFields) ? provider.accountSetupFields.filter((field) => field?.id) : [];
|
|
77
|
+
if (fields.length) return fields;
|
|
78
|
+
const emailEnv = provider.accountProbe?.emailEnv;
|
|
79
|
+
const keyEnv = provider.accountProbe?.keyEnv;
|
|
80
|
+
if (!emailEnv || !keyEnv) return [];
|
|
81
|
+
return [
|
|
82
|
+
{ id: "email", label: "Account email", required: true, envRef: emailEnv, credentialRole: "basicAuthUsername" },
|
|
83
|
+
{ id: "apiKey", label: "API key", required: true, envRef: keyEnv, credentialRole: "basicAuthPassword" },
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getCredentialValue(credentials, body, field) {
|
|
88
|
+
return clean(credentials?.[field.id] ?? body?.[field.id]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function deriveBasicAuthCredentials(provider, credentials, body) {
|
|
92
|
+
const fields = getProviderSetupFields(provider);
|
|
93
|
+
const usernameField = fields.find((field) => field.credentialRole === "basicAuthUsername");
|
|
94
|
+
const passwordField = fields.find((field) => field.credentialRole === "basicAuthPassword");
|
|
95
|
+
const username = clean(
|
|
96
|
+
usernameField ? getCredentialValue(credentials, body, usernameField) : credentials?.email ?? body?.email,
|
|
97
|
+
);
|
|
98
|
+
const password = clean(
|
|
99
|
+
passwordField ? getCredentialValue(credentials, body, passwordField) : credentials?.apiKey ?? body?.apiKey,
|
|
100
|
+
);
|
|
101
|
+
return { fields, usernameField, passwordField, username, password };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function deriveEnvUpdates(fields, credentials, body) {
|
|
105
|
+
return Object.fromEntries(fields
|
|
106
|
+
.filter((field) => field.envRef)
|
|
107
|
+
.map((field) => [field.envRef, getCredentialValue(credentials, body, field)])
|
|
108
|
+
.filter(([, value]) => value));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function verifyProviderAccount(provider, email, apiKey) {
|
|
112
|
+
const authHeader = `Basic ${Buffer.from(`${email}:${apiKey}`).toString("base64")}`;
|
|
113
|
+
const paths = ["/v2/teams", ...(Array.isArray(provider.accountProbe?.paths) ? provider.accountProbe.paths : [])];
|
|
114
|
+
let last = null;
|
|
115
|
+
for (const probePath of paths) {
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetchWithTimeout(safeUrl(provider.baseUrl, probePath), {
|
|
118
|
+
method: "GET",
|
|
119
|
+
headers: { authorization: authHeader, accept: "application/json" },
|
|
120
|
+
});
|
|
121
|
+
last = { path: probePath, status: response.status };
|
|
122
|
+
if (!response.ok) continue;
|
|
123
|
+
const payload = await readJsonSafe(response);
|
|
124
|
+
const options = compactAccountOptions(payload, probePath, email);
|
|
125
|
+
return { ok: true, path: probePath, status: response.status, options };
|
|
126
|
+
} catch (error) {
|
|
127
|
+
last = { path: probePath, status: 0, error: error?.message || "network error" };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { ok: false, last };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function deriveUpstashQstashRuntimeEnv(provider, email, apiKey) {
|
|
134
|
+
if (provider?.providerId !== "upstash") return { updates: {}, resolvedEnv: [] };
|
|
135
|
+
const authHeader = `Basic ${Buffer.from(`${email}:${apiKey}`).toString("base64")}`;
|
|
136
|
+
const userResponse = await fetchWithTimeout(safeUrl(provider.baseUrl, "/v2/qstash/user"), {
|
|
137
|
+
method: "GET",
|
|
138
|
+
headers: { authorization: authHeader, accept: "application/json" },
|
|
139
|
+
});
|
|
140
|
+
if (!userResponse.ok) return { updates: {}, resolvedEnv: [] };
|
|
141
|
+
const userPayload = await readJsonSafe(userResponse);
|
|
142
|
+
const token = clean(userPayload?.token);
|
|
143
|
+
if (!token) return { updates: {}, resolvedEnv: [] };
|
|
144
|
+
|
|
145
|
+
const updates = { QSTASH_TOKEN: token };
|
|
146
|
+
const keysResponse = await fetchWithTimeout("https://qstash.upstash.io/v2/keys", {
|
|
147
|
+
method: "GET",
|
|
148
|
+
headers: { authorization: `Bearer ${token}`, accept: "application/json" },
|
|
149
|
+
});
|
|
150
|
+
if (keysResponse.ok) {
|
|
151
|
+
const keysPayload = await readJsonSafe(keysResponse);
|
|
152
|
+
if (clean(keysPayload?.current)) updates.QSTASH_CURRENT_SIGNING_KEY = clean(keysPayload.current);
|
|
153
|
+
if (clean(keysPayload?.next)) updates.QSTASH_NEXT_SIGNING_KEY = clean(keysPayload.next);
|
|
154
|
+
}
|
|
155
|
+
return { updates, resolvedEnv: Object.keys(updates) };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function quoteEnv(value) {
|
|
159
|
+
return JSON.stringify(String(value || ""));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function writeLocalEnv(updates) {
|
|
163
|
+
const envPath = path.join(process.cwd(), ".env.local");
|
|
164
|
+
let raw = "";
|
|
165
|
+
try {
|
|
166
|
+
raw = await fs.readFile(envPath, "utf8");
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error?.code !== "ENOENT") throw error;
|
|
169
|
+
}
|
|
170
|
+
const keys = Object.keys(updates);
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
const lines = raw.split(/\n/).map((line) => {
|
|
173
|
+
const match = line.match(/^\s*([A-Z0-9_]+)\s*=/);
|
|
174
|
+
if (!match || !keys.includes(match[1])) return line;
|
|
175
|
+
seen.add(match[1]);
|
|
176
|
+
return `${match[1]}=${quoteEnv(updates[match[1]])}`;
|
|
177
|
+
});
|
|
178
|
+
for (const key of keys) {
|
|
179
|
+
if (!seen.has(key)) lines.push(`${key}=${quoteEnv(updates[key])}`);
|
|
180
|
+
process.env[key] = updates[key];
|
|
181
|
+
}
|
|
182
|
+
await fs.writeFile(envPath, `${lines.filter((line, index) => index < lines.length - 1 || line.trim()).join("\n")}\n`, "utf8");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function POST(request, context) {
|
|
186
|
+
const params = await context?.params;
|
|
187
|
+
const providerId = clean(params?.providerId);
|
|
188
|
+
const provider = getMarketplaceProvider(providerId);
|
|
189
|
+
if (!provider) return jsonError("unknown marketplace provider", 404, { providerId });
|
|
190
|
+
|
|
191
|
+
const auth = requireWorkspaceOperator(request);
|
|
192
|
+
if (!auth.ok) return jsonError(auth.error, auth.status);
|
|
193
|
+
|
|
194
|
+
let body = {};
|
|
195
|
+
try {
|
|
196
|
+
body = await request.json();
|
|
197
|
+
} catch {
|
|
198
|
+
return jsonError("invalid json body", 400);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const credentials = body && typeof body.credentials === "object" && !Array.isArray(body.credentials)
|
|
202
|
+
? body.credentials
|
|
203
|
+
: {};
|
|
204
|
+
const { fields, usernameField, passwordField, username: email, password: apiKey } = deriveBasicAuthCredentials(provider, credentials, body);
|
|
205
|
+
const missingFields = fields
|
|
206
|
+
.filter((field) => field.required && !getCredentialValue(credentials, body, field))
|
|
207
|
+
.map((field) => field.id);
|
|
208
|
+
const envUpdates = deriveEnvUpdates(fields, credentials, body);
|
|
209
|
+
const emailEnv = usernameField?.envRef || provider.accountProbe?.emailEnv;
|
|
210
|
+
const keyEnv = passwordField?.envRef || provider.accountProbe?.keyEnv;
|
|
211
|
+
if (!fields.length || !emailEnv || !keyEnv) return jsonError("provider does not support account credential setup", 400);
|
|
212
|
+
if (missingFields.length || !email || !apiKey) {
|
|
213
|
+
return jsonError(`${provider.label} account credentials are required`, 400, {
|
|
214
|
+
providerId: provider.providerId,
|
|
215
|
+
missingFields,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const verified = await verifyProviderAccount(provider, email, apiKey);
|
|
220
|
+
if (!verified.ok) {
|
|
221
|
+
return jsonError(`${provider.label} account API key could not be verified`, 422, {
|
|
222
|
+
providerId: provider.providerId,
|
|
223
|
+
checked: verified.last ? { path: verified.last.path, status: verified.last.status } : null,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const qstashRuntime = await deriveUpstashQstashRuntimeEnv(provider, email, apiKey);
|
|
228
|
+
const envToWrite = {
|
|
229
|
+
...(Object.keys(envUpdates).length ? envUpdates : { [emailEnv]: email, [keyEnv]: apiKey }),
|
|
230
|
+
...qstashRuntime.updates,
|
|
231
|
+
};
|
|
232
|
+
await writeLocalEnv(envToWrite);
|
|
233
|
+
|
|
234
|
+
const selected = verified.options[0] || { id: email, label: email };
|
|
235
|
+
const now = new Date().toISOString();
|
|
236
|
+
const syncResult = {
|
|
237
|
+
ok: true,
|
|
238
|
+
syncStatus: "verified",
|
|
239
|
+
status: "connected",
|
|
240
|
+
testedAt: now,
|
|
241
|
+
proof: `${provider.label} Developer API account verified (GET ${verified.path} -> HTTP ${verified.status}).`,
|
|
242
|
+
summary: `${provider.label} provider account verified and stored as local runtime env refs.`,
|
|
243
|
+
resolvedEnv: Array.from(new Set([emailEnv, keyEnv, ...(qstashRuntime.resolvedEnv || [])])),
|
|
244
|
+
providerAccountOptions: verified.options,
|
|
245
|
+
selectedProviderAccountId: selected.id || "",
|
|
246
|
+
selectedProviderAccountLabel: selected.label || "",
|
|
247
|
+
providerAccountSource: verified.path,
|
|
248
|
+
};
|
|
249
|
+
const currentConfig = await readWorkspaceConfig();
|
|
250
|
+
const nextConfig = withMarketplaceProviderRegistry(currentConfig, { providerId: provider.providerId, syncResult });
|
|
251
|
+
const persisted = await writeWorkspaceConfig({ dataModel: nextConfig.dataModel });
|
|
252
|
+
const { receipt } = await appendOutcomeReceipt({
|
|
253
|
+
kind: "workspace-add-on-provider-credentials",
|
|
254
|
+
lane: "server-authoritative",
|
|
255
|
+
outcomeStatus: "published",
|
|
256
|
+
actor: "workspace-marketplace",
|
|
257
|
+
objectRefs: [{ objectId: "api-registry", objectType: "api-registry", rowName: provider.label }],
|
|
258
|
+
changedFields: ["dataModel.api-registry"],
|
|
259
|
+
policyVerdict: { ok: true },
|
|
260
|
+
schemaVerdict: { ok: true },
|
|
261
|
+
summary: syncResult.summary,
|
|
262
|
+
nextActions: [`Install ${provider.label} products from the marketplace page.`],
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return NextResponse.json({
|
|
266
|
+
ok: true,
|
|
267
|
+
providerId: provider.providerId,
|
|
268
|
+
accountState: "verified",
|
|
269
|
+
workspaceConfig: persisted,
|
|
270
|
+
accountOptions: verified.options,
|
|
271
|
+
resolvedEnv: Array.from(new Set([emailEnv, keyEnv, ...(qstashRuntime.resolvedEnv || [])])),
|
|
272
|
+
receiptId: receipt.receiptId,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export { POST };
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import {
|
|
3
|
+
getMarketplaceProvider,
|
|
4
|
+
getMarketplaceProduct,
|
|
5
|
+
} from "@/lib/workspace-add-ons";
|
|
6
|
+
import { readEnvVar } from "@/lib/server-secrets";
|
|
7
|
+
import { requireWorkspaceOperator } from "@/lib/workspace-operator-auth";
|
|
8
|
+
|
|
9
|
+
const PROBE_TIMEOUT_MS = 8000;
|
|
10
|
+
|
|
11
|
+
function clean(value) {
|
|
12
|
+
return String(value == null ? "" : value).trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function jsonError(message, status = 400, extra = {}) {
|
|
16
|
+
return NextResponse.json({ error: message, ...extra }, { status });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function envValue(key) {
|
|
20
|
+
return clean(readEnvVar(key, process.env)?.value || "");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function fetchWithTimeout(url, init = {}) {
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
26
|
+
try {
|
|
27
|
+
return await fetch(url, { ...init, signal: controller.signal, cache: "no-store" });
|
|
28
|
+
} finally {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function readJsonSafe(response) {
|
|
34
|
+
try {
|
|
35
|
+
return await response.json();
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function safeUrl(baseUrl, path) {
|
|
42
|
+
const base = clean(baseUrl).replace(/\/+$/, "");
|
|
43
|
+
const suffix = clean(path).startsWith("/") ? clean(path) : `/${clean(path)}`;
|
|
44
|
+
return `${base}${suffix}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resourcePaths(product) {
|
|
48
|
+
if (Array.isArray(product?.resourceDiscovery?.paths) && product.resourceDiscovery.paths.length) {
|
|
49
|
+
return product.resourceDiscovery.paths;
|
|
50
|
+
}
|
|
51
|
+
const productId = product?.productId;
|
|
52
|
+
if (productId === "upstash-redis") return ["/v2/redis/databases"];
|
|
53
|
+
if (productId === "upstash-vector") return ["/v2/vector/index"];
|
|
54
|
+
if (productId === "upstash-search") return ["/v2/search"];
|
|
55
|
+
if (productId === "upstash-qstash") return ["/v2/qstash/users", "/v2/qstash/user"];
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function pickArray(payload) {
|
|
60
|
+
if (Array.isArray(payload)) return payload;
|
|
61
|
+
for (const key of ["databases", "indexes", "indices", "schedules", "queues", "resources", "items", "data"]) {
|
|
62
|
+
if (Array.isArray(payload?.[key])) return payload[key];
|
|
63
|
+
}
|
|
64
|
+
if (payload && typeof payload === "object") return [payload];
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeResource(item, index, source) {
|
|
69
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
70
|
+
const id = clean(
|
|
71
|
+
item.database_id
|
|
72
|
+
|| item.databaseId
|
|
73
|
+
|| item.id
|
|
74
|
+
|| item.uuid
|
|
75
|
+
|| item.customer_id
|
|
76
|
+
|| item.customerId
|
|
77
|
+
|| item.created_by
|
|
78
|
+
|| item.createdBy
|
|
79
|
+
|| item.index_id
|
|
80
|
+
|| item.indexId
|
|
81
|
+
|| item.index_name
|
|
82
|
+
|| item.indexName
|
|
83
|
+
|| item.name
|
|
84
|
+
|| item.endpoint
|
|
85
|
+
|| `resource-${index + 1}`
|
|
86
|
+
);
|
|
87
|
+
const label = clean(
|
|
88
|
+
item.database_name
|
|
89
|
+
|| item.databaseName
|
|
90
|
+
|| item.name
|
|
91
|
+
|| item.database_id
|
|
92
|
+
|| item.databaseId
|
|
93
|
+
|| item.index_name
|
|
94
|
+
|| item.indexName
|
|
95
|
+
|| item.slug
|
|
96
|
+
|| item.region
|
|
97
|
+
|| item.type
|
|
98
|
+
|| item.id
|
|
99
|
+
|| item.customer_id
|
|
100
|
+
|| item.endpoint
|
|
101
|
+
|| item.url
|
|
102
|
+
|| id
|
|
103
|
+
);
|
|
104
|
+
if (!id || !label) return null;
|
|
105
|
+
return {
|
|
106
|
+
id,
|
|
107
|
+
label,
|
|
108
|
+
source,
|
|
109
|
+
region: clean(item.region || item.primary_region || item.primaryRegion || ""),
|
|
110
|
+
type: clean(item.type || item.kind || ""),
|
|
111
|
+
endpoint: clean(item.endpoint || item.url || item.rest_url || item.restUrl || ""),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function GET(request, context) {
|
|
116
|
+
const auth = requireWorkspaceOperator(request);
|
|
117
|
+
if (!auth.ok) return jsonError(auth.error, auth.status);
|
|
118
|
+
|
|
119
|
+
const params = await context?.params;
|
|
120
|
+
const providerId = clean(params?.providerId);
|
|
121
|
+
const productId = clean(params?.productId);
|
|
122
|
+
const provider = getMarketplaceProvider(providerId);
|
|
123
|
+
const product = getMarketplaceProduct(providerId, productId);
|
|
124
|
+
if (!provider || !product) return jsonError("unknown provider product", 404, { providerId, productId });
|
|
125
|
+
|
|
126
|
+
const emailKey = provider.accountProbe?.emailEnv;
|
|
127
|
+
const apiKey = provider.accountProbe?.keyEnv;
|
|
128
|
+
const email = envValue(emailKey);
|
|
129
|
+
const key = envValue(apiKey);
|
|
130
|
+
if (!email || !key) {
|
|
131
|
+
return jsonError(`${provider.label} account auth is not connected`, 422, {
|
|
132
|
+
providerId,
|
|
133
|
+
productId,
|
|
134
|
+
missingEnv: [emailKey, apiKey].filter((name) => name && !readEnvVar(name, process.env)),
|
|
135
|
+
resolvedEnv: [emailKey, apiKey].filter((name) => name && readEnvVar(name, process.env)),
|
|
136
|
+
resources: [],
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const authHeader = `Basic ${Buffer.from(`${email}:${key}`).toString("base64")}`;
|
|
141
|
+
const resources = [];
|
|
142
|
+
const failures = [];
|
|
143
|
+
for (const path of resourcePaths(product)) {
|
|
144
|
+
try {
|
|
145
|
+
const response = await fetchWithTimeout(safeUrl(provider.baseUrl, path), {
|
|
146
|
+
headers: { authorization: authHeader, accept: "application/json" },
|
|
147
|
+
});
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
failures.push({ path, status: response.status });
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const rows = pickArray(await readJsonSafe(response));
|
|
153
|
+
for (const [index, row] of rows.entries()) {
|
|
154
|
+
const resource = normalizeResource(row, index, path);
|
|
155
|
+
if (resource) resources.push(resource);
|
|
156
|
+
}
|
|
157
|
+
if (resources.length) break;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
failures.push({ path, status: 0, error: error?.message || "network error" });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return NextResponse.json({
|
|
164
|
+
ok: true,
|
|
165
|
+
providerId,
|
|
166
|
+
productId: product.productId,
|
|
167
|
+
resources,
|
|
168
|
+
failures,
|
|
169
|
+
resolvedEnv: [emailKey, apiKey].filter((name) => name && readEnvVar(name, process.env)),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export { GET };
|