@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,957 @@
1
+ import { readEnvVar } from "./server-secrets.js";
2
+
3
+ const UPSTASH_QSTASH_INTEGRATION_ID = "upstash-qstash-workflow";
4
+ const UPSTASH_AUTH_REF = "QSTASH";
5
+ const UPSTASH_PROVIDER_INTEGRATION_ID = "upstash-provider";
6
+ const UPSTASH_REGION_OPTIONS = [
7
+ { id: "us-east-1", label: "Washington, D.C., USA (East)", baseUrl: "https://qstash-us-east-1.upstash.io" },
8
+ { id: "us-west-1", label: "San Francisco, USA (West)", baseUrl: "https://qstash-us-west-1.upstash.io" },
9
+ { id: "eu-central-1", label: "Frankfurt, EU (Central)", baseUrl: "https://qstash-eu-central-1.upstash.io" },
10
+ { id: "eu-west-1", label: "Frankfurt, EU (Central)", baseUrl: "https://qstash-eu-west-1.upstash.io" },
11
+ ];
12
+ const UPSTASH_PRODUCTS = [
13
+ {
14
+ productId: "upstash-qstash",
15
+ integrationId: UPSTASH_QSTASH_INTEGRATION_ID,
16
+ authRef: UPSTASH_AUTH_REF,
17
+ label: "Upstash QStash/Workflow",
18
+ shortLabel: "QStash/Workflow",
19
+ icon: "Q",
20
+ iconClass: "is-upstash",
21
+ iconSrc: "/integrations/upstash/qstash.png",
22
+ connectorKind: "upstash-qstash",
23
+ endpoint: "/v2/publish/<workspace-sandbox-run-url>",
24
+ method: "POST",
25
+ description: "QStash-backed scheduler for Growthub serverless workflow runs. Secrets stay in env; this row stores only refs and routing metadata.",
26
+ subtitle: "Messaging for the Serverless",
27
+ plans: "Free, Pay as You Go, Pro Plans",
28
+ entityTypes: "workflow-run,scheduler",
29
+ capabilities: "scheduler,workflow,queue",
30
+ executionLane: "serverless-scheduler",
31
+ // QSTASH_URL is optional: the schedule API is region-based, so the adapter
32
+ // derives https://qstash-{region}.upstash.io from the selected region when
33
+ // QSTASH_URL is absent. Only the token is truly required.
34
+ requiredEnv: ["QSTASH_TOKEN"],
35
+ optionalEnv: ["QSTASH_URL", "QSTASH_CURRENT_SIGNING_KEY", "QSTASH_NEXT_SIGNING_KEY"],
36
+ consoleUrl: "https://console.upstash.com/qstash",
37
+ probe: {
38
+ baseUrlEnv: "QSTASH_URL",
39
+ tokenEnv: "QSTASH_TOKEN",
40
+ paths: ["/v2/schedules", "/v2/dlq"],
41
+ fallbackRegionBaseUrl: true,
42
+ },
43
+ resourceDiscovery: {
44
+ auth: "provider-basic",
45
+ paths: ["/v2/qstash/users", "/v2/qstash/user"],
46
+ emptyLabel: "No QStash workflow resources returned for this account.",
47
+ createDividerLabel: "Or create a new QStash resource",
48
+ envFromResource: [
49
+ { envRef: "QSTASH_TOKEN", field: "token" },
50
+ ],
51
+ },
52
+ regionOptions: UPSTASH_REGION_OPTIONS,
53
+ },
54
+ {
55
+ productId: "upstash-redis",
56
+ integrationId: "upstash-redis",
57
+ authRef: "UPSTASH_REDIS",
58
+ label: "Upstash for Redis",
59
+ shortLabel: "Redis",
60
+ icon: "R",
61
+ iconClass: "is-redis",
62
+ iconSrc: "/integrations/upstash/redis.png",
63
+ connectorKind: "upstash-redis",
64
+ endpoint: "/ping",
65
+ method: "GET",
66
+ description: "Upstash Redis REST database connection registered for governed workspace add-ons.",
67
+ subtitle: "Redis Compatible Database",
68
+ plans: "Free, Pay as You Go, Fixed",
69
+ entityTypes: "cache,kv,redis",
70
+ capabilities: "kv,cache,rate-limit",
71
+ executionLane: "workspace-data",
72
+ requiredEnv: ["UPSTASH_REDIS_REST_URL", "UPSTASH_REDIS_REST_TOKEN"],
73
+ optionalEnv: [],
74
+ consoleUrl: "https://console.upstash.com/redis",
75
+ resourceDiscovery: {
76
+ auth: "provider-basic",
77
+ paths: ["/v2/redis/databases"],
78
+ emptyLabel: "No Redis databases returned for this account.",
79
+ createDividerLabel: "Or create a new Redis database",
80
+ envFromResource: [
81
+ { envRef: "UPSTASH_REDIS_REST_URL", fieldCandidates: ["rest_url", "restUrl", "endpoint", "url"], ensureHttps: true },
82
+ { envRef: "UPSTASH_REDIS_REST_TOKEN", fieldCandidates: ["rest_token", "restToken", "token"] },
83
+ ],
84
+ },
85
+ probe: {
86
+ baseUrlEnv: "UPSTASH_REDIS_REST_URL",
87
+ tokenEnv: "UPSTASH_REDIS_REST_TOKEN",
88
+ paths: ["/ping"],
89
+ },
90
+ regionOptions: UPSTASH_REGION_OPTIONS,
91
+ },
92
+ {
93
+ productId: "upstash-search",
94
+ integrationId: "upstash-search",
95
+ authRef: "UPSTASH_SEARCH",
96
+ label: "Upstash Search",
97
+ shortLabel: "Search",
98
+ icon: "S",
99
+ iconClass: "is-search",
100
+ iconSrc: "/integrations/upstash/search.png",
101
+ connectorKind: "upstash-search",
102
+ endpoint: "/stats",
103
+ method: "GET",
104
+ description: "Upstash Search REST connection registered for workspace retrieval/search add-ons.",
105
+ subtitle: "Serverless AI search at scale",
106
+ plans: "Free, Pay as You Go",
107
+ entityTypes: "search,index,documents",
108
+ capabilities: "search,indexing,retrieval",
109
+ executionLane: "workspace-retrieval",
110
+ requiredEnv: ["UPSTASH_SEARCH_REST_URL", "UPSTASH_SEARCH_REST_TOKEN"],
111
+ optionalEnv: [],
112
+ consoleUrl: "https://console.upstash.com/search",
113
+ resourceDiscovery: {
114
+ auth: "provider-basic",
115
+ paths: ["/v2/search"],
116
+ emptyLabel: "No Search indexes returned for this account.",
117
+ createDividerLabel: "Or create a new Search index",
118
+ envFromResource: [
119
+ { envRef: "UPSTASH_SEARCH_REST_URL", fieldCandidates: ["rest_url", "restUrl", "endpoint", "url"], ensureHttps: true },
120
+ { envRef: "UPSTASH_SEARCH_REST_TOKEN", fieldCandidates: ["rest_token", "restToken", "token"] },
121
+ ],
122
+ },
123
+ probe: {
124
+ baseUrlEnv: "UPSTASH_SEARCH_REST_URL",
125
+ tokenEnv: "UPSTASH_SEARCH_REST_TOKEN",
126
+ paths: ["/stats", "/info"],
127
+ },
128
+ regionOptions: UPSTASH_REGION_OPTIONS,
129
+ },
130
+ {
131
+ productId: "upstash-vector",
132
+ integrationId: "upstash-vector",
133
+ authRef: "UPSTASH_VECTOR",
134
+ label: "Upstash Vector",
135
+ shortLabel: "Vector",
136
+ icon: "V",
137
+ iconClass: "is-vector",
138
+ iconSrc: "/integrations/upstash/vector.png",
139
+ connectorKind: "upstash-vector",
140
+ endpoint: "/info",
141
+ method: "GET",
142
+ description: "Upstash Vector REST index registered for governed workspace retrieval add-ons.",
143
+ subtitle: "Serverless Vector Database",
144
+ plans: "Free, Pay as You Go, Fixed",
145
+ entityTypes: "vector,index,embedding",
146
+ capabilities: "vector-search,semantic-retrieval",
147
+ executionLane: "workspace-retrieval",
148
+ requiredEnv: ["UPSTASH_VECTOR_REST_URL", "UPSTASH_VECTOR_REST_TOKEN"],
149
+ optionalEnv: [],
150
+ consoleUrl: "https://console.upstash.com/vector",
151
+ resourceDiscovery: {
152
+ auth: "provider-basic",
153
+ paths: ["/v2/vector/index"],
154
+ emptyLabel: "No Vector indexes returned for this account.",
155
+ createDividerLabel: "Or create a new Vector index",
156
+ envFromResource: [
157
+ { envRef: "UPSTASH_VECTOR_REST_URL", fieldCandidates: ["rest_url", "restUrl", "endpoint", "url"], ensureHttps: true },
158
+ { envRef: "UPSTASH_VECTOR_REST_TOKEN", fieldCandidates: ["rest_token", "restToken", "token"] },
159
+ ],
160
+ },
161
+ probe: {
162
+ baseUrlEnv: "UPSTASH_VECTOR_REST_URL",
163
+ tokenEnv: "UPSTASH_VECTOR_REST_TOKEN",
164
+ paths: ["/info"],
165
+ },
166
+ regionOptions: UPSTASH_REGION_OPTIONS,
167
+ },
168
+ ];
169
+ const MARKETPLACE_PROVIDERS = [
170
+ {
171
+ providerId: "upstash",
172
+ integrationId: UPSTASH_PROVIDER_INTEGRATION_ID,
173
+ authRef: "UPSTASH",
174
+ label: "Upstash",
175
+ developer: "Upstash",
176
+ iconSrc: "/integrations/upstash/provider.png",
177
+ baseUrl: "https://api.upstash.com",
178
+ endpoint: "/v2",
179
+ method: "GET",
180
+ // Provider/account-management lane (Developer API): HTTP Basic EMAIL:API_KEY.
181
+ // Available to native Upstash accounts only; absence ⇒ account-linked, not verified.
182
+ accountProbe: {
183
+ emailEnv: "UPSTASH_EMAIL",
184
+ keyEnv: "UPSTASH_API_KEY",
185
+ paths: ["/v2/redis/databases", "/v2/teams"],
186
+ },
187
+ accountSetupFields: [
188
+ {
189
+ id: "email",
190
+ label: "Upstash account email",
191
+ type: "email",
192
+ autocomplete: "email",
193
+ required: true,
194
+ envRef: "UPSTASH_EMAIL",
195
+ credentialRole: "basicAuthUsername",
196
+ },
197
+ {
198
+ id: "apiKey",
199
+ label: "Management API key",
200
+ type: "password",
201
+ autocomplete: "off",
202
+ required: true,
203
+ envRef: "UPSTASH_API_KEY",
204
+ credentialRole: "basicAuthPassword",
205
+ },
206
+ ],
207
+ consoleUrl: "https://console.upstash.com/",
208
+ accountSetupUrl: "https://console.upstash.com/account/api",
209
+ supportUrl: "https://upstash.com/support",
210
+ websiteUrl: "https://upstash.com",
211
+ docsUrl: "https://upstash.com/docs",
212
+ termsUrl: "https://upstash.com/terms",
213
+ privacyUrl: "https://upstash.com/privacy",
214
+ providerProductsLabel: "Serverless DB (Redis, Vector, Queue, Search)",
215
+ products: UPSTASH_PRODUCTS,
216
+ entityTypes: "provider,marketplace,account",
217
+ connectorKind: "upstash-provider",
218
+ capabilities: "provider-account,env-provisioning,marketplace-products",
219
+ executionLane: "workspace-provider",
220
+ description: "Provider-level Upstash account binding for workspace add-ons. Product rows are installed after this account is verified.",
221
+ },
222
+ ];
223
+
224
+ function apiRegistryColumns(existing = []) {
225
+ return Array.from(new Set([
226
+ "Name",
227
+ "integrationId",
228
+ "authRef",
229
+ "requiredEnv",
230
+ "optionalEnv",
231
+ "resolvedEnv",
232
+ "selectedResourceId",
233
+ "selectedResourceLabel",
234
+ "selectedResourceSource",
235
+ "baseUrl",
236
+ "endpoint",
237
+ "method",
238
+ "status",
239
+ "lastTested",
240
+ "lastResponse",
241
+ "entityTypes",
242
+ "description",
243
+ "connectorKind",
244
+ "resolverTemplateId",
245
+ "schemaVersion",
246
+ "capabilities",
247
+ "executionLane",
248
+ "region",
249
+ "productId",
250
+ "plan",
251
+ "syncStatus",
252
+ "syncCheckedAt",
253
+ "syncProof",
254
+ "missingEnv",
255
+ "providerAccountRequiredEnv",
256
+ "providerAccountOptions",
257
+ "selectedProviderAccountId",
258
+ "selectedProviderAccountLabel",
259
+ "providerAccountSource",
260
+ ...existing,
261
+ ]));
262
+ }
263
+
264
+ // NOTE: per-workflow schedule state (scheduleId, cron, callback URLs, last
265
+ // scheduled-run proof) is OWNED BY THE WORKFLOW ROW (sandbox-environment), NOT
266
+ // this provider capability row — see `withWorkflowServerlessBind`. The API
267
+ // Registry row is a pure capability row: verified provider/product, token/probe
268
+ // proof (syncStatus / syncProof / syncCheckedAt). It intentionally carries no
269
+ // per-schedule columns so two scheduled workflows never collide on one row.
270
+
271
+ const SERVERLESS_LOCAL_ADAPTERS = ["local-agent-host", "local-intelligence"];
272
+
273
+ function parseGraphValue(value) {
274
+ if (value && typeof value === "object") return value;
275
+ if (typeof value === "string" && value.trim()) {
276
+ try {
277
+ return JSON.parse(value);
278
+ } catch {
279
+ return null;
280
+ }
281
+ }
282
+ return null;
283
+ }
284
+
285
+ /**
286
+ * Sync the orchestration config's TRIGGER node (and result node) to the
287
+ * serverless-scheduler binding so the graph's own logic matches the schedule
288
+ * that drives it. The trigger node (a `data-trigger`, else the entry `input`
289
+ * node) records who invokes the run; the `tool-result` node keeps
290
+ * `writeLastResponse` on so the scheduled run's last response is recorded on
291
+ * the row. Preserves the stored value's shape (string vs object). `clear:true`
292
+ * reverts the trigger to manual on uninstall.
293
+ */
294
+ const CANONICAL_TRIGGER_NODE_ID = "schedule-trigger";
295
+
296
+ function scheduleTriggerConfig(meta) {
297
+ return {
298
+ trigger: "serverless-scheduler",
299
+ triggerKind: "serverless-scheduler",
300
+ schedule: {
301
+ schedulerRegistryId: meta.schedulerRegistryId || "",
302
+ scheduleId: meta.scheduleId || "",
303
+ cron: meta.cron || "",
304
+ providerId: meta.schedulerProviderId || "",
305
+ productId: meta.schedulerProductId || "",
306
+ destinationUrl: meta.destinationUrl || "",
307
+ callbackUrl: meta.callbackUrl || "",
308
+ triggerInput: meta.triggerInput || "",
309
+ },
310
+ enabled: true,
311
+ };
312
+ }
313
+
314
+ function syncTriggerNodeForSchedule(value, meta = {}, { clear = false } = {}) {
315
+ const graph = parseGraphValue(value);
316
+ if (!graph || !Array.isArray(graph.nodes) || !graph.nodes.length) {
317
+ return { value, triggerNodeId: null, changed: false };
318
+ }
319
+ // Pick the trigger node deterministically: a `data-trigger`, else the entry
320
+ // `input` node. NEVER fall back to mutating an arbitrary node — if neither
321
+ // exists, create a canonical `data-trigger` node instead.
322
+ const byType = graph.nodes.findIndex((n) => n?.type === "data-trigger");
323
+ const byInput = byType >= 0 ? -1 : graph.nodes.findIndex((n) => n?.type === "input" || n?.id === "input");
324
+ const triggerIndex = byType >= 0 ? byType : byInput;
325
+
326
+ if (triggerIndex < 0) {
327
+ // No canonical trigger/input node — create one rather than mutate node 0.
328
+ if (clear) return { value, triggerNodeId: null, changed: false };
329
+ const triggerNode = {
330
+ id: CANONICAL_TRIGGER_NODE_ID,
331
+ type: "data-trigger",
332
+ label: "Schedule trigger",
333
+ subtitle: "Serverless scheduler",
334
+ config: { action: "schedule-fired", ...scheduleTriggerConfig(meta) },
335
+ };
336
+ const nextNodes = graph.nodes.map((node) =>
337
+ node?.type === "tool-result" ? { ...node, config: { ...(node.config || {}), writeLastResponse: true } } : node,
338
+ );
339
+ const nextGraph = { ...graph, nodes: [triggerNode, ...nextNodes] };
340
+ return {
341
+ value: typeof value === "string" ? JSON.stringify(nextGraph) : nextGraph,
342
+ triggerNodeId: CANONICAL_TRIGGER_NODE_ID,
343
+ changed: true,
344
+ };
345
+ }
346
+
347
+ const triggerNodeId = String(graph.nodes[triggerIndex]?.id || "").trim() || `node-${triggerIndex}`;
348
+ const nextNodes = graph.nodes.map((node, index) => {
349
+ if (index === triggerIndex) {
350
+ const config = { ...(node.config || {}) };
351
+ const isInputTrigger = node?.type === "input" || node?.id === "input";
352
+ if (clear) {
353
+ config.trigger = "manual";
354
+ config.triggerKind = "manual";
355
+ if (isInputTrigger) config.inputMode = "manual";
356
+ delete config.schedule;
357
+ delete config.enabled;
358
+ } else {
359
+ Object.assign(config, scheduleTriggerConfig(meta));
360
+ if (isInputTrigger) config.inputMode = "serverless-schedule";
361
+ }
362
+ return { ...node, config };
363
+ }
364
+ if (node?.type === "tool-result") {
365
+ return { ...node, config: { ...(node.config || {}), writeLastResponse: true } };
366
+ }
367
+ return node;
368
+ });
369
+ const nextGraph = { ...graph, nodes: nextNodes };
370
+ return {
371
+ value: typeof value === "string" ? JSON.stringify(nextGraph) : nextGraph,
372
+ triggerNodeId,
373
+ changed: true,
374
+ };
375
+ }
376
+
377
+ /** Resolve the runtime-live graph field (precedence matches the runner). */
378
+ function liveGraphField(row) {
379
+ return parseGraphValue(row?.orchestrationGraph) ? "orchestrationGraph" : "orchestrationConfig";
380
+ }
381
+
382
+ /** Read the schedule binding recorded on a graph's trigger node (or null). */
383
+ function readTriggerScheduleBinding(value) {
384
+ const graph = parseGraphValue(value);
385
+ if (!graph || !Array.isArray(graph.nodes)) return null;
386
+ const node =
387
+ graph.nodes.find((n) => n?.type === "data-trigger") ||
388
+ graph.nodes.find((n) => n?.type === "input" || n?.id === "input");
389
+ const schedule = node?.config?.schedule;
390
+ if (!schedule || node?.config?.trigger !== "serverless-scheduler") return null;
391
+ return {
392
+ triggerNodeId: String(node.id || "").trim(),
393
+ triggerKind: String(node.config.triggerKind || node.config.trigger || "").trim(),
394
+ scheduleId: String(schedule.scheduleId || "").trim(),
395
+ schedulerRegistryId: String(schedule.schedulerRegistryId || "").trim(),
396
+ providerId: String(schedule.providerId || "").trim(),
397
+ productId: String(schedule.productId || "").trim(),
398
+ enabled: node.config.enabled !== false,
399
+ };
400
+ }
401
+
402
+ const SANDBOX_SCHEDULE_CLEAR_PATCH = {
403
+ scheduleId: "",
404
+ schedulerProviderId: "",
405
+ schedulerProductId: "",
406
+ schedulerRegion: "",
407
+ schedulerCron: "",
408
+ schedulerTriggerInput: "",
409
+ schedulerDestination: "",
410
+ schedulerCallbackUrl: "",
411
+ schedulerFailureCallbackUrl: "",
412
+ schedulerInstalledAt: "",
413
+ schedulerPaused: "",
414
+ schedulerPausedAt: "",
415
+ schedulerResumedAt: "",
416
+ };
417
+
418
+ /**
419
+ * Bind a sandbox/workflow ROW to a serverless schedule in a config object
420
+ * (pure). Schedule state lives on the OWNING ROW (not the global provider row),
421
+ * so multiple workflows can each own their own schedule. In ONE write it:
422
+ * - flips runLocality=serverless + schedulerRegistryId (+ adapter normalize),
423
+ * - records the row-level schedule proof (scheduleId, cron, destination, …),
424
+ * - syncs the orchestration trigger node so the graph logic matches.
425
+ * `clear:true` reverts the row to local + manual trigger (uninstall path).
426
+ * Returns { config, bound }.
427
+ */
428
+ function withWorkflowServerlessBind(workspaceConfig, params = {}) {
429
+ const { objectId, rowId, schedulerRegistryId, clear = false } = params;
430
+ const targetObject = String(objectId || "").trim();
431
+ const targetRow = String(rowId || "").trim();
432
+ if (!targetObject || !targetRow) return { config: workspaceConfig, bound: false };
433
+ if (!clear && !String(schedulerRegistryId || "").trim()) return { config: workspaceConfig, bound: false };
434
+ const dm = workspaceConfig?.dataModel && typeof workspaceConfig.dataModel === "object" ? workspaceConfig.dataModel : {};
435
+ const objects = Array.isArray(dm.objects) ? dm.objects : [];
436
+ let bound = false;
437
+ let liveField = "orchestrationConfig";
438
+ let triggerNodeId = null;
439
+ const nextObjects = objects.map((object) => {
440
+ if (object?.id !== targetObject || object?.objectType !== "sandbox-environment") return object;
441
+ const rows = Array.isArray(object.rows) ? object.rows : [];
442
+ const nextRows = rows.map((row) => {
443
+ if (String(row?.Name || "").trim() !== targetRow) return row;
444
+ bound = true;
445
+ const adapterId = String(row?.adapter || "").trim();
446
+ // Mutate the RUNTIME-LIVE graph field (precedence matches the runner:
447
+ // orchestrationGraph, else orchestrationConfig) — never the draft. We keep
448
+ // both live fields consistent when both are present.
449
+ liveField = liveGraphField(row);
450
+ const triggerMeta = {
451
+ schedulerRegistryId: String(schedulerRegistryId || "").trim(),
452
+ scheduleId: params.scheduleId || "",
453
+ cron: params.cron || "",
454
+ schedulerProviderId: params.schedulerProviderId || "",
455
+ schedulerProductId: params.schedulerProductId || "",
456
+ destinationUrl: params.destinationUrl || "",
457
+ callbackUrl: params.callbackUrl || "",
458
+ triggerInput: params.triggerInput || "",
459
+ };
460
+ const graphSync = syncTriggerNodeForSchedule(row.orchestrationGraph, triggerMeta, { clear });
461
+ const configSync = syncTriggerNodeForSchedule(row.orchestrationConfig, triggerMeta, { clear });
462
+ triggerNodeId = (liveField === "orchestrationGraph" ? graphSync.triggerNodeId : configSync.triggerNodeId)
463
+ || configSync.triggerNodeId || graphSync.triggerNodeId;
464
+ const base = { ...row, orchestrationGraph: graphSync.value, orchestrationConfig: configSync.value };
465
+ if (clear) {
466
+ return { ...base, runLocality: "local", ...SANDBOX_SCHEDULE_CLEAR_PATCH };
467
+ }
468
+ return {
469
+ ...base,
470
+ runLocality: "serverless",
471
+ schedulerRegistryId: triggerMeta.schedulerRegistryId,
472
+ adapter: SERVERLESS_LOCAL_ADAPTERS.includes(adapterId) ? "local-process" : (adapterId || "local-process"),
473
+ schedulerProviderId: triggerMeta.schedulerProviderId,
474
+ schedulerProductId: triggerMeta.schedulerProductId,
475
+ schedulerRegion: params.region || "",
476
+ scheduleId: triggerMeta.scheduleId,
477
+ schedulerCron: triggerMeta.cron,
478
+ schedulerTriggerInput: triggerMeta.triggerInput,
479
+ schedulerDestination: triggerMeta.destinationUrl,
480
+ schedulerCallbackUrl: triggerMeta.callbackUrl,
481
+ schedulerFailureCallbackUrl: params.failureCallbackUrl || "",
482
+ schedulerInstalledAt: params.installedAt || "",
483
+ };
484
+ });
485
+ return { ...object, rows: nextRows };
486
+ });
487
+ if (!bound) return { config: workspaceConfig, bound: false };
488
+ const changedFields = clear
489
+ ? [`${targetObject}.${targetRow}.runLocality`, `${targetObject}.${targetRow}.scheduleId`, `${targetObject}.${targetRow}.${liveField}.${triggerNodeId || "trigger"}`]
490
+ : [`${targetObject}.${targetRow}.runLocality`, `${targetObject}.${targetRow}.scheduleId`, `${targetObject}.${targetRow}.${liveField}.${triggerNodeId || "trigger"}`];
491
+ return { config: { ...workspaceConfig, dataModel: { ...dm, objects: nextObjects } }, bound: true, liveField, triggerNodeId, changedFields };
492
+ }
493
+
494
+ /**
495
+ * Resolve a sandbox/workflow row eligible for serverless scheduling. Used by
496
+ * the schedule route to validate BEFORE any remote provider call so we never
497
+ * create remote infrastructure for a row the workspace cannot bind.
498
+ */
499
+ function findEligibleSandboxRow(workspaceConfig, objectId, rowId) {
500
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
501
+ const object = objects.find((o) => o?.id === String(objectId || "").trim());
502
+ if (!object) return { ok: false, status: 404, error: `no object with id ${objectId}` };
503
+ if (object.objectType !== "sandbox-environment") return { ok: false, status: 409, error: `object ${objectId} is not a sandbox-environment` };
504
+ const row = (Array.isArray(object.rows) ? object.rows : []).find((r) => String(r?.Name || "").trim() === String(rowId || "").trim());
505
+ if (!row) return { ok: false, status: 404, error: `no workflow row ${rowId} in object ${objectId}` };
506
+ return { ok: true, object, row };
507
+ }
508
+
509
+ /** Find the sandbox row that owns a given scheduleId (callback → owning row). */
510
+ function findSandboxRowByScheduleId(workspaceConfig, scheduleId) {
511
+ const target = String(scheduleId || "").trim();
512
+ if (!target) return null;
513
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
514
+ for (const object of objects) {
515
+ if (object?.objectType !== "sandbox-environment") continue;
516
+ for (const row of Array.isArray(object.rows) ? object.rows : []) {
517
+ if (String(row?.scheduleId || "").trim() === target) return { objectId: object.id, object, row };
518
+ }
519
+ }
520
+ return null;
521
+ }
522
+
523
+ /** Merge scheduled-run proof onto the owning sandbox row (callback sync). */
524
+ function withSandboxScheduledRunProof(workspaceConfig, { objectId, rowId, patch } = {}) {
525
+ const targetObject = String(objectId || "").trim();
526
+ const targetRow = String(rowId || "").trim();
527
+ if (!targetObject || !targetRow) return { config: workspaceConfig, found: false };
528
+ const safe = patch && typeof patch === "object" ? patch : {};
529
+ const dm = workspaceConfig?.dataModel && typeof workspaceConfig.dataModel === "object" ? workspaceConfig.dataModel : {};
530
+ const objects = Array.isArray(dm.objects) ? dm.objects : [];
531
+ let found = false;
532
+ const nextObjects = objects.map((object) => {
533
+ if (object?.id !== targetObject || object?.objectType !== "sandbox-environment") return object;
534
+ const rows = Array.isArray(object.rows) ? object.rows : [];
535
+ const nextRows = rows.map((row) => {
536
+ if (String(row?.Name || "").trim() !== targetRow) return row;
537
+ found = true;
538
+ return { ...row, ...safe };
539
+ });
540
+ return { ...object, rows: nextRows };
541
+ });
542
+ if (!found) return { config: workspaceConfig, found: false };
543
+ return { config: { ...workspaceConfig, dataModel: { ...dm, objects: nextObjects } }, found: true };
544
+ }
545
+
546
+ /** Merge scheduler control state onto the owning sandbox row. */
547
+ function withSandboxSchedulerControlState(workspaceConfig, { objectId, rowId, patch } = {}) {
548
+ return withSandboxScheduledRunProof(workspaceConfig, { objectId, rowId, patch });
549
+ }
550
+
551
+ function findRegistryRowByIntegrationId(workspaceConfig, integrationId) {
552
+ const targetId = String(integrationId || "").trim();
553
+ if (!targetId) return null;
554
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
555
+ for (const object of objects) {
556
+ if (!isApiRegistryObject(object)) continue;
557
+ for (const row of Array.isArray(object.rows) ? object.rows : []) {
558
+ if (String(row?.integrationId || "").trim() === targetId) return row;
559
+ }
560
+ }
561
+ return null;
562
+ }
563
+
564
+ function isApiRegistryObject(object) {
565
+ const objectType = String(object?.objectType || "").trim();
566
+ const id = String(object?.id || object?.objectId || "").trim();
567
+ return objectType === "api-registry" || id === "api-registry";
568
+ }
569
+
570
+ /**
571
+ * Per-provider product readiness keyed by providerId — the exact `envSignals`
572
+ * shape the Add-ons settings client consumes (`providerProductReadiness`).
573
+ * Centralizing it here keeps the server page and the client contract in lockstep
574
+ * (regression-tested) so per-product install state actually renders.
575
+ */
576
+ function listAllProviderProductReadiness(env = process.env) {
577
+ const out = {};
578
+ for (const provider of MARKETPLACE_PROVIDERS) {
579
+ out[provider.providerId] = listProviderProductReadiness(provider.providerId, env);
580
+ }
581
+ return out;
582
+ }
583
+
584
+ function getMarketplaceProvider(providerId) {
585
+ return MARKETPLACE_PROVIDERS.find((provider) => provider.providerId === providerId || provider.integrationId === providerId) || null;
586
+ }
587
+
588
+ function listMarketplaceProducts() {
589
+ return MARKETPLACE_PROVIDERS.flatMap((provider) => provider.products.map((product) => ({ ...product, providerId: provider.providerId })));
590
+ }
591
+
592
+ function getMarketplaceProduct(providerId, productId) {
593
+ const provider = getMarketplaceProvider(providerId);
594
+ if (!provider) return null;
595
+ return provider.products.find((product) => product.productId === productId || product.integrationId === productId) || null;
596
+ }
597
+
598
+ function makeMarketplaceProviderRow(providerId, { syncResult = null } = {}) {
599
+ const provider = getMarketplaceProvider(providerId);
600
+ if (!provider) return null;
601
+ const testedAt = syncResult?.testedAt || "";
602
+ const isConnected = syncResult?.ok === true;
603
+ // A live account probe yields `verified`. Do not treat a console-open event
604
+ // as a connected account; the UI must only show Manage after real provider
605
+ // account metadata or a verified probe is persisted.
606
+ const syncStatus = syncResult?.syncStatus || (isConnected ? "verified" : "setup-required");
607
+ const status = syncResult?.status || (isConnected ? "connected" : "draft");
608
+ return {
609
+ Name: provider.label,
610
+ integrationId: provider.integrationId,
611
+ authRef: provider.authRef,
612
+ requiredEnv: provider.accountProbe?.emailEnv && provider.accountProbe?.keyEnv
613
+ ? [provider.accountProbe.emailEnv, provider.accountProbe.keyEnv].join(",")
614
+ : "",
615
+ optionalEnv: "",
616
+ resolvedEnv: Array.isArray(syncResult?.resolvedEnv) ? syncResult.resolvedEnv.join(",") : "",
617
+ baseUrl: provider.baseUrl,
618
+ endpoint: provider.endpoint,
619
+ method: provider.method,
620
+ status,
621
+ lastTested: testedAt,
622
+ lastResponse: syncResult?.summary || `Connect a ${provider.label} provider account before installing workspace products.`,
623
+ entityTypes: provider.entityTypes,
624
+ description: provider.description,
625
+ connectorKind: provider.connectorKind,
626
+ resolverTemplateId: "",
627
+ schemaVersion: "growthub-marketplace-provider-v1",
628
+ capabilities: provider.capabilities,
629
+ executionLane: provider.executionLane,
630
+ region: "",
631
+ productId: "",
632
+ plan: "",
633
+ syncStatus,
634
+ syncCheckedAt: testedAt,
635
+ syncProof: syncResult?.proof || "",
636
+ missingEnv: Array.isArray(syncResult?.missingEnv) ? syncResult.missingEnv.join(",") : "",
637
+ providerAccountRequiredEnv: provider.accountProbe?.emailEnv && provider.accountProbe?.keyEnv
638
+ ? [provider.accountProbe.emailEnv, provider.accountProbe.keyEnv].join(",")
639
+ : "",
640
+ providerAccountOptions: Array.isArray(syncResult?.providerAccountOptions) ? JSON.stringify(syncResult.providerAccountOptions) : "",
641
+ selectedProviderAccountId: syncResult?.selectedProviderAccountId || "",
642
+ selectedProviderAccountLabel: syncResult?.selectedProviderAccountLabel || "",
643
+ providerAccountSource: syncResult?.providerAccountSource || "",
644
+ };
645
+ }
646
+
647
+ function makeUpstashProviderRow(options = {}) {
648
+ return makeMarketplaceProviderRow("upstash", options);
649
+ }
650
+
651
+ function getUpstashProduct(productId) {
652
+ return getMarketplaceProduct("upstash", productId);
653
+ }
654
+
655
+ function listProviderProductReadiness(providerId, env = process.env) {
656
+ const provider = getMarketplaceProvider(providerId);
657
+ if (!provider) return [];
658
+ const source = env && typeof env === "object" ? env : {};
659
+ return provider.products.map((product) => {
660
+ // Use the canonical readEnvVar so product readiness and schedule-runtime
661
+ // resolution share one env-key contract (concrete UPPER_SNAKE keys).
662
+ const missingEnv = product.requiredEnv.filter((key) => !readEnvVar(key, source));
663
+ const configuredOptionalEnv = product.optionalEnv.filter((key) => Boolean(readEnvVar(key, source)));
664
+ return {
665
+ productId: product.productId,
666
+ integrationId: product.integrationId,
667
+ label: product.label,
668
+ authRef: product.authRef,
669
+ requiredEnv: product.requiredEnv,
670
+ optionalEnv: product.optionalEnv,
671
+ configured: missingEnv.length === 0,
672
+ missingEnv,
673
+ configuredOptionalEnv,
674
+ };
675
+ });
676
+ }
677
+
678
+ function listUpstashProductReadiness(env = process.env) {
679
+ return listProviderProductReadiness("upstash", env);
680
+ }
681
+
682
+ function makeUpstashProductRow({ productId, region, plan = "free", syncResult = null, authReady = false }) {
683
+ const product = getUpstashProduct(productId) || getMarketplaceProduct("upstash", "upstash-qstash");
684
+ const selectedRegion = UPSTASH_REGION_OPTIONS.find((option) => option.id === region) || UPSTASH_REGION_OPTIONS[0];
685
+ const baseUrl = product.productId === "upstash-qstash" ? selectedRegion.baseUrl : syncResult?.baseUrl || "";
686
+ const testedAt = syncResult?.testedAt || "";
687
+ const isConnected = syncResult?.ok === true || authReady;
688
+ const status = syncResult?.status || (isConnected ? "connected" : "draft");
689
+ const syncStatus = syncResult?.syncStatus || (isConnected ? "verified" : "missing-env");
690
+ return {
691
+ Name: product.label,
692
+ integrationId: product.integrationId,
693
+ authRef: product.authRef,
694
+ requiredEnv: Array.isArray(product.requiredEnv) ? product.requiredEnv.join(",") : "",
695
+ optionalEnv: Array.isArray(product.optionalEnv) ? product.optionalEnv.join(",") : "",
696
+ resolvedEnv: Array.isArray(syncResult?.resolvedEnv) ? syncResult.resolvedEnv.join(",") : "",
697
+ selectedResourceId: syncResult?.selectedResourceId || "",
698
+ selectedResourceLabel: syncResult?.selectedResourceLabel || "",
699
+ selectedResourceSource: syncResult?.selectedResourceSource || "",
700
+ baseUrl,
701
+ endpoint: product.endpoint,
702
+ method: product.method,
703
+ status,
704
+ lastTested: testedAt || (authReady ? "env-ready" : ""),
705
+ lastResponse: syncResult?.summary || (authReady
706
+ ? `${product.label} env ref resolves in this runtime.`
707
+ : `Complete ${product.label} provider setup, then retry sync.`),
708
+ entityTypes: product.entityTypes,
709
+ description: product.description,
710
+ connectorKind: product.connectorKind,
711
+ resolverTemplateId: "",
712
+ schemaVersion: "growthub-marketplace-upstash-v1",
713
+ capabilities: product.capabilities,
714
+ executionLane: product.executionLane,
715
+ region: product.productId === "upstash-qstash" ? selectedRegion.id : "",
716
+ productId: product.productId,
717
+ plan,
718
+ syncStatus,
719
+ syncCheckedAt: testedAt,
720
+ syncProof: syncResult?.proof || "",
721
+ missingEnv: Array.isArray(syncResult?.missingEnv) ? syncResult.missingEnv.join(",") : "",
722
+ };
723
+ }
724
+
725
+ function makeUpstashSchedulerRow({ region, authReady }) {
726
+ return makeUpstashProductRow({ productId: "upstash-qstash", region, authReady });
727
+ }
728
+
729
+ function withUpstashProductRegistry(workspaceConfig, { productId = "upstash-qstash", region = "us-east-1", plan = "free", syncResult = null, authReady = false } = {}) {
730
+ const dm = workspaceConfig?.dataModel && typeof workspaceConfig.dataModel === "object" ? workspaceConfig.dataModel : {};
731
+ const objects = Array.isArray(dm.objects) ? dm.objects : [];
732
+ const product = getUpstashProduct(productId) || getUpstashProduct("upstash-qstash");
733
+ const productRow = makeUpstashProductRow({ productId: product.productId, region, plan, syncResult, authReady });
734
+ let found = false;
735
+ const nextObjects = objects.map((object) => {
736
+ if (!isApiRegistryObject(object) || found) return object;
737
+ found = true;
738
+ const rows = Array.isArray(object.rows) ? object.rows : [];
739
+ const hasRow = rows.some((row) => String(row?.integrationId || "").trim() === product.integrationId);
740
+ return {
741
+ ...object,
742
+ columns: apiRegistryColumns(object.columns),
743
+ rows: hasRow
744
+ ? rows.map((row) => String(row?.integrationId || "").trim() === product.integrationId ? { ...row, ...productRow } : row)
745
+ : [productRow, ...rows],
746
+ };
747
+ });
748
+ if (!found) {
749
+ nextObjects.push({
750
+ id: "api-registry",
751
+ label: "API Registry",
752
+ name: "API Registry",
753
+ source: "API Registry",
754
+ objectType: "api-registry",
755
+ icon: "Code2",
756
+ columns: apiRegistryColumns(),
757
+ rows: [productRow],
758
+ binding: { mode: "manual", source: "API Registry" },
759
+ relations: [],
760
+ });
761
+ }
762
+ return { ...workspaceConfig, dataModel: { ...dm, objects: nextObjects } };
763
+ }
764
+
765
+ function withMarketplaceProductRegistry(workspaceConfig, { providerId, productId, region = "us-east-1", plan = "free", syncResult = null, authReady = false } = {}) {
766
+ if (providerId === "upstash") {
767
+ return withUpstashProductRegistry(workspaceConfig, { productId, region, plan, syncResult, authReady });
768
+ }
769
+ return workspaceConfig;
770
+ }
771
+
772
+ function withMarketplaceProviderRegistry(workspaceConfig, { providerId, syncResult = null } = {}) {
773
+ const dm = workspaceConfig?.dataModel && typeof workspaceConfig.dataModel === "object" ? workspaceConfig.dataModel : {};
774
+ const objects = Array.isArray(dm.objects) ? dm.objects : [];
775
+ const provider = getMarketplaceProvider(providerId);
776
+ const providerRow = makeMarketplaceProviderRow(providerId, { syncResult });
777
+ if (!provider || !providerRow) return workspaceConfig;
778
+ let found = false;
779
+ const nextObjects = objects.map((object) => {
780
+ if (!isApiRegistryObject(object) || found) return object;
781
+ found = true;
782
+ const rows = Array.isArray(object.rows) ? object.rows : [];
783
+ const hasRow = rows.some((row) => String(row?.integrationId || "").trim() === provider.integrationId);
784
+ return {
785
+ ...object,
786
+ columns: apiRegistryColumns(object.columns),
787
+ rows: hasRow
788
+ ? rows.map((row) => String(row?.integrationId || "").trim() === provider.integrationId ? { ...row, ...providerRow } : row)
789
+ : [providerRow, ...rows],
790
+ };
791
+ });
792
+ if (!found) {
793
+ nextObjects.push({
794
+ id: "api-registry",
795
+ label: "API Registry",
796
+ name: "API Registry",
797
+ source: "API Registry",
798
+ objectType: "api-registry",
799
+ icon: "Code2",
800
+ columns: apiRegistryColumns(),
801
+ rows: [providerRow],
802
+ binding: { mode: "manual", source: "API Registry" },
803
+ relations: [],
804
+ });
805
+ }
806
+ return { ...workspaceConfig, dataModel: { ...dm, objects: nextObjects } };
807
+ }
808
+
809
+ function withUpstashProviderRegistry(workspaceConfig, options = {}) {
810
+ return withMarketplaceProviderRegistry(workspaceConfig, { providerId: "upstash", ...options });
811
+ }
812
+
813
+ function withUpstashSchedulerRegistry(workspaceConfig, { region = "us-east-1", authReady = false } = {}) {
814
+ return withUpstashProductRegistry(workspaceConfig, { productId: "upstash-qstash", region, authReady });
815
+ }
816
+
817
+ function findMarketplaceProviderRow(workspaceConfig, providerId) {
818
+ const provider = getMarketplaceProvider(providerId);
819
+ if (!provider) return null;
820
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
821
+ for (const object of objects) {
822
+ if (!isApiRegistryObject(object)) continue;
823
+ for (const row of Array.isArray(object.rows) ? object.rows : []) {
824
+ if (String(row?.integrationId || "").trim() === provider.integrationId) {
825
+ const syncStatus = String(row?.syncStatus || "").trim();
826
+ const status = String(row?.status || "").trim();
827
+ const verified = Boolean(syncStatus === "verified"
828
+ && String(row?.syncProof || "").trim()
829
+ && String(row?.syncCheckedAt || "").trim());
830
+ let accountOptions = [];
831
+ if (typeof row?.providerAccountOptions === "string" && row.providerAccountOptions.trim()) {
832
+ try {
833
+ const parsed = JSON.parse(row.providerAccountOptions);
834
+ if (Array.isArray(parsed)) accountOptions = parsed;
835
+ } catch {
836
+ accountOptions = [];
837
+ }
838
+ } else if (Array.isArray(row?.providerAccountOptions)) {
839
+ accountOptions = row.providerAccountOptions;
840
+ }
841
+ const linked = Boolean(verified);
842
+ const setupPending = syncStatus === "setup-pending"
843
+ || syncStatus === "setup-opened"
844
+ || status === "setup-pending"
845
+ || status === "setup-opened";
846
+ return {
847
+ ...row,
848
+ isConnectedProvider: linked,
849
+ isSetupPendingProvider: setupPending,
850
+ isVerifiedProvider: verified,
851
+ };
852
+ }
853
+ }
854
+ }
855
+ return null;
856
+ }
857
+
858
+ function findUpstashProviderRow(workspaceConfig) {
859
+ return findMarketplaceProviderRow(workspaceConfig, "upstash");
860
+ }
861
+
862
+ function findInstalledWorkspaceAddOns(workspaceConfig) {
863
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
864
+ const products = listMarketplaceProducts();
865
+ const rows = [];
866
+ for (const object of objects) {
867
+ if (!isApiRegistryObject(object)) continue;
868
+ for (const row of Array.isArray(object.rows) ? object.rows : []) {
869
+ const product = products.find((item) => item.integrationId === String(row?.integrationId || "").trim());
870
+ if (product) {
871
+ const verified = Boolean(String(row?.syncStatus || "").trim() === "verified"
872
+ && String(row?.syncProof || "").trim()
873
+ && String(row?.syncCheckedAt || "").trim());
874
+ if (verified) rows.push({ ...row, productId: product.productId });
875
+ }
876
+ }
877
+ }
878
+ return rows;
879
+ }
880
+
881
+ function findWorkspaceAddOnRows(workspaceConfig) {
882
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
883
+ const products = listMarketplaceProducts();
884
+ const rows = [];
885
+ for (const object of objects) {
886
+ if (!isApiRegistryObject(object)) continue;
887
+ for (const row of Array.isArray(object.rows) ? object.rows : []) {
888
+ const product = products.find((item) => item.integrationId === String(row?.integrationId || "").trim());
889
+ if (product) {
890
+ const verified = Boolean(String(row?.syncStatus || "").trim() === "verified"
891
+ && String(row?.syncProof || "").trim()
892
+ && String(row?.syncCheckedAt || "").trim());
893
+ rows.push({ ...row, productId: product.productId, isVerifiedAddOn: verified });
894
+ }
895
+ }
896
+ }
897
+ return rows;
898
+ }
899
+
900
+ function deriveWorkspaceAddOnsState(workspaceConfig) {
901
+ const installed = findInstalledWorkspaceAddOns(workspaceConfig);
902
+ const upstashProvider = findUpstashProviderRow(workspaceConfig);
903
+ const qstashWorkflow = installed.find((row) => row.productId === "upstash-qstash") || null;
904
+ // Capability = the QStash product is installed + verified (read-probe). That
905
+ // is what lets the canvas OFFER a bind; the per-workflow schedule itself is
906
+ // created on bind and stored on the owning sandbox row, not here.
907
+ const qstashScheduler = qstashWorkflow;
908
+ return {
909
+ kind: "growthub-workspace-add-ons-state-v1",
910
+ upstashProvider,
911
+ hasUpstashProvider: Boolean(upstashProvider?.isConnectedProvider),
912
+ installed,
913
+ hasQstashWorkflow: Boolean(qstashWorkflow),
914
+ qstashWorkflow,
915
+ qstashScheduler,
916
+ hasQstashSchedulerCapability: Boolean(qstashWorkflow),
917
+ };
918
+ }
919
+
920
+ export {
921
+ MARKETPLACE_PROVIDERS,
922
+ UPSTASH_AUTH_REF,
923
+ UPSTASH_PRODUCTS,
924
+ UPSTASH_PROVIDER_INTEGRATION_ID,
925
+ UPSTASH_QSTASH_INTEGRATION_ID,
926
+ UPSTASH_REGION_OPTIONS,
927
+ deriveWorkspaceAddOnsState,
928
+ findMarketplaceProviderRow,
929
+ findUpstashProviderRow,
930
+ findInstalledWorkspaceAddOns,
931
+ findWorkspaceAddOnRows,
932
+ getMarketplaceProvider,
933
+ getMarketplaceProduct,
934
+ getUpstashProduct,
935
+ findRegistryRowByIntegrationId,
936
+ findEligibleSandboxRow,
937
+ findSandboxRowByScheduleId,
938
+ withSandboxScheduledRunProof,
939
+ withSandboxSchedulerControlState,
940
+ syncTriggerNodeForSchedule,
941
+ readTriggerScheduleBinding,
942
+ liveGraphField,
943
+ listAllProviderProductReadiness,
944
+ listMarketplaceProducts,
945
+ listProviderProductReadiness,
946
+ listUpstashProductReadiness,
947
+ withWorkflowServerlessBind,
948
+ makeMarketplaceProviderRow,
949
+ makeUpstashProductRow,
950
+ makeUpstashProviderRow,
951
+ makeUpstashSchedulerRow,
952
+ withMarketplaceProductRegistry,
953
+ withMarketplaceProviderRegistry,
954
+ withUpstashProductRegistry,
955
+ withUpstashProviderRegistry,
956
+ withUpstashSchedulerRegistry,
957
+ };