From 57ce9d61f4486fa7e1a21c1d7b449b5ea3dac88c Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Mon, 9 Nov 2026 13:04:47 +0000 Subject: [PATCH] fix(apps): signed host allowlist per app; e2e tests never join a live network; v0.3.5 Found live on dev2: apps_e2e's loopback nodes discovered the real network over mDNS, took copies of a real app (they had no owner allowlist), received its sealed secrets from the real holder as legitimate newcomers, promoted at higher epochs and published "snapshots" of the test's fake state encrypted with the real app key. The real node then restored those ("tar: invalid tar magic") and the demo app's state was lost. - AppRecord.hosts (signed; omitted when empty so existing records verify): peers allowed to hold the app. Each app sees a view scoped to its list, so nodes off it never take a copy, are never elected, are never sealed secrets and are never a snapshot source; a holder dropped from the list lets go. `c0mpute apps deploy --host `, `hosts = [...]` in c0mpute.toml, `hosts` in the MCP deploy tool. Docs say plainly that without it every holder gets the app's secrets. - apps_e2e and hosting_parts_e2e set C0MPUTE_DISABLE_MDNS: loopback peers only. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 34 ++++++------ Cargo.toml | 2 +- apps/tui/package.json | 2 +- apps/web/package.json | 2 +- .../web/src/app/releases/latest.json/route.ts | 2 +- docs/apps.md | 6 +++ node/crates/c0mpute-cli/src/apps_mcp.rs | 6 +++ node/crates/c0mpute-cli/src/main.rs | 7 +++ node/crates/c0mpute-core/src/apps/deploy.rs | 7 +++ node/crates/c0mpute-core/src/apps/mod.rs | 30 +++++++++-- node/crates/c0mpute-core/src/apps/record.rs | 54 +++++++++++++++++++ node/crates/c0mpute-core/src/hosting/site.rs | 2 +- node/crates/c0mpute-core/tests/apps_e2e.rs | 4 ++ .../c0mpute-core/tests/hosting_parts_e2e.rs | 54 +++++++++++++++---- package.json | 2 +- packages/captcha/package.json | 2 +- packages/shared/package.json | 2 +- plugins/ad-generator-agent/module.toml | 2 +- plugins/ads-manager/module.toml | 2 +- plugins/apps/module.toml | 2 +- plugins/captcha/module.toml | 2 +- plugins/coinpay/module.toml | 2 +- plugins/crawl/module.toml | 2 +- plugins/crowdgpt/module.toml | 2 +- plugins/ctv-ads/module.toml | 2 +- plugins/diffusion/module.toml | 2 +- plugins/embed/module.toml | 2 +- plugins/gameserver/module.toml | 2 +- plugins/hosting/module.toml | 2 +- plugins/infernet/module.toml | 2 +- plugins/live-stream/module.toml | 2 +- plugins/music/module.toml | 2 +- plugins/ocr/module.toml | 2 +- plugins/podcast-ads/module.toml | 2 +- plugins/podcasting/module.toml | 2 +- plugins/podcasts/module.toml | 2 +- plugins/render/module.toml | 2 +- plugins/secure-chat/module.toml | 2 +- plugins/storage/module.toml | 2 +- plugins/train/module.toml | 2 +- plugins/transcode/module.toml | 2 +- plugins/tts/module.toml | 2 +- plugins/video/module.toml | 2 +- plugins/webhook/module.toml | 2 +- plugins/whisper/module.toml | 2 +- plugins/workflow-step/module.toml | 2 +- 46 files changed, 209 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 185fe345..3d735ff8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -486,7 +486,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "c0mpute-api" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "c0mpute-proto", @@ -501,7 +501,7 @@ dependencies = [ [[package]] name = "c0mpute-bench" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "blake3", @@ -512,7 +512,7 @@ dependencies = [ [[package]] name = "c0mpute-cli" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "axum", @@ -546,7 +546,7 @@ dependencies = [ [[package]] name = "c0mpute-core" -version = "0.3.4" +version = "0.3.5" dependencies = [ "aes-gcm", "anyhow", @@ -596,7 +596,7 @@ dependencies = [ [[package]] name = "c0mpute-doctor" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "serde", @@ -608,7 +608,7 @@ dependencies = [ [[package]] name = "c0mpute-envelope" -version = "0.3.4" +version = "0.3.5" dependencies = [ "blake3", "bs58", @@ -621,7 +621,7 @@ dependencies = [ [[package]] name = "c0mpute-gateway" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "axum", @@ -643,7 +643,7 @@ dependencies = [ [[package]] name = "c0mpute-market" -version = "0.3.4" +version = "0.3.5" dependencies = [ "c0mpute-envelope", "serde", @@ -653,7 +653,7 @@ dependencies = [ [[package]] name = "c0mpute-net" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "async-trait", @@ -671,7 +671,7 @@ dependencies = [ [[package]] name = "c0mpute-placement" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "async-trait", @@ -692,7 +692,7 @@ dependencies = [ [[package]] name = "c0mpute-proto" -version = "0.3.4" +version = "0.3.5" dependencies = [ "blake3", "hex", @@ -704,7 +704,7 @@ dependencies = [ [[package]] name = "c0mpute-sealed" -version = "0.3.4" +version = "0.3.5" dependencies = [ "aes", "aes-gcm", @@ -726,7 +726,7 @@ dependencies = [ [[package]] name = "c0mpute-secure-chat" -version = "0.3.4" +version = "0.3.5" dependencies = [ "aes-gcm", "anyhow", @@ -749,7 +749,7 @@ dependencies = [ [[package]] name = "c0mpute-store" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "blake3", @@ -767,7 +767,7 @@ dependencies = [ [[package]] name = "c0mpute-transcode" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "c0mpute-proto", @@ -780,7 +780,7 @@ dependencies = [ [[package]] name = "c0mpute-update" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "hex", @@ -795,7 +795,7 @@ dependencies = [ [[package]] name = "c0mpute-verify" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "blake3", diff --git a/Cargo.toml b/Cargo.toml index 09d9c622..939e857d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ members = [ ] [workspace.package] -version = "0.3.4" +version = "0.3.5" edition = "2024" license = "MIT" repository = "https://github.com/profullstack/c0mpute" diff --git a/apps/tui/package.json b/apps/tui/package.json index 05c215d1..9a1de6eb 100644 --- a/apps/tui/package.json +++ b/apps/tui/package.json @@ -1,6 +1,6 @@ { "name": "@c0mpute/tui", - "version": "0.3.4", + "version": "0.3.5", "private": true, "type": "module", "bin": { diff --git a/apps/web/package.json b/apps/web/package.json index 4dddd73d..635dec47 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "c0mpute", - "version": "0.3.4", + "version": "0.3.5", "private": true, "type": "module", "scripts": { diff --git a/apps/web/src/app/releases/latest.json/route.ts b/apps/web/src/app/releases/latest.json/route.ts index 054b949e..86ef0ecf 100644 --- a/apps/web/src/app/releases/latest.json/route.ts +++ b/apps/web/src/app/releases/latest.json/route.ts @@ -28,7 +28,7 @@ export const dynamic = "force-dynamic"; * this in sync with the workspace version in `Cargo.toml` on each release so a * freshly built CLI reports "already latest" rather than a phantom upgrade. */ -const FALLBACK_VERSION = "0.3.4"; +const FALLBACK_VERSION = "0.3.5"; const FALLBACK_MIN_REQUIRED = "0.0.1"; type Channel = "stable" | "beta" | "nightly"; diff --git a/docs/apps.md b/docs/apps.md index 80344775..6b335a42 100644 --- a/docs/apps.md +++ b/docs/apps.md @@ -97,6 +97,12 @@ Every gateway routes to apps whether or not it runs any. snapshot are lost. This is a replicated single-node database, not a distributed one (no Raft yet; rqlite/dqlite as an app needs node-to-node links, which come next). +- **Pin your hosts for anything private.** By default any `apps` node may + hold an app, and every holder receives its secrets and app key. `--host + ` (repeatable; `hosts = [...]` in `c0mpute.toml`) signs an allowlist + into the record: holders seal secrets only to, and take snapshots only from, + those peers, and nodes off the list never take a copy. Operators can + likewise limit whose apps they run with `[apps] owners`. - **Operators can read a running app.** The node running the container sees its memory and its state volume, as with any VPS host. Snapshots at rest on standbys are encrypted. diff --git a/node/crates/c0mpute-cli/src/apps_mcp.rs b/node/crates/c0mpute-cli/src/apps_mcp.rs index 9abe2ca4..fd6fa1a4 100644 --- a/node/crates/c0mpute-cli/src/apps_mcp.rs +++ b/node/crates/c0mpute-cli/src/apps_mcp.rs @@ -34,6 +34,7 @@ fn tools() -> Value { "memory_mb": {"type": "integer"}, "replicas": {"type": "integer"}, "domains": {"type": "array", "items": {"type": "string"}}, + "hosts": {"type": "array", "items": {"type": "string"}, "description": "Peer ids allowed to hold the app; empty = any apps node (each holder gets the app's secrets)"}, "wait_secs": {"type": "integer"} }, "required": ["name"] @@ -113,6 +114,10 @@ async fn call(gateway: &str, name: &str, a: &Value) -> Result { } } } + if let Some(h) = a.get("hosts").and_then(Value::as_array) { + opts.hosts + .extend(h.iter().filter_map(Value::as_str).map(str::to_string)); + } if let Some(d) = a.get("domains").and_then(Value::as_array) { opts.domains .extend(d.iter().filter_map(Value::as_str).map(str::to_string)); @@ -168,6 +173,7 @@ fn base(name: &str, source: deploy::Source, wait: Duration) -> deploy::DeployOpt millicpus: 500, replicas: c0mpute_core::apps::record::DEFAULT_REPLICAS, domains: vec![], + hosts: vec![], wait, } } diff --git a/node/crates/c0mpute-cli/src/main.rs b/node/crates/c0mpute-cli/src/main.rs index 7111dc49..9721d2ee 100644 --- a/node/crates/c0mpute-cli/src/main.rs +++ b/node/crates/c0mpute-cli/src/main.rs @@ -749,6 +749,10 @@ enum AppsCmd { /// `_c0mpute-app.` = `app=`. #[arg(long = "domain")] domains: Vec, + /// Peer id allowed to hold and run the app (repeatable). Without it + /// any apps node may, and every holder receives the app's secrets. + #[arg(long = "host")] + hosts: Vec, /// Give up waiting for a healthy primary after this many seconds. #[arg(long, default_value_t = 600)] wait_secs: u64, @@ -3859,6 +3863,7 @@ async fn run_apps(cmd: AppsCmd) -> Result<()> { cpus, replicas, domains, + hosts, wait_secs, } => { let wait = std::time::Duration::from_secs(wait_secs); @@ -3883,6 +3888,7 @@ async fn run_apps(cmd: AppsCmd) -> Result<()> { millicpus: 500, replicas: c0mpute_core::apps::record::DEFAULT_REPLICAS, domains: vec![], + hosts: vec![], wait, }, }; @@ -3924,6 +3930,7 @@ async fn run_apps(cmd: AppsCmd) -> Result<()> { opts.replicas = r; } opts.domains.extend(domains); + opts.hosts.extend(hosts); opts.wait = wait; let rec = deploy::deploy(opts).await?; println!("{}", rec.app_id); diff --git a/node/crates/c0mpute-core/src/apps/deploy.rs b/node/crates/c0mpute-core/src/apps/deploy.rs index f30eef0f..7340e490 100644 --- a/node/crates/c0mpute-core/src/apps/deploy.rs +++ b/node/crates/c0mpute-core/src/apps/deploy.rs @@ -48,6 +48,8 @@ pub struct DeployOptions { pub millicpus: u32, pub replicas: u8, pub domains: Vec, + /// Peer ids allowed to hold the app (empty = any apps node). + pub hosts: Vec, pub wait: Duration, } @@ -256,6 +258,7 @@ pub async fn deploy(opts: DeployOptions) -> Result { millicpus: opts.millicpus, replicas: opts.replicas, domains: opts.domains.clone(), + hosts: opts.hosts.clone(), }; let rec = AppRecord::sign(&keypair, spec, seq)?; if let Some(s) = &secrets { @@ -396,6 +399,9 @@ pub struct AppSection { pub replicas: u8, #[serde(default)] pub domains: Vec, + /// Peer ids allowed to hold the app (empty = any apps node). + #[serde(default)] + pub hosts: Vec, /// A pinned registry image instead of building the repository. pub image: Option, #[serde(default)] @@ -438,6 +444,7 @@ impl AppSection { millicpus: (self.cpus * 1000.0).round() as u32, replicas: self.replicas, domains: self.domains.clone(), + hosts: self.hosts.clone(), wait, } } diff --git a/node/crates/c0mpute-core/src/apps/mod.rs b/node/crates/c0mpute-core/src/apps/mod.rs index ab610c22..c72a45aa 100644 --- a/node/crates/c0mpute-core/src/apps/mod.rs +++ b/node/crates/c0mpute-core/src/apps/mod.rs @@ -300,6 +300,18 @@ fn load_x25519(dir: Option<&PathBuf>) -> crypto::KeyPair { kp } +/// The view as this app sees it: nodes off its host allowlist do not exist +/// for its placement, primary election, snapshots or secrets. +fn scoped(rec: &AppRecord, view: &HashMap) -> HashMap { + if rec.hosts.is_empty() { + return view.clone(); + } + view.iter() + .filter(|(p, _)| rec.allows(p)) + .map(|(p, n)| (p.clone(), n.clone())) + .collect() +} + /// Write a file only its owner can read. pub fn write_private(path: &std::path::Path, bytes: &[u8]) -> std::io::Result<()> { let tmp = path.with_extension("tmp"); @@ -567,6 +579,9 @@ impl Apps { } // Only the owner or a node that holds the app may hand out secrets. let from_owner = rec.owner_peer().as_deref() == Some(src); + if !from_owner && !rec.allows(src) { + return; + } let from_holder = self .0 .nodes @@ -775,8 +790,9 @@ impl Apps { let view = self.live_view().await; let mut changed = false; for rec in records { - changed |= self.step(&rec, &view).await; - self.seal_to_holders(&rec, &view).await; + let v = scoped(&rec, &view); + changed |= self.step(&rec, &v).await; + self.seal_to_holders(&rec, &v).await; } // Apps this node holds whose record vanished: nothing to do but wait. if changed { @@ -790,6 +806,11 @@ impl Apps { async fn step(&self, rec: &AppRecord, view: &HashMap) -> bool { let id = rec.app_id.clone(); let holding = self.0.local.read().await.contains_key(&id); + if holding && !rec.deleted && !rec.allows(&self.0.me) { + info!(app = %id, "apps: no longer on the app's host list; dropping"); + self.drop_app(&id).await; + return true; + } if rec.deleted { if holding { info!(app = %id, "apps: deleted by owner; dropping"); @@ -1469,6 +1490,7 @@ impl Apps { } pub async fn info(&self, rec: &AppRecord, view: &HashMap) -> AppInfo { + let view = &scoped(rec, view); let holders = placement::holders(&rec.app_id, view); let p = placement::primary(&rec.app_id, rec.needs_secrets(), view); let (primary, epoch, healthy) = match &p { @@ -1520,7 +1542,7 @@ impl Apps { /// Owner side: the last `tail` log lines from the primary. pub async fn logs(&self, app_id: &str, tail: u32) -> Result { let rec = self.lookup_record(app_id).await.context("unknown app")?; - let view = self.live_view().await; + let view = scoped(&rec, &self.live_view().await); let peer = placement::primary(app_id, rec.needs_secrets(), &view) .peer() .map(str::to_string) @@ -1538,7 +1560,7 @@ impl Apps { let Some(rec) = self.lookup_record(&req.app_id).await.filter(|r| !r.deleted) else { return (StatusCode::NOT_FOUND, "no such app").into_response(); }; - let view = self.live_view().await; + let view = scoped(&rec, &self.live_view().await); let Some(peer) = placement::primary(&rec.app_id, rec.needs_secrets(), &view) .peer() .map(str::to_string) diff --git a/node/crates/c0mpute-core/src/apps/record.rs b/node/crates/c0mpute-core/src/apps/record.rs index e3e9e9cd..798fd0cb 100644 --- a/node/crates/c0mpute-core/src/apps/record.rs +++ b/node/crates/c0mpute-core/src/apps/record.rs @@ -23,6 +23,8 @@ pub const MAX_REPLICAS: u8 = 5; pub const MAX_ENV: usize = 64; pub const MAX_ENV_VALUE: usize = 4096; pub const MAX_DOMAINS: usize = 8; +/// Most peers an app's host allowlist may name. +pub const MAX_HOSTS: usize = 32; /// Memory an app may ask for, MiB. pub const MIN_MEMORY_MB: u32 = 64; pub const MAX_MEMORY_MB: u32 = 8192; @@ -90,6 +92,11 @@ pub struct AppRecord { /// `app=`). #[serde(default)] pub domains: Vec, + /// Peer ids (base58) allowed to hold and run this app. Empty = any apps + /// node. Holders seal secrets only to, and take snapshots only from, + /// peers on this list, so a private app never reaches a stranger's box. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub hosts: Vec, /// A tombstone: holders stop the app and drop its copies. #[serde(default)] pub deleted: bool, @@ -172,6 +179,7 @@ pub struct AppSpec { pub millicpus: u32, pub replicas: u8, pub domains: Vec, + pub hosts: Vec, } impl AppRecord { @@ -210,6 +218,7 @@ impl AppRecord { millicpus: spec.millicpus, replicas: spec.replicas, domains: spec.domains, + hosts: spec.hosts, deleted: false, sig: String::new(), }; @@ -316,6 +325,20 @@ impl AppRecord { if self.domains.len() > MAX_DOMAINS || !self.domains.iter().all(|d| valid_domain(d)) { bail!("invalid domain list"); } + if self.hosts.len() > MAX_HOSTS + || !self + .hosts + .iter() + .all(|h| crate::hosting::site::valid_peer_id(h)) + || self + .hosts + .iter() + .collect::>() + .len() + != self.hosts.len() + { + bail!("hosts must be up to {MAX_HOSTS} distinct peer ids"); + } Ok(()) } @@ -343,6 +366,11 @@ impl AppRecord { Some(pk.to_peer_id().to_base58()) } + /// May `peer` hold and run this app? + pub fn allows(&self, peer: &str) -> bool { + self.hosts.is_empty() || self.hosts.iter().any(|h| h == peer) + } + /// The docker image reference a holder runs. pub fn image_ref(&self) -> &str { match &self.image { @@ -378,9 +406,35 @@ pub(crate) mod tests { millicpus: 500, replicas: 3, domains: vec!["api.example.org".into()], + hosts: vec![], } } + #[test] + fn host_allowlist_is_signed_and_old_records_still_verify() { + let kp = c0mpute_net::libp2p::identity::Keypair::generate_ed25519(); + let open = AppRecord::sign(&kp, spec("demo"), 1).unwrap(); + assert!( + !serde_json::to_string(&open).unwrap().contains("hosts"), + "empty list stays off the wire" + ); + assert!(open.allows("anyone")); + let peer = c0mpute_net::libp2p::identity::Keypair::generate_ed25519() + .public() + .to_peer_id() + .to_base58(); + let mut s = spec("demo"); + s.hosts = vec![peer.clone()]; + let pinned = AppRecord::sign(&kp, s, 2).unwrap(); + assert!(pinned.verify() && pinned.allows(&peer) && !pinned.allows("someone-else")); + let mut bad = pinned.clone(); + bad.hosts.push(peer.clone()); + assert!(!bad.verify(), "the list is covered by the signature"); + let mut s = spec("demo"); + s.hosts = vec!["not a peer".into()]; + assert!(AppRecord::sign(&kp, s, 3).is_err()); + } + #[test] fn sign_verify_and_tamper() { let kp = c0mpute_net::libp2p::identity::Keypair::generate_ed25519(); diff --git a/node/crates/c0mpute-core/src/hosting/site.rs b/node/crates/c0mpute-core/src/hosting/site.rs index 81b85b0e..e7015520 100644 --- a/node/crates/c0mpute-core/src/hosting/site.rs +++ b/node/crates/c0mpute-core/src/hosting/site.rs @@ -308,7 +308,7 @@ pub struct HostingTerms { pub payer: Option, } -fn valid_peer_id(p: &str) -> bool { +pub fn valid_peer_id(p: &str) -> bool { p.len() <= 64 && p.parse::().is_ok() } diff --git a/node/crates/c0mpute-core/tests/apps_e2e.rs b/node/crates/c0mpute-core/tests/apps_e2e.rs index 6808ef1c..c21a9806 100644 --- a/node/crates/c0mpute-core/tests/apps_e2e.rs +++ b/node/crates/c0mpute-core/tests/apps_e2e.rs @@ -144,6 +144,9 @@ async fn apps_place_three_copies_serve_snapshot_and_fail_over() { std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).unwrap(); } unsafe { + // Never discover (and join) a live network on the machine running + // the tests: loopback peers only. + std::env::set_var("C0MPUTE_DISABLE_MDNS", "1"); std::env::set_var(c0mpute_core::container::ENV, &bin); std::env::set_var("FAKE_DIR", &fake); } @@ -210,6 +213,7 @@ async fn apps_place_three_copies_serve_snapshot_and_fail_over() { millicpus: 500, replicas: 3, domains: vec![], + hosts: vec![], }, 1, ) diff --git a/node/crates/c0mpute-core/tests/hosting_parts_e2e.rs b/node/crates/c0mpute-core/tests/hosting_parts_e2e.rs index 019259ef..80029ffb 100644 --- a/node/crates/c0mpute-core/tests/hosting_parts_e2e.rs +++ b/node/crates/c0mpute-core/tests/hosting_parts_e2e.rs @@ -24,6 +24,8 @@ impl ChunkSource for StoreSource { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn a_nine_thousand_file_site_publishes_and_serves() { + // Loopback only: never publish test sites to a live network via mDNS. + unsafe { std::env::set_var("C0MPUTE_DISABLE_MDNS", "1") }; let tmp = tempdir().unwrap(); let site = tmp.path().join("site"); for i in 0..9_000 { @@ -38,8 +40,14 @@ async fn a_nine_thousand_file_site_publishes_and_serves() { let built = build_from_dir(&site, "big").unwrap(); assert!(built.manifest.files.len() > INLINE_FILES); let stored = built.stored(); - assert!(!stored.parts.is_empty(), "a large site gets a parted manifest"); - assert!(matches!(Manifest::from_bytes(&stored.root).unwrap(), Manifest::Parted(_))); + assert!( + !stored.parts.is_empty(), + "a large site gets a parted manifest" + ); + assert!(matches!( + Manifest::from_bytes(&stored.root).unwrap(), + Manifest::Parted(_) + )); let store = ChunkStore::open(tmp.path().join("store")).await.unwrap(); let net = Libp2pNetwork::spawn( @@ -49,14 +57,32 @@ async fn a_nine_thousand_file_site_publishes_and_serves() { ) .await .unwrap(); - let h = Hosting::start(Arc::new(net), store, Options::default()).await.unwrap(); + let h = Hosting::start(Arc::new(net), store, Options::default()) + .await + .unwrap(); let kp = libp2p::identity::Keypair::generate_ed25519(); - let rec = SiteRecord::sign(&kp, "big", &built.manifest_hash(), 1, built.manifest.total_bytes(), vec![]).unwrap(); + let rec = SiteRecord::sign( + &kp, + "big", + &built.manifest_hash(), + 1, + built.manifest.total_bytes(), + vec![], + ) + .unwrap(); h.publish_site(&built, rec.clone()).await.unwrap(); - let (_, body) = h.read_file(&rec, "d7/p8197.html").await.unwrap().expect("file in a late part"); + let (_, body) = h + .read_file(&rec, "d7/p8197.html") + .await + .unwrap() + .expect("file in a late part"); assert_eq!(body, b"page 8197"); - let (_, body) = h.read_file(&rec, "d0/p0.html").await.unwrap().expect("file in the first part"); + let (_, body) = h + .read_file(&rec, "d0/p0.html") + .await + .unwrap() + .expect("file in the first part"); assert_eq!(body, b"page 0"); assert!(h.read_file(&rec, "d0/nope.html").await.unwrap().is_none()); @@ -65,8 +91,18 @@ async fn a_nine_thousand_file_site_publishes_and_serves() { let b = axum::body::to_bytes(r.into_body(), 1 << 20).await.unwrap(); (status, String::from_utf8_lossy(&b).into_owned()) }; - assert_eq!(body_of(h.serve(&rec, "/d3/p4503").await).await, (200, "page 4503".into()), "extensionless path finds .html"); - assert_eq!(body_of(h.serve(&rec, "/docs/").await).await, (200, "docs home".into())); - assert_eq!(body_of(h.serve(&rec, "/missing/thing").await).await, (404, "custom 404".into())); + assert_eq!( + body_of(h.serve(&rec, "/d3/p4503").await).await, + (200, "page 4503".into()), + "extensionless path finds .html" + ); + assert_eq!( + body_of(h.serve(&rec, "/docs/").await).await, + (200, "docs home".into()) + ); + assert_eq!( + body_of(h.serve(&rec, "/missing/thing").await).await, + (404, "custom 404".into()) + ); let _ = Duration::from_secs(0); } diff --git a/package.json b/package.json index 16f37dc3..02f04d02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "c0mpute-monorepo", - "version": "0.3.4", + "version": "0.3.5", "private": true, "workspaces": [ "apps/*", diff --git a/packages/captcha/package.json b/packages/captcha/package.json index d5b07131..55ce5b3d 100644 --- a/packages/captcha/package.json +++ b/packages/captcha/package.json @@ -1,6 +1,6 @@ { "name": "@profullstack/c0mpute-captcha", - "version": "0.3.4", + "version": "0.3.5", "description": "Solve image, slider, gap, click-text, audio and math captchas on the c0mpute network: library (fetch().then(solveCaptcha)), CLI and stdio MCP server.", "type": "module", "license": "MIT", diff --git a/packages/shared/package.json b/packages/shared/package.json index 3d563d9a..82c724c5 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@c0mpute/shared", - "version": "0.3.4", + "version": "0.3.5", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/plugins/ad-generator-agent/module.toml b/plugins/ad-generator-agent/module.toml index 77c33a1e..a92a6b54 100644 --- a/plugins/ad-generator-agent/module.toml +++ b/plugins/ad-generator-agent/module.toml @@ -21,7 +21,7 @@ [module] id = "ad-generator-agent" name = "Ad Generator Agent — every ad type from one URL" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "applications" status = "available" diff --git a/plugins/ads-manager/module.toml b/plugins/ads-manager/module.toml index 77bc8667..d3cf9b02 100644 --- a/plugins/ads-manager/module.toml +++ b/plugins/ads-manager/module.toml @@ -38,7 +38,7 @@ [module] id = "ads-manager" name = "Ads Manager — shared ad operations layer" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "applications" status = "partial" diff --git a/plugins/apps/module.toml b/plugins/apps/module.toml index 8b03b8a6..6c698ba1 100644 --- a/plugins/apps/module.toml +++ b/plugins/apps/module.toml @@ -18,7 +18,7 @@ [module] id = "apps" name = "Apps — backends with 3x redundancy" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "infrastructure" status = "available" diff --git a/plugins/captcha/module.toml b/plugins/captcha/module.toml index 5c7af001..9a4813f3 100644 --- a/plugins/captcha/module.toml +++ b/plugins/captcha/module.toml @@ -22,7 +22,7 @@ [module] id = "captcha" name = "Captcha — image, slider, audio and math solving" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/coinpay/module.toml b/plugins/coinpay/module.toml index 4971456f..aa7256c7 100644 --- a/plugins/coinpay/module.toml +++ b/plugins/coinpay/module.toml @@ -7,7 +7,7 @@ [module] id = "coinpay" name = "CoinPay — DID, payments, escrow, reputation" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "applications" status = "available" diff --git a/plugins/crawl/module.toml b/plugins/crawl/module.toml index aabf9a13..a4d046e9 100644 --- a/plugins/crawl/module.toml +++ b/plugins/crawl/module.toml @@ -8,7 +8,7 @@ [module] id = "crawl" name = "Crawl — distributed web fetching" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "data" status = "available" diff --git a/plugins/crowdgpt/module.toml b/plugins/crowdgpt/module.toml index cb27cb38..45a9e7ed 100644 --- a/plugins/crowdgpt/module.toml +++ b/plugins/crowdgpt/module.toml @@ -43,7 +43,7 @@ [module] id = "crowdgpt" name = "CrowdGPT: donate idle compute" -version = "0.3.4" +version = "0.3.5" kind = "donation" category = "ai" status = "available" diff --git a/plugins/ctv-ads/module.toml b/plugins/ctv-ads/module.toml index f3728056..6c579978 100644 --- a/plugins/ctv-ads/module.toml +++ b/plugins/ctv-ads/module.toml @@ -28,7 +28,7 @@ [module] id = "ctv-ads" name = "CTV Ads — VAST and VMAP from every gateway" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "applications" status = "partial" diff --git a/plugins/diffusion/module.toml b/plugins/diffusion/module.toml index d8b439df..60b3c189 100644 --- a/plugins/diffusion/module.toml +++ b/plugins/diffusion/module.toml @@ -11,7 +11,7 @@ [module] id = "diffusion" name = "Diffusion — image generation" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/embed/module.toml b/plugins/embed/module.toml index 4a340fb9..e5069de0 100644 --- a/plugins/embed/module.toml +++ b/plugins/embed/module.toml @@ -19,7 +19,7 @@ [module] id = "embed" name = "Embeddings — vector generation" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/gameserver/module.toml b/plugins/gameserver/module.toml index 364cac7d..b61b3acc 100644 --- a/plugins/gameserver/module.toml +++ b/plugins/gameserver/module.toml @@ -55,7 +55,7 @@ [module] id = "gameserver" name = "Game server — Minecraft / Factorio / Valheim" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "infrastructure" status = "available" diff --git a/plugins/hosting/module.toml b/plugins/hosting/module.toml index de916b81..701a34a0 100644 --- a/plugins/hosting/module.toml +++ b/plugins/hosting/module.toml @@ -30,7 +30,7 @@ [module] id = "hosting" name = "Hosting — p2p static sites" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "data" status = "available" diff --git a/plugins/infernet/module.toml b/plugins/infernet/module.toml index 1b05edcd..e7e4de8b 100644 --- a/plugins/infernet/module.toml +++ b/plugins/infernet/module.toml @@ -22,7 +22,7 @@ [module] id = "infernet" name = "Infernet Protocol — AI inference" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/live-stream/module.toml b/plugins/live-stream/module.toml index a9f402bd..5c148d36 100644 --- a/plugins/live-stream/module.toml +++ b/plugins/live-stream/module.toml @@ -34,7 +34,7 @@ [module] id = "live-stream" name = "Live Stream — p2p HLS live streaming" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "media" status = "available" diff --git a/plugins/music/module.toml b/plugins/music/module.toml index d3f85188..d205bbf2 100644 --- a/plugins/music/module.toml +++ b/plugins/music/module.toml @@ -15,7 +15,7 @@ [module] id = "music" name = "Music — song generation (ACE-Step 1.5)" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/ocr/module.toml b/plugins/ocr/module.toml index 23530f34..f532c213 100644 --- a/plugins/ocr/module.toml +++ b/plugins/ocr/module.toml @@ -19,7 +19,7 @@ [module] id = "ocr" name = "OCR — document text extraction" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/podcast-ads/module.toml b/plugins/podcast-ads/module.toml index 327b8097..2095ecbf 100644 --- a/plugins/podcast-ads/module.toml +++ b/plugins/podcast-ads/module.toml @@ -31,7 +31,7 @@ [module] id = "podcast-ads" name = "Podcast Ads — dynamic ad insertion" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "applications" status = "partial" diff --git a/plugins/podcasting/module.toml b/plugins/podcasting/module.toml index a2fdd7ba..8e356548 100644 --- a/plugins/podcasting/module.toml +++ b/plugins/podcasting/module.toml @@ -24,7 +24,7 @@ [module] id = "podcasting" name = "Podcasting — p2p podcast publishing" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "media" status = "available" diff --git a/plugins/podcasts/module.toml b/plugins/podcasts/module.toml index cb0baf64..f837427d 100644 --- a/plugins/podcasts/module.toml +++ b/plugins/podcasts/module.toml @@ -24,7 +24,7 @@ [module] id = "podcasts" name = "Podcasts — p2p podcast listener" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "media" status = "available" diff --git a/plugins/render/module.toml b/plugins/render/module.toml index 7c6f555a..c753dc53 100644 --- a/plugins/render/module.toml +++ b/plugins/render/module.toml @@ -33,7 +33,7 @@ [module] id = "render" name = "Render — Blender 3D rendering" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "media" status = "available" diff --git a/plugins/secure-chat/module.toml b/plugins/secure-chat/module.toml index 64550ad7..9bd35597 100644 --- a/plugins/secure-chat/module.toml +++ b/plugins/secure-chat/module.toml @@ -21,7 +21,7 @@ [module] id = "secure-chat" name = "Secure Chat — E2E encrypted p2p messaging" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "applications" status = "available" diff --git a/plugins/storage/module.toml b/plugins/storage/module.toml index 1a56631c..ddb1e165 100644 --- a/plugins/storage/module.toml +++ b/plugins/storage/module.toml @@ -20,7 +20,7 @@ [module] id = "storage" name = "Storage — Reed-Solomon shard service" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "data" status = "available" diff --git a/plugins/train/module.toml b/plugins/train/module.toml index 307127ef..7034f1b4 100644 --- a/plugins/train/module.toml +++ b/plugins/train/module.toml @@ -55,7 +55,7 @@ [module] id = "train" name = "Fine-tune — LoRA training" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/transcode/module.toml b/plugins/transcode/module.toml index d2c5b1ec..453a305c 100644 --- a/plugins/transcode/module.toml +++ b/plugins/transcode/module.toml @@ -7,7 +7,7 @@ [module] id = "transcode" name = "FFmpeg Transcoding" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "media" status = "available" diff --git a/plugins/tts/module.toml b/plugins/tts/module.toml index 45428e0f..6a7f7813 100644 --- a/plugins/tts/module.toml +++ b/plugins/tts/module.toml @@ -15,7 +15,7 @@ [module] id = "tts" name = "TTS — text-to-speech" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/video/module.toml b/plugins/video/module.toml index 89ea3bf8..3af1496e 100644 --- a/plugins/video/module.toml +++ b/plugins/video/module.toml @@ -65,7 +65,7 @@ [module] id = "video" name = "Video — generation (Wan 2.2, Wan 2.1 1.3B on 8 GiB or CPU, LTX-2.3 with audio)" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/webhook/module.toml b/plugins/webhook/module.toml index 1695348b..056eb973 100644 --- a/plugins/webhook/module.toml +++ b/plugins/webhook/module.toml @@ -7,7 +7,7 @@ [module] id = "webhook" name = "Webhooks — paid, end-to-end encrypted delivery" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "infrastructure" status = "available" diff --git a/plugins/whisper/module.toml b/plugins/whisper/module.toml index ad351413..5a382e74 100644 --- a/plugins/whisper/module.toml +++ b/plugins/whisper/module.toml @@ -7,7 +7,7 @@ [module] id = "whisper" name = "Whisper — speech-to-text" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/workflow-step/module.toml b/plugins/workflow-step/module.toml index 8092fd6f..a906a3f1 100644 --- a/plugins/workflow-step/module.toml +++ b/plugins/workflow-step/module.toml @@ -14,7 +14,7 @@ [module] id = "workflow-step" name = "Workflow step — webhook workflow steps (meshhook)" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "infrastructure" status = "available"