@growthub/cli 0.14.9 → 0.14.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/callback/route.js +35 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/failure/route.js +35 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/schedule/route.js +423 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/connect/route.js +78 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/credentials/route.js +276 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/products/[productId]/resources/route.js +173 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/products/sync/route.js +347 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/sync/route.js +293 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/provider/connect/route.js +7 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/provider/sync/route.js +7 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/sync/route.js +197 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/apps/route.js +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/patch/preflight/route.js +38 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +3 -20
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-api-record/route.js +3 -20
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflow/publish/route.js +407 -290
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflows/[providerId]/route.js +209 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceAddOnsMarketplace.jsx +806 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +141 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/CeoCockpit.jsx +15 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +42 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +5 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +86 -20
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ScheduleCockpit.jsx +363 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +8 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +322 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/add-ons/add-ons-client.jsx +197 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/add-ons/page.jsx +23 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/settings-shell.jsx +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +734 -61
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +15 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +2 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +29 -19
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +8 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/schedule-cockpit-console.js +287 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/scheduler-orchestration.js +449 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-secrets.js +77 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-readiness.js +583 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-on-callback.js +63 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-on-scheduler.js +519 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-ons.js +957 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-app-readiness.js +212 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +607 -63
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-contract-compliance.js +168 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +21 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-operator-auth.js +32 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-patch-impact.js +133 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-provenance-lineage.js +214 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-stale-surfaces.js +217 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-workflow-impact.js +170 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/provider.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/qstash.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/redis.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/search.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/vector.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/scripts/scheduler-ingress-smoke.mjs +26 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +6 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/skills/governed-workspace-mutation/SKILL.md +3 -1
- package/dist/index.js +3024 -4191
- package/package.json +1 -1
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler route ORCHESTRATION cores — dependency-injected so the full state
|
|
3
|
+
* machine (create schedule → persist row/trigger → rollback → callback sync →
|
|
4
|
+
* persist-failure) is testable offline with stubs, without the Next runtime.
|
|
5
|
+
*
|
|
6
|
+
* The route files are thin wrappers that inject real deps (fetch, workspace
|
|
7
|
+
* config read/write, outcome receipts) and translate `{ status, body }` into a
|
|
8
|
+
* NextResponse. Everything provider-specific is delegated to the scheduler
|
|
9
|
+
* adapter; everything graph/row-specific to the pure workspace-add-ons helpers.
|
|
10
|
+
*
|
|
11
|
+
* `deps`:
|
|
12
|
+
* { fetchImpl, readConfig, writeConfig, appendReceipt, env, now }
|
|
13
|
+
* — all injected so no module here imports `next/server`, `process.env`, the
|
|
14
|
+
* filesystem persistence layer, or anything `@/`-aliased. That is what makes
|
|
15
|
+
* `node --test` able to import and exercise these cores directly.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
getMarketplaceProvider,
|
|
20
|
+
getMarketplaceProduct,
|
|
21
|
+
findEligibleSandboxRow,
|
|
22
|
+
findSandboxRowByScheduleId,
|
|
23
|
+
withWorkflowServerlessBind,
|
|
24
|
+
withSandboxScheduledRunProof,
|
|
25
|
+
readTriggerScheduleBinding,
|
|
26
|
+
liveGraphField,
|
|
27
|
+
} from "./workspace-add-ons.js";
|
|
28
|
+
import {
|
|
29
|
+
getSchedulerAdapter,
|
|
30
|
+
isSchedulerProduct,
|
|
31
|
+
deriveScheduleId,
|
|
32
|
+
resolveWorkspacePublicUrl,
|
|
33
|
+
buildSchedulerCallbackUrls,
|
|
34
|
+
} from "./workspace-add-on-scheduler.js";
|
|
35
|
+
import { readEnvVar, resolveRequiredEnv } from "./server-secrets.js";
|
|
36
|
+
import { scanServerlessReadiness, READINESS_KIND } from "./serverless-readiness.js";
|
|
37
|
+
|
|
38
|
+
const SCHEDULE_TIMEOUT_MS = 10000;
|
|
39
|
+
|
|
40
|
+
function clean(value) {
|
|
41
|
+
return String(value == null ? "" : value).trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function err(status, error, extra = {}) {
|
|
45
|
+
return { status, body: { ok: false, error, ...extra } };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveSchedulerProduct(provider, productId) {
|
|
49
|
+
if (productId) return getMarketplaceProduct(provider.providerId, productId);
|
|
50
|
+
return (provider.products || []).find((p) => isSchedulerProduct(p)) || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isApiRegistryObject(object) {
|
|
54
|
+
const objectType = String(object?.objectType || "").trim();
|
|
55
|
+
const id = String(object?.id || object?.objectId || "").trim();
|
|
56
|
+
return objectType === "api-registry" || id === "api-registry";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function fetchWithTimeout(fetchImpl, url, init = {}) {
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
const timer = setTimeout(() => controller.abort(), SCHEDULE_TIMEOUT_MS);
|
|
62
|
+
try {
|
|
63
|
+
return await fetchImpl(url, { ...init, signal: controller.signal, cache: "no-store" });
|
|
64
|
+
} finally {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ------------------------------------------------------------------ *
|
|
70
|
+
* Install (create/upsert) a per-workflow schedule + bind the row. *
|
|
71
|
+
* ------------------------------------------------------------------ */
|
|
72
|
+
async function runScheduleInstall(deps, { providerId, body = {}, requestOrigin = "" } = {}) {
|
|
73
|
+
const { fetchImpl, readConfig, writeConfig, appendReceipt, env } = deps;
|
|
74
|
+
const now = (deps.now || (() => new Date().toISOString()))();
|
|
75
|
+
|
|
76
|
+
const provider = getMarketplaceProvider(clean(providerId));
|
|
77
|
+
if (!provider) return err(404, "unknown marketplace provider", { providerId });
|
|
78
|
+
const product = resolveSchedulerProduct(provider, clean(body.productId));
|
|
79
|
+
if (!product || !isSchedulerProduct(product)) return err(400, "provider has no serverless scheduler product", { providerId: provider.providerId });
|
|
80
|
+
const adapter = getSchedulerAdapter(product);
|
|
81
|
+
|
|
82
|
+
const config = await readConfig();
|
|
83
|
+
// Capability gate: product must be installed + verified (read-probe) first.
|
|
84
|
+
const objects = Array.isArray(config?.dataModel?.objects) ? config.dataModel.objects : [];
|
|
85
|
+
const installedRow = objects.flatMap((o) => (isApiRegistryObject(o) ? (o.rows || []) : [])).find((r) => clean(r?.integrationId) === product.integrationId);
|
|
86
|
+
if (!installedRow || clean(installedRow.syncStatus) !== "verified") {
|
|
87
|
+
return err(409, `${product.label} must be installed and verified before scheduling`, { productId: product.productId, nextActions: [`Sync ${product.label} from Workspace Add-ons, then create the schedule.`] });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const requiredEnv = resolveRequiredEnv(product.requiredEnv, env);
|
|
91
|
+
const tokenEnv = product.probe?.tokenEnv || (product.requiredEnv || [])[0];
|
|
92
|
+
const token = readEnvVar(tokenEnv, env)?.value || "";
|
|
93
|
+
if (!requiredEnv.ok || !token) {
|
|
94
|
+
return err(422, `${product.label} runtime credentials are not connected`, { productId: product.productId, missingEnv: requiredEnv.missing });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const objectId = clean(body.objectId);
|
|
98
|
+
const rowId = clean(body.rowId || body.name);
|
|
99
|
+
if (!objectId || !rowId) return err(400, "objectId and rowId (workflow row) are required");
|
|
100
|
+
|
|
101
|
+
// Validate the row BEFORE any remote provider call.
|
|
102
|
+
const eligible = findEligibleSandboxRow(config, objectId, rowId);
|
|
103
|
+
if (!eligible.ok) return err(eligible.status, eligible.error, { providerId: provider.providerId, productId: product.productId });
|
|
104
|
+
const targetRow = eligible.row;
|
|
105
|
+
|
|
106
|
+
// Causality gate: prove the WHOLE downstream graph is serverless-ready BEFORE
|
|
107
|
+
// any remote schedule is created or the row is bound. A scheduler install
|
|
108
|
+
// succeeding only proves the binding; it does not prove every downstream node
|
|
109
|
+
// can run with no human/local agent state and that all API Registry deps
|
|
110
|
+
// resolve through server-side env refs. If not clean, emit a draft readiness
|
|
111
|
+
// delta (blocked receipt) and refuse — the published graph stays unchanged and
|
|
112
|
+
// no remote infrastructure is created. (Caller may pass `force:true` only when
|
|
113
|
+
// an operator has acknowledged warnings; blocking nodes are never forceable.)
|
|
114
|
+
const readiness = scanServerlessReadiness({
|
|
115
|
+
row: targetRow,
|
|
116
|
+
workspaceConfig: config,
|
|
117
|
+
env,
|
|
118
|
+
phase: "pre-bind",
|
|
119
|
+
expected: {
|
|
120
|
+
schedulerRegistryId: product.integrationId,
|
|
121
|
+
providerId: provider.providerId,
|
|
122
|
+
productId: product.productId,
|
|
123
|
+
triggerInput: clean(body.triggerInput),
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
if (!readiness.ok) {
|
|
127
|
+
await appendReceipt({
|
|
128
|
+
kind: READINESS_KIND,
|
|
129
|
+
lane: "server-authoritative",
|
|
130
|
+
outcomeStatus: "blocked",
|
|
131
|
+
actor: "workspace-marketplace",
|
|
132
|
+
objectRefs: [{ objectId, objectType: "sandbox-environment", rowName: rowId }],
|
|
133
|
+
policyVerdict: { ok: false, violationCodes: readiness.deltaTags },
|
|
134
|
+
summary: `${product.label} schedule bind blocked: ${readiness.blockingNodes.length} downstream node(s) are not serverless-ready (${readiness.blockingNodes.map((n) => n.nodeId || n.nodeType).join(", ")}).`,
|
|
135
|
+
nextActions: readiness.blockingNodes.map((n) => n.helperAction).filter(Boolean),
|
|
136
|
+
});
|
|
137
|
+
return err(422, "workflow graph is not serverless-ready", { providerId: provider.providerId, productId: product.productId, readiness });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const region = clean(body.region || installedRow.region || "us-east-1");
|
|
141
|
+
const cron = clean(body.cron || "0 * * * *");
|
|
142
|
+
const version = clean(body.version || targetRow.version || "v1");
|
|
143
|
+
const workspaceId = clean(body.workspaceId || config?.id || "workspace");
|
|
144
|
+
|
|
145
|
+
const explicitPublicBaseUrl = clean(body.publicBaseUrl).replace(/\/+$/, "");
|
|
146
|
+
const baseUrl = explicitPublicBaseUrl || resolveWorkspacePublicUrl(env, requestOrigin);
|
|
147
|
+
if (!baseUrl) return err(422, "could not resolve a public workspace URL for callbacks", { nextActions: ["Set GROWTHUB_WORKSPACE_PUBLIC_URL to the deployed workspace origin, then retry."] });
|
|
148
|
+
const { destinationUrl, callbackUrl, failureCallbackUrl } = buildSchedulerCallbackUrls(baseUrl, provider.providerId);
|
|
149
|
+
const scheduleId = deriveScheduleId({ providerId: provider.providerId, workspaceId, objectId, rowId, version });
|
|
150
|
+
|
|
151
|
+
// Replace semantics: delete a prior DIFFERENT schedule before creating a new one.
|
|
152
|
+
const priorScheduleId = clean(targetRow.scheduleId);
|
|
153
|
+
if (priorScheduleId && priorScheduleId !== scheduleId) {
|
|
154
|
+
let oldDeleted = false;
|
|
155
|
+
let detail = "";
|
|
156
|
+
try {
|
|
157
|
+
const del = adapter.buildDeleteRequest({ product, region, token, scheduleId: priorScheduleId, env });
|
|
158
|
+
const r = await fetchWithTimeout(fetchImpl, del.url, { method: del.method, headers: del.headers });
|
|
159
|
+
oldDeleted = r.ok || r.status === 404;
|
|
160
|
+
detail = `HTTP ${r.status}`;
|
|
161
|
+
} catch (e) { detail = e?.message || "network error"; }
|
|
162
|
+
if (!oldDeleted) {
|
|
163
|
+
await appendReceipt({ kind: "workspace-add-on-schedule", lane: "server-authoritative", outcomeStatus: "failed", actor: "workspace-marketplace", objectRefs: [{ objectId, objectType: "sandbox-environment", rowName: rowId }], policyVerdict: { ok: false, violationCodes: ["scheduler_replace_old_delete_failed"] }, summary: `Could not delete prior ${product.label} schedule ${priorScheduleId} (${detail}); refusing to create ${scheduleId}.` });
|
|
164
|
+
return err(409, `could not replace prior schedule ${priorScheduleId} (${detail})`, { priorScheduleId });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Create the remote schedule.
|
|
169
|
+
let scheduleRequest;
|
|
170
|
+
try {
|
|
171
|
+
scheduleRequest = adapter.buildScheduleRequest({ product, region, token, scheduleId, cron, destinationUrl, callbackUrl, failureCallbackUrl, forward: { workspaceId, objectId, rowId, version, scheduleId, triggerInput: clean(body.triggerInput) }, env });
|
|
172
|
+
} catch (e) {
|
|
173
|
+
return err(400, e?.message || "could not build schedule request", { providerId: provider.providerId });
|
|
174
|
+
}
|
|
175
|
+
let syncResult;
|
|
176
|
+
try {
|
|
177
|
+
const response = await fetchWithTimeout(fetchImpl, scheduleRequest.url, { method: scheduleRequest.method, headers: scheduleRequest.headers, body: scheduleRequest.body });
|
|
178
|
+
syncResult = adapter.parseScheduleResponse({ status: response.status, body: await response.text(), scheduleId });
|
|
179
|
+
} catch (e) {
|
|
180
|
+
syncResult = { ok: false, scheduleId, proof: `schedule request failed: ${e?.message || "network error"}` };
|
|
181
|
+
}
|
|
182
|
+
if (!syncResult.ok) {
|
|
183
|
+
await appendReceipt({ kind: "workspace-add-on-schedule", lane: "server-authoritative", outcomeStatus: "blocked", actor: "workspace-marketplace", objectRefs: [{ objectId, objectType: "sandbox-environment", rowName: rowId }], policyVerdict: { ok: false, violationCodes: ["scheduler_create_failed"] }, summary: syncResult.proof || `${product.label} schedule create failed` });
|
|
184
|
+
return err(502, syncResult.proof || "schedule create failed", { providerId: provider.providerId, productId: product.productId });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ONE write: bind the owning row + sync its live trigger node.
|
|
188
|
+
const { config: nextConfig, bound, liveField, triggerNodeId, changedFields } = withWorkflowServerlessBind(config, {
|
|
189
|
+
objectId, rowId, schedulerRegistryId: product.integrationId, schedulerProviderId: provider.providerId, schedulerProductId: product.productId,
|
|
190
|
+
region, scheduleId: syncResult.scheduleId, cron, triggerInput: clean(body.triggerInput), destinationUrl, callbackUrl, failureCallbackUrl, installedAt: now,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const rollbackRemote = async () => {
|
|
194
|
+
try {
|
|
195
|
+
const del = adapter.buildDeleteRequest({ product, region, token, scheduleId: syncResult.scheduleId, env });
|
|
196
|
+
const r = await fetchWithTimeout(fetchImpl, del.url, { method: del.method, headers: del.headers });
|
|
197
|
+
return r.ok || r.status === 404;
|
|
198
|
+
} catch { return false; }
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
if (!bound) {
|
|
202
|
+
await rollbackRemote();
|
|
203
|
+
await appendReceipt({ kind: "workspace-add-on-schedule", lane: "server-authoritative", outcomeStatus: "failed", actor: "workspace-marketplace", objectRefs: [{ objectId, objectType: "sandbox-environment", rowName: rowId }], policyVerdict: { ok: false, violationCodes: ["scheduler_bind_failed"] }, summary: `${product.label} schedule ${syncResult.scheduleId} rolled back: workflow row ${rowId} could not be bound.` });
|
|
204
|
+
return err(409, `could not bind workflow row ${rowId}; remote schedule rolled back`, { providerId: provider.providerId, productId: product.productId });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let persisted;
|
|
208
|
+
try {
|
|
209
|
+
persisted = await writeConfig({ dataModel: nextConfig.dataModel });
|
|
210
|
+
} catch (writeErr) {
|
|
211
|
+
const cleanedUp = await rollbackRemote();
|
|
212
|
+
await appendReceipt({ kind: "workspace-add-on-schedule", lane: "server-authoritative", outcomeStatus: cleanedUp ? "failed" : "blocked", actor: "workspace-marketplace", objectRefs: [{ objectId, objectType: "sandbox-environment", rowName: rowId }], policyVerdict: { ok: false, violationCodes: [cleanedUp ? "schedule_rolled_back_persist_failed" : "schedule_orphaned"] }, summary: cleanedUp ? `${product.label} schedule create rolled back: workspace persistence failed (${clean(writeErr?.code) || "write error"}).` : `${product.label} schedule ${syncResult.scheduleId} is ORPHANED: persistence AND cleanup failed.` });
|
|
213
|
+
return err(424, cleanedUp ? "schedule rolled back: workspace could not persist the install" : `schedule ${syncResult.scheduleId} orphaned (persist + cleanup failed)`, { providerId: provider.providerId, productId: product.productId, persisted: false, scheduleId: syncResult.scheduleId, orphaned: !cleanedUp });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const { receipt } = await appendReceipt({ kind: "workspace-add-on-schedule", lane: "server-authoritative", outcomeStatus: "published", actor: "workspace-marketplace", objectRefs: [{ objectId, objectType: "sandbox-environment", rowName: rowId }], changedFields: (changedFields || []).map((f) => `dataModel.${f}`), policyVerdict: { ok: true }, schemaVerdict: { ok: true }, summary: `${product.label} schedule ${syncResult.scheduleId} bound to ${rowId}: row serverless + ${liveField} trigger node "${triggerNodeId}" synced (published graph).` });
|
|
217
|
+
|
|
218
|
+
return { status: 200, body: { ok: true, providerId: provider.providerId, productId: product.productId, scheduleId: syncResult.scheduleId, bound, liveField, triggerNodeId, destinationUrl, cron, region, persisted: true, workspaceConfig: persisted, receiptId: receipt?.receiptId } };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* ------------------------------------------------------------------ *
|
|
222
|
+
* Publish one manual scheduler run through the installed scheduler. *
|
|
223
|
+
* ------------------------------------------------------------------ */
|
|
224
|
+
async function runScheduleNow(deps, { providerId, body = {}, requestOrigin = "" } = {}) {
|
|
225
|
+
const { fetchImpl, readConfig, appendReceipt, env } = deps;
|
|
226
|
+
|
|
227
|
+
const provider = getMarketplaceProvider(clean(providerId));
|
|
228
|
+
if (!provider) return err(404, "unknown marketplace provider", { providerId });
|
|
229
|
+
const product = resolveSchedulerProduct(provider, clean(body.productId));
|
|
230
|
+
if (!product || !isSchedulerProduct(product)) return err(400, "provider has no serverless scheduler product", { providerId: provider.providerId });
|
|
231
|
+
const adapter = getSchedulerAdapter(product);
|
|
232
|
+
|
|
233
|
+
const config = await readConfig();
|
|
234
|
+
const objectId = clean(body.objectId);
|
|
235
|
+
const rowId = clean(body.rowId || body.name);
|
|
236
|
+
if (!objectId || !rowId) return err(400, "objectId and rowId (workflow row) are required");
|
|
237
|
+
|
|
238
|
+
const eligible = findEligibleSandboxRow(config, objectId, rowId);
|
|
239
|
+
if (!eligible.ok) return err(eligible.status, eligible.error, { providerId: provider.providerId, productId: product.productId });
|
|
240
|
+
const targetRow = eligible.row;
|
|
241
|
+
const scheduleId = clean(body.scheduleId || targetRow.scheduleId);
|
|
242
|
+
if (!scheduleId) return err(409, "workflow row has no installed scheduler", { providerId: provider.providerId, productId: product.productId, objectId, rowId });
|
|
243
|
+
if (clean(targetRow.runLocality) !== "serverless" || clean(targetRow.schedulerRegistryId) !== product.integrationId) {
|
|
244
|
+
return err(409, "workflow row is not bound to this scheduler", { providerId: provider.providerId, productId: product.productId, objectId, rowId, scheduleId });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const tokenEnv = product.probe?.tokenEnv || (product.requiredEnv || [])[0];
|
|
248
|
+
const token = readEnvVar(tokenEnv, env)?.value || "";
|
|
249
|
+
if (!token) return err(422, `${product.label} runtime credentials are not connected`, { productId: product.productId, missingEnv: [tokenEnv].filter(Boolean) });
|
|
250
|
+
|
|
251
|
+
const region = clean(body.region || targetRow.schedulerRegion || "us-east-1");
|
|
252
|
+
const workspaceId = clean(body.workspaceId || config?.id || "workspace");
|
|
253
|
+
const version = clean(body.version || targetRow.version || "v1");
|
|
254
|
+
const baseUrl = resolveWorkspacePublicUrl(env, requestOrigin);
|
|
255
|
+
if (!baseUrl) return err(422, "could not resolve a public workspace URL for scheduler run", { nextActions: ["Set GROWTHUB_WORKSPACE_PUBLIC_URL to the deployed workspace origin, then retry."] });
|
|
256
|
+
const urls = buildSchedulerCallbackUrls(baseUrl, provider.providerId);
|
|
257
|
+
const destinationUrl = clean(targetRow.schedulerDestination) || urls.destinationUrl;
|
|
258
|
+
const callbackUrl = clean(targetRow.schedulerCallbackUrl) || urls.callbackUrl;
|
|
259
|
+
const failureCallbackUrl = clean(targetRow.schedulerFailureCallbackUrl) || urls.failureCallbackUrl;
|
|
260
|
+
|
|
261
|
+
let runRequest;
|
|
262
|
+
try {
|
|
263
|
+
runRequest = adapter.buildRunRequest({
|
|
264
|
+
product,
|
|
265
|
+
region,
|
|
266
|
+
token,
|
|
267
|
+
scheduleId,
|
|
268
|
+
destinationUrl,
|
|
269
|
+
callbackUrl,
|
|
270
|
+
failureCallbackUrl,
|
|
271
|
+
forward: {
|
|
272
|
+
workspaceId,
|
|
273
|
+
objectId,
|
|
274
|
+
rowId,
|
|
275
|
+
version,
|
|
276
|
+
scheduleId,
|
|
277
|
+
triggerInput: clean(body.triggerInput || targetRow.schedulerTriggerInput),
|
|
278
|
+
runInputs: body.runInputs && typeof body.runInputs === "object" ? body.runInputs : undefined,
|
|
279
|
+
},
|
|
280
|
+
env,
|
|
281
|
+
});
|
|
282
|
+
} catch (e) {
|
|
283
|
+
return err(400, e?.message || "could not build scheduler run request", { providerId: provider.providerId });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let runResult;
|
|
287
|
+
try {
|
|
288
|
+
const response = await fetchWithTimeout(fetchImpl, runRequest.url, { method: runRequest.method, headers: runRequest.headers, body: runRequest.body });
|
|
289
|
+
runResult = adapter.parseRunResponse({ status: response.status, body: await response.text() });
|
|
290
|
+
} catch (e) {
|
|
291
|
+
runResult = { ok: false, messageId: "", proof: `scheduler run publish failed: ${e?.message || "network error"}` };
|
|
292
|
+
}
|
|
293
|
+
if (!runResult.ok) {
|
|
294
|
+
await appendReceipt({
|
|
295
|
+
kind: "workspace-add-on-schedule-run",
|
|
296
|
+
lane: "server-authoritative",
|
|
297
|
+
outcomeStatus: "blocked",
|
|
298
|
+
actor: "workspace-marketplace",
|
|
299
|
+
objectRefs: [{ objectId, objectType: "sandbox-environment", rowName: rowId }],
|
|
300
|
+
policyVerdict: { ok: false, violationCodes: ["scheduler_run_publish_failed"] },
|
|
301
|
+
summary: runResult.proof || `${product.label} manual scheduler run failed to publish`,
|
|
302
|
+
});
|
|
303
|
+
return err(502, runResult.proof || "scheduler run publish failed", { providerId: provider.providerId, productId: product.productId, scheduleId });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const { receipt } = await appendReceipt({
|
|
307
|
+
kind: "workspace-add-on-schedule-run",
|
|
308
|
+
lane: "server-authoritative",
|
|
309
|
+
outcomeStatus: "published",
|
|
310
|
+
actor: "workspace-marketplace",
|
|
311
|
+
objectRefs: [{ objectId, objectType: "sandbox-environment", rowName: rowId }],
|
|
312
|
+
policyVerdict: { ok: true },
|
|
313
|
+
summary: `${product.label} manual scheduler run published for ${rowId}${runResult.messageId ? ` (msg ${runResult.messageId})` : ""}.`,
|
|
314
|
+
runId: runResult.messageId || undefined,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return { status: 200, body: { ok: true, providerId: provider.providerId, productId: product.productId, objectId, rowId, scheduleId, messageId: runResult.messageId, receiptId: receipt?.receiptId } };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* ------------------------------------------------------------------ *
|
|
321
|
+
* Synchronize a signed scheduled-run callback to the OWNING row. *
|
|
322
|
+
* ------------------------------------------------------------------ */
|
|
323
|
+
async function runSchedulerCallback(deps, { providerId, kind = "callback", rawBody = "", signature = "", requestOrigin = "", requestUrl = "", scheduleId = "" } = {}) {
|
|
324
|
+
const { readConfig, writeConfig, appendReceipt, env } = deps;
|
|
325
|
+
const now = (deps.now || (() => new Date().toISOString()))();
|
|
326
|
+
|
|
327
|
+
const provider = getMarketplaceProvider(clean(providerId));
|
|
328
|
+
if (!provider) return err(404, "unknown marketplace provider", { providerId });
|
|
329
|
+
const product = (provider.products || []).find((p) => isSchedulerProduct(p));
|
|
330
|
+
if (!product) return err(400, "provider has no serverless scheduler product");
|
|
331
|
+
const adapter = getSchedulerAdapter(product);
|
|
332
|
+
|
|
333
|
+
const block = async (summary, code, objectRefs = []) => {
|
|
334
|
+
await appendReceipt({ kind: "workspace-add-on-callback", lane: "server-authoritative", outcomeStatus: "blocked", actor: provider.providerId, objectRefs, summary, policyVerdict: { ok: false, violationCodes: [code] } });
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const baseUrl = resolveWorkspacePublicUrl(env, requestOrigin);
|
|
338
|
+
const urls = buildSchedulerCallbackUrls(baseUrl, provider.providerId);
|
|
339
|
+
const expectedUrl = clean(requestUrl) || (kind === "failure" ? urls.failureCallbackUrl : urls.callbackUrl);
|
|
340
|
+
const verdict = adapter.verifyCallback({ signature, rawBody, expectedUrl, env });
|
|
341
|
+
if (!verdict.ok) {
|
|
342
|
+
await block(`Rejected ${kind} callback for ${product.label}: ${verdict.reason}.`, `callback_signature_${verdict.reason}`);
|
|
343
|
+
return err(401, "invalid signature", { reason: verdict.reason });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const parsed = adapter.parseCallback({ rawBody, kind });
|
|
347
|
+
if (!clean(parsed.scheduleId) && clean(scheduleId)) parsed.scheduleId = clean(scheduleId);
|
|
348
|
+
if (!clean(parsed.scheduleId)) {
|
|
349
|
+
await block(`${product.label} ${kind} callback ignored: no scheduleId.`, "callback_missing_schedule_id");
|
|
350
|
+
return err(400, "callback is missing a scheduleId");
|
|
351
|
+
}
|
|
352
|
+
const config = await readConfig();
|
|
353
|
+
const owner = findSandboxRowByScheduleId(config, parsed.scheduleId);
|
|
354
|
+
if (!owner) {
|
|
355
|
+
await block(`${product.label} ${kind} callback ignored: no workflow row owns schedule ${parsed.scheduleId}.`, "callback_no_installed_schedule");
|
|
356
|
+
return err(409, "no workflow row owns this schedule");
|
|
357
|
+
}
|
|
358
|
+
const rowRef = [{ objectId: owner.objectId, objectType: "sandbox-environment", rowName: owner.row.Name }];
|
|
359
|
+
|
|
360
|
+
// Mirror destination validation: the owning row must still be serverless and
|
|
361
|
+
// bound to THIS provider/product, and its LIVE trigger node must still carry
|
|
362
|
+
// the same schedule. Signature proves QStash sent it; binding is checked here.
|
|
363
|
+
const triggerBinding = readTriggerScheduleBinding(owner.row[liveGraphField(owner.row)]);
|
|
364
|
+
const stillBound =
|
|
365
|
+
clean(owner.row.runLocality) === "serverless" &&
|
|
366
|
+
clean(owner.row.schedulerRegistryId) === product.integrationId &&
|
|
367
|
+
triggerBinding?.triggerKind === "serverless-scheduler" &&
|
|
368
|
+
triggerBinding?.enabled === true &&
|
|
369
|
+
triggerBinding?.scheduleId === clean(parsed.scheduleId) &&
|
|
370
|
+
triggerBinding?.schedulerRegistryId === product.integrationId &&
|
|
371
|
+
(!triggerBinding?.providerId || triggerBinding.providerId === provider.providerId) &&
|
|
372
|
+
(!triggerBinding?.productId || triggerBinding.productId === product.productId);
|
|
373
|
+
if (!stillBound) {
|
|
374
|
+
await block(`${product.label} ${kind} callback ignored: ${owner.row.Name} is no longer bound to ${product.integrationId} schedule ${parsed.scheduleId} (locality=${clean(owner.row.runLocality)}, trigger.scheduleId=${triggerBinding?.scheduleId || "none"}).`, "callback_row_unbound", rowRef);
|
|
375
|
+
return err(409, "owning row/trigger is no longer bound to this schedule");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const retryStates = [parsed.retried != null ? `retried=${parsed.retried}` : "", parsed.maxRetries != null ? `maxRetries=${parsed.maxRetries}` : ""].filter(Boolean).join(",");
|
|
379
|
+
const patch = {
|
|
380
|
+
status: parsed.succeeded ? "connected" : "failed",
|
|
381
|
+
lastTested: now,
|
|
382
|
+
lastResponse: parsed.succeeded ? `Scheduled run ok (HTTP ${parsed.status}).` : `Scheduled run failed: ${parsed.failureReason}.`,
|
|
383
|
+
lastScheduledRunStatus: parsed.status == null ? "" : String(parsed.status),
|
|
384
|
+
lastScheduledRunMessageId: parsed.messageId,
|
|
385
|
+
lastScheduledRunId: parsed.runId || parsed.messageId,
|
|
386
|
+
lastScheduledRunAt: now,
|
|
387
|
+
lastScheduledRunResponse: parsed.responsePreview || parsed.bodyPreview,
|
|
388
|
+
lastScheduledRunAttemptedAt: now,
|
|
389
|
+
lastScheduledRunBodyPreview: parsed.bodyPreview,
|
|
390
|
+
lastScheduledRunFailureReason: parsed.succeeded ? "" : parsed.failureReason,
|
|
391
|
+
lastScheduledRunRetries: retryStates,
|
|
392
|
+
};
|
|
393
|
+
patch[parsed.succeeded ? "lastScheduledRunSucceededAt" : "lastScheduledRunFailedAt"] = now;
|
|
394
|
+
|
|
395
|
+
const { config: nextConfig, found } = withSandboxScheduledRunProof(config, { objectId: owner.objectId, rowId: owner.row.Name, patch });
|
|
396
|
+
let persisted = found;
|
|
397
|
+
if (found) {
|
|
398
|
+
try { await writeConfig({ dataModel: nextConfig.dataModel }); } catch { persisted = false; }
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const { receipt } = await appendReceipt({ kind: "workspace-scheduled-run-callback", lane: "server-authoritative", outcomeStatus: persisted && parsed.succeeded ? "published" : "failed", actor: provider.providerId, objectRefs: rowRef, changedFields: [`dataModel.${owner.objectId}.${owner.row.Name}.lastScheduledRunStatus`], policyVerdict: { ok: persisted && parsed.succeeded }, summary: persisted ? (parsed.succeeded ? `${owner.row.Name} scheduled run synced (HTTP ${parsed.status}, msg ${parsed.messageId || "?"}).` : `${owner.row.Name} scheduled run failed: ${parsed.failureReason}.`) : `${owner.row.Name} scheduled run proof could NOT be persisted (workspace read-only); run result not durably recorded.`, runId: parsed.messageId || undefined });
|
|
402
|
+
|
|
403
|
+
// If the workspace cannot persist the run proof, the result is lost from
|
|
404
|
+
// workspace state — do NOT report clean success.
|
|
405
|
+
if (!persisted) {
|
|
406
|
+
return err(424, "scheduled run proof could not be persisted to workspace state", { providerId: provider.providerId, productId: product.productId, objectId: owner.objectId, rowId: owner.row.Name, synced: false, persisted: false, receiptId: receipt?.receiptId });
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return { status: 200, body: { ok: true, providerId: provider.providerId, productId: product.productId, objectId: owner.objectId, rowId: owner.row.Name, synced: parsed.succeeded, persisted: true, receiptId: receipt?.receiptId } };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* ------------------------------------------------------------------ *
|
|
413
|
+
* Read-only readiness scan for a workflow row (no remote/no mutation).*
|
|
414
|
+
* Wired to the canvas: when the input trigger is switched to Serverless*
|
|
415
|
+
* Schedule, the UI calls this to surface compatibility deltas BEFORE *
|
|
416
|
+
* the operator attempts a bind. *
|
|
417
|
+
* ------------------------------------------------------------------ */
|
|
418
|
+
async function runReadinessScan(deps, { providerId, body = {} } = {}) {
|
|
419
|
+
const { readConfig, env } = deps;
|
|
420
|
+
const provider = getMarketplaceProvider(clean(providerId));
|
|
421
|
+
if (!provider) return err(404, "unknown marketplace provider", { providerId });
|
|
422
|
+
const product = resolveSchedulerProduct(provider, clean(body.productId));
|
|
423
|
+
if (!product || !isSchedulerProduct(product)) return err(400, "provider has no serverless scheduler product", { providerId: provider.providerId });
|
|
424
|
+
|
|
425
|
+
const config = await readConfig();
|
|
426
|
+
const objectId = clean(body.objectId);
|
|
427
|
+
const rowId = clean(body.rowId || body.name);
|
|
428
|
+
if (!objectId || !rowId) return err(400, "objectId and rowId (workflow row) are required");
|
|
429
|
+
const eligible = findEligibleSandboxRow(config, objectId, rowId);
|
|
430
|
+
if (!eligible.ok) return err(eligible.status, eligible.error, { providerId: provider.providerId, productId: product.productId });
|
|
431
|
+
|
|
432
|
+
const phase = clean(eligible.row.runLocality) === "serverless" && clean(eligible.row.scheduleId) ? "bound" : "pre-bind";
|
|
433
|
+
const readiness = scanServerlessReadiness({
|
|
434
|
+
row: eligible.row,
|
|
435
|
+
workspaceConfig: config,
|
|
436
|
+
env,
|
|
437
|
+
phase,
|
|
438
|
+
expected: {
|
|
439
|
+
schedulerRegistryId: product.integrationId,
|
|
440
|
+
providerId: provider.providerId,
|
|
441
|
+
productId: product.productId,
|
|
442
|
+
scheduleId: clean(eligible.row.scheduleId),
|
|
443
|
+
triggerInput: clean(body.triggerInput || eligible.row.schedulerTriggerInput),
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
return { status: 200, body: { ok: true, providerId: provider.providerId, productId: product.productId, objectId, rowId, phase, readiness } };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export { runScheduleInstall, runScheduleNow, runSchedulerCallback, runReadinessScan };
|
package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-secrets.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical server-side secret resolver — the single "stored env token for the
|
|
3
|
+
* run" entry point.
|
|
4
|
+
*
|
|
5
|
+
* The kit historically duplicated this UPPER_SNAKE candidate expansion in
|
|
6
|
+
* several routes (`sandbox-run`, `test-api-record`, `orchestration-graph-runner`,
|
|
7
|
+
* `env-status`). New governed add-on routes resolve credentials through THIS
|
|
8
|
+
* module so the canonical entry is provably the same one the sandbox run loop
|
|
9
|
+
* uses: a logical ref (`QSTASH`, `upstash-redis`) expands to `QSTASH`,
|
|
10
|
+
* `QSTASH_API_KEY`, `QSTASH_TOKEN`, and the first present `process.env` key wins.
|
|
11
|
+
*
|
|
12
|
+
* Pure + env-injectable so the resolution is deterministically testable and so
|
|
13
|
+
* secret *values* never have to travel through a route body to be proven — a
|
|
14
|
+
* route asks this module "does the run have this token?" and gets back the
|
|
15
|
+
* resolved key name (never logged value) plus the value to use server-side.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Canonical UPPER_SNAKE candidate expansion for a logical ref. */
|
|
19
|
+
function envKeyCandidates(ref) {
|
|
20
|
+
const token = String(ref || "")
|
|
21
|
+
.trim()
|
|
22
|
+
.replace(/[^a-z0-9]+/gi, "_")
|
|
23
|
+
.replace(/^_+|_+$/g, "")
|
|
24
|
+
.toUpperCase();
|
|
25
|
+
if (!token) return [];
|
|
26
|
+
return Array.from(new Set([token, `${token}_API_KEY`, `${token}_TOKEN`]));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a logical ref to the first present env key. Returns `{ key, value }`
|
|
31
|
+
* (the key NAME is safe to surface; the value must stay server-side) or null.
|
|
32
|
+
*/
|
|
33
|
+
function readServerSecret(authRef, env = process.env) {
|
|
34
|
+
const source = env && typeof env === "object" ? env : {};
|
|
35
|
+
for (const key of envKeyCandidates(authRef)) {
|
|
36
|
+
if (source[key]) return { key, value: source[key] };
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve an explicit env var name (already UPPER_SNAKE, e.g. `QSTASH_TOKEN`).
|
|
43
|
+
* Unlike `readServerSecret` this does NOT expand candidates — it is used for the
|
|
44
|
+
* concrete `requiredEnv` / `probe.tokenEnv` keys a product declares.
|
|
45
|
+
*/
|
|
46
|
+
function readEnvVar(name, env = process.env) {
|
|
47
|
+
const source = env && typeof env === "object" ? env : {};
|
|
48
|
+
const key = String(name || "").trim();
|
|
49
|
+
if (!key) return null;
|
|
50
|
+
const value = source[key];
|
|
51
|
+
return value ? { key, value } : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve a product's declared `requiredEnv` against the run environment.
|
|
56
|
+
* Returns slug-safe evidence only: which keys resolved, which are missing, and
|
|
57
|
+
* a map of resolved values for server-side use. NEVER returns secret values in
|
|
58
|
+
* `resolved` — that list is key NAMES only.
|
|
59
|
+
*/
|
|
60
|
+
function resolveRequiredEnv(requiredEnv, env = process.env) {
|
|
61
|
+
const keys = Array.isArray(requiredEnv) ? requiredEnv : [];
|
|
62
|
+
const resolvedKeys = [];
|
|
63
|
+
const missing = [];
|
|
64
|
+
const values = {};
|
|
65
|
+
for (const name of keys) {
|
|
66
|
+
const hit = readEnvVar(name, env);
|
|
67
|
+
if (hit) {
|
|
68
|
+
resolvedKeys.push(hit.key);
|
|
69
|
+
values[hit.key] = hit.value;
|
|
70
|
+
} else {
|
|
71
|
+
missing.push(String(name || "").trim());
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { resolvedKeys, missing, values, ok: missing.length === 0 };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { envKeyCandidates, readServerSecret, readEnvVar, resolveRequiredEnv };
|