Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions crates/cli/src/cloud-deploy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>`), and the user's local SSH identity file.
pub(crate) fn rsync_transport(
config: &Config,
Expand All @@ -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<String> {
Comment thread
setoelkahfi marked this conversation as resolved.
let host = configured_host
.map(str::to_owned)
.unwrap_or_else(|| runner.rsync_host());
Comment thread
setoelkahfi marked this conversation as resolved.

if !known_hosts::is_pinned_host(&host) {
Comment thread
setoelkahfi marked this conversation as resolved.
return Err(anyhow!(
"Unsupported rsync host '{host}'. Choose a smbCloud host with a pinned SSH key."
));
}
Comment thread
setoelkahfi marked this conversation as resolved.

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());
}
}
8 changes: 3 additions & 5 deletions crates/cli/src/cloud-deploy/process_deploy_vite_spa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,11 @@ pub async fn process_deploy_vite_spa(env: Environment, config: Config) -> Result
.await
.ok();

// ── Step 3: rsync <project_path>/<output_dir>/ to api.smbcloud.xyz ───────
// ── Step 3: rsync <project_path>/<output_dir>/ 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:<path>/ 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)?;
Expand Down
1 change: 1 addition & 0 deletions crates/cli/src/cloud-deploy/process_migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions crates/smbcloud-deploy/src/known_hosts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Comment thread
setoelkahfi marked this conversation as resolved.
matches!(host, "api.smbcloud.xyz" | "api-1.smbcloud.xyz")
Comment thread
setoelkahfi marked this conversation as resolved.
}
Comment thread
setoelkahfi marked this conversation as resolved.

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -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"));
}
}
4 changes: 4 additions & 0 deletions crates/smbcloud-model/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<i64>,
/// Approved rsync host override for deployments that do not use the
/// runner's default static tier.
#[serde(default)]
pub rsync_host: Option<String>,
/// Repo-relative app path for monorepo targets, e.g. "apps/web/console".
pub source_path: Option<String>,
#[serde(default = "default_datetime")]
Expand Down
Loading