@growthub/cli 0.14.10 → 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.
Files changed (52) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/callback/route.js +35 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/failure/route.js +35 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/schedule/route.js +423 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/connect/route.js +78 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/credentials/route.js +276 -0
  6. 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
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/products/sync/route.js +347 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/sync/route.js +293 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/provider/connect/route.js +7 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/provider/sync/route.js +7 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/sync/route.js +197 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/apps/route.js +1 -1
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +1 -49
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +3 -20
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-api-record/route.js +3 -20
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflow/publish/route.js +407 -290
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflows/[providerId]/route.js +209 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceAddOnsMarketplace.jsx +806 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +141 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/CeoCockpit.jsx +15 -3
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +42 -5
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +5 -1
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +86 -20
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ScheduleCockpit.jsx +363 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +8 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +322 -1
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +2 -2
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/add-ons/add-ons-client.jsx +197 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/add-ons/page.jsx +23 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/settings-shell.jsx +1 -0
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +734 -61
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +15 -10
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +2 -7
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +2 -19
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +8 -4
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/schedule-cockpit-console.js +287 -0
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/scheduler-orchestration.js +449 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-secrets.js +77 -0
  39. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-readiness.js +583 -0
  40. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-on-callback.js +63 -0
  41. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-on-scheduler.js +519 -0
  42. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-ons.js +957 -0
  43. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +607 -63
  44. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +21 -0
  45. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-operator-auth.js +32 -0
  46. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/provider.png +0 -0
  47. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/qstash.png +0 -0
  48. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/redis.png +0 -0
  49. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/search.png +0 -0
  50. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/vector.png +0 -0
  51. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/scripts/scheduler-ingress-smoke.mjs +26 -0
  52. 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 };