fix(deploy-to-hub): reset draft/publish state on workspace switch

The workspace-switch effect only reset load-derived state, so phase,
draftItems, recordings, hub/bundle metadata, hubVersion, deploymentStatus,
effectiveSlug and hubItemIds survived a switch — a draft built in one
workspace could publish its items/slug under the next workspace's auth.
Reset the full publish session on switch. Also drop explanatory comments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
tristantr
2026-06-08 17:23:11 +02:00
co-authored by Claude Opus 4.8
parent 163bcf147c
commit 8a9683d50d
3 changed files with 13 additions and 13 deletions
-8
View File
@@ -57,7 +57,6 @@ async fn publish_draft(
Json(body): Json<PublishDraftBody>,
) -> Result<impl IntoResponse, Error> {
require_admin(authed.is_admin, &authed.username)?;
// source_id is injected from the trusted `workspace` path by forward_to_hub.
forward_to_hub(
"/projects",
&workspace,
@@ -311,11 +310,6 @@ async fn get_project_export(
get_from_hub(&format!("/projects/{}/export", slug), &workspace).await
}
// `source_id` is the server-trusted workspace from the `{workspace}` path — NOT
// client-supplied — so the Hub can enforce that the targeted project belongs to
// the calling workspace. Without it, any workspace admin could mutate or read
// another workspace's Hub project by passing its `project_slug`, since the Hub
// authenticates every call with the same instance-wide HUB_DEV_TOKEN.
async fn get_from_hub(path: &str, source_id: &str) -> Result<(StatusCode, String), Error> {
let hub_token = std::env::var("HUB_DEV_TOKEN")
.map_err(|_| Error::BadRequest("HUB_DEV_TOKEN not set".into()))?;
@@ -349,8 +343,6 @@ async fn forward_to_hub<T: Serialize>(
let url = format!("{}{}", **HUB_BASE_URL.load(), path);
// Stamp the trusted workspace onto the payload so the Hub can verify
// ownership; never trust the client-supplied project_slug alone.
let mut payload = serde_json::to_value(body).map_err(to_anyhow)?;
let obj = payload
.as_object_mut()
@@ -392,6 +392,19 @@
schedulePreviews = {}
selectedFolder = undefined
manualDeselected = new Set()
phase = 'predeploy'
draftItems = []
recordings = {}
deploymentStatus = {}
hubItemIds = {}
effectiveSlug = ''
hubVersion = 0
hubName = ''
hubSummary = ''
hubReadme = ''
bundleName = ''
bundleSummary = ''
bundleReadme = ''
loadWorkspace($workspaceStore, seq)
loadTriggers($workspaceStore, seq)
}
@@ -50,7 +50,6 @@
)
if (!res.ok) throw new Error(`export ${res.status}: ${await res.text()}`)
data = JSON.parse(await res.text())
// Default the target folder to the project slug.
if (data && !folderName) folderName = data.project.slug
} catch (e: any) {
loadError = e?.message ?? String(e)
@@ -85,9 +84,6 @@
// Minimal non-public policy for re-created apps.
const defaultPolicy = { execution_mode: 'publisher', triggerables_v2: {} } as any
// Everything in the bundle lives under `f/<slug>/`. To import into a different
// folder we swap that prefix everywhere (item paths, `$res:`/script refs,
// schedule runnable paths) by rewriting the serialized bundle in one pass.
function retarget(bundle: ProjectExport, fromSlug: string, folder: string): ProjectExport {
if (folder === fromSlug) return bundle
const json = JSON.stringify(bundle).split(`f/${fromSlug}/`).join(`f/${folder}/`)
@@ -101,7 +97,6 @@
results = []
done = false
try {
// Ensure the target folder exists (no-op/ignored if it already does).
try {
await FolderService.createFolder({ workspace, requestBody: { name: folder } })
} catch {}