@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
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readWorkspaceConfig, readWorkspaceSourceRecords, writeWorkspaceConfig } from "@/lib/workspace-config";
|
|
4
|
+
import { sandboxRunSourceId } from "@/lib/workspace-data-model";
|
|
5
|
+
import { parseOrchestrationGraph, validateOrchestrationGraph } from "@/lib/orchestration-graph";
|
|
6
|
+
import { stableStringify } from "@/lib/workspace-patch-policy";
|
|
7
|
+
import { readTriggerScheduleBinding } from "@/lib/workspace-add-ons";
|
|
8
|
+
import { scanServerlessReadiness, READINESS_KIND } from "@/lib/serverless-readiness";
|
|
9
|
+
import { resolveWorkflowFieldNames, getNodeDeltaRecords, normalizeDeltaTags, patchSandboxRowInConfig } from "@/lib/orchestration-publish";
|
|
10
|
+
import { appendOutcomeReceipt } from "@/lib/workspace-outcome-receipts";
|
|
11
|
+
import { requireAppScope, checkScopedWorkflowAccess } from "@/lib/workspace-app-registry";
|
|
12
|
+
|
|
1
13
|
/**
|
|
2
14
|
* POST /api/workspace/workflow/publish
|
|
3
15
|
*
|
|
@@ -31,308 +43,413 @@
|
|
|
31
43
|
* or { ok: false, code, error, ... } with 4xx/5xx status.
|
|
32
44
|
*/
|
|
33
45
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
import { sandboxRunSourceId } from "@/lib/workspace-data-model";
|
|
42
|
-
import { parseOrchestrationGraph, validateOrchestrationGraph } from "@/lib/orchestration-graph";
|
|
43
|
-
import { stableStringify } from "@/lib/workspace-patch-policy";
|
|
44
|
-
import {
|
|
45
|
-
getNodeDeltaRecords,
|
|
46
|
-
normalizeDeltaTags,
|
|
47
|
-
patchSandboxRowInConfig,
|
|
48
|
-
resolveWorkflowFieldNames
|
|
49
|
-
} from "@/lib/orchestration-publish";
|
|
50
|
-
import { appendOutcomeReceipt } from "@/lib/workspace-outcome-receipts";
|
|
51
|
-
import { requireAppScope, checkScopedWorkflowAccess } from "@/lib/workspace-app-registry";
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
52
53
|
|
|
53
54
|
function sha256(text) {
|
|
54
|
-
|
|
55
|
+
return createHash("sha256").update(String(text), "utf8").digest("hex");
|
|
55
56
|
}
|
|
56
|
-
|
|
57
57
|
function findSandboxRow(workspaceConfig, objectId, name) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
|
|
59
|
+
const object = objects.find((entry)=>entry?.id === objectId && entry?.objectType === "sandbox-environment");
|
|
60
|
+
if (!object) return {
|
|
61
|
+
object: null,
|
|
62
|
+
row: null,
|
|
63
|
+
rowIndex: -1
|
|
64
|
+
};
|
|
65
|
+
const wantedName = String(name || "").trim();
|
|
66
|
+
const rows = Array.isArray(object.rows) ? object.rows : [];
|
|
67
|
+
const rowIndex = rows.findIndex((row)=>String(row?.Name || "").trim() === wantedName);
|
|
68
|
+
if (rowIndex === -1) return {
|
|
69
|
+
object,
|
|
70
|
+
row: null,
|
|
71
|
+
rowIndex: -1
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
object,
|
|
75
|
+
row: rows[rowIndex],
|
|
76
|
+
rowIndex
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function rowHasSuccessfulServerlessSchedulerProof(row, draft) {
|
|
80
|
+
const runLocality = String(row?.runLocality || "").trim().toLowerCase();
|
|
81
|
+
const schedulerRegistryId = String(row?.schedulerRegistryId || "").trim();
|
|
82
|
+
const scheduleId = String(row?.scheduleId || "").trim();
|
|
83
|
+
const draftGraph = String(draft || "").trim();
|
|
84
|
+
const testedConfig = String(row?.orchestrationDraftTestedConfig || "").trim();
|
|
85
|
+
const liveGraph = String(row?.orchestrationGraph || row?.orchestrationConfig || "").trim();
|
|
86
|
+
const binding = readTriggerScheduleBinding(row?.orchestrationGraph || row?.orchestrationConfig);
|
|
87
|
+
return runLocality === "serverless" && Boolean(schedulerRegistryId) && Boolean(scheduleId) && binding?.enabled === true && binding?.scheduleId === scheduleId && binding?.schedulerRegistryId === schedulerRegistryId && (testedConfig === draftGraph || liveGraph === draftGraph);
|
|
66
88
|
}
|
|
67
|
-
|
|
68
89
|
/**
|
|
69
90
|
* Gate failures are governance signal: emit a blocked outcome receipt
|
|
70
91
|
* (non-fatal) and return the structured failure envelope.
|
|
71
|
-
*/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
*/ async function publishBlocked(httpStatus, body, refs) {
|
|
93
|
+
await appendOutcomeReceipt({
|
|
94
|
+
kind: "workflow-publish",
|
|
95
|
+
lane: "server-authoritative",
|
|
96
|
+
outcomeStatus: "blocked",
|
|
97
|
+
...refs ? {
|
|
98
|
+
objectRefs: [
|
|
99
|
+
refs
|
|
100
|
+
]
|
|
101
|
+
} : {},
|
|
102
|
+
summary: `publish blocked (${body.code}): ${body.error}`,
|
|
103
|
+
nextActions: body.code === "no_draft" || body.code === "draft_not_tested" || body.code === "draft_run_not_verified" || body.code === "draft_changed_after_test" ? [
|
|
104
|
+
"Save the draft, run POST /api/workspace/sandbox-run {useDraft:true} to a passing result, attest, then publish"
|
|
105
|
+
] : []
|
|
106
|
+
});
|
|
107
|
+
return NextResponse.json(body, {
|
|
108
|
+
status: httpStatus
|
|
109
|
+
});
|
|
84
110
|
}
|
|
85
|
-
|
|
86
111
|
async function POST(request) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
{ ok: false, code: "invalid_body", error: "objectId and name are required" },
|
|
99
|
-
{ status: 400 }
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
if (requestedField && requestedField !== "orchestrationConfig" && requestedField !== "orchestrationGraph") {
|
|
103
|
-
return NextResponse.json(
|
|
104
|
-
{ ok: false, code: "invalid_body", error: 'field must be "orchestrationConfig" or "orchestrationGraph" when provided' },
|
|
105
|
-
{ status: 400 }
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const workspaceConfig = await readWorkspaceConfig();
|
|
110
|
-
|
|
111
|
-
// Unified app-scope gate (route-shopping closed): with x-growthub-app-scope,
|
|
112
|
-
// publish may only promote a workflow inside the app's governed scope.
|
|
113
|
-
// NB: publish is deliberately NOT blocked when the app's health is
|
|
114
|
-
// "blocked" — publishing is how the "workflow not live" blocker is cleared.
|
|
115
|
-
const scope = requireAppScope(request, workspaceConfig);
|
|
116
|
-
if (scope.scoped) {
|
|
117
|
-
const violation = scope.violation || checkScopedWorkflowAccess(scope.context, objectId, name);
|
|
118
|
-
if (violation) {
|
|
119
|
-
await appendOutcomeReceipt({
|
|
120
|
-
kind: "workflow-publish", lane: "server-authoritative", outcomeStatus: "blocked",
|
|
121
|
-
appId: violation.appScope || scope.appId,
|
|
122
|
-
summary: `publish rejected (422 app scope): ${violation.violationType}`,
|
|
123
|
-
nextActions: violation.repairPlan
|
|
124
|
-
});
|
|
125
|
-
return NextResponse.json(violation, { status: 422 });
|
|
112
|
+
let body;
|
|
113
|
+
try {
|
|
114
|
+
body = await request.json();
|
|
115
|
+
} catch {
|
|
116
|
+
return NextResponse.json({
|
|
117
|
+
ok: false,
|
|
118
|
+
code: "invalid_body",
|
|
119
|
+
error: "invalid json body"
|
|
120
|
+
}, {
|
|
121
|
+
status: 400
|
|
122
|
+
});
|
|
126
123
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
124
|
+
const objectId = typeof body?.objectId === "string" ? body.objectId.trim() : "";
|
|
125
|
+
const name = typeof body?.name === "string" ? body.name.trim() : "";
|
|
126
|
+
const requestedField = typeof body?.field === "string" ? body.field.trim() : "";
|
|
127
|
+
if (!objectId || !name) {
|
|
128
|
+
return NextResponse.json({
|
|
129
|
+
ok: false,
|
|
130
|
+
code: "invalid_body",
|
|
131
|
+
error: "objectId and name are required"
|
|
132
|
+
}, {
|
|
133
|
+
status: 400
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (requestedField && requestedField !== "orchestrationConfig" && requestedField !== "orchestrationGraph") {
|
|
137
|
+
return NextResponse.json({
|
|
138
|
+
ok: false,
|
|
139
|
+
code: "invalid_body",
|
|
140
|
+
error: 'field must be "orchestrationConfig" or "orchestrationGraph" when provided'
|
|
141
|
+
}, {
|
|
142
|
+
status: 400
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const workspaceConfig = await readWorkspaceConfig();
|
|
146
|
+
// Unified app-scope gate (route-shopping closed): with x-growthub-app-scope,
|
|
147
|
+
// publish may only promote a workflow inside the app's governed scope.
|
|
148
|
+
// NB: publish is deliberately NOT blocked when the app's health is
|
|
149
|
+
// "blocked" — publishing is how the "workflow not live" blocker is cleared.
|
|
150
|
+
const scope = requireAppScope(request, workspaceConfig);
|
|
151
|
+
if (scope.scoped) {
|
|
152
|
+
const violation = scope.violation || checkScopedWorkflowAccess(scope.context, objectId, name);
|
|
153
|
+
if (violation) {
|
|
154
|
+
await appendOutcomeReceipt({
|
|
155
|
+
kind: "workflow-publish",
|
|
156
|
+
lane: "server-authoritative",
|
|
157
|
+
outcomeStatus: "blocked",
|
|
158
|
+
appId: violation.appScope || scope.appId,
|
|
159
|
+
summary: `publish rejected (422 app scope): ${violation.violationType}`,
|
|
160
|
+
nextActions: violation.repairPlan
|
|
161
|
+
});
|
|
162
|
+
return NextResponse.json(violation, {
|
|
163
|
+
status: 422
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const { object, row, rowIndex } = findSandboxRow(workspaceConfig, objectId, name);
|
|
168
|
+
if (!object) {
|
|
169
|
+
return NextResponse.json({
|
|
170
|
+
ok: false,
|
|
171
|
+
code: "object_not_found",
|
|
172
|
+
error: `no sandbox-environment object with id ${objectId}`
|
|
173
|
+
}, {
|
|
174
|
+
status: 404
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (!row) {
|
|
178
|
+
return NextResponse.json({
|
|
179
|
+
ok: false,
|
|
180
|
+
code: "row_not_found",
|
|
181
|
+
error: `no sandbox row named ${name} in object ${objectId}`
|
|
182
|
+
}, {
|
|
183
|
+
status: 404
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const { liveField, draftField } = resolveWorkflowFieldNames(row, requestedField || undefined);
|
|
187
|
+
const draft = String(row[draftField] ?? "").trim();
|
|
188
|
+
if (!draft) {
|
|
189
|
+
return publishBlocked(409, {
|
|
190
|
+
ok: false,
|
|
191
|
+
code: "no_draft",
|
|
192
|
+
error: `no saved draft in ${draftField} — save the draft, test it with sandbox-run useDraft:true, then publish`
|
|
193
|
+
}, {
|
|
194
|
+
objectId,
|
|
195
|
+
rowName: name,
|
|
196
|
+
objectType: "sandbox-environment"
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const draftPassed = row.orchestrationDraftTestPassed === true || String(row.orchestrationDraftTestPassed ?? "") === "true";
|
|
200
|
+
const serverlessSchedulerProofPassed = rowHasSuccessfulServerlessSchedulerProof(row, draft);
|
|
201
|
+
// Causality gate: a serverless-bound row may only publish when its WHOLE
|
|
202
|
+
// downstream graph is still serverless-ready (binding valid ≠ graph runnable).
|
|
203
|
+
// The graph can drift after install — a downstream node, API Registry row,
|
|
204
|
+
// credential ref, or input template may have changed. Block publish until the
|
|
205
|
+
// compatibility proof is clean; keep the published graph unchanged.
|
|
206
|
+
if (serverlessSchedulerProofPassed) {
|
|
207
|
+
const readiness = scanServerlessReadiness({
|
|
208
|
+
row,
|
|
209
|
+
workspaceConfig,
|
|
210
|
+
env: process.env,
|
|
211
|
+
phase: "bound",
|
|
212
|
+
expected: {
|
|
213
|
+
scheduleId: String(row?.scheduleId || "").trim(),
|
|
214
|
+
schedulerRegistryId: String(row?.schedulerRegistryId || "").trim(),
|
|
215
|
+
providerId: String(row?.schedulerProviderId || "").trim(),
|
|
216
|
+
productId: String(row?.schedulerProductId || "").trim(),
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
if (!readiness.ok) {
|
|
220
|
+
await appendOutcomeReceipt({
|
|
221
|
+
kind: READINESS_KIND,
|
|
222
|
+
lane: "server-authoritative",
|
|
223
|
+
outcomeStatus: "blocked",
|
|
224
|
+
objectRefs: [{ objectId, rowName: name, objectType: "sandbox-environment" }],
|
|
225
|
+
policyVerdict: { ok: false, violationCodes: readiness.deltaTags },
|
|
226
|
+
summary: `publish blocked: ${name} is no longer serverless-ready (${readiness.blockingNodes.length} blocking node(s)).`,
|
|
227
|
+
nextActions: readiness.blockingNodes.map((nbl) => nbl.helperAction).filter(Boolean),
|
|
228
|
+
});
|
|
229
|
+
return NextResponse.json({
|
|
230
|
+
ok: false,
|
|
231
|
+
code: "serverless_not_ready",
|
|
232
|
+
error: "publish blocked — the serverless-bound graph is not compatible; resolve the flagged nodes before publishing",
|
|
233
|
+
readiness,
|
|
234
|
+
}, { status: 409 });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (!draftPassed && !serverlessSchedulerProofPassed) {
|
|
238
|
+
return publishBlocked(409, {
|
|
239
|
+
ok: false,
|
|
240
|
+
code: "draft_not_tested",
|
|
241
|
+
error: "publish blocked — the saved draft has no successful test run; " + "run POST /api/workspace/sandbox-run with useDraft:true or the installed serverless scheduler with a passing result first"
|
|
242
|
+
}, {
|
|
243
|
+
objectId,
|
|
244
|
+
rowName: name,
|
|
245
|
+
objectType: "sandbox-environment"
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const testedConfig = String(row.orchestrationDraftTestedConfig ?? "");
|
|
249
|
+
if (!serverlessSchedulerProofPassed && testedConfig !== draft) {
|
|
250
|
+
return publishBlocked(409, {
|
|
251
|
+
ok: false,
|
|
252
|
+
code: "draft_changed_after_test",
|
|
253
|
+
error: "publish blocked — the draft changed after its successful test; re-test this exact draft",
|
|
254
|
+
// Diagnostic raw-STRING hashes (the equality above is byte-level);
|
|
255
|
+
// the canonical graph hash everywhere else is sha256(stableStringify(parsedGraph)).
|
|
256
|
+
draftStringSha256: sha256(draft),
|
|
257
|
+
testedStringSha256: sha256(testedConfig)
|
|
258
|
+
}, {
|
|
259
|
+
objectId,
|
|
260
|
+
rowName: name,
|
|
261
|
+
objectType: "sandbox-environment"
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
// Lineage gate — the draft-field attestation (`orchestrationDraftTestPassed`,
|
|
265
|
+
// `orchestrationDraftTestedConfig`) is PATCH-writable, so it is not trusted
|
|
266
|
+
// alone. The claimed draft run must exist in the source-record run history
|
|
267
|
+
// (which only sandbox-run writes; PATCH is policy-blocked from sidecar
|
|
268
|
+
// writes), must have passed (exitCode 0, no error), and the graph it
|
|
269
|
+
// actually executed must equal this draft.
|
|
270
|
+
const draftRunId = String(row.orchestrationDraftLastRunId ?? "").trim();
|
|
271
|
+
if (!serverlessSchedulerProofPassed && !draftRunId) {
|
|
272
|
+
return publishBlocked(409, {
|
|
273
|
+
ok: false,
|
|
274
|
+
code: "draft_run_not_verified",
|
|
275
|
+
error: "publish blocked — no server-recorded draft run on this row; " + "run POST /api/workspace/sandbox-run with useDraft:true first"
|
|
276
|
+
}, {
|
|
277
|
+
objectId,
|
|
278
|
+
rowName: name,
|
|
279
|
+
objectType: "sandbox-environment"
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const sourceId = sandboxRunSourceId(objectId, row.Name || name);
|
|
283
|
+
const history = !serverlessSchedulerProofPassed && sourceId ? await readWorkspaceSourceRecords(sourceId) : null;
|
|
284
|
+
const records = Array.isArray(history?.records) ? history.records : [];
|
|
285
|
+
const runRecord = serverlessSchedulerProofPassed ? null : records.find((record)=>String(record?.runId ?? "") === draftRunId);
|
|
286
|
+
if (!serverlessSchedulerProofPassed) {
|
|
287
|
+
if (!runRecord) {
|
|
288
|
+
return publishBlocked(409, {
|
|
289
|
+
ok: false,
|
|
290
|
+
code: "draft_run_not_verified",
|
|
291
|
+
error: `publish blocked — draft run ${draftRunId} has no record in the sandbox run history (${sourceId})`
|
|
292
|
+
}, {
|
|
293
|
+
objectId,
|
|
294
|
+
rowName: name,
|
|
295
|
+
objectType: "sandbox-environment"
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
if (runRecord.exitCode !== 0 || runRecord.error) {
|
|
299
|
+
return publishBlocked(409, {
|
|
300
|
+
ok: false,
|
|
301
|
+
code: "draft_run_not_verified",
|
|
302
|
+
error: `publish blocked — draft run ${draftRunId} did not pass (exitCode ${runRecord.exitCode})`
|
|
303
|
+
}, {
|
|
304
|
+
objectId,
|
|
305
|
+
rowName: name,
|
|
306
|
+
objectType: "sandbox-environment"
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// The record's draftSha256 is stamped by sandbox-run from the exact graph
|
|
311
|
+
// it executed, before execution. It must match this saved draft.
|
|
312
|
+
const draftGraphParsed = parseOrchestrationGraph(draft);
|
|
313
|
+
const expectedSha256 = createHash("sha256").update(stableStringify(draftGraphParsed), "utf8").digest("hex");
|
|
314
|
+
if (!serverlessSchedulerProofPassed && (runRecord.useDraft !== true || runRecord.draftSha256 !== expectedSha256)) {
|
|
315
|
+
return publishBlocked(409, {
|
|
316
|
+
ok: false,
|
|
317
|
+
code: "draft_run_not_verified",
|
|
318
|
+
error: `publish blocked — draft run ${draftRunId} executed a different graph than the saved draft ` + "(or was not a draft run); re-test this exact draft with sandbox-run useDraft:true"
|
|
319
|
+
}, {
|
|
320
|
+
objectId,
|
|
321
|
+
rowName: name,
|
|
322
|
+
objectType: "sandbox-environment"
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
const parsedDraft = draftGraphParsed;
|
|
326
|
+
const validation = validateOrchestrationGraph(parsedDraft);
|
|
327
|
+
if (!validation?.ok) {
|
|
328
|
+
return publishBlocked(400, {
|
|
329
|
+
ok: false,
|
|
330
|
+
code: "invalid_graph",
|
|
331
|
+
error: "publish blocked — the draft does not parse as a valid orchestration graph",
|
|
332
|
+
details: validation?.errors ?? []
|
|
333
|
+
}, {
|
|
334
|
+
objectId,
|
|
335
|
+
rowName: name,
|
|
336
|
+
objectType: "sandbox-environment"
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
const publishedAt = new Date().toISOString();
|
|
340
|
+
const currentVersion = Number(row.version || 1);
|
|
341
|
+
const nextVersion = Number.isFinite(currentVersion) ? String(currentVersion + 1) : "1";
|
|
342
|
+
const previousDeltas = Array.isArray(row.orchestrationDeltas) ? row.orchestrationDeltas : [];
|
|
343
|
+
const previousPublishedGraph = parseOrchestrationGraph(row[liveField]);
|
|
344
|
+
const nodeDeltas = getNodeDeltaRecords(previousPublishedGraph, parsedDraft);
|
|
345
|
+
const deltaTags = normalizeDeltaTags(nodeDeltas.flatMap((delta)=>delta.deltaTags));
|
|
346
|
+
const changeReason = nodeDeltas.map((delta)=>delta.changeReason).filter(Boolean).join("\n");
|
|
347
|
+
// One canonical draft/graph hash everywhere: sha256(stableStringify(parsedGraph)).
|
|
348
|
+
// This is the same value sandbox-run stamped as the record's draftSha256,
|
|
349
|
+
// so the lineage record and the publish delta are directly comparable.
|
|
350
|
+
const publishedSha256 = expectedSha256;
|
|
351
|
+
const next = patchSandboxRowInConfig(workspaceConfig, objectId, rowIndex, {
|
|
352
|
+
[liveField]: draft,
|
|
353
|
+
[draftField]: "",
|
|
262
354
|
version: nextVersion,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
changedFields: ["dataModel"],
|
|
287
|
-
runId: draftRunId,
|
|
288
|
-
sourceId,
|
|
289
|
-
draftSha256: expectedSha256,
|
|
290
|
-
publishedSha256,
|
|
291
|
-
version: nextVersion,
|
|
292
|
-
summary: `published ${liveField} v${nextVersion} for ${objectId}/${name} (${nodeDeltas.length} node delta(s), verified draft run ${draftRunId})`,
|
|
293
|
-
rollbackRef: {
|
|
294
|
-
objectId,
|
|
295
|
-
rowName: name,
|
|
296
|
-
liveField,
|
|
297
|
-
previousVersion: String(row.version || "1"),
|
|
298
|
-
deltaIndex: previousDeltas.length,
|
|
299
|
-
sourceId
|
|
300
|
-
}
|
|
355
|
+
lifecycleStatus: "live",
|
|
356
|
+
orchestrationDraftStatus: "published",
|
|
357
|
+
orchestrationDraftTestPassed: false,
|
|
358
|
+
orchestrationDraftTestedConfig: "",
|
|
359
|
+
orchestrationPublishedAt: publishedAt,
|
|
360
|
+
orchestrationDeltas: [
|
|
361
|
+
...previousDeltas,
|
|
362
|
+
{
|
|
363
|
+
at: publishedAt,
|
|
364
|
+
version: nextVersion,
|
|
365
|
+
field: liveField,
|
|
366
|
+
action: "publish",
|
|
367
|
+
previousVersion: String(row.version || "1"),
|
|
368
|
+
draftTestedAt: row.orchestrationDraftLastTested || "",
|
|
369
|
+
draftRunId: row.orchestrationDraftLastRunId || "",
|
|
370
|
+
publishedSha256,
|
|
371
|
+
changeReason,
|
|
372
|
+
deltaTags,
|
|
373
|
+
nodeDeltas,
|
|
374
|
+
nodeCount: Array.isArray(parsedDraft?.nodes) ? parsedDraft.nodes.length : 0,
|
|
375
|
+
edgeCount: Array.isArray(parsedDraft?.edges) ? parsedDraft.edges.length : 0
|
|
376
|
+
}
|
|
377
|
+
]
|
|
301
378
|
});
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
379
|
+
try {
|
|
380
|
+
const persisted = await writeWorkspaceConfig({
|
|
381
|
+
dataModel: next.dataModel
|
|
382
|
+
});
|
|
383
|
+
const { receipt } = await appendOutcomeReceipt({
|
|
384
|
+
kind: "workflow-publish",
|
|
385
|
+
lane: "server-authoritative",
|
|
386
|
+
outcomeStatus: "published",
|
|
387
|
+
...scope.scoped ? {
|
|
388
|
+
appId: scope.appId
|
|
389
|
+
} : {},
|
|
390
|
+
objectRefs: [
|
|
391
|
+
{
|
|
392
|
+
objectId,
|
|
393
|
+
rowName: name,
|
|
394
|
+
objectType: "sandbox-environment"
|
|
395
|
+
}
|
|
396
|
+
],
|
|
397
|
+
changedFields: [
|
|
398
|
+
"dataModel"
|
|
399
|
+
],
|
|
400
|
+
runId: draftRunId,
|
|
401
|
+
sourceId,
|
|
402
|
+
draftSha256: expectedSha256,
|
|
403
|
+
publishedSha256,
|
|
404
|
+
version: nextVersion,
|
|
405
|
+
summary: `published ${liveField} v${nextVersion} for ${objectId}/${name} (${nodeDeltas.length} node delta(s), verified draft run ${draftRunId})`,
|
|
406
|
+
rollbackRef: {
|
|
407
|
+
objectId,
|
|
408
|
+
rowName: name,
|
|
409
|
+
liveField,
|
|
410
|
+
previousVersion: String(row.version || "1"),
|
|
411
|
+
deltaIndex: previousDeltas.length,
|
|
412
|
+
sourceId
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
return NextResponse.json({
|
|
416
|
+
ok: true,
|
|
417
|
+
objectId,
|
|
418
|
+
name,
|
|
419
|
+
version: nextVersion,
|
|
420
|
+
publishedAt,
|
|
421
|
+
liveField,
|
|
422
|
+
publishedSha256,
|
|
423
|
+
receiptId: receipt.receiptId,
|
|
424
|
+
workspaceConfig: persisted
|
|
425
|
+
});
|
|
426
|
+
} catch (error) {
|
|
427
|
+
if (error.code === "WORKSPACE_PERSISTENCE_READ_ONLY") {
|
|
428
|
+
return NextResponse.json({
|
|
429
|
+
ok: false,
|
|
430
|
+
code: "read_only",
|
|
431
|
+
error: "workspace config is read-only in this runtime",
|
|
432
|
+
guidance: error.guidance || null
|
|
433
|
+
}, {
|
|
434
|
+
status: 409
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
if (error.code === "INVALID_WORKSPACE_CONFIG") {
|
|
438
|
+
return NextResponse.json({
|
|
439
|
+
ok: false,
|
|
440
|
+
code: "invalid_config",
|
|
441
|
+
error: error.message,
|
|
442
|
+
details: error.details
|
|
443
|
+
}, {
|
|
444
|
+
status: 400
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
return NextResponse.json({
|
|
448
|
+
ok: false,
|
|
449
|
+
code: "write_failed",
|
|
450
|
+
error: error?.message || "failed to write workspace config"
|
|
451
|
+
}, {
|
|
452
|
+
status: 500
|
|
453
|
+
});
|
|
330
454
|
}
|
|
331
|
-
return NextResponse.json(
|
|
332
|
-
{ ok: false, code: "write_failed", error: error?.message || "failed to write workspace config" },
|
|
333
|
-
{ status: 500 }
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
455
|
}
|
|
337
|
-
|
|
338
|
-
export { POST };
|