@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.
- 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/metadata-graph/route.js +1 -49
- 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 +2 -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-config.js +607 -63
- 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/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/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
|
+
};
|