diff --git a/Cargo.lock b/Cargo.lock index c74dffb..9399072 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,6 +125,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base256emoji" version = "1.0.2" @@ -141,6 +147,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bendy" version = "0.3.3" @@ -165,6 +177,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -211,6 +232,12 @@ dependencies = [ "unsigned-varint", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const-str" version = "0.4.3" @@ -244,6 +271,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -280,6 +319,16 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -287,7 +336,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -301,6 +352,39 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "errno" version = "0.3.14" @@ -339,6 +423,16 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "find-msvc-tools" version = "0.1.8" @@ -423,6 +517,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -458,12 +553,32 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.4.0" @@ -706,6 +821,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + [[package]] name = "keccak" version = "0.1.5" @@ -804,12 +933,19 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "meta-feeder-sdk" -version = "1.7.0" -source = "git+https://github.com/worph/meta-feeder-sdk?tag=v1.7.0#027917c8cffe7ca43dadcf3b426cc5f69ecc443a" +version = "1.10.0" +source = "git+https://gitea-watch.nsl.sh/metastar/meta-feeder-sdk?tag=v1.10.0#f804d3adb19cc657cd15645e6ad134c767d30b33" dependencies = [ + "anyhow", + "base64", + "bs58", + "k256", "libc", + "rand_core 0.6.4", "serde", "serde_json", + "sha2", + "thiserror 1.0.69", "tokio", "tracing", ] @@ -836,7 +972,7 @@ dependencies = [ "sha2", "sha3", "tempfile", - "thiserror", + "thiserror 2.0.18", "tokio", "tower-http", "tracing", @@ -957,6 +1093,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -998,7 +1144,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -1019,7 +1165,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -1061,7 +1207,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.5", ] [[package]] @@ -1071,7 +1217,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -1150,6 +1305,16 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -1242,6 +1407,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "serde" version = "1.0.228" @@ -1365,6 +1544,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "slab" version = "0.4.11" @@ -1387,6 +1576,16 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -1466,13 +1665,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1059eaa..44e5967 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ serde_json = "1" # Beacon v2: advertise as metamesh.enrich/full-hash so meta-sort's Plugins # page finds it (Scan → Add). Only the SDK's light `beacon` feature. -meta-feeder-sdk = { git = "https://github.com/worph/meta-feeder-sdk", tag = "v1.7.0", default-features = false, features = ["beacon"] } +meta-feeder-sdk = { git = "https://gitea-watch.nsl.sh/metastar/meta-feeder-sdk", tag = "v1.10.0", default-features = false, features = ["beacon", "claims"] } # HTTP client reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] } diff --git a/src/client/meta_core.rs b/src/client/meta_core.rs index 7339e55..bcf930c 100644 --- a/src/client/meta_core.rs +++ b/src/client/meta_core.rs @@ -1,13 +1,34 @@ use anyhow::{Context, Result}; use reqwest::Client; -use serde_json::json; +use serde::Deserialize; use std::collections::HashMap; +use std::sync::OnceLock; use std::time::Duration; const REQUEST_TIMEOUT: Duration = Duration::from_secs(30); const MAX_RETRIES: u32 = 3; const RETRY_DELAY: Duration = Duration::from_millis(500); +/// One per-claim verdict from `POST /api/claims`. +#[derive(Deserialize)] +struct ClaimStatus { + status: String, + #[serde(default)] + reason: Option, +} + +/// This plugin's claim signer, loaded once from `METAMESH_SIGNING_KEY` / +/// `METAMESH_SIGNING_KEY_FILE` (generated on first use when only the file is +/// set). A missing key fails every write instead of signing with a silent +/// ephemeral identity. +fn signer() -> Result<&'static meta_feeder_sdk::Signer> { + static SIGNER: OnceLock> = OnceLock::new(); + SIGNER + .get_or_init(|| meta_feeder_sdk::load_or_create_key().map_err(|e| format!("{e:#}"))) + .as_ref() + .map_err(|e| anyhow::anyhow!("no signing key: {e}")) +} + pub struct MetaCoreClient { client: Client, base_url: String, @@ -26,88 +47,57 @@ impl MetaCoreClient { }) } - /// Set a single property for a hash ID - pub async fn set_property(&self, hash_id: &str, key: &str, value: &str) -> Result<()> { - let url = format!("{}/meta/{}/{}", self.base_url, hash_id, key); + /// Sign `metadata` as this plugin's claims on `cid` and submit them to + /// `POST /api/claims` (claims model §9) — the replacement for the old + /// unsigned `PATCH /meta/{hash}` / per-key `PUT`. `cid` must be the content + /// cid the request named, never a meta-core rid. Retries transient + /// failures; a refused claim (bad signature / bad request) is an error. + pub async fn merge_metadata( + &self, + cid: &str, + metadata: &HashMap, + ) -> Result<()> { + let signer = signer()?; + let fields: std::collections::BTreeMap = + metadata.iter().map(|(k, v)| (k.clone(), v.clone())).collect(); + let claims = signer.sign_fields(cid, &fields, meta_feeder_sdk::claims::now_ms()); + let url = format!("{}/api/claims", self.base_url); for attempt in 1..=MAX_RETRIES { - match self - .client - .put(&url) - .json(&json!({ "value": value })) - .send() - .await - { + match self.client.post(&url).json(&claims).send().await { Ok(resp) if resp.status().is_success() => { + let verdicts: Vec = + resp.json().await.context("decode POST /api/claims")?; + if let Some(bad) = verdicts + .iter() + .find(|v| v.status == "bad_signature" || v.status == "bad_request") + { + anyhow::bail!( + "meta-core refused a claim for {}: {} {}", + cid, + bad.status, + bad.reason.as_deref().unwrap_or("") + ); + } return Ok(()); } Ok(resp) => { let status = resp.status(); let body = resp.text().await.unwrap_or_default(); if attempt == MAX_RETRIES { - anyhow::bail!( - "Failed to set property {} for {}: {} - {}", - key, - hash_id, - status, - body - ); + anyhow::bail!("POST /api/claims for {}: {} - {}", cid, status, body); } - tracing::warn!( - "Retry {}/{} for set_property: {} - {}", - attempt, - MAX_RETRIES, - status, - body - ); + tracing::warn!("Retry {}/{} for POST /api/claims: {} - {}", attempt, MAX_RETRIES, status, body); } Err(e) => { if attempt == MAX_RETRIES { - return Err(e).context(format!( - "Failed to set property {} for {}", - key, hash_id - )); + return Err(e).context(format!("POST /api/claims for {cid}")); } - tracing::warn!("Retry {}/{} for set_property: {}", attempt, MAX_RETRIES, e); + tracing::warn!("Retry {}/{} for POST /api/claims: {}", attempt, MAX_RETRIES, e); } } tokio::time::sleep(RETRY_DELAY).await; } - - Ok(()) - } - - /// Merge multiple properties at once - pub async fn merge_metadata( - &self, - hash_id: &str, - metadata: &HashMap, - ) -> Result<()> { - // Try batch endpoint first - let url = format!("{}/meta/{}", self.base_url, hash_id); - - match self.client.patch(&url).json(metadata).send().await { - Ok(resp) if resp.status().is_success() => { - return Ok(()); - } - Ok(resp) if resp.status().as_u16() == 404 => { - // Batch endpoint not available, fall back to individual sets - tracing::debug!("Batch endpoint not available, using individual sets"); - } - Ok(resp) => { - let status = resp.status(); - tracing::warn!("Batch merge failed with {}, falling back to individual sets", status); - } - Err(e) => { - tracing::warn!("Batch merge error: {}, falling back to individual sets", e); - } - } - - // Fall back to individual property sets - for (key, value) in metadata { - self.set_property(hash_id, key, value).await?; - } - Ok(()) }