From e22401e9c83296894a3e23048400436cbeb8876c Mon Sep 17 00:00:00 2001 From: keypair34 <216233964+keypair34@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:10:10 +0200 Subject: [PATCH] Handle AstroJS rsync --- crates/cli/src/cloud-deploy/mod.rs | 44 ++++++++++++++++--- .../cloud-deploy/process_deploy_vite_spa.rs | 8 ++-- .../cli/src/cloud-deploy/process_migrate.rs | 1 + crates/smbcloud-deploy/src/known_hosts.rs | 12 +++++ crates/smbcloud-model/src/project.rs | 4 ++ 5 files changed, 58 insertions(+), 11 deletions(-) diff --git a/crates/cli/src/cloud-deploy/mod.rs b/crates/cli/src/cloud-deploy/mod.rs index 7f945d5a..77a19664 100644 --- a/crates/cli/src/cloud-deploy/mod.rs +++ b/crates/cli/src/cloud-deploy/mod.rs @@ -27,7 +27,7 @@ pub(crate) use smbcloud_deploy::known_hosts; /// Build the rsync transport for the current project. /// /// This resolves the front-end-specific bits the engine deliberately doesn't -/// know about: the server host (from the runner), the remote path (from config, +/// know about: the server host (from the runner or an approved override), the remote path (from config, /// defaulting to `apps/web/`), and the user's local SSH identity file. pub(crate) fn rsync_transport( config: &Config, @@ -42,9 +42,41 @@ pub(crate) fn rsync_transport( None => format!("apps/web/{}", config.project.name), }; - Ok(RsyncTransport::new( - runner.rsync_host(), - remote_path, - identity_file, - )) + let rsync_host = resolve_rsync_host(config.project.rsync_host.as_deref(), runner)?; + + Ok(RsyncTransport::new(rsync_host, remote_path, identity_file)) +} + +pub(crate) fn resolve_rsync_host(configured_host: Option<&str>, runner: &Runner) -> Result { + let host = configured_host + .map(str::to_owned) + .unwrap_or_else(|| runner.rsync_host()); + + if !known_hosts::is_pinned_host(&host) { + return Err(anyhow!( + "Unsupported rsync host '{host}'. Choose a smbCloud host with a pinned SSH key." + )); + } + + Ok(host) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn configured_rsync_host_overrides_runner_default() { + let host = resolve_rsync_host(Some("api-1.smbcloud.xyz"), &Runner::NodeJs) + .expect("pinned host should be accepted"); + + assert_eq!(host, "api-1.smbcloud.xyz"); + } + + #[test] + fn unpinned_rsync_host_is_rejected() { + let result = resolve_rsync_host(Some("example.com"), &Runner::NodeJs); + + assert!(result.is_err()); + } } diff --git a/crates/cli/src/cloud-deploy/process_deploy_vite_spa.rs b/crates/cli/src/cloud-deploy/process_deploy_vite_spa.rs index b353daa8..a6aad98f 100644 --- a/crates/cli/src/cloud-deploy/process_deploy_vite_spa.rs +++ b/crates/cli/src/cloud-deploy/process_deploy_vite_spa.rs @@ -62,13 +62,11 @@ pub async fn process_deploy_vite_spa(env: Environment, config: Config) -> Result .await .ok(); - // ── Step 3: rsync // to api.smbcloud.xyz ─────── + // ── Step 3: rsync // to the configured host ─── // // config.project.path holds the remote destination on the server - // (e.g. "apps/web/myapp"). rsync_deploy appends a - // trailing slash and targets git@api.smbcloud.xyz:/ using the - // pinned known-hosts and the user's smbCloud SSH key — exactly the same - // transport used for static site deployments. + // (e.g. "apps/web/myapp"). The transport appends a trailing slash and + // uses the selected host's pinned SSH key and the user's smbCloud identity. let runner = config.project.runner; let transport = crate::deploy::rsync_transport(&config, &runner, user.id)?; diff --git a/crates/cli/src/cloud-deploy/process_migrate.rs b/crates/cli/src/cloud-deploy/process_migrate.rs index 84f7800a..4432b89a 100644 --- a/crates/cli/src/cloud-deploy/process_migrate.rs +++ b/crates/cli/src/cloud-deploy/process_migrate.rs @@ -188,6 +188,7 @@ fn strip_project(project: &Project) -> Project { path: None, repository: None, deploy_repo_id: None, + rsync_host: None, source_path: None, created_at: project.created_at, updated_at: project.updated_at, diff --git a/crates/smbcloud-deploy/src/known_hosts.rs b/crates/smbcloud-deploy/src/known_hosts.rs index 94186824..c778f441 100644 --- a/crates/smbcloud-deploy/src/known_hosts.rs +++ b/crates/smbcloud-deploy/src/known_hosts.rs @@ -49,6 +49,11 @@ pub fn for_host(rsync_host: &str) -> &'static str { } } +/// Returns whether `host` has a host key pinned by this crate. +pub fn is_pinned_host(host: &str) -> bool { + matches!(host, "api.smbcloud.xyz" | "api-1.smbcloud.xyz") +} + #[cfg(test)] mod tests { use super::*; @@ -77,4 +82,11 @@ mod tests { assert!(API_SMBCLOUD_XYZ.contains("AAAA")); assert!(API_1_SMBCLOUD_XYZ.contains("AAAA")); } + + #[test] + fn rejects_unpinned_hosts() { + assert!(is_pinned_host("api.smbcloud.xyz")); + assert!(is_pinned_host("api-1.smbcloud.xyz")); + assert!(!is_pinned_host("example.com")); + } } diff --git a/crates/smbcloud-model/src/project.rs b/crates/smbcloud-model/src/project.rs index 130e4e2d..0adcbc52 100644 --- a/crates/smbcloud-model/src/project.rs +++ b/crates/smbcloud-model/src/project.rs @@ -75,6 +75,10 @@ pub struct Project { /// Repo ID backing this deploy target. Optional until the API exposes it /// consistently to the CLI. pub deploy_repo_id: Option, + /// Approved rsync host override for deployments that do not use the + /// runner's default static tier. + #[serde(default)] + pub rsync_host: Option, /// Repo-relative app path for monorepo targets, e.g. "apps/web/console". pub source_path: Option, #[serde(default = "default_datetime")]