commit d53fc30d91f923d934e714c61762336a68b162f0
parent 90caf3a977c00df1acb00b2f2aa19befe576d655
Author: triesap <tyson@radroots.org>
Date: Thu, 6 Aug 2026 19:27:47 +0000
sdk: retire duplicated rust implementation
- remove reusable Rust now owned by canonical lib
- delete the superseded SDK-local generator and fixtures
- retire obsolete Rust release and coverage contracts
- preserve product packages generated bindings and rollback history
Diffstat:
125 files changed, 0 insertions(+), 35455 deletions(-)
diff --git a/contracts/coverage.toml b/contracts/coverage.toml
@@ -1,76 +0,0 @@
-[policy]
-enforce = true
-require_regions = true
-require_functions = true
-require_executable_lines = true
-require_branches = true
-
-[toolchain]
-rust = "1.97.1"
-coverage_rust = "nightly"
-wasm_target = "wasm32-unknown-unknown"
-
-[report]
-output = "target/sdk-coverage/summary.json"
-ignore_filename_regex = "(/target/|/\\.cargo/registry/|/Cellar/rust/|/crates/.+_bindings/|/crates/replica_store_wasm/src/wasm_impl.rs|/tools/xtask/src/(check|contracts|coverage|fs|generate|main|output|package_matrix|wasm)\\.rs)"
-
-[scopes.radroots_sdk]
-paths = ["crates/sdk/src/**"]
-threshold = 90.0
-
-[scopes.radroots]
-paths = ["crates/radroots/src/**"]
-threshold = 90.0
-branches_applicable = false
-branch_reason = "the facade contains straight-line curated constructors and re-exports with no measured branch records"
-
-[scopes.xtask_policy]
-paths = ["tools/xtask/src/coverage_policy.rs"]
-threshold = 90.0
-
-[scopes.event_codec_wasm]
-paths = ["crates/event_codec_wasm/src/**"]
-threshold = 90.0
-
-[scopes.sql_wasm_runtime]
-paths = ["crates/sql_wasm_runtime/src/**"]
-threshold = 90.0
-
-[scopes.replica_sync_wasm]
-paths = ["crates/replica_sync_wasm/src/**"]
-threshold = 90.0
-
-[scopes.replica_store_snapshot]
-paths = ["crates/replica_store_wasm/src/snapshot.rs"]
-threshold = 90.0
-
-[generated]
-typescript = "generated TypeScript is excluded from line coverage and verified by generator tests, generated-output reproducibility, package export checks, and pnpm typecheck; current handwritten package entrypoints only re-export generated surfaces and must gain focused tests if runtime logic is added"
-binding_crates = "generated Rust binding crates are excluded from line coverage and verified by generator unit tests plus generated TypeScript reproducibility"
-wasm_glue = "wasm-bindgen glue is excluded only while pure SDK behavior is covered by native extracted-logic tests and generated package checks"
-
-[exclusions.generated_typescript]
-paths = ["packages/*/src/generated/**", "packages/*/dist/**"]
-reason = "generated package output is checked through cargo xtask check, cargo xtask generate ts, cargo xtask generate wasm, and pnpm typecheck"
-
-[exclusions.generated_binding_crates]
-paths = ["crates/*_bindings/**"]
-reason = "binding crates are generator-owned source facades with behavior covered by xtask generator tests"
-
-[exclusions.wasm_glue_bootstrap]
-paths = ["crates/replica_store_wasm/src/wasm_impl.rs"]
-reason = "temporary bootstrap exclusion for the generated-style DB wrapper forwarding file while exported snapshot policy and sync parsing behavior are covered natively"
-
-[exclusions.xtask_command_glue]
-paths = [
- "tools/xtask/src/check.rs",
- "tools/xtask/src/contracts.rs",
- "tools/xtask/src/coverage.rs",
- "tools/xtask/src/fs.rs",
- "tools/xtask/src/generate.rs",
- "tools/xtask/src/main.rs",
- "tools/xtask/src/output.rs",
- "tools/xtask/src/package_matrix.rs",
- "tools/xtask/src/wasm.rs",
-]
-reason = "xtask command, filesystem, package-generation, and process-runner glue is validated by cargo xtask generate ts, cargo xtask generate wasm, cargo xtask check, package build/typecheck, and cargo xtask coverage run; pure coverage policy enforcement remains under the xtask_policy strict gate"
diff --git a/contracts/releases/api_boundaries.toml b/contracts/releases/api_boundaries.toml
@@ -1,156 +0,0 @@
-schema_version = 1
-spec_id = "radroots.crates.release.v1"
-forbidden_public_paths = [
- "core_foundation",
- "dispatch",
- "jni",
- "keyring",
- "libc",
- "mach2",
- "nostr",
- "nostr_sdk",
- "objc",
- "reqwest",
- "security_framework",
- "sqlx",
- "std::os",
- "tokio",
- "wasm_bindgen",
- "web_sys",
- "windows",
- "windows_core",
- "windows_sys",
-]
-
-[[package]]
-name = "radroots_core"
-allowed_public_paths = []
-
-[[package]]
-name = "radroots_identity"
-allowed_public_paths = []
-
-[[package]]
-name = "radroots_blossom"
-allowed_public_paths = []
-
-[[package]]
-name = "radroots_protocol"
-allowed_public_paths = []
-
-[[package]]
-name = "radroots_event"
-allowed_public_paths = []
-
-[[package]]
-name = "radroots_event_codec"
-allowed_public_paths = []
-
-[[package]]
-name = "radroots_trade"
-allowed_public_paths = []
-
-[[package]]
-name = "radroots_signing"
-allowed_public_paths = []
-
-[[package]]
-name = "radroots_transport"
-allowed_public_paths = []
-
-[[package]]
-name = "radroots_nostr"
-allowed_public_paths = ["nostr"]
-
-[[package]]
-name = "radroots_nostr_connect"
-allowed_public_paths = []
-
-[[package]]
-name = "radroots_secrets"
-allowed_public_paths = []
-
-[[package]]
-name = "radroots_storage"
-allowed_public_paths = []
-
-[[package]]
-name = "radroots_storage_sqlite"
-allowed_public_paths = []
-
-[[package]]
-name = "radroots_transport_nostr"
-allowed_public_paths = []
-
-[[package]]
-name = "radroots_sync"
-allowed_public_paths = []
-
-[[package]]
-name = "radroots_geonames"
-allowed_public_paths = []
-
-[[package]]
-name = "radroots_sdk"
-allowed_public_paths = []
-
-[[package]]
-name = "radroots"
-allowed_public_paths = []
-[[exception]]
-id = "RCRV1-API-006"
-package = "radroots_nostr_connect"
-source = "src/client.rs"
-forbidden_path = "nostr"
-items = [
- "client::RadrootsNostrConnectClientTarget",
- "client::RadrootsNostrConnectClientTarget::new",
- "client::RadrootsNostrConnectClientTransport",
- "client::build_request_event",
- "client::execute_request_with_transport",
- "client::parse_response_event",
-]
-observed_paths = [
- "nostr::Event",
- "nostr::Keys",
- "nostr::PublicKey",
- "nostr::RelayUrl",
-]
-adr = "docs/decisions/0001-public-api-leakage-migration-baseline.md"
-removal_step = 313
-rationale = "Hidden, publish-frozen client signatures remain only for CLI, Myc, and residual first-party cutovers in Steps 271, 288, and 293."
-
-[[exception]]
-id = "RCRV1-API-007"
-package = "radroots_nostr_connect"
-source = "src/message.rs"
-forbidden_path = "nostr"
-items = [
- "message::RadrootsNostrConnectPendingConnectionPollOutcome",
- "message::RadrootsNostrConnectRemoteSessionCapability",
- "message::RadrootsNostrConnectRequest",
- "message::RadrootsNostrConnectResponse",
-]
-observed_paths = [
- "nostr::Event",
- "nostr::PublicKey",
- "nostr::RelayUrl",
- "nostr::UnsignedEvent",
-]
-adr = "docs/decisions/0001-public-api-leakage-migration-baseline.md"
-removal_step = 313
-rationale = "Hidden, publish-frozen aliases remain only for CLI, Myc, and residual first-party cutovers in Steps 271, 288, and 293."
-
-[[exception]]
-id = "RCRV1-API-008"
-package = "radroots_nostr_connect"
-source = "src/uri.rs"
-forbidden_path = "nostr"
-items = [
- "uri::RadrootsNostrConnectBunkerUri",
- "uri::RadrootsNostrConnectClientUri",
-]
-observed_paths = ["nostr::PublicKey", "nostr::RelayUrl"]
-adr = "docs/decisions/0001-public-api-leakage-migration-baseline.md"
-removal_step = 313
-rationale = "Hidden, publish-frozen URI aliases remain only for CLI, Myc, and residual first-party cutovers in Steps 271, 288, and 293."
diff --git a/contracts/releases/api_semver.toml b/contracts/releases/api_semver.toml
@@ -1,8 +0,0 @@
-schema_version = 2
-baseline_revision = "5217d73"
-package_version = "0.1.0-alpha"
-tool = "cargo-public-api"
-minimum_tool_version = "0.52.0"
-policy = "reviewed-breaking-snapshot"
-feature_policy = "all"
-packages = ["radroots", "radroots_sdk"]
diff --git a/contracts/releases/package_tiers.toml b/contracts/releases/package_tiers.toml
@@ -1,78 +0,0 @@
-schema_version = 1
-spec_id = "radroots.crates.release.v1"
-direction = "same_or_lower"
-enforced_dependency_kinds = ["normal", "dev", "build"]
-
-[[tier]]
-id = "foundation"
-rank = 0
-packages = [
- "radroots_core",
- "radroots_identity",
- "radroots_blossom",
- "radroots_protocol",
-]
-
-[[tier]]
-id = "domain"
-rank = 1
-packages = ["radroots_event", "radroots_event_codec", "radroots_trade"]
-
-[[tier]]
-id = "spi"
-rank = 2
-packages = [
- "radroots_signing",
- "radroots_transport",
- "radroots_secrets",
- "radroots_storage",
-]
-
-[[tier]]
-id = "adapter"
-rank = 3
-packages = [
- "radroots_nostr",
- "radroots_nostr_connect",
- "radroots_storage_sqlite",
- "radroots_transport_nostr",
- "radroots_geonames",
-]
-
-[[tier]]
-id = "orchestration"
-rank = 4
-packages = ["radroots_sync"]
-
-[[tier]]
-id = "sdk"
-rank = 5
-packages = ["radroots_sdk"]
-
-[[tier]]
-id = "facade"
-rank = 6
-packages = ["radroots"]
-
-# These exact development-only edges predate the final trade boundary and are
-# removed by the manifest-alignment checkpoint. Normal, build, target, or
-# feature-broadened forms remain forbidden.
-[[temporary_exception]]
-owner = "radroots_trade"
-dependency = "radroots_nostr"
-kind = "dev"
-target = "all"
-features = ["events", "std"]
-uses_default_features = false
-removal_step = 88
-rationale = "Existing trade conformance tests consume Nostr vectors until the Step 088 package boundary alignment."
-
-[[temporary_exception]]
-owner = "radroots_trade"
-dependency = "radroots_transport"
-kind = "dev"
-target = "all"
-features = []
-uses_default_features = false
-removal_step = 88
-rationale = "Existing trade workflow tests use transport test types until the Step 088 package boundary alignment."
diff --git a/contracts/releases/portable_matrix.toml b/contracts/releases/portable_matrix.toml
@@ -1,7 +0,0 @@
-schema_version = 1
-no_std_target = "thumbv7em-none-eabihf"
-no_std_cross_compiler = "zig cc"
-no_std_cross_cflags = "-target thumb-freestanding-eabihf -mcpu=cortex_m4"
-wasm_target = "wasm32-unknown-unknown"
-portable_public_packages = []
-std_only_public_packages = ["radroots", "radroots_sdk"]
diff --git a/contracts/releases/publication.toml b/contracts/releases/publication.toml
@@ -1,67 +0,0 @@
-[publication]
-frozen = false
-registry = "crates-io"
-final_enablement_step = 305
-spec_id = "radroots.crates.release.v1"
-approved_packages = [
- "radroots_core",
- "radroots_identity",
- "radroots_blossom",
- "radroots_protocol",
- "radroots_event",
- "radroots_event_codec",
- "radroots_trade",
- "radroots_signing",
- "radroots_transport",
- "radroots_nostr",
- "radroots_nostr_connect",
- "radroots_secrets",
- "radroots_storage",
- "radroots_storage_sqlite",
- "radroots_transport_nostr",
- "radroots_sync",
- "radroots_geonames",
- "radroots_sdk",
- "radroots",
-]
-local_packages = ["radroots_sdk", "radroots"]
-external_packages = [
- "radroots_core",
- "radroots_identity",
- "radroots_blossom",
- "radroots_protocol",
- "radroots_event",
- "radroots_event_codec",
- "radroots_trade",
- "radroots_signing",
- "radroots_transport",
- "radroots_nostr",
- "radroots_nostr_connect",
- "radroots_secrets",
- "radroots_storage",
- "radroots_storage_sqlite",
- "radroots_transport_nostr",
- "radroots_sync",
- "radroots_geonames",
-]
-
-[workspace_classification]
-private = ["radroots_sdk_ffi"]
-build_codegen = [
- "radroots_core_bindings",
- "radroots_event_bindings",
- "radroots_event_codec_wasm",
- "radroots_identity_bindings",
- "radroots_replica_schema_bindings",
- "radroots_replica_store_wasm",
- "radroots_replica_sync_wasm",
- "radroots_sdk_sql_wasm_runtime",
- "radroots_sdk_xtask",
- "radroots_trade_bindings",
-]
-test_support = []
-preview = []
-retired = []
-
-[publish_order]
-crates = ["radroots_sdk", "radroots"]
diff --git a/contracts/releases/supply_chain.toml b/contracts/releases/supply_chain.toml
@@ -1,49 +0,0 @@
-schema_version = 1
-spec_id = "radroots.crates.release.v1"
-package_version = "0.1.0-alpha"
-
-[tools]
-cargo_deny = "0.19.8"
-cargo_cyclonedx = "0.5.9"
-
-[sbom]
-format = "json"
-spec_version = "1.5"
-target = "all"
-all_features = true
-source_date_epoch = 0
-
-[[advisory_exception]]
-id = "RUSTSEC-2024-0384"
-package = "instant"
-affected_version = "0.1.13"
-introduced_by = "nostr 0.44.7"
-classification = "unmaintained"
-mitigation = "No Radroots public API exposes instant; replace the dependency when upstream Nostr removes it."
-remove_when = "nostr >=0.45.0 stable"
-
-[[advisory_exception]]
-id = "RUSTSEC-2024-0421"
-package = "idna"
-affected_version = "0.5.0"
-introduced_by = "nostr 0.44.7 via url-fork 3.0.1"
-classification = "vulnerability"
-mitigation = "radroots_transport_nostr canonicalizes every relay URL with url >=2.5.4 before upstream Nostr receives it."
-remove_when = "nostr >=0.45.0 stable"
-
-[[advisory_exception]]
-id = "CARGO-YANKED-SPIN-0.9.8"
-package = "spin"
-affected_version = "0.9.8"
-introduced_by = "sqlx 0.9.0 via flume 0.12.0"
-classification = "yanked"
-mitigation = "The package has no RustSec vulnerability; the gate rejects every yanked name/version except this exact transitive dependency."
-remove_when = "sqlx no longer resolves flume 0.12.0 with spin 0.9.8"
-
-[[package]]
-name = "radroots_sdk"
-manifest_path = "crates/sdk/Cargo.toml"
-
-[[package]]
-name = "radroots"
-manifest_path = "crates/radroots/Cargo.toml"
diff --git a/contracts/releases/target_matrix.toml b/contracts/releases/target_matrix.toml
@@ -1,18 +0,0 @@
-schema_version = 1
-msrv_toolchain = "1.97.1"
-current_toolchain = "stable"
-
-[[operating_system]]
-name = "linux"
-target = "x86_64-unknown-linux-gnu"
-cross_compiler = "zig cc"
-cross_cflags = "-target x86_64-linux-gnu"
-
-[[operating_system]]
-name = "macos"
-target = "aarch64-apple-darwin"
-
-[[operating_system]]
-name = "windows"
-target = "x86_64-pc-windows-gnu"
-cross_compiler = "x86_64-w64-mingw32-gcc"
diff --git a/crates/core_bindings/Cargo.toml b/crates/core_bindings/Cargo.toml
@@ -1,15 +0,0 @@
-[package]
-name = "radroots_core_bindings"
-description = "Generated bindings for Radroots core primitives"
-version.workspace = true
-edition.workspace = true
-rust-version.workspace = true
-license.workspace = true
-repository.workspace = true
-homepage.workspace = true
-publish = false
-
-[dependencies]
-dto_bindgen = { workspace = true }
-dto_bindgen_core = { workspace = true }
-radroots_core = { workspace = true, features = ["serde", "std"] }
diff --git a/crates/core_bindings/src/dto.rs b/crates/core_bindings/src/dto.rs
@@ -1,139 +0,0 @@
-#![allow(dead_code)]
-
-use dto_bindgen_core::RootDescriptor;
-
-pub fn dto_roots() -> Vec<RootDescriptor> {
- vec![
- RootDescriptor::new::<DiscountDescriptor>(),
- RootDescriptor::new::<DiscountScopeDescriptor>(),
- RootDescriptor::new::<DiscountThresholdDescriptor>(),
- RootDescriptor::new::<DiscountValueDescriptor>(),
- RootDescriptor::new::<MoneyDescriptor>(),
- RootDescriptor::new::<PercentDescriptor>(),
- RootDescriptor::new::<QuantityDescriptor>(),
- RootDescriptor::new::<QuantityPriceDescriptor>(),
- RootDescriptor::new::<UnitDescriptor>(),
- RootDescriptor::new::<UnitDimensionDescriptor>(),
- ]
-}
-
-#[derive(dto_bindgen::Dto)]
-#[dto(as = "string")]
-#[dto(ts(name = "Currency"))]
-struct CurrencyDescriptor(String);
-
-#[derive(dto_bindgen::Dto)]
-#[dto(as = "string")]
-#[dto(ts(name = "Decimal"))]
-struct DecimalDescriptor(String);
-
-#[derive(dto_bindgen::Dto)]
-#[dto(ts(name = "Money"))]
-struct MoneyDescriptor {
- amount: DecimalDescriptor,
- currency: CurrencyDescriptor,
-}
-
-#[derive(dto_bindgen::Dto)]
-#[dto(ts(name = "Percent"))]
-struct PercentDescriptor {
- value: DecimalDescriptor,
-}
-
-#[derive(dto_bindgen::Dto)]
-#[dto(as = "string_enum")]
-#[dto(ts(name = "Unit"))]
-enum UnitDescriptor {
- #[dto(rename = "each")]
- Each,
- #[dto(rename = "kg")]
- MassKg,
- #[dto(rename = "g")]
- MassG,
- #[dto(rename = "oz")]
- MassOz,
- #[dto(rename = "lb")]
- MassLb,
- #[dto(rename = "l")]
- VolumeL,
- #[dto(rename = "ml")]
- VolumeMl,
-}
-
-#[derive(dto_bindgen::Dto)]
-#[dto(as = "string_enum")]
-#[dto(ts(name = "UnitDimension"))]
-enum UnitDimensionDescriptor {
- #[dto(rename = "count")]
- Count,
- #[dto(rename = "mass")]
- Mass,
- #[dto(rename = "volume")]
- Volume,
-}
-
-#[derive(dto_bindgen::Dto)]
-#[dto(ts(name = "Quantity"))]
-struct QuantityDescriptor {
- amount: DecimalDescriptor,
- unit: UnitDescriptor,
- #[serde(skip_serializing_if = "Option::is_none")]
- label: Option<String>,
-}
-
-#[derive(dto_bindgen::Dto)]
-#[dto(ts(name = "QuantityPrice"))]
-struct QuantityPriceDescriptor {
- amount: MoneyDescriptor,
- quantity: QuantityDescriptor,
-}
-
-#[derive(dto_bindgen::Dto)]
-#[dto(as = "string_enum")]
-#[dto(ts(name = "DiscountScope"))]
-enum DiscountScopeDescriptor {
- #[dto(rename = "bin")]
- Bin,
- #[dto(rename = "order_total")]
- OrderTotal,
-}
-
-#[derive(dto_bindgen::Dto)]
-#[dto(ts(name = "DiscountThreshold"))]
-#[serde(rename_all = "snake_case", tag = "kind", content = "amount")]
-enum DiscountThresholdDescriptor {
- BinCount { bin_id: String, min: u32 },
- OrderQuantity { min: QuantityDescriptor },
-}
-
-#[derive(dto_bindgen::Dto)]
-#[dto(ts(name = "DiscountValue"))]
-#[serde(rename_all = "snake_case", tag = "kind", content = "amount")]
-enum DiscountValueDescriptor {
- MoneyPerBin(MoneyDescriptor),
- Percent(PercentDescriptor),
-}
-
-#[derive(dto_bindgen::Dto)]
-#[dto(ts(name = "Discount"))]
-struct DiscountDescriptor {
- scope: DiscountScopeDescriptor,
- threshold: DiscountThresholdDescriptor,
- value: DiscountValueDescriptor,
-}
-
-#[cfg(test)]
-mod tests {
- use dto_bindgen_core::build_registry;
-
- use super::dto_roots;
-
- #[test]
- fn canonical_core_descriptor_roots_build_without_diagnostics() {
- let roots = dto_roots();
- let registry = build_registry(roots.clone());
-
- assert!(!registry.has_errors(), "{:?}", registry.diagnostics);
- assert_eq!(registry.roots.len(), roots.len());
- }
-}
diff --git a/crates/core_bindings/src/lib.rs b/crates/core_bindings/src/lib.rs
@@ -1,20 +0,0 @@
-mod dto;
-
-pub use dto::dto_roots;
-pub use radroots_core as upstream;
-
-#[cfg(test)]
-mod tests {
- const GENERATED_TYPES_TS: &str =
- include_str!("../../../packages/core-bindings/src/generated/types.ts");
-
- #[test]
- fn generated_core_types_are_source_rendered() {
- assert!(GENERATED_TYPES_TS.contains("export type Money"));
- assert!(GENERATED_TYPES_TS.contains("export type QuantityPrice"));
- assert!(GENERATED_TYPES_TS.contains("export type UnitDimension"));
- assert!(GENERATED_TYPES_TS.contains("label?: string | null"));
- assert!(!GENERATED_TYPES_TS.contains("label: string | null"));
- assert!(!GENERATED_TYPES_TS.contains("RadrootsCore"));
- }
-}
diff --git a/crates/event_bindings/Cargo.toml b/crates/event_bindings/Cargo.toml
@@ -1,14 +0,0 @@
-[package]
-name = "radroots_event_bindings"
-description = "Generated bindings for Radroots event contracts"
-version.workspace = true
-edition.workspace = true
-rust-version.workspace = true
-license.workspace = true
-repository.workspace = true
-homepage.workspace = true
-publish = false
-
-[dependencies]
-dto_bindgen_backend_ts = { workspace = true }
-radroots_event = { workspace = true }
diff --git a/crates/event_bindings/src/lib.rs b/crates/event_bindings/src/lib.rs
@@ -1,28 +0,0 @@
-pub use radroots_event as upstream;
-
-mod model;
-
-pub use model::{constants_module, kinds_module};
-
-#[cfg(test)]
-mod tests {
- use radroots_event::{
- envelope::kind as kinds,
- listing::operational::RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS,
- };
-
- use super::{constants_module, kinds_module};
-
- #[test]
- fn preserves_event_constant_exports() {
- let constants = constants_module().render_source();
- let kinds_ts = kinds_module().render_source();
- assert!(constants.contains("RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS"));
- assert!(constants.contains(RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS[0]));
- assert!(kinds_ts.contains("KIND_CLASSIFIED_LISTING"));
- assert!(kinds_ts.contains(&kinds::KIND_CLASSIFIED_LISTING.to_string()));
- assert!(kinds_ts.contains("KIND_TRADE_PROPOSAL"));
- assert!(kinds_ts.contains(&kinds::KIND_TRADE_PROPOSAL.to_string()));
- assert!(!kinds_ts.contains("KIND_ORDER_REQUEST"));
- }
-}
diff --git a/crates/event_bindings/src/model.rs b/crates/event_bindings/src/model.rs
@@ -1,161 +0,0 @@
-use dto_bindgen_backend_ts::{
- TypeScriptDeclaration, TypeScriptImport, TypeScriptModule, TypeScriptType, TypeScriptValue,
-};
-use radroots_event::{
- envelope::kind as kinds, listing::operational::RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS,
-};
-
-pub fn constants_module() -> TypeScriptModule {
- TypeScriptModule::new("src/generated/constants.ts")
- .with_import(TypeScriptImport::type_only(
- ["RadrootsOperationalListingProductTagKeys"],
- "./types.js",
- ))
- .with_declaration(TypeScriptDeclaration::constant(
- "RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS",
- Some(TypeScriptType::named(
- "RadrootsOperationalListingProductTagKeys",
- )),
- TypeScriptValue::array(
- RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS
- .iter()
- .map(|value| TypeScriptValue::string(*value))
- .collect::<Vec<_>>(),
- ),
- ))
-}
-
-pub fn kinds_module() -> TypeScriptModule {
- EVENT_KIND_EXPORTS.iter().fold(
- TypeScriptModule::new("src/generated/kinds.ts"),
- |module, (name, value)| {
- module.with_declaration(TypeScriptDeclaration::constant(
- *name,
- None,
- TypeScriptValue::number(i64::from(*value)),
- ))
- },
- )
-}
-
-const EVENT_KIND_EXPORTS: &[(&str, u32)] = &[
- ("KIND_PROFILE", kinds::KIND_PROFILE),
- ("KIND_POST", kinds::KIND_POST),
- ("KIND_FOLLOW", kinds::KIND_FOLLOW),
- ("KIND_REACTION", kinds::KIND_REACTION),
- ("KIND_SEAL", kinds::KIND_SEAL),
- ("KIND_MESSAGE", kinds::KIND_MESSAGE),
- ("KIND_MESSAGE_FILE", kinds::KIND_MESSAGE_FILE),
- ("KIND_GIFT_WRAP", kinds::KIND_GIFT_WRAP),
- ("KIND_COMMENT", kinds::KIND_COMMENT),
- ("KIND_GEOCHAT", kinds::KIND_GEOCHAT),
- ("KIND_WIKI_MERGE_REQUEST", kinds::KIND_WIKI_MERGE_REQUEST),
- ("KIND_WIKI_ARTICLE", kinds::KIND_WIKI_ARTICLE),
- ("KIND_WIKI_REDIRECT", kinds::KIND_WIKI_REDIRECT),
- ("KIND_LIST_MUTE", kinds::KIND_LIST_MUTE),
- ("KIND_LIST_PINNED_NOTES", kinds::KIND_LIST_PINNED_NOTES),
- (
- "KIND_LIST_READ_WRITE_RELAYS",
- kinds::KIND_LIST_READ_WRITE_RELAYS,
- ),
- ("KIND_LIST_BOOKMARKS", kinds::KIND_LIST_BOOKMARKS),
- ("KIND_LIST_COMMUNITIES", kinds::KIND_LIST_COMMUNITIES),
- ("KIND_LIST_PUBLIC_CHATS", kinds::KIND_LIST_PUBLIC_CHATS),
- ("KIND_LIST_BLOCKED_RELAYS", kinds::KIND_LIST_BLOCKED_RELAYS),
- ("KIND_LIST_SEARCH_RELAYS", kinds::KIND_LIST_SEARCH_RELAYS),
- ("KIND_LIST_SIMPLE_GROUPS", kinds::KIND_LIST_SIMPLE_GROUPS),
- ("KIND_LIST_RELAY_FEEDS", kinds::KIND_LIST_RELAY_FEEDS),
- ("KIND_LIST_INTERESTS", kinds::KIND_LIST_INTERESTS),
- ("KIND_LIST_MEDIA_FOLLOWS", kinds::KIND_LIST_MEDIA_FOLLOWS),
- ("KIND_LIST_EMOJIS", kinds::KIND_LIST_EMOJIS),
- ("KIND_LIST_DM_RELAYS", kinds::KIND_LIST_DM_RELAYS),
- (
- "KIND_LIST_GOOD_WIKI_AUTHORS",
- kinds::KIND_LIST_GOOD_WIKI_AUTHORS,
- ),
- (
- "KIND_LIST_GOOD_WIKI_RELAYS",
- kinds::KIND_LIST_GOOD_WIKI_RELAYS,
- ),
- ("KIND_LIST_SET_FOLLOW", kinds::KIND_LIST_SET_FOLLOW),
- ("KIND_LIST_SET_GENERIC", kinds::KIND_LIST_SET_GENERIC),
- ("KIND_LIST_SET_RELAY", kinds::KIND_LIST_SET_RELAY),
- ("KIND_LIST_SET_BOOKMARK", kinds::KIND_LIST_SET_BOOKMARK),
- ("KIND_LIST_SET_CURATION", kinds::KIND_LIST_SET_CURATION),
- ("KIND_LIST_SET_VIDEO", kinds::KIND_LIST_SET_VIDEO),
- ("KIND_LIST_SET_PICTURE", kinds::KIND_LIST_SET_PICTURE),
- ("KIND_LIST_SET_KIND_MUTE", kinds::KIND_LIST_SET_KIND_MUTE),
- ("KIND_LIST_SET_INTEREST", kinds::KIND_LIST_SET_INTEREST),
- ("KIND_LIST_SET_EMOJI", kinds::KIND_LIST_SET_EMOJI),
- (
- "KIND_LIST_SET_RELEASE_ARTIFACT",
- kinds::KIND_LIST_SET_RELEASE_ARTIFACT,
- ),
- (
- "KIND_LIST_SET_APP_CURATION",
- kinds::KIND_LIST_SET_APP_CURATION,
- ),
- ("KIND_LIST_SET_CALENDAR", kinds::KIND_LIST_SET_CALENDAR),
- (
- "KIND_LIST_SET_STARTER_PACK",
- kinds::KIND_LIST_SET_STARTER_PACK,
- ),
- (
- "KIND_LIST_SET_MEDIA_STARTER_PACK",
- kinds::KIND_LIST_SET_MEDIA_STARTER_PACK,
- ),
- ("KIND_FARM", kinds::KIND_FARM),
- ("KIND_PLOT", kinds::KIND_PLOT),
- ("KIND_COOP", kinds::KIND_COOP),
- ("KIND_DOCUMENT", kinds::KIND_DOCUMENT),
- ("KIND_RESOURCE_AREA", kinds::KIND_RESOURCE_AREA),
- (
- "KIND_RESOURCE_HARVEST_CAP",
- kinds::KIND_RESOURCE_HARVEST_CAP,
- ),
- ("KIND_ACCOUNT_CLAIM", kinds::KIND_ACCOUNT_CLAIM),
- ("KIND_APP_DATA", kinds::KIND_APP_DATA),
- ("KIND_CLASSIFIED_LISTING", kinds::KIND_CLASSIFIED_LISTING),
- ("KIND_APPLICATION_HANDLER", kinds::KIND_APPLICATION_HANDLER),
- ("KIND_TRADE_PROPOSAL", kinds::KIND_TRADE_PROPOSAL),
- ("KIND_TRADE_DECISION", kinds::KIND_TRADE_DECISION),
- (
- "KIND_TRADE_REVISION_PROPOSAL",
- kinds::KIND_TRADE_REVISION_PROPOSAL,
- ),
- (
- "KIND_TRADE_REVISION_DECISION",
- kinds::KIND_TRADE_REVISION_DECISION,
- ),
- ("KIND_TRADE_CANCELLATION", kinds::KIND_TRADE_CANCELLATION),
- (
- "KIND_TRADE_SELLER_RESERVATION_ASSERTION",
- kinds::KIND_TRADE_SELLER_RESERVATION_ASSERTION,
- ),
- (
- "KIND_TRADE_VALIDATION_RECEIPT",
- kinds::KIND_TRADE_VALIDATION_RECEIPT,
- ),
- ("KIND_KNOWLEDGE_CLAIM", kinds::KIND_KNOWLEDGE_CLAIM),
- ("KIND_KNOWLEDGE_RELATION", kinds::KIND_KNOWLEDGE_RELATION),
- ("KIND_KNOWLEDGE_REVIEW", kinds::KIND_KNOWLEDGE_REVIEW),
- (
- "KIND_KNOWLEDGE_FIELD_REPORT",
- kinds::KIND_KNOWLEDGE_FIELD_REPORT,
- ),
- (
- "KIND_KNOWLEDGE_CHANGE_PROPOSAL",
- kinds::KIND_KNOWLEDGE_CHANGE_PROPOSAL,
- ),
- (
- "KIND_CONTRIBUTION_ATTESTATION",
- kinds::KIND_CONTRIBUTION_ATTESTATION,
- ),
- ("KIND_KNOWLEDGE_SOURCE", kinds::KIND_KNOWLEDGE_SOURCE),
- ("KIND_EVIDENCE_BOUNTY", kinds::KIND_EVIDENCE_BOUNTY),
- ("KIND_JOB_REQUEST_MIN", kinds::KIND_JOB_REQUEST_MIN),
- ("KIND_JOB_REQUEST_MAX", kinds::KIND_JOB_REQUEST_MAX),
- ("KIND_JOB_RESULT_MIN", kinds::KIND_JOB_RESULT_MIN),
- ("KIND_JOB_RESULT_MAX", kinds::KIND_JOB_RESULT_MAX),
- ("KIND_JOB_FEEDBACK", kinds::KIND_JOB_FEEDBACK),
-];
diff --git a/crates/event_codec_wasm/Cargo.toml b/crates/event_codec_wasm/Cargo.toml
@@ -1,41 +0,0 @@
-[package]
-name = "radroots_event_codec_wasm"
-publish = false
-version = "0.1.0-alpha"
-edition.workspace = true
-authors = ["Tyson Lupul <tyson@radroots.org>"]
-rust-version.workspace = true
-license.workspace = true
-description = "WASM boundary for Radroots event codecs"
-repository.workspace = true
-homepage.workspace = true
-readme = "README"
-
-[lib]
-crate-type = ["cdylib", "rlib"]
-
-[dependencies]
-radroots_blossom = { workspace = true, default-features = false, features = [
- "std",
-] }
-radroots_event = { workspace = true, default-features = false, features = [
- "std",
- "serde",
- "knowledge",
-] }
-radroots_event_codec = { workspace = true, default-features = false, features = [
- "std",
- "manifests",
-] }
-serde = { workspace = true }
-serde_json = { workspace = true }
-wasm-bindgen = { workspace = true }
-
-[dev-dependencies]
-nostr = { workspace = true }
-radroots_core = { workspace = true, default-features = false, features = [
- "std",
-] }
-radroots_identity = { workspace = true, default-features = false, features = [
- "std",
-] }
diff --git a/crates/event_codec_wasm/README b/crates/event_codec_wasm/README
@@ -1,24 +0,0 @@
-# radroots_event_codec_wasm
-
-This is the README for `radroots_event_codec_wasm`, which provides WebAssembly
-bindings for `radroots_event_codec` in the `radroots` core libraries.
-
-## Overview
-
- * wasm-bindgen entry points for event content and tag encoding and decoding;
- * specialized tag helpers for farm, list, job, message, plot, and reaction
- event families;
- * JSON and `JsValue` boundaries built around `serde-wasm-bindgen` and base64
- helpers;
- * built as `cdylib` and `rlib` artifacts for wasm consumers.
-
-## Copyright
-
-Except as otherwise noted, all files in the `radroots_event_codec_wasm`
-distribution are
-
- Copyright (c) 2026 Tyson Lupul
-
-For information on usage and redistribution, and for a DISCLAIMER OF ALL
-WARRANTIES, see LICENSE included in the `radroots_event_codec_wasm`
-distribution.
diff --git a/crates/event_codec_wasm/src/lib.rs b/crates/event_codec_wasm/src/lib.rs
@@ -1,2184 +0,0 @@
-#![forbid(unsafe_code)]
-
-use radroots_blossom::{BlobDescriptor, BlobUrl, Error, MediaType, Sha256};
-use radroots_event::envelope::{EventEnvelope, EventEnvelopeError, EventEnvelopeParts};
-use radroots_event::farm::Farm;
-use radroots_event::farm::coop::Coop;
-use radroots_event::farm::crdt::FarmCrdtChange;
-use radroots_event::farm::file::FarmFileMetadata;
-use radroots_event::farm::plot::Plot;
-use radroots_event::farm::workspace::FarmWorkspaceManifest;
-use radroots_event::knowledge::{
- KnowledgeClaim, KnowledgeFieldReport, KnowledgeRelation, KnowledgeReview, KnowledgeSource,
- WikiArticle, WikiMergeRequest, WikiRedirect,
-};
-use radroots_event::listing::operational::OperationalListing;
-use radroots_event::media::file_metadata::FileMetadata;
-use radroots_event::media::{AuthoredImage, AuthoredImageError};
-use radroots_event::post::article::Article;
-use radroots_event::post::comment::{
- AuthoredNip22Comment, Nip22AddressRootReference, Nip22CommentError,
- Nip22CommentParentReference, Nip22CommentRoot, Nip22EventRootReference,
-};
-use radroots_event::post::document::Document;
-use radroots_event::post::reaction::Reaction;
-use radroots_event::post::report::Report;
-use radroots_event::post::repost::{GenericRepost, Repost};
-use radroots_event::post::{
- AuthoredAsk, AuthoredPhotoUpdate, AuthoredPostError, AuthoredPostImage, AuthoredUpdate,
- PostImageDimensions,
-};
-use radroots_event::social::follow::Follow;
-use radroots_event::social::gift_wrap::GiftWrap;
-use radroots_event::social::group::{
- GroupAdmins, GroupCreateGroup, GroupCreateInvite, GroupDeleteEvent, GroupDeleteGroup,
- GroupEditMetadata, GroupJoinRequest, GroupLeaveRequest, GroupMembers, GroupMetadata,
- GroupPutUser, GroupRemoveUser, GroupRoles,
-};
-use radroots_event::social::http_auth::HttpAuth;
-use radroots_event::social::job_feedback::JobFeedback;
-use radroots_event::social::job_request::JobRequest;
-use radroots_event::social::job_result::JobResult;
-use radroots_event::social::list::List;
-use radroots_event::social::list_set::ListSet;
-use radroots_event::social::message::Message;
-use radroots_event::social::message_file::MessageFile;
-use radroots_event::social::relay_auth::RelayAuth;
-use radroots_event::social::seal::Seal;
-use radroots_event::wire::Nip01EventWireParts;
-use radroots_event_codec::encode::article::article_build_tags;
-use radroots_event_codec::encode::comment::authored_nip22_comment_to_wire_parts;
-use radroots_event_codec::encode::coop::coop_build_tags;
-use radroots_event_codec::encode::document::document_build_tags;
-use radroots_event_codec::encode::farm::farm_build_tags;
-use radroots_event_codec::encode::farm_crdt::farm_crdt_change_build_tags_with_author;
-use radroots_event_codec::encode::farm_file::farm_file_metadata_build_tags;
-use radroots_event_codec::encode::farm_workspace::farm_workspace_build_tags;
-use radroots_event_codec::encode::file_metadata::file_metadata_build_tags;
-use radroots_event_codec::encode::follow::follow_build_tags;
-use radroots_event_codec::encode::gift_wrap::gift_wrap_build_tags;
-use radroots_event_codec::encode::group::{
- group_admins_build_tags, group_create_group_build_tags, group_create_invite_build_tags,
- group_delete_event_build_tags, group_delete_group_build_tags, group_edit_metadata_build_tags,
- group_join_request_build_tags, group_leave_request_build_tags, group_members_build_tags,
- group_metadata_build_tags, group_put_user_build_tags, group_remove_user_build_tags,
- group_roles_build_tags,
-};
-use radroots_event_codec::encode::http_auth::http_auth_build_tags;
-use radroots_event_codec::encode::job::feedback::job_feedback_build_tags;
-use radroots_event_codec::encode::job::request::job_request_build_tags;
-use radroots_event_codec::encode::job::result::job_result_build_tags;
-use radroots_event_codec::encode::knowledge::{
- knowledge_claim_build_tags, knowledge_field_report_build_tags, knowledge_relation_build_tags,
- knowledge_review_build_tags, knowledge_source_build_tags, wiki_article_build_tags,
- wiki_merge_request_build_tags, wiki_redirect_build_tags,
-};
-use radroots_event_codec::encode::list::list_build_tags;
-use radroots_event_codec::encode::list_set::list_set_build_tags;
-use radroots_event_codec::encode::message::message_build_tags;
-use radroots_event_codec::encode::message_file::message_file_build_tags;
-use radroots_event_codec::encode::operational_listing::{
- operational_listing_tags as operational_listing_tags_impl,
- operational_listing_tags_full as operational_listing_tags_full_impl,
-};
-use radroots_event_codec::encode::plot::plot_build_tags;
-use radroots_event_codec::encode::post::{
- authored_ask_to_wire_parts, authored_photo_update_to_wire_parts, authored_update_to_wire_parts,
-};
-use radroots_event_codec::encode::reaction::reaction_build_tags;
-use radroots_event_codec::encode::relay_auth::relay_auth_build_tags;
-use radroots_event_codec::encode::report::report_build_tags;
-use radroots_event_codec::encode::repost::{generic_repost_build_tags, repost_build_tags};
-use radroots_event_codec::encode::seal::seal_build_tags;
-use radroots_event_codec::verify::{RadrootsDecodeError, RadrootsDecodedEvent};
-use serde::de::DeserializeOwned;
-use serde::{Deserialize, Serialize};
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::JsValue;
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::prelude::*;
-
-#[cfg(target_arch = "wasm32")]
-type RadrootsJsValue = JsValue;
-
-#[cfg(not(target_arch = "wasm32"))]
-type RadrootsJsValue = String;
-
-fn err_js<E: ToString>(err: E) -> RadrootsJsValue {
- #[cfg(target_arch = "wasm32")]
- {
- JsValue::from_str(&err.to_string())
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- err.to_string()
- }
-}
-
-fn error_json(code: &str, inner_code: Option<&str>) -> RadrootsJsValue {
- let value = serde_json::json!({
- "code": code,
- "inner_code": inner_code,
- });
- err_js(value)
-}
-
-fn normalized_payload(input: &str) -> &str {
- if input.is_empty() { "{}" } else { input }
-}
-
-fn parse_json<T: DeserializeOwned>(input: &str) -> Result<T, RadrootsJsValue> {
- serde_json::from_str(normalized_payload(input)).map_err(err_js)
-}
-
-fn parse_authored_json<T: DeserializeOwned>(input: &str) -> Result<T, RadrootsJsValue> {
- serde_json::from_str(input).map_err(|_| error_json("invalid_json", Some("authored_input")))
-}
-
-fn authored_error(code: &str) -> RadrootsJsValue {
- error_json("encode_error", Some(code))
-}
-
-fn blossom_authored_error(error: Error) -> RadrootsJsValue {
- authored_error(error.code())
-}
-
-fn post_authored_error(error: AuthoredPostError) -> RadrootsJsValue {
- authored_error(error.code())
-}
-
-fn image_authored_error(error: AuthoredImageError) -> RadrootsJsValue {
- authored_error(error.code())
-}
-
-fn comment_authored_error(error: Nip22CommentError) -> RadrootsJsValue {
- authored_error(error.code())
-}
-
-fn wire_parts_to_json(parts: &Nip01EventWireParts) -> Result<String, RadrootsJsValue> {
- serde_json::to_string(parts)
- .map_err(|_| error_json("internal_error", Some("wire_parts_serialization")))
-}
-
-fn tags_to_json(tags: Vec<Vec<String>>) -> Result<String, RadrootsJsValue> {
- serde_json::to_string(&tags).map_err(err_js)
-}
-
-#[derive(Deserialize)]
-#[serde(deny_unknown_fields)]
-struct EventEnvelopeInput {
- id: String,
- author: String,
- created_at: u64,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: String,
- sig: String,
-}
-
-fn envelope_error_code(error: &EventEnvelopeError) -> &'static str {
- match error {
- EventEnvelopeError::InvalidId(_) => "id_invalid",
- EventEnvelopeError::InvalidAuthor(_) => "author_invalid",
- EventEnvelopeError::InvalidSignature(_) => "signature_invalid",
- EventEnvelopeError::NonCanonicalId => "id_non_canonical",
- EventEnvelopeError::NonCanonicalAuthor => "author_non_canonical",
- EventEnvelopeError::NonCanonicalSignature => "signature_non_canonical",
- EventEnvelopeError::EmptyTag { .. } => "tag_empty",
- EventEnvelopeError::EmptyTagKey { .. } => "tag_key_empty",
- EventEnvelopeError::ControlCharacterTagKey { .. } => "tag_key_control_character",
- EventEnvelopeError::ContentTooLarge { .. } => "content_too_large",
- EventEnvelopeError::TooManyTags { .. } => "too_many_tags",
- EventEnvelopeError::TooManyTagElements { .. } => "too_many_tag_elements",
- EventEnvelopeError::TagElementTooLarge { .. } => "tag_element_too_large",
- EventEnvelopeError::TagsTooLarge { .. } => "tags_too_large",
- }
-}
-
-fn parse_event_json(input: &str) -> Result<EventEnvelope, RadrootsJsValue> {
- let envelope: EventEnvelopeInput =
- serde_json::from_str(input).map_err(|_| error_json("invalid_json", Some("event_json")))?;
- EventEnvelope::new(EventEnvelopeParts {
- id: envelope.id,
- author: envelope.author,
- created_at: envelope.created_at,
- kind: envelope.kind,
- tags: envelope.tags,
- content: envelope.content,
- sig: envelope.sig,
- })
- .map_err(|error| error_json("invalid_event", Some(envelope_error_code(&error))))
-}
-
-fn build_tags_json<T, E, F>(input: &str, build: F) -> Result<String, RadrootsJsValue>
-where
- T: DeserializeOwned,
- E: ToString,
- F: FnOnce(&T) -> Result<Vec<Vec<String>>, E>,
-{
- let value = parse_json::<T>(input)?;
- let tags = build(&value).map_err(err_js)?;
- tags_to_json(tags)
-}
-
-fn build_tags_json_infallible<T, F>(input: &str, build: F) -> Result<String, RadrootsJsValue>
-where
- T: DeserializeOwned,
- F: FnOnce(&T) -> Vec<Vec<String>>,
-{
- let value = parse_json::<T>(input)?;
- let tags = build(&value);
- tags_to_json(tags)
-}
-
-#[derive(Serialize)]
-struct DecodedEventJson<'a, T>
-where
- T: Serialize,
-{
- event_type: &'static str,
- contract_id: &'static str,
- event: &'a radroots_event::envelope::EventEnvelope,
- payload: &'a T,
-}
-
-fn decoded_event_to_json(decoded: RadrootsDecodedEvent) -> Result<String, RadrootsJsValue> {
- match decoded {
- RadrootsDecodedEvent::WikiArticle(parsed) => {
- decoded_payload_to_json("wiki_article", "radroots.wiki.article.v1", &parsed)
- }
- RadrootsDecodedEvent::WikiRedirect(parsed) => {
- decoded_payload_to_json("wiki_redirect", "radroots.wiki.redirect.v1", &parsed)
- }
- RadrootsDecodedEvent::WikiMergeRequest(parsed) => decoded_payload_to_json(
- "wiki_merge_request",
- "radroots.wiki.merge_request.v1",
- &parsed,
- ),
- RadrootsDecodedEvent::KnowledgeSource(parsed) => {
- decoded_payload_to_json("knowledge_source", "radroots.knowledge.source.v1", &parsed)
- }
- RadrootsDecodedEvent::KnowledgeClaim(parsed) => {
- decoded_payload_to_json("knowledge_claim", "radroots.knowledge.claim.v1", &parsed)
- }
- RadrootsDecodedEvent::KnowledgeRelation(parsed) => decoded_payload_to_json(
- "knowledge_relation",
- "radroots.knowledge.relation.v1",
- &parsed,
- ),
- RadrootsDecodedEvent::KnowledgeReview(parsed) => {
- decoded_payload_to_json("knowledge_review", "radroots.knowledge.review.v1", &parsed)
- }
- RadrootsDecodedEvent::KnowledgeFieldReport(parsed) => decoded_payload_to_json(
- "knowledge_field_report",
- "radroots.knowledge.field_report.v1",
- &parsed,
- ),
- RadrootsDecodedEvent::EvidenceBounty(parsed) => decoded_payload_to_json(
- "evidence_bounty",
- "radroots.knowledge.evidence_bounty.v1",
- &parsed,
- ),
- RadrootsDecodedEvent::KnowledgeChangeProposal(parsed) => decoded_payload_to_json(
- "knowledge_change_proposal",
- "radroots.knowledge.change_proposal.v1",
- &parsed,
- ),
- RadrootsDecodedEvent::ContributionAttestation(parsed) => decoded_payload_to_json(
- "contribution_attestation",
- "radroots.knowledge.contribution_attestation.v1",
- &parsed,
- ),
- }
-}
-
-fn decoded_payload_to_json<T>(
- event_type: &'static str,
- contract_id: &'static str,
- parsed: &radroots_event_codec::decode::RadrootsParsedEvent<T>,
-) -> Result<String, RadrootsJsValue>
-where
- T: Serialize,
-{
- serde_json::to_string(&DecodedEventJson {
- event_type,
- contract_id,
- event: &parsed.event,
- payload: &parsed.data.data,
- })
- .map_err(err_js)
-}
-
-fn decode_error_json(error: RadrootsDecodeError) -> RadrootsJsValue {
- let inner = match &error {
- RadrootsDecodeError::Nip01Verification(error) => Some(error.code()),
- RadrootsDecodeError::ContractValidation(error) => Some(error.code()),
- RadrootsDecodeError::EventParse(error) => Some(error.code()),
- RadrootsDecodeError::UnsupportedContract { .. } => None,
- };
- error_json(error.code(), inner)
-}
-
-#[derive(serde::Deserialize)]
-struct FarmCrdtTagsInput {
- change: FarmCrdtChange,
- author_pubkey: String,
-}
-
-#[derive(Deserialize)]
-#[serde(deny_unknown_fields)]
-struct AuthoredUpdateInput {
- content: String,
-}
-
-#[derive(Deserialize)]
-#[serde(deny_unknown_fields)]
-struct AuthoredMediaPostInput {
- content: String,
- #[serde(default)]
- images: Vec<AuthoredImageInput>,
-}
-
-#[derive(Deserialize)]
-#[serde(deny_unknown_fields)]
-struct AuthoredImageInput {
- bytes: Vec<u8>,
- url: String,
- media_type: String,
- uploaded: u64,
- width: u32,
- height: u32,
- alt: String,
- #[serde(default)]
- fallbacks: Vec<String>,
-}
-
-#[derive(Deserialize)]
-#[serde(deny_unknown_fields)]
-struct AuthoredCommentInput {
- content: String,
- root: AuthoredCommentRootInput,
- position: AuthoredCommentPositionInput,
-}
-
-#[derive(Deserialize)]
-#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
-enum AuthoredCommentRootInput {
- Event {
- event_id: String,
- author: String,
- kind: u32,
- #[serde(default)]
- relay: Option<String>,
- },
- Address {
- coordinate: String,
- author: String,
- kind: u32,
- #[serde(default)]
- relay: Option<String>,
- },
-}
-
-#[derive(Deserialize)]
-#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
-enum AuthoredCommentPositionInput {
- TopEvent,
- TopAddress { current_revision: String },
- Nested { parent: AuthoredCommentParentInput },
-}
-
-#[derive(Deserialize)]
-#[serde(deny_unknown_fields)]
-struct AuthoredCommentParentInput {
- event_id: String,
- author: String,
- #[serde(default)]
- relay: Option<String>,
-}
-
-fn authored_post_images(
- inputs: Vec<AuthoredImageInput>,
-) -> Result<Vec<AuthoredPostImage>, RadrootsJsValue> {
- inputs.into_iter().map(authored_post_image).collect()
-}
-
-fn authored_post_image(input: AuthoredImageInput) -> Result<AuthoredPostImage, RadrootsJsValue> {
- let media_type = MediaType::parse(&input.media_type).map_err(blossom_authored_error)?;
- let sha256 = Sha256::digest(&input.bytes);
- let size = u64::try_from(input.bytes.len())
- .map_err(|_| authored_error("blob_size_platform_overflow"))?;
- let descriptor = BlobDescriptor::new(
- BlobUrl::parse(&input.url).map_err(blossom_authored_error)?,
- sha256,
- size,
- media_type.clone(),
- input.uploaded,
- )
- .map_err(blossom_authored_error)?
- .approve_reference()
- .map_err(blossom_authored_error)?
- .verify_bytes(&input.bytes, &media_type)
- .map_err(blossom_authored_error)?;
- let image =
- AuthoredImage::try_from_verified_descriptor(descriptor).map_err(image_authored_error)?;
- let dimensions =
- PostImageDimensions::new(input.width, input.height).map_err(post_authored_error)?;
- let mut authored =
- AuthoredPostImage::new(image, dimensions, input.alt).map_err(post_authored_error)?;
- for fallback in input.fallbacks {
- let fallback = BlobUrl::parse(&fallback)
- .map_err(blossom_authored_error)?
- .approve()
- .map_err(blossom_authored_error)?;
- authored = authored
- .try_with_fallback(fallback)
- .map_err(post_authored_error)?;
- }
- Ok(authored)
-}
-
-fn authored_comment_from_input(
- input: AuthoredCommentInput,
-) -> Result<AuthoredNip22Comment, RadrootsJsValue> {
- let root = match input.root {
- AuthoredCommentRootInput::Event {
- event_id,
- author,
- kind,
- relay,
- } => Nip22CommentRoot::Event(
- Nip22EventRootReference::parse(event_id, author, kind, relay.as_deref())
- .map_err(comment_authored_error)?,
- ),
- AuthoredCommentRootInput::Address {
- coordinate,
- author,
- kind,
- relay,
- } => {
- let root = Nip22AddressRootReference::parse(coordinate, relay.as_deref())
- .map_err(comment_authored_error)?;
- if root.author().to_hex() != author {
- return Err(authored_error("comment_root_author_mismatch"));
- }
- if root.kind().as_u32() != kind {
- return Err(authored_error("comment_root_kind_mismatch"));
- }
- Nip22CommentRoot::Address(root)
- }
- };
-
- match (root, input.position) {
- (Nip22CommentRoot::Event(root), AuthoredCommentPositionInput::TopEvent) => {
- AuthoredNip22Comment::top_level_event(input.content, root)
- .map_err(comment_authored_error)
- }
- (
- Nip22CommentRoot::Address(root),
- AuthoredCommentPositionInput::TopAddress { current_revision },
- ) => AuthoredNip22Comment::parse_top_level_address(input.content, root, current_revision)
- .map_err(comment_authored_error),
- (root, AuthoredCommentPositionInput::Nested { parent }) => {
- let parent = Nip22CommentParentReference::parse(
- parent.event_id,
- parent.author,
- parent.relay.as_deref(),
- )
- .map_err(comment_authored_error)?;
- AuthoredNip22Comment::nested(input.content, root, parent)
- .map_err(comment_authored_error)
- }
- _ => Err(authored_error("comment_root_position_incompatible")),
- }
-}
-
-/// Builds deterministic unsigned kind-1 wire parts for a strict Update.
-#[cfg_attr(
- target_arch = "wasm32",
- wasm_bindgen(js_name = social_update_build_authored_draft)
-)]
-pub fn social_update_build_authored_draft(input_json: &str) -> Result<String, RadrootsJsValue> {
- let input = parse_authored_json::<AuthoredUpdateInput>(input_json)?;
- let update = AuthoredUpdate::new(input.content).map_err(post_authored_error)?;
- wire_parts_to_json(&authored_update_to_wire_parts(&update))
-}
-
-/// Builds deterministic unsigned kind-1 wire parts for a strict PhotoUpdate.
-#[cfg_attr(
- target_arch = "wasm32",
- wasm_bindgen(js_name = social_photo_update_build_authored_draft)
-)]
-pub fn social_photo_update_build_authored_draft(
- input_json: &str,
-) -> Result<String, RadrootsJsValue> {
- let input = parse_authored_json::<AuthoredMediaPostInput>(input_json)?;
- let images = authored_post_images(input.images)?;
- let photo = AuthoredPhotoUpdate::new(input.content, images).map_err(post_authored_error)?;
- wire_parts_to_json(&authored_photo_update_to_wire_parts(&photo))
-}
-
-/// Builds deterministic unsigned kind-1 wire parts for a strict Ask.
-#[cfg_attr(
- target_arch = "wasm32",
- wasm_bindgen(js_name = social_ask_build_authored_draft)
-)]
-pub fn social_ask_build_authored_draft(input_json: &str) -> Result<String, RadrootsJsValue> {
- let input = parse_authored_json::<AuthoredMediaPostInput>(input_json)?;
- let images = authored_post_images(input.images)?;
- let ask = AuthoredAsk::new(input.content, images).map_err(post_authored_error)?;
- wire_parts_to_json(&authored_ask_to_wire_parts(&ask))
-}
-
-/// Builds deterministic unsigned kind-1111 wire parts for a strict NIP-22 Comment.
-#[cfg_attr(
- target_arch = "wasm32",
- wasm_bindgen(js_name = social_comment_build_authored_draft)
-)]
-pub fn social_comment_build_authored_draft(input_json: &str) -> Result<String, RadrootsJsValue> {
- let input = parse_authored_json::<AuthoredCommentInput>(input_json)?;
- let comment = authored_comment_from_input(input)?;
- wire_parts_to_json(&authored_nip22_comment_to_wire_parts(&comment))
-}
-
-#[cfg_attr(
- target_arch = "wasm32",
- wasm_bindgen(js_name = operational_listing_tags)
-)]
-pub fn operational_listing_tags(listing_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<OperationalListing, _, _>(listing_json, operational_listing_tags_impl)
-}
-
-#[cfg_attr(
- target_arch = "wasm32",
- wasm_bindgen(js_name = operational_listing_tags_full)
-)]
-pub fn operational_listing_tags_full(listing_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<OperationalListing, _, _>(listing_json, operational_listing_tags_full_impl)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = article_tags))]
-pub fn article_tags(article_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<Article, _, _>(article_json, article_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = wiki_article_tags))]
-pub fn wiki_article_tags(article_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<WikiArticle, _, _>(article_json, wiki_article_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = wiki_redirect_tags))]
-pub fn wiki_redirect_tags(redirect_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<WikiRedirect, _, _>(redirect_json, wiki_redirect_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = wiki_merge_request_tags))]
-pub fn wiki_merge_request_tags(request_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<WikiMergeRequest, _, _>(request_json, wiki_merge_request_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_source_tags))]
-pub fn knowledge_source_tags(source_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<KnowledgeSource, _, _>(source_json, knowledge_source_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_claim_tags))]
-pub fn knowledge_claim_tags(claim_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<KnowledgeClaim, _, _>(claim_json, knowledge_claim_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_relation_tags))]
-pub fn knowledge_relation_tags(relation_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<KnowledgeRelation, _, _>(relation_json, knowledge_relation_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_review_tags))]
-pub fn knowledge_review_tags(review_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<KnowledgeReview, _, _>(review_json, knowledge_review_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_field_report_tags))]
-pub fn knowledge_field_report_tags(report_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<KnowledgeFieldReport, _, _>(report_json, knowledge_field_report_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = verify_and_decode_event_json))]
-pub fn verify_and_decode_event_json(event_json: &str) -> Result<String, RadrootsJsValue> {
- let event = parse_event_json(event_json)?;
- let decoded = radroots_event_codec::verify::verify_and_decode_radroots_event(event)
- .map_err(decode_error_json)?;
- decoded_event_to_json(decoded)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = contract_manifest_json))]
-pub fn contract_manifest_json() -> Result<String, RadrootsJsValue> {
- radroots_event_codec::manifest::contract_manifest_json().map_err(err_js)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = file_metadata_tags))]
-pub fn file_metadata_tags(metadata_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<FileMetadata, _, _>(metadata_json, file_metadata_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = repost_tags))]
-pub fn repost_tags(repost_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<Repost, _, _>(repost_json, repost_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = generic_repost_tags))]
-pub fn generic_repost_tags(repost_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<GenericRepost, _, _>(repost_json, generic_repost_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = report_tags))]
-pub fn report_tags(report_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<Report, _, _>(report_json, report_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = follow_tags))]
-pub fn follow_tags(follow_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<Follow, _, _>(follow_json, follow_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = document_tags))]
-pub fn document_tags(document_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<Document, _, _>(document_json, document_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = coop_tags))]
-pub fn coop_tags(coop_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<Coop, _, _>(coop_json, coop_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = farm_tags))]
-pub fn farm_tags(farm_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<Farm, _, _>(farm_json, farm_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = list_tags))]
-pub fn list_tags(list_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<List, _, _>(list_json, list_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = list_set_tags))]
-pub fn list_set_tags(list_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<ListSet, _, _>(list_json, list_set_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = plot_tags))]
-pub fn plot_tags(plot_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<Plot, _, _>(plot_json, plot_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = job_request_tags))]
-pub fn job_request_tags(job_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json_infallible::<JobRequest, _>(job_json, job_request_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = job_result_tags))]
-pub fn job_result_tags(job_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json_infallible::<JobResult, _>(job_json, job_result_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = job_feedback_tags))]
-pub fn job_feedback_tags(job_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json_infallible::<JobFeedback, _>(job_json, job_feedback_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = reaction_tags))]
-pub fn reaction_tags(reaction_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<Reaction, _, _>(reaction_json, reaction_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = message_tags))]
-pub fn message_tags(message_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<Message, _, _>(message_json, message_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = message_file_tags))]
-pub fn message_file_tags(message_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<MessageFile, _, _>(message_json, message_file_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = seal_tags))]
-pub fn seal_tags(seal_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<Seal, _, _>(seal_json, seal_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = gift_wrap_tags))]
-pub fn gift_wrap_tags(gift_wrap_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<GiftWrap, _, _>(gift_wrap_json, gift_wrap_build_tags)
-}
-
-#[cfg_attr(
- target_arch = "wasm32",
- wasm_bindgen(js_name = farm_workspace_manifest_tags)
-)]
-pub fn farm_workspace_manifest_tags(workspace_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<FarmWorkspaceManifest, _, _>(workspace_json, farm_workspace_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = farm_crdt_change_tags))]
-pub fn farm_crdt_change_tags(input_json: &str) -> Result<String, RadrootsJsValue> {
- let input = parse_json::<FarmCrdtTagsInput>(input_json)?;
- let tags = farm_crdt_change_build_tags_with_author(&input.change, Some(&input.author_pubkey))
- .map_err(err_js)?;
- tags_to_json(tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = farm_file_metadata_tags))]
-pub fn farm_file_metadata_tags(file_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<FarmFileMetadata, _, _>(file_json, farm_file_metadata_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = relay_auth_tags))]
-pub fn relay_auth_tags(auth_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<RelayAuth, _, _>(auth_json, relay_auth_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = http_auth_tags))]
-pub fn http_auth_tags(auth_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<HttpAuth, _, _>(auth_json, http_auth_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_put_user_tags))]
-pub fn group_put_user_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<GroupPutUser, _, _>(group_json, group_put_user_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_remove_user_tags))]
-pub fn group_remove_user_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<GroupRemoveUser, _, _>(group_json, group_remove_user_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_create_group_tags))]
-pub fn group_create_group_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<GroupCreateGroup, _, _>(group_json, group_create_group_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_edit_metadata_tags))]
-pub fn group_edit_metadata_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<GroupEditMetadata, _, _>(group_json, group_edit_metadata_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_delete_group_tags))]
-pub fn group_delete_group_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<GroupDeleteGroup, _, _>(group_json, group_delete_group_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_delete_event_tags))]
-pub fn group_delete_event_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<GroupDeleteEvent, _, _>(group_json, group_delete_event_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_create_invite_tags))]
-pub fn group_create_invite_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<GroupCreateInvite, _, _>(group_json, group_create_invite_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_join_request_tags))]
-pub fn group_join_request_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<GroupJoinRequest, _, _>(group_json, group_join_request_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_leave_request_tags))]
-pub fn group_leave_request_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<GroupLeaveRequest, _, _>(group_json, group_leave_request_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_metadata_tags))]
-pub fn group_metadata_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<GroupMetadata, _, _>(group_json, group_metadata_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_admins_tags))]
-pub fn group_admins_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<GroupAdmins, _, _>(group_json, group_admins_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_members_tags))]
-pub fn group_members_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<GroupMembers, _, _>(group_json, group_members_build_tags)
-}
-
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_roles_tags))]
-pub fn group_roles_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<GroupRoles, _, _>(group_json, group_roles_build_tags)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit};
- use radroots_event::envelope::kind::{
- KIND_FARM_FILE_METADATA, KIND_FILE_METADATA, KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_SOURCE,
- KIND_WIKI_ARTICLE,
- };
- use radroots_event::envelope::{EventEnvelope, EventEnvelopeLimits, EventEnvelopeParts};
- use radroots_event::farm::FarmRef;
- use radroots_event::farm::crdt::{
- CrdtBackend, FarmCrdtDocumentKind, FarmSemanticKind, RADROOTS_FARM_CRDT_CHANGE_SCHEMA,
- };
- use radroots_event::farm::file::{FarmFileDimensions, FarmFileMetadata, FarmFileSource};
- use radroots_event::farm::workspace::{
- FarmWorkspaceManifest, FarmWorkspaceMediaServer, FarmWorkspaceRef, FarmWorkspaceRelay,
- FarmWorkspaceRelayMode, RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION,
- RADROOTS_FARM_WORKSPACE_SCHEMA,
- };
- use radroots_event::knowledge::{
- AddressableRef, KnowledgeCitationSpan, KnowledgeFieldContext, KnowledgeLocation,
- KnowledgeLocationPrecision, KnowledgeNodeRef, KnowledgeObservation,
- KnowledgeObservationValue, KnowledgeReviewScope, KnowledgeReviewScore,
- KnowledgeReviewTarget, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA,
- RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, RADROOTS_KNOWLEDGE_RELATION_SCHEMA,
- RADROOTS_KNOWLEDGE_REVIEW_SCHEMA, RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
- RADROOTS_KNOWLEDGE_SOURCE_SCHEMA, WikiArticleVersionRef,
- };
- use radroots_event::listing::operational::{OperationalListingBin, OperationalListingProduct};
- use radroots_event::social::group::{
- GroupAdmins, GroupCreateGroup, GroupCreateInvite, GroupDeleteEvent, GroupDeleteGroup,
- GroupEditMetadata, GroupEditableMetadata, GroupJoinRequest, GroupLeaveRequest,
- GroupMembers, GroupMetadata, GroupPutUser, GroupRemoveUser, GroupRole, GroupRoles,
- GroupUserRef,
- };
- use radroots_event::social::http_auth::HttpAuth;
- use radroots_event::social::job::JobInputType;
- use radroots_event::social::job_request::{JobInput, JobParam};
- use radroots_event::social::relay_auth::RelayAuth;
- use radroots_event::social::{
- ReportFileTarget, ReportType, SocialFarmAnchor, SocialLocation, SocialMediaDimensions,
- SocialTarget,
- };
-
- fn sample_listing() -> OperationalListing {
- let quantity =
- Quantity::try_new(Decimal::from(1u32), Unit::Each).expect("positive fixture quantity");
- let price = QuantityPrice::try_new(
- Money::try_new(Decimal::from(10u32), Currency::USD)
- .expect("non-negative fixture money"),
- quantity.clone(),
- )
- .expect("non-zero fixture pricing quantity");
-
- OperationalListing {
- d_tag: "AAAAAAAAAAAAAAAAAAAAAg".parse().expect("listing d tag"),
- published_at: None,
- farm: FarmRef {
- pubkey: "farm_pubkey".to_string(),
- d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
- },
- product: OperationalListingProduct {
- key: "sku".to_string(),
- title: "widget".to_string(),
- category: "tools".to_string(),
- summary: None,
- process: None,
- lot: None,
- location: None,
- profile: None,
- year: None,
- },
- primary_bin_id: "bin-1".parse().expect("primary bin id"),
- bins: vec![OperationalListingBin {
- bin_id: "bin-1".parse().expect("bin id"),
- quantity,
- price_per_canonical_unit: price,
- display_amount: None,
- display_unit: None,
- display_label: None,
- display_price: None,
- display_price_unit: None,
- }],
- resource_area: None,
- plot: None,
- discounts: None,
- inventory_available: None,
- availability: None,
- delivery_method: None,
- location: None,
- images: None,
- }
- }
-
- fn synthetic_pubkey(seed: char) -> String {
- seed.to_string().repeat(64)
- }
-
- fn synthetic_event_id(seed: char) -> String {
- seed.to_string().repeat(64)
- }
-
- fn social_farm_anchor() -> SocialFarmAnchor {
- SocialFarmAnchor {
- farm: FarmRef {
- pubkey: synthetic_pubkey('a'),
- d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
- },
- relays: Some(vec!["wss://relay.example.test".to_string()]),
- }
- }
-
- fn event_target(kind: u32, seed: char) -> SocialTarget {
- SocialTarget::Event {
- id: synthetic_event_id(seed),
- author: Some(synthetic_pubkey('b')),
- event_kind: Some(kind),
- relays: Some(vec!["wss://relay.example.test".to_string()]),
- }
- }
-
- fn address_target(kind: u32, d_tag: &str) -> SocialTarget {
- let author = synthetic_pubkey('c');
- SocialTarget::Address {
- address: format!("{kind}:{author}:{d_tag}"),
- author: Some(author),
- event_kind: Some(kind),
- relays: Some(vec!["wss://relay2.example.test".to_string()]),
- }
- }
-
- fn knowledge_event_ref(seed: char, kind: u32) -> radroots_event::tag::EventRef {
- radroots_event::tag::EventRef {
- id: synthetic_event_id(seed),
- author: radroots_identity::PublicKey::from_hex(&synthetic_pubkey('a'))
- .expect("fixture public key"),
- kind,
- d_tag: None,
- relays: Some(vec!["wss://relay.example.test".to_string()]),
- }
- }
-
- fn knowledge_address_ref() -> AddressableRef {
- AddressableRef {
- kind: KIND_WIKI_ARTICLE,
- pubkey: synthetic_pubkey('a'),
- d_tag: "soil-health".to_string(),
- relays: vec!["wss://relay.example.test".to_string()],
- }
- }
-
- fn sample_wiki_article() -> WikiArticle {
- WikiArticle {
- d_tag: "soil-health".to_string(),
- title: Some("Soil health".to_string()),
- content_djot: "# Soil health".to_string(),
- summary: Some("Living soil basics".to_string()),
- topics: vec!["soil".to_string()],
- references: vec![knowledge_event_ref('1', KIND_KNOWLEDGE_SOURCE)],
- forked_from: vec![WikiArticleVersionRef {
- event_id: synthetic_event_id('b'),
- address_ref: knowledge_address_ref(),
- }],
- deferred_to: None,
- }
- }
-
- fn sample_wiki_redirect() -> WikiRedirect {
- WikiRedirect {
- d_tag: "soil".to_string(),
- target: knowledge_address_ref(),
- }
- }
-
- fn sample_wiki_merge_request() -> WikiMergeRequest {
- WikiMergeRequest {
- target_article: knowledge_address_ref(),
- destination_pubkey: synthetic_pubkey('a'),
- base_version_event_id: Some(synthetic_event_id('e')),
- source_version_event_id: synthetic_event_id('f'),
- explanation: Some("Merge synthetic soil article updates".to_string()),
- }
- }
-
- fn sample_knowledge_source() -> KnowledgeSource {
- KnowledgeSource {
- schema: RADROOTS_KNOWLEDGE_SOURCE_SCHEMA.to_string(),
- schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
- d_tag: "soil-source".to_string(),
- title: "Soil Source".to_string(),
- source_type: "book".to_string(),
- authors: vec!["A. Example".to_string()],
- publisher: Some("Radroots Synthetic Press".to_string()),
- publication_year: Some(2026),
- edition: None,
- canonical_url: Some("https://source.example.test/soil-source".to_string()),
- artifact_refs: vec![knowledge_event_ref('3', KIND_FILE_METADATA)],
- author_asserted_rights: None,
- topics: vec!["soil".to_string()],
- summary: Some("Synthetic source for wasm coverage".to_string()),
- }
- }
-
- fn sample_knowledge_claim() -> KnowledgeClaim {
- KnowledgeClaim {
- schema: RADROOTS_KNOWLEDGE_CLAIM_SCHEMA.to_string(),
- schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
- claim_type: "practice_effect".to_string(),
- text: "Cover crops improve soil structure.".to_string(),
- citation_spans: vec![KnowledgeCitationSpan {
- source_ref: knowledge_event_ref('4', KIND_KNOWLEDGE_SOURCE),
- artifact_ref: None,
- page_start: Some(12),
- page_end: Some(13),
- section_path: vec!["chapter-1".to_string()],
- quote_hash: Some(synthetic_event_id('5')),
- chunk_id: Some("chunk-1".to_string()),
- }],
- topics: vec!["cover-crops".to_string()],
- applies_to: vec!["local-food".to_string()],
- author_asserted_confidence: Some("medium".to_string()),
- supersedes: Vec::new(),
- }
- }
-
- fn sample_knowledge_node_ref(label: &str) -> KnowledgeNodeRef {
- KnowledgeNodeRef {
- node_type: "event".to_string(),
- event_ref: Some(knowledge_event_ref('6', KIND_KNOWLEDGE_CLAIM)),
- address_ref: None,
- external_id: None,
- label: Some(label.to_string()),
- }
- }
-
- fn sample_knowledge_relation() -> KnowledgeRelation {
- KnowledgeRelation {
- schema: RADROOTS_KNOWLEDGE_RELATION_SCHEMA.to_string(),
- schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
- subject: sample_knowledge_node_ref("cover crops"),
- predicate: "supports".to_string(),
- object: sample_knowledge_node_ref("soil structure"),
- support_refs: vec![knowledge_event_ref('7', KIND_KNOWLEDGE_CLAIM)],
- author_asserted_confidence: Some("medium".to_string()),
- supersedes: Vec::new(),
- }
- }
-
- fn sample_knowledge_review() -> KnowledgeReview {
- KnowledgeReview {
- schema: RADROOTS_KNOWLEDGE_REVIEW_SCHEMA.to_string(),
- schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
- target: KnowledgeReviewTarget {
- event_id: synthetic_event_id('8'),
- author_pubkey: synthetic_pubkey('a'),
- kind: KIND_KNOWLEDGE_CLAIM,
- address: None,
- relays: vec!["wss://relay.example.test".to_string()],
- review_scope: KnowledgeReviewScope::SpecificVersion,
- },
- reviewer_role: "peer".to_string(),
- verdict: "needs_more_evidence".to_string(),
- scores: vec![KnowledgeReviewScore {
- dimension: "evidence".to_string(),
- value: "partial".to_string(),
- note: None,
- }],
- notes: Some("Synthetic review".to_string()),
- evidence_refs: vec![knowledge_event_ref('9', KIND_KNOWLEDGE_SOURCE)],
- }
- }
-
- fn sample_knowledge_field_report() -> KnowledgeFieldReport {
- KnowledgeFieldReport {
- schema: RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA.to_string(),
- schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
- report_type: "observation".to_string(),
- title: "Field observation".to_string(),
- summary: Some("Observed cover crop residue.".to_string()),
- context: KnowledgeFieldContext {
- location_precision: KnowledgeLocationPrecision::CoarseGeohash,
- public_location: Some(KnowledgeLocation {
- label: Some("watershed".to_string()),
- region: Some("synthetic-region".to_string()),
- locality: None,
- geohash: Some("c23".to_string()),
- }),
- private_location_ref: None,
- topics: vec!["field".to_string()],
- context_tags: vec!["observation".to_string()],
- },
- observations: vec![KnowledgeObservation {
- observation_type: "residue".to_string(),
- text: "Residue was visible across beds.".to_string(),
- observed_at: Some("2026-07-05".to_string()),
- values: vec![KnowledgeObservationValue {
- key: "coverage".to_string(),
- value: "medium".to_string(),
- unit: None,
- }],
- }],
- artifact_refs: vec![knowledge_event_ref('c', KIND_FILE_METADATA)],
- related_refs: vec![knowledge_event_ref('d', KIND_KNOWLEDGE_CLAIM)],
- limitations: vec!["single observer".to_string()],
- }
- }
-
- fn signed_claim_event_json() -> String {
- let claim_json = serde_json::to_string(&sample_knowledge_claim()).expect("claim json");
- let tags = serde_json::from_str::<Vec<Vec<String>>>(
- &knowledge_claim_tags(&claim_json).expect("claim tags"),
- )
- .expect("tags");
- let tags = tags
- .into_iter()
- .map(nostr::Tag::parse)
- .collect::<Result<Vec<_>, _>>()
- .expect("parsed tags");
- let keys = nostr::Keys::generate();
- let event =
- nostr::EventBuilder::new(nostr::Kind::Custom(KIND_KNOWLEDGE_CLAIM as u16), claim_json)
- .tags(tags)
- .custom_created_at(nostr::Timestamp::from_secs(1_800_000_000))
- .sign_with_keys(&keys)
- .expect("signed event");
- let envelope = EventEnvelope::new(EventEnvelopeParts {
- id: event.id.to_hex(),
- author: event.pubkey.to_hex(),
- created_at: event.created_at.as_secs(),
- kind: u32::from(event.kind.as_u16()),
- tags: event
- .tags
- .as_slice()
- .iter()
- .map(|tag| tag.as_slice().to_vec())
- .collect(),
- content: event.content,
- sig: event.sig.to_string(),
- })
- .expect("event envelope");
- serde_json::to_string(&envelope).expect("event json")
- }
-
- fn social_location() -> SocialLocation {
- SocialLocation {
- name: Some("field edge".to_string()),
- geohash: Some("c23nb62w20st".to_string()),
- }
- }
-
- fn sample_article() -> Article {
- Article {
- d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
- title: "soil notes".to_string(),
- content: "# soil notes".to_string(),
- summary: Some("cover crop observations".to_string()),
- image: Some("https://media.example.test/article.jpg".to_string()),
- published_at: Some(1_780_000_000),
- farm: Some(social_farm_anchor()),
- location: Some(social_location()),
- topics: Some(vec!["soil".to_string(), "cover-crops".to_string()]),
- }
- }
-
- fn sample_public_file_metadata() -> FileMetadata {
- FileMetadata {
- url: "https://media.example.test/public.jpg".to_string(),
- mime_type: "image/jpeg".to_string(),
- sha256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
- original_sha256: Some(
- "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".to_string(),
- ),
- size: Some(4096),
- dimensions: Some(SocialMediaDimensions {
- width: 1200,
- height: 800,
- }),
- blurhash: None,
- thumbnails: None,
- summary: Some("public field photo".to_string()),
- alt: Some("rows after harvest".to_string()),
- fallback: Some("https://media.example.test/fallback.jpg".to_string()),
- magnet: Some("magnet:?xt=urn:btih:abc".to_string()),
- content_hashes: Some(vec!["sha256:field".to_string()]),
- services: Some(vec!["https://media.example.test".to_string()]),
- content: Some("caption".to_string()),
- }
- }
-
- fn sample_reaction() -> Reaction {
- Reaction {
- target: address_target(30023, "AAAAAAAAAAAAAAAAAAAAAg"),
- content: String::new(),
- }
- }
-
- fn sample_repost() -> Repost {
- Repost {
- target: event_target(1, 'b'),
- content: Some("field update".to_string()),
- }
- }
-
- fn sample_generic_repost() -> GenericRepost {
- GenericRepost {
- target: address_target(30023, "AAAAAAAAAAAAAAAAAAAAAg"),
- target_kind: 30023,
- content: Some("article share".to_string()),
- }
- }
-
- fn sample_report() -> Report {
- Report {
- reported_pubkey: synthetic_pubkey('b'),
- report_type: ReportType::Spam,
- event: Some(event_target(1, 'c')),
- file: Some(ReportFileTarget {
- sha256: Some(
- "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
- ),
- url: Some("https://media.example.test/bad.jpg".to_string()),
- magnet: None,
- }),
- content: Some("spam report".to_string()),
- }
- }
-
- fn sample_job_request() -> JobRequest {
- JobRequest {
- kind: 5100,
- inputs: vec![JobInput {
- data: "alpha".to_string(),
- input_type: JobInputType::Text,
- relay: None,
- marker: None,
- }],
- output: None,
- params: vec![JobParam {
- key: "mode".to_string(),
- value: "fast".to_string(),
- }],
- bid_sat: Some(42),
- relays: vec!["wss://relay.example.com".to_string()],
- providers: vec!["provider-a".to_string()],
- topics: vec!["topic-a".to_string()],
- encrypted: false,
- }
- }
-
- fn sample_workspace_manifest() -> FarmWorkspaceManifest {
- FarmWorkspaceManifest {
- d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
- schema: RADROOTS_FARM_WORKSPACE_SCHEMA.to_string(),
- farm_group_id: "field-group".to_string(),
- name: "Small Regen Farm".to_string(),
- owner_pubkey: "workspace_owner_pubkey".to_string(),
- farm: Some(FarmRef {
- pubkey: "farm_pubkey".to_string(),
- d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
- }),
- relays: vec![FarmWorkspaceRelay {
- url: "wss://relay.example.invalid/farm/field-group".to_string(),
- mode: FarmWorkspaceRelayMode::ReadWrite,
- }],
- media_servers: vec![FarmWorkspaceMediaServer {
- url: "https://media.example.invalid/farm/field-group".to_string(),
- service: "RadrootsPrivateMedia".to_string(),
- }],
- supported_kinds: vec![78, 30078, KIND_FARM_FILE_METADATA],
- protocol_version: RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION.to_string(),
- created_at_ms: 1_780_000_000_000,
- updated_at_ms: None,
- }
- }
-
- fn sample_crdt_change() -> FarmCrdtChange {
- FarmCrdtChange {
- schema: RADROOTS_FARM_CRDT_CHANGE_SCHEMA.to_string(),
- workspace: FarmWorkspaceRef {
- pubkey: "workspace_pubkey".to_string(),
- d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
- },
- farm_group_id: "field-group".to_string(),
- document_id: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
- document_kind: FarmCrdtDocumentKind::FarmTask,
- crdt_backend: CrdtBackend::Automerge,
- crdt_backend_version: Some("0.x".to_string()),
- actor_id: "actor_abc".to_string(),
- change_hash: "crdt_hash_abc".to_string(),
- dependencies: Vec::new(),
- encoded_change: "abc-DEF_012".to_string(),
- semantic_kind: FarmSemanticKind::FarmTaskCreate,
- business_time_ms: 1_780_000_000_000,
- author_member_id: Some("member_abc".to_string()),
- app_version: Some("0.1.0".to_string()),
- }
- }
-
- fn sample_file_metadata() -> FarmFileMetadata {
- FarmFileMetadata {
- d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
- workspace: FarmWorkspaceRef {
- pubkey: "workspace_pubkey".to_string(),
- d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
- },
- farm_group_id: "field-group".to_string(),
- owner_document_id: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
- owner_document_kind: FarmCrdtDocumentKind::FarmTask,
- caption: Some("Tomatoes harvested from Patch Y.".to_string()),
- url: "https://media.example.invalid/blob/sha256".to_string(),
- mime_type: "image/jpeg".to_string(),
- sha256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
- original_sha256: None,
- size_bytes: Some(123_456),
- dimensions: Some(FarmFileDimensions { w: 1600, h: 1200 }),
- blurhash: None,
- thumb: Some(FarmFileSource {
- url: "https://media.example.invalid/thumb/sha256".to_string(),
- mime_type: Some("image/jpeg".to_string()),
- dimensions: Some(FarmFileDimensions { w: 320, h: 240 }),
- }),
- image: None,
- alt: Some("Harvested tomatoes in a crate".to_string()),
- fallbacks: Vec::new(),
- }
- }
-
- fn sample_group_metadata() -> GroupEditableMetadata {
- GroupEditableMetadata {
- name: Some("Small Regen Farm".to_string()),
- about: Some("Field app group".to_string()),
- picture: Some("https://media.example.invalid/group.png".to_string()),
- is_private: false,
- is_restricted: true,
- is_closed: false,
- is_hidden: false,
- supported_kinds: Some(vec![78, 30078, KIND_FARM_FILE_METADATA]),
- }
- }
-
- fn sample_group_user(role: &str) -> GroupUserRef {
- GroupUserRef {
- pubkey: format!("{role}_pubkey"),
- roles: vec![role.to_string()],
- }
- }
-
- fn sample_group_role() -> GroupRole {
- GroupRole {
- name: "member".to_string(),
- description: Some("can read and write group events".to_string()),
- permissions: vec!["read".to_string(), "write".to_string()],
- }
- }
-
- fn assert_tags_json(value: Result<String, RadrootsJsValue>) {
- let tags = tags_json(value);
- assert!(!tags.is_empty());
- }
-
- fn tags_json(value: Result<String, RadrootsJsValue>) -> Vec<Vec<String>> {
- let json = value.expect("tags json");
- serde_json::from_str(&json).expect("tags")
- }
-
- fn has_tag(tags: &[Vec<String>], key: &str, value: &str) -> bool {
- tags.iter().any(|tag| {
- tag.first().map(|entry| entry.as_str()) == Some(key)
- && tag.get(1).map(|entry| entry.as_str()) == Some(value)
- })
- }
-
- fn has_exact_tag(tags: &[Vec<String>], expected: &[&str]) -> bool {
- tags.iter().any(|tag| {
- tag.iter()
- .map(|entry| entry.as_str())
- .eq(expected.iter().copied())
- })
- }
-
- type BindingEncoder = fn(&str) -> Result<String, RadrootsJsValue>;
-
- const POST_AUTHORED_VECTORS: &str = include_str!(
- "../../../../lib/contracts/conformance/vectors/post/verified_profiles.v1.json"
- );
- const COMMENT_AUTHORED_VECTORS: &str = include_str!(
- "../../../../lib/contracts/conformance/vectors/comment/verified_profile.v1.json"
- );
-
- #[derive(Deserialize)]
- #[serde(deny_unknown_fields)]
- struct AuthoredVectorSuite {
- suite: String,
- contract_version: String,
- vectors: Vec<AuthoredVector>,
- }
-
- #[derive(Deserialize)]
- #[serde(deny_unknown_fields)]
- struct AuthoredVector {
- id: String,
- kind: String,
- input: serde_json::Value,
- expected: serde_json::Value,
- }
-
- fn authored_binding(kind: &str) -> Option<BindingEncoder> {
- if kind.starts_with("social.update.build_authored_draft.") {
- Some(social_update_build_authored_draft)
- } else if kind.starts_with("social.photo_update.build_authored_draft.") {
- Some(social_photo_update_build_authored_draft)
- } else if kind.starts_with("social.ask.build_authored_draft.") {
- Some(social_ask_build_authored_draft)
- } else if kind.starts_with("social.comment.build_authored_draft.") {
- Some(social_comment_build_authored_draft)
- } else {
- None
- }
- }
-
- fn authored_vector_request(vector: &AuthoredVector) -> String {
- let mut input = vector.input.clone();
- if let Some(images) = input
- .get_mut("images")
- .and_then(serde_json::Value::as_array_mut)
- {
- for image in images {
- let image = image
- .as_object_mut()
- .unwrap_or_else(|| panic!("{} image must be an object", vector.id));
- let bytes_utf8 = image
- .remove("bytes_utf8")
- .unwrap_or_else(|| panic!("{} image must carry vector bytes", vector.id));
- let bytes_utf8 = bytes_utf8
- .as_str()
- .unwrap_or_else(|| panic!("{} vector bytes must be UTF-8 text", vector.id));
- image.insert(
- "bytes".to_owned(),
- serde_json::Value::Array(
- bytes_utf8
- .as_bytes()
- .iter()
- .copied()
- .map(serde_json::Value::from)
- .collect(),
- ),
- );
- }
- }
- serde_json::to_string(&input)
- .unwrap_or_else(|error| panic!("{} request JSON failed: {error}", vector.id))
- }
-
- fn authored_error_value(error: RadrootsJsValue) -> serde_json::Value {
- serde_json::from_str(&error).expect("authored binding error must be coded JSON")
- }
-
- fn authored_error_inner(error: RadrootsJsValue) -> String {
- let error = authored_error_value(error);
- assert_eq!(error["code"], "encode_error");
- error["inner_code"]
- .as_str()
- .expect("authored error inner_code")
- .to_owned()
- }
-
- fn execute_authored_vectors(raw: &str, expected_suite: &str, expected_vector_count: usize) {
- let suite: AuthoredVectorSuite =
- serde_json::from_str(raw).expect("owner conformance vectors");
- assert_eq!(suite.suite, expected_suite);
- assert_eq!(suite.contract_version, "1.0.0");
- let mut executed = 0usize;
- for vector in suite.vectors {
- let Some(binding) = authored_binding(&vector.kind) else {
- continue;
- };
- executed += 1;
- let request = authored_vector_request(&vector);
- if vector.kind.ends_with(".valid") {
- let first = binding(&request)
- .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
- let second = binding(&request)
- .unwrap_or_else(|error| panic!("{} repeat failed: {error}", vector.id));
- assert_eq!(first, second, "{} repeat encoding drifted", vector.id);
- let actual: serde_json::Value = serde_json::from_str(&first)
- .unwrap_or_else(|error| panic!("{} output JSON failed: {error}", vector.id));
- assert_eq!(actual, vector.expected, "{}", vector.id);
- } else if vector.kind.ends_with(".invalid") {
- let first = binding(&request).expect_err("invalid authored vector must fail");
- let second =
- binding(&request).expect_err("repeated invalid authored vector must fail");
- let expected = vector.expected["error"]
- .as_str()
- .unwrap_or_else(|| panic!("{} expected error must be a string", vector.id));
- assert_eq!(authored_error_inner(first), expected, "{}", vector.id);
- assert_eq!(authored_error_inner(second), expected, "{}", vector.id);
- } else {
- panic!("{} has unsupported authored vector kind", vector.id);
- }
- }
- assert_eq!(executed, expected_vector_count);
- }
-
- #[test]
- fn bindings_reject_invalid_json() {
- let bindings: [BindingEncoder; 44] = [
- social_update_build_authored_draft,
- social_photo_update_build_authored_draft,
- social_ask_build_authored_draft,
- social_comment_build_authored_draft,
- operational_listing_tags,
- operational_listing_tags_full,
- article_tags,
- file_metadata_tags,
- repost_tags,
- generic_repost_tags,
- report_tags,
- follow_tags,
- document_tags,
- coop_tags,
- farm_tags,
- list_tags,
- list_set_tags,
- plot_tags,
- job_request_tags,
- job_result_tags,
- job_feedback_tags,
- reaction_tags,
- message_tags,
- message_file_tags,
- seal_tags,
- gift_wrap_tags,
- farm_workspace_manifest_tags,
- farm_crdt_change_tags,
- farm_file_metadata_tags,
- relay_auth_tags,
- http_auth_tags,
- group_put_user_tags,
- group_remove_user_tags,
- group_create_group_tags,
- group_edit_metadata_tags,
- group_delete_group_tags,
- group_delete_event_tags,
- group_create_invite_tags,
- group_join_request_tags,
- group_leave_request_tags,
- group_metadata_tags,
- group_admins_tags,
- group_members_tags,
- group_roles_tags,
- ];
-
- for binding in bindings {
- assert!(binding("{").is_err());
- }
- assert!(operational_listing_tags("").is_err());
- }
-
- #[test]
- fn strict_authored_post_bindings_execute_owner_conformance_vectors() {
- execute_authored_vectors(POST_AUTHORED_VECTORS, "post_profiles", 6);
- }
-
- #[test]
- fn strict_authored_comment_binding_executes_owner_conformance_vectors() {
- execute_authored_vectors(COMMENT_AUTHORED_VECTORS, "nip22_comment_profile", 31);
- }
-
- #[test]
- fn authored_comment_rejects_address_coordinate_author_and_kind_mismatches() {
- let author = synthetic_pubkey('e');
- let coordinate = format!("30402:{author}:victoria-market");
- let request = |author: &str, kind: u32| {
- serde_json::json!({
- "content": "Available this week",
- "root": {
- "type": "address",
- "coordinate": coordinate,
- "author": author,
- "kind": kind,
- "relay": null,
- },
- "position": {
- "type": "top_address",
- "current_revision": synthetic_event_id('b'),
- },
- })
- .to_string()
- };
-
- let author_error =
- social_comment_build_authored_draft(&request(&synthetic_pubkey('f'), 30402))
- .expect_err("coordinate author mismatch");
- assert_eq!(
- authored_error_inner(author_error),
- "comment_root_author_mismatch"
- );
-
- let kind_error = social_comment_build_authored_draft(&request(&author, 31922))
- .expect_err("coordinate kind mismatch");
- assert_eq!(
- authored_error_inner(kind_error),
- "comment_root_kind_mismatch"
- );
- }
-
- #[test]
- fn authored_image_errors_keep_the_stable_binding_code() {
- let error = image_authored_error(AuthoredImageError::MediaTypeNotImage);
- assert_eq!(authored_error_inner(error), "media_type_not_image");
- }
-
- #[test]
- fn authored_media_binding_verifies_exact_arbitrary_bytes() {
- let bytes = vec![0, 159, 146, 150];
- let hash = Sha256::digest(&bytes);
- let url = format!("https://media.example/{hash}.webp");
- let input = serde_json::json!({
- "content": format!("Harvest {url}"),
- "images": [{
- "bytes": bytes,
- "url": url,
- "media_type": "image/webp",
- "uploaded": 1_784_347_200_u64,
- "width": 1200,
- "height": 900,
- "alt": "Harvest",
- "fallbacks": [],
- }],
- });
-
- let encoded = social_photo_update_build_authored_draft(
- &serde_json::to_string(&input).expect("media request"),
- )
- .expect("byte-verified PhotoUpdate");
- let encoded: Nip01EventWireParts = serde_json::from_str(&encoded).expect("wire parts");
- assert_eq!(encoded.tags.len(), 1);
- assert!(encoded.tags[0].contains(&format!("x {hash}")));
- assert!(encoded.tags[0].contains(&"size 4".to_owned()));
-
- let mut wrong_bytes = input.clone();
- wrong_bytes["images"][0]["bytes"] = serde_json::json!([1, 2, 3, 4]);
- let error = social_photo_update_build_authored_draft(
- &serde_json::to_string(&wrong_bytes).expect("wrong-byte request"),
- )
- .expect_err("URL digest must bind exact bytes");
- assert_eq!(authored_error_inner(error), "descriptor_hash_mismatch");
- }
-
- #[test]
- fn authored_media_binding_rejects_fabricated_descriptor_fields() {
- let bytes = b"image".to_vec();
- let hash = Sha256::digest(&bytes);
- let url = format!("https://media.example/{hash}.webp");
- let mut input = serde_json::json!({
- "content": format!("Harvest {url}"),
- "images": [{
- "bytes": bytes,
- "url": url,
- "media_type": "image/webp",
- "uploaded": 1_784_347_200_u64,
- "width": 1200,
- "height": 900,
- "alt": "Harvest",
- }],
- });
- input["images"][0]["sha256"] = serde_json::json!(hash.to_string());
- input["images"][0]["size"] = serde_json::json!(5);
-
- let error = social_photo_update_build_authored_draft(
- &serde_json::to_string(&input).expect("fabricated descriptor request"),
- )
- .expect_err("caller-supplied descriptor commitments must be rejected");
- let error = authored_error_value(error);
- assert_eq!(error["code"], "invalid_json");
- assert_eq!(error["inner_code"], "authored_input");
- }
-
- #[test]
- fn ask_allows_zero_media_while_photo_update_requires_media() {
- let ask = social_ask_build_authored_draft(r#"{"content":"When is harvest?","images":[]}"#)
- .expect("Ask without media");
- let ask: Nip01EventWireParts = serde_json::from_str(&ask).expect("Ask wire parts");
- assert_eq!(
- ask.tags,
- vec![vec!["t".to_owned(), "radroots-ask".to_owned()]]
- );
-
- let error =
- social_photo_update_build_authored_draft(r#"{"content":"Harvest","images":[]}"#)
- .expect_err("PhotoUpdate without media");
- assert_eq!(authored_error_inner(error), "photo_imeta_missing");
- }
-
- #[test]
- fn bindings_encode_to_json_when_input_is_valid() {
- let listing_json = serde_json::to_string(&sample_listing()).expect("listing json");
- let listing_tags_json =
- operational_listing_tags(&listing_json).expect("operational listing tags");
- let listing_tags: Vec<Vec<String>> =
- serde_json::from_str(&listing_tags_json).expect("listing tags json");
- assert!(!listing_tags.is_empty());
-
- let request_json = serde_json::to_string(&sample_job_request()).expect("request json");
- let request_tags_json = job_request_tags(&request_json).expect("request tags");
- let request_tags: Vec<Vec<String>> =
- serde_json::from_str(&request_tags_json).expect("request tags json");
- assert!(!request_tags.is_empty());
- }
-
- #[test]
- fn social_bindings_encode_to_json_when_input_is_valid() {
- assert_tags_json(article_tags(
- &serde_json::to_string(&sample_article()).expect("article json"),
- ));
- assert_tags_json(file_metadata_tags(
- &serde_json::to_string(&sample_public_file_metadata()).expect("file json"),
- ));
- assert_tags_json(reaction_tags(
- &serde_json::to_string(&sample_reaction()).expect("reaction json"),
- ));
- assert_tags_json(repost_tags(
- &serde_json::to_string(&sample_repost()).expect("repost json"),
- ));
- assert_tags_json(generic_repost_tags(
- &serde_json::to_string(&sample_generic_repost()).expect("generic repost json"),
- ));
- assert_tags_json(report_tags(
- &serde_json::to_string(&sample_report()).expect("report json"),
- ));
- }
-
- #[test]
- fn social_bindings_surface_builder_errors() {
- let mut article = sample_article();
- article.d_tag.clear();
- assert!(article_tags(&serde_json::to_string(&article).expect("article json")).is_err());
-
- let mut reaction = sample_reaction();
- reaction.target = SocialTarget::External {
- id: "https://example.test/object".to_string(),
- external_kind: "web".to_string(),
- hint: None,
- };
- assert!(reaction_tags(&serde_json::to_string(&reaction).expect("reaction json")).is_err());
-
- let mut report = sample_report();
- report.reported_pubkey.clear();
- assert!(report_tags(&serde_json::to_string(&report).expect("report json")).is_err());
- }
-
- #[test]
- fn knowledge_bindings_encode_to_json_when_input_is_valid() {
- let article_tags = tags_json(wiki_article_tags(
- &serde_json::to_string(&sample_wiki_article()).expect("wiki article json"),
- ));
- assert!(has_tag(&article_tags, "title", "Soil health"));
- let fork_address = format!(
- "{}:{}:soil-health",
- KIND_WIKI_ARTICLE,
- synthetic_pubkey('a')
- );
- let fork_event_id = synthetic_event_id('b');
- assert!(has_exact_tag(
- &article_tags,
- &[
- "a",
- fork_address.as_str(),
- "wss://relay.example.test",
- "fork"
- ]
- ));
- assert!(has_exact_tag(
- &article_tags,
- &[
- "e",
- fork_event_id.as_str(),
- "wss://relay.example.test",
- "fork"
- ]
- ));
-
- let redirect_tags = tags_json(wiki_redirect_tags(
- &serde_json::to_string(&sample_wiki_redirect()).expect("wiki redirect json"),
- ));
- let redirect_address = format!(
- "{}:{}:soil-health",
- KIND_WIKI_ARTICLE,
- synthetic_pubkey('a')
- );
- assert!(has_exact_tag(
- &redirect_tags,
- &["a", redirect_address.as_str(), "wss://relay.example.test"]
- ));
-
- let merge_request = sample_wiki_merge_request();
- let merge_parts =
- radroots_event_codec::encode::knowledge::wiki_merge_request_to_wire_parts(
- &merge_request,
- )
- .expect("merge request parts");
- assert_eq!(merge_parts.content, "Merge synthetic soil article updates");
- let merge_tags = tags_json(wiki_merge_request_tags(
- &serde_json::to_string(&sample_wiki_merge_request()).expect("merge request json"),
- ));
- let source_event_id = synthetic_event_id('f');
- assert!(has_exact_tag(
- &merge_tags,
- &["e", source_event_id.as_str(), "", "source"]
- ));
-
- let source_tags = tags_json(knowledge_source_tags(
- &serde_json::to_string(&sample_knowledge_source()).expect("source json"),
- ));
- assert!(has_tag(
- &source_tags,
- "contract",
- RADROOTS_KNOWLEDGE_SOURCE_SCHEMA
- ));
-
- let claim_tags = tags_json(knowledge_claim_tags(
- &serde_json::to_string(&sample_knowledge_claim()).expect("claim json"),
- ));
- assert!(has_tag(
- &claim_tags,
- "contract",
- RADROOTS_KNOWLEDGE_CLAIM_SCHEMA
- ));
-
- assert_tags_json(knowledge_relation_tags(
- &serde_json::to_string(&sample_knowledge_relation()).expect("relation json"),
- ));
- assert_tags_json(knowledge_review_tags(
- &serde_json::to_string(&sample_knowledge_review()).expect("review json"),
- ));
- assert_tags_json(knowledge_field_report_tags(
- &serde_json::to_string(&sample_knowledge_field_report()).expect("field report json"),
- ));
- }
-
- #[test]
- fn wiki_article_tags_accept_missing_title() {
- let mut article = sample_wiki_article();
- article.title = None;
- let tags = tags_json(wiki_article_tags(
- &serde_json::to_string(&article).expect("wiki article json"),
- ));
- assert!(
- !tags
- .iter()
- .any(|tag| tag.first().map(String::as_str) == Some("title"))
- );
- }
-
- #[test]
- fn knowledge_claim_tags_enforce_citation_rules() {
- let mut claim = sample_knowledge_claim();
- claim.citation_spans.clear();
- let error = knowledge_claim_tags(
- &serde_json::to_string(&claim).expect("uncited source-backed claim json"),
- )
- .expect_err("source-backed claim requires citations");
- assert!(error.contains("citation_spans"));
-
- assert_tags_json(knowledge_claim_tags(
- &serde_json::to_string(&sample_knowledge_claim()).expect("claim json"),
- ));
-
- for claim_type in ["hypothesis", "observation", "question"] {
- let mut uncited = sample_knowledge_claim();
- uncited.claim_type = claim_type.to_string();
- uncited.citation_spans.clear();
- assert_tags_json(knowledge_claim_tags(
- &serde_json::to_string(&uncited).expect("uncited claim json"),
- ));
- }
- }
-
- #[test]
- fn knowledge_bindings_verify_decode_and_manifest_json() {
- let decoded = verify_and_decode_event_json(&signed_claim_event_json()).expect("decoded");
- let decoded: serde_json::Value = serde_json::from_str(&decoded).expect("decoded json");
- assert_eq!(decoded["event_type"], "knowledge_claim");
- assert_eq!(decoded["contract_id"], RADROOTS_KNOWLEDGE_CLAIM_SCHEMA);
- assert_eq!(
- decoded["payload"]["text"],
- "Cover crops improve soil structure."
- );
-
- let manifest = contract_manifest_json().expect("manifest");
- let manifest: serde_json::Value = serde_json::from_str(&manifest).expect("manifest json");
- assert_eq!(manifest["schema_version"], 2);
- assert_eq!(manifest["contract_count"], 11);
- let claim_contract = manifest["contracts"]
- .as_array()
- .expect("contracts")
- .iter()
- .find(|contract| contract["contract_id"] == RADROOTS_KNOWLEDGE_CLAIM_SCHEMA)
- .expect("claim contract");
- assert_eq!(claim_contract["sdk_builder_support"], true);
- assert_eq!(claim_contract["sdk_draft_support"], true);
- assert_eq!(claim_contract["wasm_tag_builder_support"], true);
- assert_eq!(claim_contract["wasm_verified_decode_support"], true);
- }
-
- #[test]
- fn knowledge_bindings_verify_decode_errors_are_coded_json() {
- let invalid_json = verify_and_decode_event_json("not json").expect_err("invalid json");
- let invalid_json: serde_json::Value =
- serde_json::from_str(&invalid_json).expect("error json");
- assert_eq!(invalid_json["code"], "invalid_json");
- assert_eq!(invalid_json["inner_code"], "event_json");
-
- let mut event: serde_json::Value =
- serde_json::from_str(&signed_claim_event_json()).expect("event json");
- event["sig"] = serde_json::Value::String("0".repeat(128));
- let signature_error =
- verify_and_decode_event_json(&event.to_string()).expect_err("signature error");
- let signature_error: serde_json::Value =
- serde_json::from_str(&signature_error).expect("signature error json");
- assert_eq!(signature_error["code"], "nip01_verification");
- assert_eq!(signature_error["inner_code"], "signature_invalid");
- }
-
- #[test]
- fn knowledge_bindings_reject_unchecked_event_envelope_input() {
- let mut oversized: serde_json::Value =
- serde_json::from_str(&signed_claim_event_json()).expect("event json");
- oversized["content"] = serde_json::Value::String(
- "x".repeat(EventEnvelopeLimits::default().max_content_bytes + 1),
- );
- let oversized_error =
- verify_and_decode_event_json(&oversized.to_string()).expect_err("oversized content");
- let oversized_error: serde_json::Value =
- serde_json::from_str(&oversized_error).expect("oversized error json");
- assert_eq!(oversized_error["code"], "invalid_event");
- assert_eq!(oversized_error["inner_code"], "content_too_large");
-
- let mut non_canonical: serde_json::Value =
- serde_json::from_str(&signed_claim_event_json()).expect("event json");
- non_canonical["id"] = serde_json::Value::String("A".repeat(64));
- let canonical_error =
- verify_and_decode_event_json(&non_canonical.to_string()).expect_err("non-canonical id");
- let canonical_error: serde_json::Value =
- serde_json::from_str(&canonical_error).expect("canonical error json");
- assert_eq!(canonical_error["code"], "invalid_event");
- assert_eq!(canonical_error["inner_code"], "id_non_canonical");
-
- let mut unknown_field: serde_json::Value =
- serde_json::from_str(&signed_claim_event_json()).expect("event json");
- unknown_field["verified"] = serde_json::Value::Bool(true);
- let unknown_error = verify_and_decode_event_json(&unknown_field.to_string())
- .expect_err("unknown event field");
- let unknown_error: serde_json::Value =
- serde_json::from_str(&unknown_error).expect("unknown-field error json");
- assert_eq!(unknown_error["code"], "invalid_json");
- assert_eq!(unknown_error["inner_code"], "event_json");
- }
-
- #[test]
- fn event_envelope_error_codes_cover_every_structural_limit_class() {
- let cases = [
- (
- EventEnvelopeError::NonCanonicalAuthor,
- "author_non_canonical",
- ),
- (
- EventEnvelopeError::NonCanonicalSignature,
- "signature_non_canonical",
- ),
- (EventEnvelopeError::EmptyTag { index: 0 }, "tag_empty"),
- (
- EventEnvelopeError::EmptyTagKey { index: 0 },
- "tag_key_empty",
- ),
- (
- EventEnvelopeError::ControlCharacterTagKey { index: 0 },
- "tag_key_control_character",
- ),
- (
- EventEnvelopeError::TooManyTags { max: 1, actual: 2 },
- "too_many_tags",
- ),
- (
- EventEnvelopeError::TooManyTagElements { max: 1, actual: 2 },
- "too_many_tag_elements",
- ),
- (
- EventEnvelopeError::TagElementTooLarge {
- tag_index: 0,
- element_index: 0,
- max: 1,
- actual: 2,
- },
- "tag_element_too_large",
- ),
- (
- EventEnvelopeError::TagsTooLarge { max: 1, actual: 2 },
- "tags_too_large",
- ),
- ];
- for (error, code) in cases {
- assert_eq!(envelope_error_code(&error), code);
- }
- }
-
- #[test]
- fn field_bindings_encode_to_json_when_input_is_valid() {
- let workspace_json =
- serde_json::to_string(&sample_workspace_manifest()).expect("workspace json");
- assert_tags_json(farm_workspace_manifest_tags(&workspace_json));
-
- let crdt_json = serde_json::json!({
- "change": sample_crdt_change(),
- "author_pubkey": "author_pubkey"
- })
- .to_string();
- assert_tags_json(farm_crdt_change_tags(&crdt_json));
-
- let file_json = serde_json::to_string(&sample_file_metadata()).expect("file json");
- assert_tags_json(farm_file_metadata_tags(&file_json));
-
- let relay_auth_json = serde_json::to_string(&RelayAuth {
- relay: "wss://relay.example.invalid/farm/field-group".to_string(),
- challenge: "relay-provided-challenge".to_string(),
- })
- .expect("relay auth json");
- assert_tags_json(relay_auth_tags(&relay_auth_json));
-
- let http_auth_json = serde_json::to_string(&HttpAuth {
- url: "https://media.example.invalid/upload".to_string(),
- method: "POST".to_string(),
- payload_sha256: Some(
- "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
- ),
- })
- .expect("http auth json");
- assert_tags_json(http_auth_tags(&http_auth_json));
- }
-
- #[test]
- fn field_bindings_surface_builder_errors() {
- let crdt_json = serde_json::json!({
- "change": sample_crdt_change(),
- "author_pubkey": " "
- })
- .to_string();
-
- assert!(farm_crdt_change_tags(&crdt_json).is_err());
- }
-
- #[test]
- fn group_bindings_encode_to_json_when_input_is_valid() {
- let metadata = sample_group_metadata();
- assert_tags_json(group_put_user_tags(
- &serde_json::to_string(&GroupPutUser {
- group_id: "field-group".to_string(),
- message: Some("add member".to_string()),
- pubkey: "member_pubkey".to_string(),
- roles: vec!["member".to_string()],
- })
- .expect("put user json"),
- ));
- assert_tags_json(group_remove_user_tags(
- &serde_json::to_string(&GroupRemoveUser {
- group_id: "field-group".to_string(),
- message: Some("remove member".to_string()),
- pubkey: "member_pubkey".to_string(),
- })
- .expect("remove user json"),
- ));
- assert_tags_json(group_create_group_tags(
- &serde_json::to_string(&GroupCreateGroup {
- group_id: "field-group".to_string(),
- message: Some("create group".to_string()),
- metadata: metadata.clone(),
- })
- .expect("create group json"),
- ));
- assert_tags_json(group_edit_metadata_tags(
- &serde_json::to_string(&GroupEditMetadata {
- group_id: "field-group".to_string(),
- message: Some("edit metadata".to_string()),
- metadata: metadata.clone(),
- })
- .expect("edit metadata json"),
- ));
- assert_tags_json(group_delete_group_tags(
- &serde_json::to_string(&GroupDeleteGroup {
- group_id: "field-group".to_string(),
- message: Some("delete group".to_string()),
- })
- .expect("delete group json"),
- ));
- assert_tags_json(group_delete_event_tags(
- &serde_json::to_string(&GroupDeleteEvent {
- group_id: "field-group".to_string(),
- message: Some("delete event".to_string()),
- event_id: "event_id".to_string(),
- })
- .expect("delete event json"),
- ));
- let invite_tags = tags_json(group_create_invite_tags(
- &serde_json::to_string(&GroupCreateInvite {
- group_id: "field-group".to_string(),
- message: Some("join the field group".to_string()),
- code: "invite-code".to_string(),
- })
- .expect("invite json"),
- ));
- assert!(invite_tags.contains(&vec!["code".to_string(), "invite-code".to_string()]));
- assert_tags_json(group_join_request_tags(
- &serde_json::to_string(&GroupJoinRequest {
- group_id: "field-group".to_string(),
- message: Some("requesting access".to_string()),
- code: Some("invite-code".to_string()),
- })
- .expect("join json"),
- ));
- assert_tags_json(group_leave_request_tags(
- &serde_json::to_string(&GroupLeaveRequest {
- group_id: "field-group".to_string(),
- message: Some("leaving".to_string()),
- })
- .expect("leave json"),
- ));
- let metadata_tags = tags_json(group_metadata_tags(
- &serde_json::to_string(&GroupMetadata {
- d_tag: "field-group".to_string(),
- metadata,
- })
- .expect("metadata json"),
- ));
- assert!(metadata_tags.contains(&vec!["restricted".to_string()]));
- assert!(metadata_tags.contains(&vec![
- "supported_kinds".to_string(),
- "78".to_string(),
- "30078".to_string(),
- KIND_FARM_FILE_METADATA.to_string()
- ]));
- assert_tags_json(group_admins_tags(
- &serde_json::to_string(&GroupAdmins {
- d_tag: "field-group".to_string(),
- description: Some("group admins".to_string()),
- admins: vec![sample_group_user("admin")],
- })
- .expect("admins json"),
- ));
- assert_tags_json(group_members_tags(
- &serde_json::to_string(&GroupMembers {
- d_tag: "field-group".to_string(),
- description: Some("group members".to_string()),
- members: vec![sample_group_user("member")],
- })
- .expect("members json"),
- ));
- assert_tags_json(group_roles_tags(
- &serde_json::to_string(&GroupRoles {
- d_tag: "field-group".to_string(),
- description: Some("group roles".to_string()),
- roles: vec![sample_group_role()],
- })
- .expect("roles json"),
- ));
- }
-
- #[test]
- fn listing_bindings_surface_builder_errors() {
- let mut listing_json = serde_json::to_value(sample_listing()).expect("listing value");
- listing_json["bins"] = serde_json::Value::Array(Vec::new());
- let listing_json = serde_json::to_string(&listing_json).expect("listing json");
-
- assert!(operational_listing_tags(&listing_json).is_err());
- assert!(operational_listing_tags_full(&listing_json).is_err());
- }
-}
diff --git a/crates/ffi/Cargo.toml b/crates/ffi/Cargo.toml
@@ -1,30 +0,0 @@
-[package]
-name = "radroots_sdk_ffi"
-publish = false
-version.workspace = true
-edition.workspace = true
-authors.workspace = true
-rust-version.workspace = true
-license.workspace = true
-description = "Private UniFFI wrapper over the shared Radroots SDK engine"
-repository.workspace = true
-homepage.workspace = true
-readme = "README.md"
-
-[lib]
-crate-type = ["lib", "staticlib", "cdylib"]
-
-[[bin]]
-name = "radroots-sdk-bindgen"
-path = "src/bin/bindgen.rs"
-
-[dependencies]
-radroots_sdk = { workspace = true, features = ["memory"] }
-thiserror = { workspace = true }
-uniffi = { workspace = true, features = ["cli"] }
-
-[dev-dependencies]
-tokio = { workspace = true, features = ["macros", "rt"] }
-
-[lints]
-workspace = true
diff --git a/crates/ffi/README.md b/crates/ffi/README.md
@@ -1,10 +0,0 @@
-# radroots_sdk_ffi
-
-Private UniFFI bindings over the host-neutral `radroots_sdk` engine. The V1
-surface exposes deterministic memory-client construction, versioned capability
-DTOs, secret-safe errors, and explicit asynchronous close. It does not own
-keychain prompts, background execution, application scheduling, or presentation
-DTOs; those remain mobile-host responsibilities.
-
-This Rust build crate is never published. Generated Swift and Kotlin artifacts
-are qualified independently from the public 19-crate Rust inventory.
diff --git a/crates/ffi/src/bin/bindgen.rs b/crates/ffi/src/bin/bindgen.rs
@@ -1,3 +0,0 @@
-fn main() {
- uniffi::uniffi_bindgen_main();
-}
diff --git a/crates/ffi/src/lib.rs b/crates/ffi/src/lib.rs
@@ -1,173 +0,0 @@
-//! Private mobile FFI over the shared SDK engine.
-
-#![forbid(unsafe_code)]
-#![doc = include_str!("../README.md")]
-
-use std::sync::Arc;
-
-use radroots_sdk::{Client, ClientBuilder};
-
-uniffi::setup_scaffolding!("radroots_sdk");
-
-/// Generation-1 mobile DTOs.
-pub mod v1 {
- use super::*;
-
- /// Stable capability maturity independent of runtime availability.
- #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
- pub enum CapabilityMaturity {
- Stable,
- Preview,
- Experimental,
- }
-
- /// Current side-effect-free capability availability.
- #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
- pub enum CapabilityAvailability {
- Available,
- Degraded,
- Unavailable,
- Unsupported,
- }
-
- /// Versioned, presentation-independent capability observation.
- #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
- pub struct CapabilityStatus {
- pub id: String,
- pub compiled: bool,
- pub configured: bool,
- pub availability: CapabilityAvailability,
- pub maturity: CapabilityMaturity,
- }
-
- /// Secret-safe SDK failure at the language boundary.
- #[derive(Debug, thiserror::Error, uniffi::Error)]
- pub enum Error {
- #[error("SDK {code}: {message}")]
- Sdk {
- code: String,
- message: String,
- retryable: bool,
- },
- }
-
- impl From<radroots_sdk::Error> for Error {
- fn from(error: radroots_sdk::Error) -> Self {
- let descriptor = error.descriptor();
- Self::Sdk {
- code: descriptor.code().as_str().to_owned(),
- message: descriptor.message().to_owned(),
- retryable: descriptor.retryable(),
- }
- }
- }
-
- /// Thread-safe mobile handle delegating lifecycle to [`radroots_sdk::Client`].
- #[derive(uniffi::Object)]
- pub struct MobileClient {
- client: Client,
- }
-
- #[uniffi::export]
- impl MobileClient {
- /// Creates a deterministic, local-only memory client without I/O.
- #[uniffi::constructor]
- pub fn memory() -> Result<Arc<Self>, Error> {
- let client = ClientBuilder::memory_default().build()?;
- Ok(Arc::new(Self { client }))
- }
-
- /// Returns stable capability DTOs without probing host resources.
- pub fn capabilities(&self) -> Vec<CapabilityStatus> {
- self.client
- .capabilities()
- .iter()
- .copied()
- .map(CapabilityStatus::from)
- .collect()
- }
-
- /// Returns whether explicit SDK close completed across all clones.
- pub fn is_closed(&self) -> bool {
- self.client.is_closed()
- }
-
- /// Explicitly closes SDK-owned resources without installing a runtime.
- pub async fn close(&self) -> Result<(), Error> {
- self.client.close().await.map_err(Error::from)
- }
- }
-
- impl From<radroots_sdk::capability::CapabilityStatus> for CapabilityStatus {
- fn from(status: radroots_sdk::capability::CapabilityStatus) -> Self {
- Self {
- id: status.id().as_str().to_owned(),
- compiled: status.is_compiled(),
- configured: status.is_configured(),
- availability: match status.availability() {
- radroots_sdk::capability::Availability::Available => {
- CapabilityAvailability::Available
- }
- radroots_sdk::capability::Availability::Degraded => {
- CapabilityAvailability::Degraded
- }
- radroots_sdk::capability::Availability::Unavailable => {
- CapabilityAvailability::Unavailable
- }
- radroots_sdk::capability::Availability::Unsupported => {
- CapabilityAvailability::Unsupported
- }
- },
- maturity: match status.maturity() {
- radroots_sdk::capability::Maturity::Stable => CapabilityMaturity::Stable,
- radroots_sdk::capability::Maturity::Preview => CapabilityMaturity::Preview,
- radroots_sdk::capability::Maturity::Experimental => {
- CapabilityMaturity::Experimental
- }
- },
- }
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::v1::{CapabilityAvailability, Error, MobileClient};
-
- fn assert_send_sync<T: Send + Sync>() {}
-
- #[tokio::test(flavor = "current_thread")]
- async fn memory_client_delegates_capabilities_and_lifecycle() {
- assert_send_sync::<MobileClient>();
- let client = MobileClient::memory().expect("memory client");
- let capabilities = client.capabilities();
- let storage = capabilities
- .iter()
- .find(|status| status.id == "storage.canonical")
- .expect("canonical storage");
- assert!(storage.compiled);
- assert!(storage.configured);
- assert_eq!(storage.availability, CapabilityAvailability::Available);
- assert!(!client.is_closed());
-
- client.close().await.expect("close");
- assert!(client.is_closed());
- client.close().await.expect("idempotent close");
- }
-
- #[test]
- fn sdk_error_mapping_is_versioned_and_secret_safe() {
- let native = radroots_sdk::ClientBuilder::new()
- .build()
- .expect_err("missing storage");
- let error = Error::from(native);
- let Error::Sdk {
- code,
- message,
- retryable,
- } = error;
- assert_eq!(code, "missing_storage");
- assert_eq!(message, "SDK storage capability is not configured");
- assert!(!retryable);
- }
-}
diff --git a/crates/identity_bindings/Cargo.toml b/crates/identity_bindings/Cargo.toml
@@ -1,14 +0,0 @@
-[package]
-name = "radroots_identity_bindings"
-description = "Generated bindings for Radroots identity contracts"
-version.workspace = true
-edition.workspace = true
-rust-version.workspace = true
-license.workspace = true
-repository.workspace = true
-homepage.workspace = true
-publish = false
-
-[dependencies]
-dto_bindgen_backend_ts = { workspace = true }
-radroots_identity = { workspace = true }
diff --git a/crates/identity_bindings/src/lib.rs b/crates/identity_bindings/src/lib.rs
@@ -1,37 +0,0 @@
-pub use radroots_identity as upstream;
-
-use dto_bindgen_backend_ts::{TypeScriptDeclaration, TypeScriptModule, TypeScriptValue};
-use radroots_identity::username::{MAX_LENGTH, MIN_LENGTH};
-
-pub fn constants_module() -> TypeScriptModule {
- TypeScriptModule::new("src/generated/constants.ts")
- .with_declaration(TypeScriptDeclaration::constant(
- "RADROOTS_USERNAME_MIN_LENGTH",
- None,
- usize_value(MIN_LENGTH),
- ))
- .with_declaration(TypeScriptDeclaration::constant(
- "RADROOTS_USERNAME_MAX_LENGTH",
- None,
- usize_value(MAX_LENGTH),
- ))
-}
-
-fn usize_value(value: usize) -> TypeScriptValue {
- TypeScriptValue::number(i64::try_from(value).expect("TypeScript constant fits in i64"))
-}
-
-#[cfg(test)]
-mod tests {
- use super::{MAX_LENGTH, MIN_LENGTH, constants_module};
-
- #[test]
- fn preserves_username_constant_exports() {
- let rendered = constants_module().render_source();
- assert!(rendered.contains("RADROOTS_USERNAME_MIN_LENGTH"));
- assert!(rendered.contains(&MIN_LENGTH.to_string()));
- assert!(rendered.contains("RADROOTS_USERNAME_MAX_LENGTH"));
- assert!(rendered.contains(&MAX_LENGTH.to_string()));
- assert!(!rendered.contains("REGEX"));
- }
-}
diff --git a/crates/radroots/Cargo.toml b/crates/radroots/Cargo.toml
@@ -1,51 +0,0 @@
-[package]
-name = "radroots"
-description = "Curated ordinary-user Rust facade for Radroots"
-version.workspace = true
-edition.workspace = true
-rust-version.workspace = true
-license.workspace = true
-repository.workspace = true
-homepage.workspace = true
-authors.workspace = true
-readme = "README.md"
-documentation = "https://docs.rs/radroots"
-keywords = ["radroots", "local-food", "sdk", "nostr"]
-categories = ["api-bindings", "asynchronous", "network-programming"]
-include = ["src/**", "tests/**", "!tests/package_boundary.rs", "examples/**", "Cargo.toml", "README.md", "LICENSE-APACHE", "LICENSE-MIT"]
-publish = ["crates-io"]
-autoexamples = false
-
-[package.metadata.docs.rs]
-features = ["client", "nostr", "nip46", "geonames", "knowledge"]
-
-[lints]
-workspace = true
-
-[lib]
-name = "radroots"
-path = "src/lib.rs"
-
-[dependencies]
-radroots_core = { workspace = true }
-radroots_event = { workspace = true }
-radroots_identity = { workspace = true }
-radroots_sdk = { workspace = true }
-radroots_trade = { workspace = true }
-radroots_transport = { workspace = true }
-
-[features]
-default = ["client"]
-client = ["radroots_sdk/default"]
-native = ["client", "radroots_sdk/native"]
-nostr = ["client", "radroots_sdk/nostr"]
-nip46 = ["nostr", "radroots_sdk/nip46"]
-radrootsd = ["client", "radroots_sdk/radrootsd"]
-geonames = ["client", "radroots_sdk/geonames"]
-knowledge = ["client", "radroots_sdk/knowledge"]
-full = ["radroots_sdk/full"]
-
-[[example]]
-name = "ordinary_memory"
-path = "examples/ordinary_memory.rs"
-required-features = ["client"]
diff --git a/crates/radroots/LICENSE-APACHE b/crates/radroots/LICENSE-APACHE
@@ -1,201 +0,0 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
-TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
-2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
-3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
-4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
-5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
-6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
-7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
-8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
-9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
-END OF TERMS AND CONDITIONS
-
-APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
-Copyright 2026 Tyson Lupul
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
diff --git a/crates/radroots/LICENSE-MIT b/crates/radroots/LICENSE-MIT
@@ -1,21 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2026 Tyson Lupul
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
diff --git a/crates/radroots/README.md b/crates/radroots/README.md
@@ -1,94 +0,0 @@
-# radroots
-
-`radroots` is the canonical ordinary-user Rust package for Radroots. It offers
-safe local construction, deliberate domain paths, and the common client
-boundary without exposing every lower crate or duplicating the advanced SDK
-engine.
-
-The package is enabled for validation-only release qualification. After the
-separately approved publication gate, the onboarding command is:
-
-```sh
-cargo add radroots
-```
-
-## Start locally
-
-The default `client` feature enables deterministic in-process memory storage.
-Construction creates no file, contacts no network or daemon, reads no keyring,
-installs no runtime or subscriber, and starts no worker. The initial transport
-profile is explicitly local-only.
-
-```rust
-# async fn run() -> radroots::Result<()> {
-let client = radroots::client::memory().build()?;
-let profile = radroots::client::local_only();
-assert!(profile.is_local_only());
-
-// Clone handles share lifecycle state; shutdown is explicit and asynchronous.
-client.close().await?;
-# Ok(())
-# }
-```
-
-The default memory generation is process-local and deterministic. Hosts that
-persist cursors or compose their own storage generation should use
-`radroots_sdk::ClientBuilder` directly.
-
-## Product operations
-
-Farm, listing, and trade writes follow one reliability boundary:
-
-1. `prepare` validates and freezes a side-effect-free plan.
-2. The configured signer authorizes and signs that plan.
-3. Enqueue durably accepts it under an operation ID and idempotency key.
-4. Delivery runs only for an explicitly selected transport profile.
-
-Cancellation before durable acceptance makes no commit claim. Cancellation
-after acceptance cannot roll back the accepted event; resume with the same
-idempotency identity and inspect the canonical receipt. Public event models do
-not contain exact farm coordinates, private trade terms, credentials, or key
-material.
-
-Errors expose stable classifications and recovery metadata while retaining
-their lower source chain for local diagnostics. Display, debug, diagnostics,
-and protocol reports redact bearer credentials and signer material. Canonical
-lower crates own serialization and versioned wire contracts; facade aliases
-are Rust paths, not a second persistence format.
-
-## Features
-
-| Feature | Behavior |
-| --- | --- |
-| `client` | safe memory-backed client; the default |
-| `native` | explicit SQLite, sync, and local-signing SDK capabilities |
-| `nostr` | explicit Nostr source/sink composition |
-| `nip46` | host-owned NIP-46 signer composition; implies `nostr` |
-| `radrootsd` | explicitly invoked daemon delivery |
-| `geonames` | concrete GeoNames provider capability |
-| `knowledge` | canonical knowledge event contracts |
-| `full` | the governed complete SDK capability bundle |
-
-Enabling a feature compiles capability; it never performs I/O. Native storage
-opens only through `client::native`, transport is caller-injected, and daemon
-delivery happens only when its `deliver` method is invoked.
-
-## When to use radroots_sdk
-
-Use `radroots` for ordinary applications and the primary farm, listing, trade,
-identity, event, and transport paths. Use `radroots_sdk` directly when the host
-must own source-generation identity, inject arbitrary storage/signing/sync
-implementations, coordinate FFI/mobile lifecycle, or inspect advanced
-capability and diagnostics contracts. There is intentionally no
-`radroots::sdk` namespace or wildcard SDK reexport.
-
-The namespace separation is a compiled contract:
-
-```compile_fail
-use radroots::sdk;
-```
-
-The normative package charter is the [`radroots` release-v1
-specification](../../docs/specs/radroots_crates_release_v1.md#19-radroots).
-The reviewed pre-release public API is recorded in the
-[`radroots` baseline](../../docs/api/radroots-0.1.0-alpha.txt).
diff --git a/crates/radroots/examples/ordinary_memory.rs b/crates/radroots/examples/ordinary_memory.rs
@@ -1,10 +0,0 @@
-//! Constructs the ordinary, safe, local-only Radroots front door.
-
-fn main() -> radroots::Result<()> {
- let client = radroots::client::memory().build()?;
- let profile = radroots::client::local_only();
-
- assert!(!client.is_closed());
- assert!(profile.is_local_only());
- Ok(())
-}
diff --git a/crates/radroots/src/client.rs b/crates/radroots/src/client.rs
@@ -1,145 +0,0 @@
-//! Curated client construction and operation entry points.
-
-#[cfg(any(feature = "nostr", feature = "full"))]
-use std::sync::Arc;
-
-#[cfg(any(feature = "nostr", feature = "full"))]
-use radroots_transport::{EventSink, EventSource};
-
-#[cfg(any(
- feature = "client",
- feature = "native",
- feature = "nostr",
- feature = "nip46",
- feature = "full"
-))]
-use crate::ClientBuilder;
-use crate::transport::Profile;
-
-/// Creates the safe ordinary client builder with deterministic in-process
-/// storage and no transport, signer, runtime, worker, or file authority.
-#[cfg(feature = "client")]
-#[must_use]
-pub fn memory() -> ClientBuilder {
- ClientBuilder::memory_default()
-}
-
-/// Returns the explicit no-transport profile used by ordinary local work.
-#[must_use]
-pub const fn local_only() -> Profile {
- Profile::local_only()
-}
-
-/// Adds caller-owned source and sink capabilities without connecting them or
-/// selecting a fallback transport.
-#[cfg(any(feature = "nostr", feature = "full"))]
-#[must_use]
-pub fn with_transport(
- builder: ClientBuilder,
- source: Arc<dyn EventSource>,
- sink: Arc<dyn EventSink>,
-) -> ClientBuilder {
- builder.source(source).sink(sink)
-}
-
-/// Adds an explicitly constructed NIP-46 signer provider.
-#[cfg(any(feature = "nip46", feature = "full"))]
-#[must_use]
-#[cfg_attr(coverage_nightly, coverage(off))]
-pub fn with_nip46_signer(
- builder: ClientBuilder,
- provider: crate::signing::Provider,
-) -> ClientBuilder {
- builder.signing(provider)
-}
-
-/// Explicitly opens validated native SQLite storage.
-#[cfg(any(feature = "native", feature = "full"))]
-#[cfg_attr(coverage_nightly, coverage(off))]
-pub async fn native(options: crate::storage::SqliteOptions) -> crate::Result<ClientBuilder> {
- ClientBuilder::sqlite(options).await
-}
-
-/// Reports whether the concrete GeoNames capability was selected at compile
-/// time. Asset inspection, acquisition, and database opening remain explicit.
-#[cfg(any(feature = "geonames", feature = "full"))]
-#[must_use]
-pub const fn geonames_enabled() -> bool {
- true
-}
-
-/// Reports whether canonical knowledge contracts were selected at compile
-/// time. Merely selecting the feature performs no event or storage operation.
-#[cfg(any(feature = "knowledge", feature = "full"))]
-#[must_use]
-pub const fn knowledge_enabled() -> bool {
- true
-}
-
-#[cfg(all(test, feature = "full"))]
-mod tests {
- use super::{
- Arc, EventSink, EventSource, Profile, geonames_enabled, knowledge_enabled, local_only,
- memory, with_transport,
- };
- use radroots_transport::{
- DeliveryReceipt, DeliveryRequest, Error as TransportError, FetchPage, FetchRequest,
- SinkFailure, SinkStatus, SourceStatus, outcome::Retryability,
- source::BoxFuture as TransportFuture,
- };
-
- struct TestSource;
- struct TestSink;
-
- impl EventSource for TestSource {
- fn status(&self) -> TransportFuture<'_, Result<SourceStatus, TransportError>> {
- Box::pin(async { Err(TransportError::UnsupportedOperation) })
- }
-
- fn fetch(
- &self,
- _request: FetchRequest,
- ) -> TransportFuture<'_, Result<FetchPage, TransportError>> {
- Box::pin(async { Err(TransportError::UnsupportedOperation) })
- }
- }
-
- impl EventSink for TestSink {
- fn status(&self) -> TransportFuture<'_, Result<SinkStatus, TransportError>> {
- Box::pin(async { Err(TransportError::UnsupportedOperation) })
- }
-
- fn deliver(
- &self,
- request: DeliveryRequest,
- ) -> TransportFuture<'_, Result<DeliveryReceipt, SinkFailure>> {
- Box::pin(async move {
- Err(SinkFailure::for_request(
- &request,
- "test_sink_unavailable",
- Retryability::Terminal,
- None,
- None,
- Vec::new(),
- )
- .expect("test sink failure"))
- })
- }
- }
-
- #[test]
- fn curated_builders_cover_every_directly_testable_entry_point() {
- let local = memory().build().expect("memory client");
- assert_eq!(local_only(), Profile::local_only());
- assert!(local.source().expect("source").is_none());
-
- let composed = with_transport(memory(), Arc::new(TestSource), Arc::new(TestSink))
- .build()
- .expect("transport client");
- assert!(composed.source().expect("source").is_some());
- assert!(composed.sink().expect("sink").is_some());
-
- assert!(geonames_enabled());
- assert!(knowledge_enabled());
- }
-}
diff --git a/crates/radroots/src/event.rs b/crates/radroots/src/event.rs
@@ -1,5 +0,0 @@
-//! Curated event authoring and inspection entry points.
-
-pub use radroots_event::{
- Error, Event, EventId, EventKind, EventTag, GenericEventDraft, SignedEvent, VerifiedEvent,
-};
diff --git a/crates/radroots/src/farm.rs b/crates/radroots/src/farm.rs
@@ -1,4 +0,0 @@
-//! Curated farm-domain entry points.
-
-pub use radroots_event::farm::{Farm, FarmPublicLocation, FarmRef};
-pub use radroots_sdk::farm::{Plan, PrepareError, PrepareErrorKind, PrepareRequest, prepare};
diff --git a/crates/radroots/src/identity.rs b/crates/radroots/src/identity.rs
@@ -1,5 +0,0 @@
-//! Curated public identity entry points.
-
-pub use radroots_identity::{
- AccountId, Error, IdentityId, Profile, PublicIdentity, PublicKey, Username,
-};
diff --git a/crates/radroots/src/knowledge.rs b/crates/radroots/src/knowledge.rs
@@ -1,8 +0,0 @@
-//! Curated knowledge-domain entry points.
-
-#[cfg(any(feature = "knowledge", feature = "full"))]
-pub use radroots_event::knowledge::{
- AddressableRef, KnowledgeClaim, KnowledgeFieldReport, KnowledgeRelation, KnowledgeReview,
- KnowledgeSource, KnowledgeValidationError, WikiArticle, WikiDTagError, WikiMergeRequest,
- WikiRedirect, normalize_wiki_d_tag, validate_wiki_d_tag,
-};
diff --git a/crates/radroots/src/lib.rs b/crates/radroots/src/lib.rs
@@ -1,18 +0,0 @@
-#![forbid(unsafe_code)]
-#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
-#![warn(missing_docs)]
-#![doc = include_str!("../README.md")]
-
-pub mod client;
-pub mod event;
-pub mod farm;
-pub mod identity;
-pub mod knowledge;
-pub mod listing;
-pub mod signing;
-pub mod storage;
-pub mod sync;
-pub mod trade;
-pub mod transport;
-
-pub use radroots_sdk::{Client, ClientBuilder, Error, Result};
diff --git a/crates/radroots/src/listing.rs b/crates/radroots/src/listing.rs
@@ -1,12 +0,0 @@
-//! Curated listing-domain entry points.
-
-pub use radroots_event::listing::operational::{
- OperationalListing, OperationalListingAvailability, OperationalListingBin,
- OperationalListingDeliveryMethod, OperationalListingImage, OperationalListingProduct,
- OperationalListingPublicLocation, OperationalListingStatus,
-};
-pub use radroots_sdk::listing::{
- Action, Plan, PrepareError, PrepareErrorKind, PrepareRequest,
- RadrootsOperationalListingEditDocumentV1 as EditV1,
- RadrootsOperationalListingLifecycleState as Lifecycle, prepare,
-};
diff --git a/crates/radroots/src/signing.rs b/crates/radroots/src/signing.rs
@@ -1,3 +0,0 @@
-//! Curated signing entry points.
-
-pub use radroots_sdk::signing::{Mode, Provider};
diff --git a/crates/radroots/src/storage.rs b/crates/radroots/src/storage.rs
@@ -1,6 +0,0 @@
-//! Curated storage entry points.
-
-pub use radroots_sdk::storage::{IntegrityStatus, Operations, Status};
-
-#[cfg(any(feature = "native", feature = "full"))]
-pub use radroots_sdk::storage::{SqliteOpenMode, SqliteOptions, SqlitePaths};
diff --git a/crates/radroots/src/sync.rs b/crates/radroots/src/sync.rs
@@ -1 +0,0 @@
-//! Curated synchronization entry points.
diff --git a/crates/radroots/src/trade.rs b/crates/radroots/src/trade.rs
@@ -1,13 +0,0 @@
-//! Curated trade-domain entry points.
-
-pub use radroots_core::{Currency, Decimal, Money, Percent, Quantity, QuantityPrice, Unit};
-pub use radroots_event::trade::{
- FulfillmentProfileV1, TradeCancellationProfileV1, TradeCandidateLineV1, TradeCandidateTermsV1,
- TradeDecisionV1, TradeEconomicAdjustmentV1, TradeEconomicsProfileV1,
- TradeMutationBodyV1 as MutationBodyV1, TradeMutationEnvelopeV1 as MutationV1,
- TradeMutationKindV1 as MutationKindV1, TradePrivateTermsRefV1,
-};
-pub use radroots_sdk::trade::{
- Plan, PrepareError, PrepareErrorKind, PrepareRequest, prepare, project,
-};
-pub use radroots_trade::{Projection, ReducerIssue, ReductionInput, ValidationError, WorkflowPlan};
diff --git a/crates/radroots/src/transport.rs b/crates/radroots/src/transport.rs
@@ -1,10 +0,0 @@
-//! Curated transport entry points.
-
-pub use radroots_sdk::transport::Profile;
-pub use radroots_transport::{
- Error, EventSink, EventSource, Target, TargetSet, TransportId,
- policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy},
-};
-
-#[cfg(any(feature = "radrootsd", feature = "full"))]
-pub use radroots_sdk::transport::{DaemonAuth, DaemonConfig, DaemonDelivery, DaemonError};
diff --git a/crates/radroots/tests/package_boundary.rs b/crates/radroots/tests/package_boundary.rs
@@ -1,93 +0,0 @@
-use std::collections::BTreeSet;
-
-const MANIFEST: &str = include_str!("../Cargo.toml");
-const WORKSPACE: &str = include_str!("../../../Cargo.toml");
-const ROOT: &str = include_str!("../src/lib.rs");
-const README: &str = include_str!("../README.md");
-
-#[test]
-fn package_has_final_identity_and_exact_dependencies() {
- assert!(MANIFEST.contains("name = \"radroots\""));
- assert!(MANIFEST.contains("publish = [\"crates-io\"]"));
- assert_eq!(
- dependency_names(MANIFEST),
- BTreeSet::from([
- "radroots_core",
- "radroots_event",
- "radroots_identity",
- "radroots_sdk",
- "radroots_trade",
- "radroots_transport",
- ])
- );
- assert!(WORKSPACE.contains(
- "radroots_sdk = { path = \"crates/sdk\", version = \"=0.1.0-alpha\", default-features = false }"
- ));
-}
-
-#[test]
-fn features_forward_the_exact_user_oriented_graph() {
- for forwarding in [
- "default = [\"client\"]",
- "client = [\"radroots_sdk/default\"]",
- "native = [\"client\", \"radroots_sdk/native\"]",
- "nostr = [\"client\", \"radroots_sdk/nostr\"]",
- "nip46 = [\"nostr\", \"radroots_sdk/nip46\"]",
- "radrootsd = [\"client\", \"radroots_sdk/radrootsd\"]",
- "geonames = [\"client\", \"radroots_sdk/geonames\"]",
- "knowledge = [\"client\", \"radroots_sdk/knowledge\"]",
- "full = [\"radroots_sdk/full\"]",
- ] {
- assert!(MANIFEST.contains(forwarding), "missing `{forwarding}`");
- }
- for forbidden in ["reticulum", "mesh", "simplex", "nostrdb", "replica", "sp1"] {
- assert!(
- !MANIFEST.to_ascii_lowercase().contains(forbidden),
- "forbidden feature vocabulary `{forbidden}`"
- );
- }
-}
-
-#[test]
-fn root_has_exact_exports_and_no_sdk_namespace() {
- assert!(ROOT.contains("pub use radroots_sdk::{Client, ClientBuilder, Error, Result};"));
- assert!(!ROOT.contains("pub mod sdk"));
- assert!(!ROOT.contains("pub use radroots_sdk::*"));
- assert_eq!(ROOT.matches("pub use ").count(), 1);
- assert!(README.contains("```compile_fail\nuse radroots::sdk;\n```"));
-}
-
-#[test]
-fn modules_use_deliberate_reexports_without_wildcards() {
- let source_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
- for entry in std::fs::read_dir(source_root).expect("read facade source") {
- let path = entry.expect("read facade source entry").path();
- if path.extension().and_then(|value| value.to_str()) != Some("rs") {
- continue;
- }
- let source = std::fs::read_to_string(&path).expect("read facade module");
- assert!(
- !source.contains("::*"),
- "{} contains a wildcard reexport",
- path.display()
- );
- assert!(
- !source.contains("pub mod sdk"),
- "{} exposes the forbidden SDK namespace",
- path.display()
- );
- }
-}
-
-fn dependency_names(manifest: &str) -> BTreeSet<&str> {
- manifest
- .split_once("[dependencies]")
- .expect("dependency section")
- .1
- .split_once("\n[")
- .expect("section after dependencies")
- .0
- .lines()
- .filter_map(|line| line.split_once(" = ").map(|(name, _)| name.trim()))
- .collect()
-}
diff --git a/crates/radroots/tests/public_surface.rs b/crates/radroots/tests/public_surface.rs
@@ -1,120 +0,0 @@
-#[test]
-fn approved_module_skeleton_is_public() {
- #[allow(unused_imports)]
- use radroots::{
- client, event, farm, identity, knowledge, listing, signing, storage, sync, trade, transport,
- };
-}
-
-#[test]
-fn root_exports_only_the_client_boundary() {
- fn assert_client<T: Clone + Send + Sync>() {}
- fn assert_builder<T: Send + Sync>() {}
- fn assert_error<T: std::error::Error + Send + Sync + 'static>() {}
-
- assert_client::<radroots::Client>();
- assert_builder::<radroots::ClientBuilder>();
- assert_error::<radroots::Error>();
- let result: radroots::Result<()> = Ok(());
- assert!(result.is_ok());
-}
-
-#[test]
-fn canonical_domain_paths_compile() {
- #[allow(unused_imports)]
- use radroots::{
- event::{Event, EventId, GenericEventDraft},
- farm::{Farm, FarmPublicLocation, Plan as FarmPlan, PrepareRequest as FarmRequest},
- identity::{AccountId, PublicKey, Username},
- listing::{EditV1, OperationalListing, Plan as ListingPlan},
- signing::{Mode, Provider},
- storage::{IntegrityStatus, Operations as StorageOperations, Status},
- trade::{Money, MutationV1, Plan as TradePlan, Projection},
- transport::{Profile, SatisfactionPolicy, Target, TargetSet, TransportId},
- };
-}
-
-#[cfg(feature = "client")]
-#[test]
-fn ordinary_memory_and_local_only_construction_is_inert() {
- let client = radroots::client::memory().build().expect("memory client");
- assert!(!client.is_closed());
- assert!(radroots::client::local_only().is_local_only());
-}
-
-#[cfg(any(feature = "nostr", feature = "full"))]
-#[test]
-fn explicit_transport_composition_is_inert() {
- use std::sync::Arc;
-
- use radroots::transport::{EventSink, EventSource};
- use radroots_transport::{
- DeliveryReceipt, DeliveryRequest, Error, FetchPage, FetchRequest, SinkFailure, SinkStatus,
- SourceStatus, outcome::Retryability, source::BoxFuture,
- };
-
- struct Source;
- struct Sink;
-
- impl EventSource for Source {
- fn status(&self) -> BoxFuture<'_, Result<SourceStatus, Error>> {
- Box::pin(async { Err(Error::UnsupportedOperation) })
- }
-
- fn fetch(&self, _request: FetchRequest) -> BoxFuture<'_, Result<FetchPage, Error>> {
- Box::pin(async { Err(Error::UnsupportedOperation) })
- }
- }
-
- impl EventSink for Sink {
- fn status(&self) -> BoxFuture<'_, Result<SinkStatus, Error>> {
- Box::pin(async { Err(Error::UnsupportedOperation) })
- }
-
- fn deliver(
- &self,
- request: DeliveryRequest,
- ) -> BoxFuture<'_, Result<DeliveryReceipt, SinkFailure>> {
- Box::pin(async move {
- Err(SinkFailure::for_request(
- &request,
- "test_sink_unavailable",
- Retryability::Terminal,
- None,
- None,
- Vec::new(),
- )
- .expect("test sink failure"))
- })
- }
- }
-
- let client = radroots::client::with_transport(
- radroots::client::memory(),
- Arc::new(Source),
- Arc::new(Sink),
- )
- .build()
- .expect("explicit transport client");
- assert!(client.source().expect("source").is_some());
- assert!(client.sink().expect("sink").is_some());
-}
-
-#[cfg(any(feature = "geonames", feature = "full"))]
-#[test]
-fn geonames_selection_is_explicit() {
- assert!(radroots::client::geonames_enabled());
-}
-
-#[cfg(any(feature = "knowledge", feature = "full"))]
-#[test]
-fn knowledge_selection_is_explicit() {
- assert!(radroots::client::knowledge_enabled());
-}
-
-#[cfg(any(feature = "knowledge", feature = "full"))]
-#[test]
-fn canonical_knowledge_paths_compile() {
- #[allow(unused_imports)]
- use radroots::knowledge::{KnowledgeClaim, WikiArticle, normalize_wiki_d_tag};
-}
diff --git a/crates/replica_schema_bindings/Cargo.toml b/crates/replica_schema_bindings/Cargo.toml
@@ -1,14 +0,0 @@
-[package]
-name = "radroots_replica_schema_bindings"
-description = "Generated bindings for Radroots replica schemas"
-version.workspace = true
-edition.workspace = true
-rust-version.workspace = true
-license.workspace = true
-repository.workspace = true
-homepage.workspace = true
-publish = false
-
-[dependencies]
-dto_bindgen_core = { workspace = true }
-radroots_replica_schema = { workspace = true, features = ["dto-bindgen"] }
diff --git a/crates/replica_schema_bindings/src/lib.rs b/crates/replica_schema_bindings/src/lib.rs
@@ -1,31 +0,0 @@
-pub use radroots_replica_schema as upstream;
-
-pub fn dto_registry() -> dto_bindgen_core::Registry {
- upstream::dto::dto_registry()
-}
-
-#[cfg(test)]
-mod tests {
- use super::dto_registry;
-
- #[test]
- fn preserves_replica_schema_registry_exports() {
- let registry = dto_registry();
- let actual = registry
- .types_by_id
- .values()
- .map(|type_def| match type_def {
- dto_bindgen_core::TypeDef::Struct(def) => def.export_name.as_str(),
- dto_bindgen_core::TypeDef::Enum(def) => def.export_name.as_str(),
- })
- .collect::<Vec<_>>();
-
- assert!(actual.contains(&"Farm"));
- assert!(actual.contains(&"GcsLocation"));
- assert!(actual.contains(&"IGcsLocationFindMany"));
- assert!(actual.contains(&"IGcsLocationFindManyResolve"));
- assert!(actual.contains(&"IMediaImageFindMany"));
- assert!(actual.contains(&"INostrProfileFindMany"));
- assert!(actual.contains(&"INostrRelayFindMany"));
- }
-}
diff --git a/crates/replica_store_wasm/Cargo.toml b/crates/replica_store_wasm/Cargo.toml
@@ -1,33 +0,0 @@
-[package]
-name = "radroots_replica_store_wasm"
-publish = false
-version = "0.1.0-alpha"
-edition.workspace = true
-authors = ["Tyson Lupul <tyson@radroots.org>"]
-rust-version.workspace = true
-license.workspace = true
-description = "WASM boundary for Radroots replica store access"
-repository.workspace = true
-homepage.workspace = true
-readme = "README"
-
-[lib]
-crate-type = ["cdylib", "rlib"]
-
-[dependencies]
-radroots_sql_core = { workspace = true, features = ["web"] }
-radroots_sdk_sql_wasm_runtime = { workspace = true }
-radroots_replica_store = { workspace = true }
-radroots_replica_schema = { workspace = true }
-radroots_replica_sync = { workspace = true, features = ["std"] }
-js-sys = { workspace = true }
-serde = { workspace = true, features = ["derive"] }
-serde_json = { workspace = true }
-serde-wasm-bindgen = { workspace = true }
-wasm-bindgen = { workspace = true }
-
-[dev-dependencies]
-wasm-bindgen-test = { workspace = true }
-
-[lints.rust]
-unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
diff --git a/crates/replica_store_wasm/README b/crates/replica_store_wasm/README
@@ -1,25 +0,0 @@
-# radroots_replica_store_wasm
-
-This is the README for `radroots_replica_store_wasm`, which provides WebAssembly
-bindings for `radroots_replica_store` in the `radroots` core libraries.
-
-## Overview
-
- * a wasm32-only wrapper around the replica store runtime surface;
- * integration with `radroots_sql_wasm_core` for browser and worker SQL
- execution;
- * a small target-specific entry point layer rather than a separate database
- implementation;
- * intended for JavaScript-facing callers that need the replica store APIs
- in wasm builds.
-
-## Copyright
-
-Except as otherwise noted, all files in the `radroots_replica_store_wasm`
-distribution are
-
- Copyright (c) 2026 Tyson Lupul
-
-For information on usage and redistribution, and for a DISCLAIMER OF ALL
-WARRANTIES, see LICENSE included in the `radroots_replica_store_wasm`
-distribution.
diff --git a/crates/replica_store_wasm/src/lib.rs b/crates/replica_store_wasm/src/lib.rs
@@ -1,10 +0,0 @@
-#![forbid(unsafe_code)]
-
-mod snapshot;
-#[cfg(target_arch = "wasm32")]
-mod utils;
-#[cfg(target_arch = "wasm32")]
-mod wasm_impl;
-pub use snapshot::*;
-#[cfg(target_arch = "wasm32")]
-pub use wasm_impl::*;
diff --git a/crates/replica_store_wasm/src/snapshot.rs b/crates/replica_store_wasm/src/snapshot.rs
@@ -1,87 +0,0 @@
-use radroots_replica_store::ReplicaStoreExportManifestRs;
-
-pub const EXPORT_MANIFEST_FIELD: &str = "manifest_rs";
-pub const EXPORT_DB_BYTES_FIELD: &str = "db_bytes";
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct ExportManifestSummary {
- pub export_version: String,
- pub replica_store_version: String,
- pub backup_format_version: String,
- pub schema_hash: String,
- pub schema_table_count: usize,
- pub migration_count: usize,
- pub table_count_count: usize,
-}
-
-pub fn synced_export_error(pending_count: usize, expected_count: usize) -> Option<String> {
- if pending_count == 0 {
- None
- } else {
- Some(format!(
- "replica store export requires synced state (pending {pending_count}/{expected_count})"
- ))
- }
-}
-
-pub fn export_manifest_summary(manifest: &ReplicaStoreExportManifestRs) -> ExportManifestSummary {
- ExportManifestSummary {
- export_version: manifest.export_version.clone(),
- replica_store_version: manifest.replica_store_version.clone(),
- backup_format_version: manifest.backup_format_version.clone(),
- schema_hash: manifest.schema_hash.clone(),
- schema_table_count: manifest.schema.len(),
- migration_count: manifest.migrations.len(),
- table_count_count: manifest.table_counts.len(),
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- EXPORT_DB_BYTES_FIELD, EXPORT_MANIFEST_FIELD, export_manifest_summary, synced_export_error,
- };
-
- fn manifest() -> radroots_replica_store::ReplicaStoreExportManifestRs {
- radroots_replica_store::ReplicaStoreExportManifestRs {
- export_version: "1".to_owned(),
- replica_store_version: "0.1.0".to_owned(),
- backup_format_version: "1".to_owned(),
- schema_hash: "schema-hash".to_owned(),
- schema: Vec::new(),
- migrations: Vec::new(),
- table_counts: Vec::new(),
- }
- }
-
- #[test]
- fn export_snapshot_field_names_are_stable() {
- assert_eq!(EXPORT_MANIFEST_FIELD, "manifest_rs");
- assert_eq!(EXPORT_DB_BYTES_FIELD, "db_bytes");
- }
-
- #[test]
- fn synced_export_error_allows_empty_pending_queue() {
- assert_eq!(synced_export_error(0, 4), None);
- }
-
- #[test]
- fn synced_export_error_reports_pending_and_expected_counts() {
- assert_eq!(
- synced_export_error(2, 4).expect("error"),
- "replica store export requires synced state (pending 2/4)"
- );
- }
-
- #[test]
- fn export_manifest_summary_preserves_versions_and_counts() {
- let summary = export_manifest_summary(&manifest());
- assert_eq!(summary.export_version, "1");
- assert_eq!(summary.replica_store_version, "0.1.0");
- assert_eq!(summary.backup_format_version, "1");
- assert_eq!(summary.schema_hash, "schema-hash");
- assert_eq!(summary.schema_table_count, 0);
- assert_eq!(summary.migration_count, 0);
- assert_eq!(summary.table_count_count, 0);
- }
-}
diff --git a/crates/replica_store_wasm/src/utils.rs b/crates/replica_store_wasm/src/utils.rs
@@ -1,13 +0,0 @@
-use serde::Serialize;
-use wasm_bindgen::prelude::*;
-
-use radroots_sql_core::SqlError;
-
-pub fn value_to_js<T>(value: T) -> Result<JsValue, JsValue>
-where
- T: Serialize,
-{
- let json = serde_json::to_string(&value)
- .map_err(|err| radroots_sdk_sql_wasm_runtime::err_js(SqlError::from(err)))?;
- Ok(JsValue::from_str(&json))
-}
diff --git a/crates/replica_store_wasm/src/wasm_impl.rs b/crates/replica_store_wasm/src/wasm_impl.rs
@@ -1,923 +0,0 @@
-use crate::{
- snapshot::{EXPORT_DB_BYTES_FIELD, EXPORT_MANIFEST_FIELD, synced_export_error},
- utils::value_to_js,
-};
-use radroots_replica_store::migrations;
-use radroots_replica_store::{ReplicaStoreExportManifestRs, export_manifest};
-use radroots_replica_sync::radroots_replica_sync_status;
-use radroots_sdk_sql_wasm_runtime::parse_json;
-use radroots_sdk_sql_wasm_runtime::{
- WasmSqlExecutor, err_js, export_bytes, export_lock_begin, export_lock_end,
- with_export_lock_bypass,
-};
-use wasm_bindgen::JsValue;
-use wasm_bindgen::prelude::*;
-
-use radroots_replica_schema::farm::{
- IFarmCreate, IFarmDelete, IFarmFindMany, IFarmFindOne, IFarmUpdate,
-};
-
-use radroots_replica_schema::farm_gcs_location::{
- IFarmGcsLocationCreate, IFarmGcsLocationDelete, IFarmGcsLocationFindMany,
- IFarmGcsLocationFindOne, IFarmGcsLocationUpdate,
-};
-
-use radroots_replica_schema::farm_member::{
- IFarmMemberCreate, IFarmMemberDelete, IFarmMemberFindMany, IFarmMemberFindOne,
- IFarmMemberUpdate,
-};
-
-use radroots_replica_schema::farm_member_claim::{
- IFarmMemberClaimCreate, IFarmMemberClaimDelete, IFarmMemberClaimFindMany,
- IFarmMemberClaimFindOne, IFarmMemberClaimUpdate,
-};
-
-use radroots_replica_schema::farm_tag::{
- IFarmTagCreate, IFarmTagDelete, IFarmTagFindMany, IFarmTagFindOne, IFarmTagUpdate,
-};
-
-use radroots_replica_schema::gcs_location::{
- IGcsLocationCreate, IGcsLocationDelete, IGcsLocationFindMany, IGcsLocationFindOne,
- IGcsLocationUpdate,
-};
-
-use radroots_replica_schema::log_error::{
- ILogErrorCreate, ILogErrorDelete, ILogErrorFindMany, ILogErrorFindOne, ILogErrorUpdate,
-};
-
-use radroots_replica_schema::media_image::{
- IMediaImageCreate, IMediaImageDelete, IMediaImageFindMany, IMediaImageFindOne,
- IMediaImageUpdate,
-};
-
-use radroots_replica_schema::nostr_profile::{
- INostrProfileCreate, INostrProfileDelete, INostrProfileFindMany, INostrProfileFindOne,
- INostrProfileUpdate,
-};
-
-use radroots_replica_schema::nostr_event_head::{
- INostrEventHeadCreate, INostrEventHeadDelete, INostrEventHeadFindMany, INostrEventHeadFindOne,
- INostrEventHeadUpdate,
-};
-
-use radroots_replica_schema::nostr_relay::{
- INostrRelayCreate, INostrRelayDelete, INostrRelayFindMany, INostrRelayFindOne,
- INostrRelayUpdate,
-};
-
-use radroots_replica_schema::trade_product::{
- ITradeProductCreate, ITradeProductDelete, ITradeProductFindMany, ITradeProductFindOne,
- ITradeProductUpdate,
-};
-
-use radroots_replica_schema::plot::{
- IPlotCreate, IPlotDelete, IPlotFindMany, IPlotFindOne, IPlotUpdate,
-};
-
-use radroots_replica_schema::plot_gcs_location::{
- IPlotGcsLocationCreate, IPlotGcsLocationDelete, IPlotGcsLocationFindMany,
- IPlotGcsLocationFindOne, IPlotGcsLocationUpdate,
-};
-
-use radroots_replica_schema::plot_tag::{
- IPlotTagCreate, IPlotTagDelete, IPlotTagFindMany, IPlotTagFindOne, IPlotTagUpdate,
-};
-
-use radroots_replica_schema::nostr_profile_relay::INostrProfileRelayRelation;
-
-use radroots_replica_schema::trade_product_location::ITradeProductLocationRelation;
-
-use radroots_replica_schema::trade_product_media::ITradeProductMediaRelation;
-
-#[wasm_bindgen(js_name = replica_store_run_migrations)]
-pub fn replica_store_run_migrations() -> Result<(), JsValue> {
- let exec = WasmSqlExecutor::new();
- migrations::run_all_up(&exec).map_err(err_js)
-}
-
-#[wasm_bindgen(js_name = replica_store_reset_database)]
-pub fn replica_store_reset_database() -> Result<(), JsValue> {
- let exec = WasmSqlExecutor::new();
- migrations::run_all_down(&exec).map_err(err_js)
-}
-
-#[wasm_bindgen(js_name = replica_store_export_json)]
-pub fn replica_store_export_json() -> Result<JsValue, JsValue> {
- let exec = WasmSqlExecutor::new();
- let dump = radroots_replica_store::backup::export_database_backup(&exec).map_err(err_js)?;
- value_to_js(dump)
-}
-
-#[wasm_bindgen(js_name = replica_store_import_json)]
-pub fn replica_store_import_json(dump_json: &str) -> Result<(), JsValue> {
- let exec = WasmSqlExecutor::new();
- radroots_replica_store::backup::restore_database_backup_json(&exec, dump_json).map_err(err_js)
-}
-
-#[wasm_bindgen(js_name = replica_store_export_begin)]
-pub fn replica_store_export_begin() -> Result<JsValue, JsValue> {
- export_lock_begin().map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let result = with_export_lock_bypass(|| export_snapshot(&exec));
- match result {
- Ok(value) => Ok(value),
- Err(err) => {
- export_lock_end();
- Err(err)
- }
- }
-}
-
-#[wasm_bindgen(js_name = replica_store_export_finish)]
-pub fn replica_store_export_finish() -> Result<(), JsValue> {
- export_lock_end();
- Ok(())
-}
-
-fn export_snapshot(exec: &WasmSqlExecutor) -> Result<JsValue, JsValue> {
- let status = radroots_replica_sync_status(exec).map_err(|err| {
- err_js(radroots_sql_core::SqlError::InvalidArgument(
- err.to_string(),
- ))
- })?;
- if let Some(message) = synced_export_error(status.pending_count, status.expected_count) {
- return Err(err_js(radroots_sql_core::SqlError::InvalidArgument(
- message,
- )));
- }
- let manifest = export_manifest(exec).map_err(err_js)?;
- export_snapshot_value(manifest)
-}
-
-fn export_snapshot_value(manifest: ReplicaStoreExportManifestRs) -> Result<JsValue, JsValue> {
- let bytes_js = export_bytes();
- export_snapshot_value_with_bytes(manifest, bytes_js)
-}
-
-fn export_snapshot_value_with_bytes(
- manifest: ReplicaStoreExportManifestRs,
- bytes_js: JsValue,
-) -> Result<JsValue, JsValue> {
- let manifest_js = serde_wasm_bindgen::to_value(&manifest).map_err(|err| {
- err_js(radroots_sql_core::SqlError::SerializationError(
- err.to_string(),
- ))
- })?;
- let obj = js_sys::Object::new();
- js_sys::Reflect::set(
- &obj,
- &JsValue::from_str(EXPORT_MANIFEST_FIELD),
- &manifest_js,
- )
- .map_err(|_| err_js(radroots_sql_core::SqlError::Internal))?;
- js_sys::Reflect::set(&obj, &JsValue::from_str(EXPORT_DB_BYTES_FIELD), &bytes_js)
- .map_err(|_| err_js(radroots_sql_core::SqlError::Internal))?;
- Ok(JsValue::from(obj))
-}
-
-#[cfg(all(test, target_arch = "wasm32"))]
-mod tests {
- use super::export_snapshot_value_with_bytes;
- use js_sys::{Reflect, Uint8Array};
- use wasm_bindgen::JsValue;
-
- #[wasm_bindgen_test::wasm_bindgen_test]
- fn export_snapshot_value_includes_fields() {
- let manifest = radroots_replica_store::ReplicaStoreExportManifestRs {
- export_version: "1".to_string(),
- replica_store_version: "0.0.0".to_string(),
- backup_format_version: "0.0.0".to_string(),
- schema_hash: "hash".to_string(),
- schema: Vec::new(),
- migrations: Vec::new(),
- table_counts: Vec::new(),
- };
- let bytes = Uint8Array::new_with_length(2);
- let js =
- export_snapshot_value_with_bytes(manifest, JsValue::from(bytes)).expect("snapshot");
- let manifest_rs =
- Reflect::get(&js, &JsValue::from_str("manifest_rs")).expect("manifest_rs");
- let db_bytes = Reflect::get(&js, &JsValue::from_str("db_bytes")).expect("db_bytes");
- assert!(manifest_rs.is_object());
- assert!(db_bytes.is_object());
- }
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_create)]
-pub fn replica_store_farm_create(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmCreate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::farm::create(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_find_one)]
-pub fn replica_store_farm_find_one(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmFindOne = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::farm::find_one(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_find_many)]
-pub fn replica_store_farm_find_many(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmFindMany = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::farm::find_many(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_update)]
-pub fn replica_store_farm_update(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmUpdate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::farm::update(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_delete)]
-pub fn replica_store_farm_delete(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmDelete = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::farm::delete(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_plot_create)]
-pub fn replica_store_plot_create(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IPlotCreate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::plot::create(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_plot_find_one)]
-pub fn replica_store_plot_find_one(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IPlotFindOne = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::plot::find_one(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_plot_find_many)]
-pub fn replica_store_plot_find_many(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IPlotFindMany = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::plot::find_many(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_plot_update)]
-pub fn replica_store_plot_update(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IPlotUpdate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::plot::update(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_plot_delete)]
-pub fn replica_store_plot_delete(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IPlotDelete = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::plot::delete(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_gcs_location_create)]
-pub fn replica_store_gcs_location_create(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IGcsLocationCreate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::gcs_location::create(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_gcs_location_find_one)]
-pub fn replica_store_gcs_location_find_one(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IGcsLocationFindOne = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::gcs_location::find_one(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_gcs_location_find_many)]
-pub fn replica_store_gcs_location_find_many(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IGcsLocationFindMany = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::gcs_location::find_many(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_gcs_location_update)]
-pub fn replica_store_gcs_location_update(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IGcsLocationUpdate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::gcs_location::update(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_gcs_location_delete)]
-pub fn replica_store_gcs_location_delete(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IGcsLocationDelete = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::gcs_location::delete(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_gcs_location_create)]
-pub fn replica_store_farm_gcs_location_create(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmGcsLocationCreate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::farm_gcs_location::create(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_gcs_location_find_one)]
-pub fn replica_store_farm_gcs_location_find_one(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmGcsLocationFindOne = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::farm_gcs_location::find_one(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_gcs_location_find_many)]
-pub fn replica_store_farm_gcs_location_find_many(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmGcsLocationFindMany = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::farm_gcs_location::find_many(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_gcs_location_update)]
-pub fn replica_store_farm_gcs_location_update(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmGcsLocationUpdate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::farm_gcs_location::update(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_gcs_location_delete)]
-pub fn replica_store_farm_gcs_location_delete(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmGcsLocationDelete = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::farm_gcs_location::delete(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_plot_gcs_location_create)]
-pub fn replica_store_plot_gcs_location_create(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IPlotGcsLocationCreate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::plot_gcs_location::create(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_plot_gcs_location_find_one)]
-pub fn replica_store_plot_gcs_location_find_one(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IPlotGcsLocationFindOne = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::plot_gcs_location::find_one(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_plot_gcs_location_find_many)]
-pub fn replica_store_plot_gcs_location_find_many(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IPlotGcsLocationFindMany = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::plot_gcs_location::find_many(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_plot_gcs_location_update)]
-pub fn replica_store_plot_gcs_location_update(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IPlotGcsLocationUpdate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::plot_gcs_location::update(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_plot_gcs_location_delete)]
-pub fn replica_store_plot_gcs_location_delete(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IPlotGcsLocationDelete = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::plot_gcs_location::delete(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_tag_create)]
-pub fn replica_store_farm_tag_create(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmTagCreate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::farm_tag::create(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_tag_find_one)]
-pub fn replica_store_farm_tag_find_one(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmTagFindOne = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::farm_tag::find_one(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_tag_find_many)]
-pub fn replica_store_farm_tag_find_many(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmTagFindMany = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::farm_tag::find_many(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_tag_update)]
-pub fn replica_store_farm_tag_update(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmTagUpdate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::farm_tag::update(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_tag_delete)]
-pub fn replica_store_farm_tag_delete(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmTagDelete = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::farm_tag::delete(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_plot_tag_create)]
-pub fn replica_store_plot_tag_create(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IPlotTagCreate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::plot_tag::create(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_plot_tag_find_one)]
-pub fn replica_store_plot_tag_find_one(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IPlotTagFindOne = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::plot_tag::find_one(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_plot_tag_find_many)]
-pub fn replica_store_plot_tag_find_many(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IPlotTagFindMany = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::plot_tag::find_many(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_plot_tag_update)]
-pub fn replica_store_plot_tag_update(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IPlotTagUpdate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::plot_tag::update(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_plot_tag_delete)]
-pub fn replica_store_plot_tag_delete(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IPlotTagDelete = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::plot_tag::delete(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_member_create)]
-pub fn replica_store_farm_member_create(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmMemberCreate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::farm_member::create(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_member_find_one)]
-pub fn replica_store_farm_member_find_one(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmMemberFindOne = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::farm_member::find_one(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_member_find_many)]
-pub fn replica_store_farm_member_find_many(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmMemberFindMany = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::farm_member::find_many(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_member_update)]
-pub fn replica_store_farm_member_update(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmMemberUpdate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::farm_member::update(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_member_delete)]
-pub fn replica_store_farm_member_delete(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmMemberDelete = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::farm_member::delete(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_member_claim_create)]
-pub fn replica_store_farm_member_claim_create(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmMemberClaimCreate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::farm_member_claim::create(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_member_claim_find_one)]
-pub fn replica_store_farm_member_claim_find_one(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmMemberClaimFindOne = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::farm_member_claim::find_one(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_member_claim_find_many)]
-pub fn replica_store_farm_member_claim_find_many(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmMemberClaimFindMany = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::farm_member_claim::find_many(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_member_claim_update)]
-pub fn replica_store_farm_member_claim_update(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmMemberClaimUpdate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::farm_member_claim::update(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_farm_member_claim_delete)]
-pub fn replica_store_farm_member_claim_delete(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IFarmMemberClaimDelete = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::farm_member_claim::delete(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_log_error_create)]
-pub fn replica_store_log_error_create(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ILogErrorCreate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::log_error::create(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_log_error_find_one)]
-pub fn replica_store_log_error_find_one(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ILogErrorFindOne = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::log_error::find_one(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_log_error_find_many)]
-pub fn replica_store_log_error_find_many(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ILogErrorFindMany = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::log_error::find_many(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_log_error_update)]
-pub fn replica_store_log_error_update(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ILogErrorUpdate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::log_error::update(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_log_error_delete)]
-pub fn replica_store_log_error_delete(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ILogErrorDelete = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::log_error::delete(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_media_image_create)]
-pub fn replica_store_media_image_create(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IMediaImageCreate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::media_image::create(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_media_image_find_one)]
-pub fn replica_store_media_image_find_one(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IMediaImageFindOne = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::media_image::find_one(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_media_image_find_many)]
-pub fn replica_store_media_image_find_many(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IMediaImageFindMany = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::media_image::find_many(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_media_image_update)]
-pub fn replica_store_media_image_update(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IMediaImageUpdate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::media_image::update(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_media_image_delete)]
-pub fn replica_store_media_image_delete(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: IMediaImageDelete = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::media_image::delete(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_nostr_profile_create)]
-pub fn replica_store_nostr_profile_create(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: INostrProfileCreate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::nostr_profile::create(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_nostr_profile_find_one)]
-pub fn replica_store_nostr_profile_find_one(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: INostrProfileFindOne = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::nostr_profile::find_one(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_nostr_profile_find_many)]
-pub fn replica_store_nostr_profile_find_many(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: INostrProfileFindMany = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::nostr_profile::find_many(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_nostr_profile_update)]
-pub fn replica_store_nostr_profile_update(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: INostrProfileUpdate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::nostr_profile::update(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_nostr_profile_delete)]
-pub fn replica_store_nostr_profile_delete(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: INostrProfileDelete = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::nostr_profile::delete(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_nostr_event_head_create)]
-pub fn replica_store_nostr_event_head_create(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: INostrEventHeadCreate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::nostr_event_head::create(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_nostr_event_head_find_one)]
-pub fn replica_store_nostr_event_head_find_one(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: INostrEventHeadFindOne = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::nostr_event_head::find_one(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_nostr_event_head_find_many)]
-pub fn replica_store_nostr_event_head_find_many(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: INostrEventHeadFindMany = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::nostr_event_head::find_many(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_nostr_event_head_update)]
-pub fn replica_store_nostr_event_head_update(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: INostrEventHeadUpdate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::nostr_event_head::update(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_nostr_event_head_delete)]
-pub fn replica_store_nostr_event_head_delete(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: INostrEventHeadDelete = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::nostr_event_head::delete(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_nostr_relay_create)]
-pub fn replica_store_nostr_relay_create(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: INostrRelayCreate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::nostr_relay::create(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_nostr_relay_find_one)]
-pub fn replica_store_nostr_relay_find_one(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: INostrRelayFindOne = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::nostr_relay::find_one(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_nostr_relay_find_many)]
-pub fn replica_store_nostr_relay_find_many(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: INostrRelayFindMany = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::nostr_relay::find_many(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_nostr_relay_update)]
-pub fn replica_store_nostr_relay_update(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: INostrRelayUpdate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::nostr_relay::update(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_nostr_relay_delete)]
-pub fn replica_store_nostr_relay_delete(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: INostrRelayDelete = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::nostr_relay::delete(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_trade_product_create)]
-pub fn replica_store_trade_product_create(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ITradeProductCreate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::trade_product::create(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_trade_product_find_one)]
-pub fn replica_store_trade_product_find_one(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ITradeProductFindOne = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::trade_product::find_one(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_trade_product_find_many)]
-pub fn replica_store_trade_product_find_many(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ITradeProductFindMany = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::trade_product::find_many(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_trade_product_update)]
-pub fn replica_store_trade_product_update(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ITradeProductUpdate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::trade_product::update(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_trade_product_delete)]
-pub fn replica_store_trade_product_delete(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ITradeProductDelete = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_replica_store::trade_product::delete(&exec, &opts).map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_nostr_profile_relay_set)]
-pub fn replica_store_nostr_profile_relay_set(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: INostrProfileRelayRelation = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::nostr_profile_relay::set(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_nostr_profile_relay_unset)]
-pub fn replica_store_nostr_profile_relay_unset(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: INostrProfileRelayRelation = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::nostr_profile_relay::unset(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_trade_product_location_set)]
-pub fn replica_store_trade_product_location_set(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ITradeProductLocationRelation = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::trade_product_location::set(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_trade_product_location_unset)]
-pub fn replica_store_trade_product_location_unset(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ITradeProductLocationRelation = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::trade_product_location::unset(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_trade_product_media_set)]
-pub fn replica_store_trade_product_media_set(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ITradeProductMediaRelation = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::trade_product_media::set(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = replica_store_trade_product_media_unset)]
-pub fn replica_store_trade_product_media_unset(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ITradeProductMediaRelation = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out = radroots_replica_store::trade_product_media::unset(&exec, &opts)
- .map_err(|e| err_js(e.error))?;
- value_to_js(out)
-}
diff --git a/crates/replica_sync_wasm/Cargo.toml b/crates/replica_sync_wasm/Cargo.toml
@@ -1,31 +0,0 @@
-[package]
-name = "radroots_replica_sync_wasm"
-publish = false
-version = "0.1.0-alpha"
-edition.workspace = true
-authors = ["Tyson Lupul <tyson@radroots.org>"]
-rust-version.workspace = true
-license.workspace = true
-description = "WASM boundary for Radroots replica synchronization"
-repository.workspace = true
-homepage.workspace = true
-readme = "README"
-
-[lib]
-crate-type = ["cdylib", "rlib"]
-
-[dependencies]
-base64 = { workspace = true }
-radroots_event = { workspace = true, default-features = false, features = [
- "serde",
-] }
-radroots_sdk_sql_wasm_runtime = { workspace = true }
-radroots_replica_sync = { workspace = true, features = ["std", "legacy-ingest"] }
-serde = { workspace = true, features = ["derive"] }
-serde_json = { workspace = true }
-serde-wasm-bindgen = { workspace = true }
-uuid = { workspace = true, features = ["js"] }
-wasm-bindgen = { workspace = true }
-
-[lints.rust]
-unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
diff --git a/crates/replica_sync_wasm/README b/crates/replica_sync_wasm/README
@@ -1,25 +0,0 @@
-# radroots_replica_sync_wasm
-
-This is the README for `radroots_replica_sync_wasm`, which provides WebAssembly
-bindings for `radroots_replica_sync` in the `radroots` core libraries.
-
-## Overview
-
- * wasm32 entry points for full sync and single-event ingest operations;
- * integration with `WasmSqlExecutor` from `radroots_sql_core` for database
- access;
- * UUIDv7 id generation and JSON and base64 boundary handling for JavaScript
- callers;
- * a small target-specific wrapper around the Rust sync crate rather than a
- separate sync engine.
-
-## Copyright
-
-Except as otherwise noted, all files in the `radroots_replica_sync_wasm`
-distribution are
-
- Copyright (c) 2026 Tyson Lupul
-
-For information on usage and redistribution, and for a DISCLAIMER OF ALL
-WARRANTIES, see LICENSE included in the `radroots_replica_sync_wasm`
-distribution.
diff --git a/crates/replica_sync_wasm/src/lib.rs b/crates/replica_sync_wasm/src/lib.rs
@@ -1,179 +0,0 @@
-#![forbid(unsafe_code)]
-
-#[cfg(target_arch = "wasm32")]
-use base64::Engine;
-#[cfg(target_arch = "wasm32")]
-use base64::engine::general_purpose::URL_SAFE_NO_PAD;
-use radroots_event::envelope::{EventEnvelope, EventEnvelopeParts};
-#[cfg(any(target_arch = "wasm32", test))]
-use radroots_replica_sync::RadrootsReplicaIngestOutcome;
-use radroots_replica_sync::RadrootsReplicaSyncRequest;
-#[cfg(target_arch = "wasm32")]
-use radroots_replica_sync::{
- RadrootsReplicaIdFactory, radroots_replica_ingest_event_with_factory, radroots_replica_sync_all,
-};
-#[cfg(target_arch = "wasm32")]
-use radroots_sdk_sql_wasm_runtime::WasmSqlExecutor;
-use serde::Deserialize;
-#[cfg(target_arch = "wasm32")]
-use uuid::Uuid;
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::prelude::*;
-
-#[cfg(target_arch = "wasm32")]
-fn err_js<E: ToString>(err: E) -> JsValue {
- JsValue::from_str(&err.to_string())
-}
-
-#[cfg(target_arch = "wasm32")]
-struct WasmIdFactory;
-
-#[cfg(target_arch = "wasm32")]
-impl RadrootsReplicaIdFactory for WasmIdFactory {
- fn new_d_tag(&self) -> String {
- let uuid = Uuid::now_v7();
- URL_SAFE_NO_PAD.encode(uuid.as_bytes())
- }
-}
-
-#[derive(Deserialize)]
-#[serde(deny_unknown_fields)]
-struct EventEnvelopeInput {
- id: String,
- author: String,
- created_at: u64,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: String,
- sig: String,
-}
-
-pub fn parse_request_model(request_json: &str) -> Result<RadrootsReplicaSyncRequest, String> {
- serde_json::from_str(request_json).map_err(|error| error.to_string())
-}
-
-pub fn parse_event_model(event_json: &str) -> Result<EventEnvelope, String> {
- let envelope: EventEnvelopeInput =
- serde_json::from_str(event_json).map_err(|error| error.to_string())?;
- EventEnvelope::new(EventEnvelopeParts {
- id: envelope.id,
- author: envelope.author,
- created_at: envelope.created_at,
- kind: envelope.kind,
- tags: envelope.tags,
- content: envelope.content,
- sig: envelope.sig,
- })
- .map_err(|error| error.to_string())
-}
-
-#[cfg(any(target_arch = "wasm32", test))]
-fn ingest_outcome_label(outcome: RadrootsReplicaIngestOutcome) -> &'static str {
- match outcome {
- RadrootsReplicaIngestOutcome::Applied => "applied",
- RadrootsReplicaIngestOutcome::Excluded => "excluded",
- RadrootsReplicaIngestOutcome::Rejected => "rejected",
- RadrootsReplicaIngestOutcome::Skipped => "skipped",
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-#[wasm_bindgen(js_name = replica_sync_sync_all)]
-pub fn replica_sync_sync_all(request_json: &str) -> Result<JsValue, JsValue> {
- let request = parse_request_model(request_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let bundle = radroots_replica_sync_all(&exec, &request).map_err(err_js)?;
- serde_wasm_bindgen::to_value(&bundle).map_err(err_js)
-}
-
-#[cfg(target_arch = "wasm32")]
-#[wasm_bindgen(js_name = replica_sync_ingest_event)]
-pub fn replica_sync_ingest_event(event_json: &str) -> Result<JsValue, JsValue> {
- let event = parse_event_model(event_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let factory = WasmIdFactory;
- let outcome =
- radroots_replica_ingest_event_with_factory(&exec, &event, &factory).map_err(err_js)?;
- Ok(JsValue::from_str(ingest_outcome_label(outcome)))
-}
-
-#[cfg(test)]
-mod tests {
- use super::{ingest_outcome_label, parse_event_model, parse_request_model};
- use radroots_replica_sync::RadrootsReplicaIngestOutcome;
-
- fn event_json(author: Option<&str>, pubkey: Option<&str>) -> String {
- let mut fields = vec![
- format!(r#""id":"{}""#, "0".repeat(64)),
- r#""created_at":123"#.to_owned(),
- r#""kind":30023"#.to_owned(),
- r#""tags":[["d","one"]]"#.to_owned(),
- r#""content":"content""#.to_owned(),
- format!(r#""sig":"{}""#, "f".repeat(128)),
- ];
- if let Some(author) = author {
- fields.push(format!(r#""author":"{author}""#));
- }
- if let Some(pubkey) = pubkey {
- fields.push(format!(r#""pubkey":"{pubkey}""#));
- }
- format!("{{{}}}", fields.join(","))
- }
-
- #[test]
- fn parse_event_accepts_author_domain_envelope() {
- let author = "a".repeat(64);
- let event = parse_event_model(&event_json(Some(author.as_str()), None)).expect("event");
- assert_eq!(event.author().to_hex(), author);
- assert_eq!(
- event.tags_as_vec(),
- vec![vec!["d".to_owned(), "one".to_owned()]]
- );
- }
-
- #[test]
- fn parse_event_rejects_pubkey_wire_alias() {
- let author = "a".repeat(64);
- let error = parse_event_model(&event_json(Some(author.as_str()), Some(author.as_str())))
- .expect_err("error");
- assert!(error.contains("unknown field `pubkey`"));
- }
-
- #[test]
- fn parse_event_rejects_missing_author() {
- let error = parse_event_model(&event_json(None, None)).expect_err("error");
- assert!(error.contains("missing field `author`"));
- }
-
- #[test]
- fn parse_event_rejects_malformed_json() {
- let error = parse_event_model("{").expect_err("error");
- assert!(error.contains("EOF"));
- }
-
- #[test]
- fn parse_request_rejects_malformed_json() {
- let error = parse_request_model("{").expect_err("error");
- assert!(error.contains("EOF"));
- }
-
- #[test]
- fn ingest_outcome_labels_cover_the_replica_contract() {
- assert_eq!(
- ingest_outcome_label(RadrootsReplicaIngestOutcome::Applied),
- "applied"
- );
- assert_eq!(
- ingest_outcome_label(RadrootsReplicaIngestOutcome::Excluded),
- "excluded"
- );
- assert_eq!(
- ingest_outcome_label(RadrootsReplicaIngestOutcome::Rejected),
- "rejected"
- );
- assert_eq!(
- ingest_outcome_label(RadrootsReplicaIngestOutcome::Skipped),
- "skipped"
- );
- }
-}
diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml
@@ -1,123 +0,0 @@
-[package]
-name = "radroots_sdk"
-publish = ["crates-io"]
-version.workspace = true
-edition.workspace = true
-authors.workspace = true
-rust-version.workspace = true
-license.workspace = true
-description = "Host-neutral asynchronous client engine for Radroots"
-repository.workspace = true
-homepage.workspace = true
-readme = "README.md"
-documentation = "https://docs.rs/radroots_sdk"
-keywords = ["radroots", "sdk", "local-food", "nostr"]
-categories = ["api-bindings", "asynchronous", "network-programming"]
-include = ["src/**", "tests/**", "!tests/package_boundary.rs", "examples/**", "Cargo.toml", "README.md", "LICENSE-APACHE", "LICENSE-MIT"]
-autotests = false
-autoexamples = false
-
-[package.metadata.docs.rs]
-features = ["memory", "sync", "nostr", "nip46", "local-signing", "geonames", "knowledge"]
-
-[lib]
-name = "radroots_sdk"
-
-[features]
-default = ["memory"]
-memory = ["radroots_storage/memory"]
-sqlite = ["dep:radroots_storage_sqlite"]
-sync = ["dep:radroots_sync", "dep:uuid"]
-nostr = [
- "sync",
- "dep:radroots_nostr",
- "dep:radroots_transport_nostr",
-]
-nip46 = [
- "nostr",
- "dep:radroots_nostr_connect",
-]
-local-signing = [
- "dep:radroots_nostr",
- "dep:radroots_secrets",
- "radroots_nostr/signing",
-]
-radrootsd = [
- "sync",
- "dep:reqwest",
- "dep:serde",
- "dep:serde_json",
-]
-geonames = ["dep:radroots_geonames"]
-knowledge = [
- "radroots_event/knowledge",
- "radroots_event_codec/knowledge",
-]
-native = ["sqlite", "sync", "local-signing"]
-full = [
- "native",
- "nostr",
- "nip46",
- "radrootsd",
- "geonames",
- "knowledge",
-]
-
-[dependencies]
-radroots_core = { workspace = true, default-features = false }
-radroots_event = { workspace = true, default-features = false }
-radroots_event_codec = { workspace = true, default-features = false, features = [
- "json",
- "std",
-] }
-radroots_identity = { workspace = true, default-features = false }
-radroots_protocol = { workspace = true, default-features = false }
-radroots_signing = { workspace = true, default-features = false }
-radroots_storage = { workspace = true, default-features = false }
-radroots_trade = { workspace = true, default-features = false }
-radroots_transport = { workspace = true, default-features = false }
-
-radroots_geonames = { workspace = true, optional = true, default-features = false }
-radroots_nostr = { workspace = true, optional = true, default-features = false }
-radroots_nostr_connect = { workspace = true, optional = true, default-features = false }
-radroots_secrets = { workspace = true, optional = true, default-features = false }
-radroots_storage_sqlite = { workspace = true, optional = true, default-features = false }
-radroots_sync = { workspace = true, optional = true, default-features = false }
-radroots_transport_nostr = { workspace = true, optional = true, default-features = false }
-
-reqwest = { workspace = true, optional = true, default-features = false, features = [
- "json",
- "rustls-tls",
-] }
-serde = { workspace = true, optional = true, features = ["derive"] }
-serde_json = { workspace = true, optional = true, features = ["std"] }
-uuid = { workspace = true, optional = true, features = ["v4"] }
-
-[dev-dependencies]
-nostr = { workspace = true }
-tempfile = { workspace = true }
-tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
-
-[[test]]
-name = "package_boundary"
-path = "tests/package_boundary.rs"
-
-[[test]]
-name = "lifecycle"
-path = "tests/lifecycle.rs"
-
-[[test]]
-name = "public_api"
-path = "tests/public_api.rs"
-
-[[example]]
-name = "safe_memory_client"
-path = "examples/safe_memory_client.rs"
-required-features = ["memory"]
-
-[[example]]
-name = "transport_profile"
-path = "examples/transport_profile.rs"
-
-[lints]
-workspace = true
diff --git a/crates/sdk/LICENSE-APACHE b/crates/sdk/LICENSE-APACHE
@@ -1,201 +0,0 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
-TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
-2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
-3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
-4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
-5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
-6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
-7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
-8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
-9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
-END OF TERMS AND CONDITIONS
-
-APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
-Copyright 2026 Tyson Lupul
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
diff --git a/crates/sdk/LICENSE-MIT b/crates/sdk/LICENSE-MIT
@@ -1,21 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2026 Tyson Lupul
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
diff --git a/crates/sdk/README.md b/crates/sdk/README.md
@@ -1,171 +0,0 @@
-# radroots_sdk
-
-`radroots_sdk` is the host-neutral asynchronous client engine for Radroots.
-It composes the canonical event, trade, signing, transport, storage, and sync
-crates without installing a runtime, starting workers, opening files, probing
-the network, selecting an account, or choosing fallback transports.
-
-The crate root intentionally exports only `Client`, `ClientBuilder`, `Error`,
-and `Result`. Advanced operations live in the `farm`, `listing`, `trade`,
-`signing`, `transport`, `storage`, `sync`, `diagnostics`, and `capability`
-modules.
-
-The package charter is the normative [`radroots_sdk` crate
-specification](../../docs/specs/radroots_crates_release_v1.md#18-radroots_sdk).
-This advanced front door is intended for CLI, Studio, FFI/mobile, and native
-applications that need to compose storage, signing, transport, or sync
-capabilities directly. Ordinary Rust applications should use the curated
-`radroots` facade.
-
-## Getting started
-
-The default `memory` feature provides an inert, in-process backend. The caller
-supplies its source generation; building the client creates no files, opens no
-network connection, installs no runtime, and starts no worker.
-
-```rust
-use radroots_sdk::{ClientBuilder, capability::CapabilityId};
-use radroots_storage::event::SourceGeneration;
-
-let generation = SourceGeneration::new([1; 32])?;
-let client = ClientBuilder::memory(generation).build()?;
-let storage = client
- .capabilities()
- .get(CapabilityId::CANONICAL_STORAGE);
-assert!(storage.is_some());
-
-# Ok::<(), Box<dyn std::error::Error>>(())
-```
-
-The complete executable form, including explicit asynchronous shutdown, lives
-in [`examples/safe_memory_client.rs`](examples/safe_memory_client.rs). The
-side-effect-free transport-selection example lives in
-[`examples/transport_profile.rs`](examples/transport_profile.rs).
-
-## Feature contract
-
-The complete public feature vocabulary is:
-
-| Feature | Capability |
-| --- | --- |
-| `memory` | deterministic in-process reference storage; the default feature |
-| `sqlite` | explicit canonical SQLite storage construction |
-| `sync` | composition with a caller-supplied canonical sync engine |
-| `nostr` | Nostr conversion and concrete source/sink adapters; implies `sync` |
-| `nip46` | NIP-46 signer provider; implies `nostr` |
-| `local-signing` | explicit local signing and secret-provider adapters |
-| `radrootsd` | explicitly invoked private daemon execution adapter; implies `sync` |
-| `geonames` | concrete GeoNames provider integration |
-| `knowledge` | deterministic knowledge event contracts/codecs |
-| `native` | `sqlite`, `sync`, and `local-signing` |
-| `full` | every supported production capability |
-
-There are no `runtime`, `local-runtime`, `signer-adapters`,
-`transport-nostr-runtime`, `transport-nostr-client`, or fixture features.
-Features compile capabilities; they do not perform I/O. Optional dependencies
-are activated only by their owning feature.
-
-The supported qualification matrix is:
-
-```sh
-cargo check -p radroots_sdk --all-targets --no-default-features
-cargo check -p radroots_sdk --all-targets
-cargo check -p radroots_sdk --all-targets --no-default-features --features memory
-cargo check -p radroots_sdk --all-targets --no-default-features --features sqlite
-cargo check -p radroots_sdk --all-targets --no-default-features --features sync
-cargo check -p radroots_sdk --all-targets --no-default-features --features nostr
-cargo check -p radroots_sdk --all-targets --no-default-features --features nip46
-cargo check -p radroots_sdk --all-targets --no-default-features --features local-signing
-cargo check -p radroots_sdk --all-targets --no-default-features --features radrootsd
-cargo check -p radroots_sdk --all-targets --no-default-features --features geonames
-cargo check -p radroots_sdk --all-targets --no-default-features --features knowledge
-cargo check -p radroots_sdk --all-targets --no-default-features --features native
-cargo check -p radroots_sdk --all-targets --no-default-features --features full
-cargo check -p radroots_sdk --all-targets --all-features
-```
-
-## Explicit composition
-
-`ClientBuilder` requires a storage capability. `ClientBuilder::memory(...)`
-and `ClientBuilder::sqlite(...)` are explicit constructors; merely enabling a
-feature or constructing an empty builder creates no resource. Signers, event
-sources, event sinks, and the sync engine are injected separately.
-
-Native mobile hosts can retain one client while changing host-owned identity
-and relay selection. `signing::Slot` accepts a key restored from secure host
-storage or generates a one-time `nsec` handoff; it never persists that secret.
-`transport::NostrSlot` validates a complete relay set before atomically
-installing it. `ClientBuilder::host_sync(sync::HostPolicy)` explicitly opts
-SDK-created memory or SQLite storage into system-clock and random operation-ID
-policy without creating a runtime, timer, retry loop, or worker.
-
-With `sync`, `nostr`, and `local-signing`, `Client::social()` exposes bounded
-profile/post fetch and publish operations. Fetch verifies and durably ingests
-each accepted event. Publish durably commits a signed event, then performs one
-explicit delivery pass and reports whether delivery remains pending. Host UI
-lifecycle code owns polling, background policy, keychain access, and any later
-retry. Profile authoring is deliberately media-free until the host can supply
-the canonical byte-verified media descriptor required by the event contract.
-
-Transport profiles are explicit. `Profile::local_only()` contains no target.
-`Profile::delivery(...)` retains the exact canonical target set and
-satisfaction policy. Preview transports report unavailable and never
-substitute Nostr, daemon, local persistence, or another route.
-
-Farm, listing, and trade preparation is deterministic and side-effect-free.
-Commit operations accept native operation and idempotency identities,
-cancellation policy, and an explicit transport profile, then return the
-canonical sync receipt. Repeating the same idempotent request is the supported
-resume/replay path.
-
-Preparation has no commit point. During commit, durable local acceptance is
-the first commit point; a cancellation observed before that point returns
-without claiming acceptance. Cancellation after durable acceptance cannot
-roll the accepted event back: the returned error/receipt identifies the safe
-resume path, and retrying with the same idempotency identity must not create a
-second logical operation. Dropping `Client::close` before completion leaves
-the shared client in a retry-required closing state; call `close` again.
-
-The SDK does not define a second wire model. Canonical lower-crate domain and
-protocol types own serialization, validation, versioning, and size limits.
-SDK request, plan, and receipt structs are constructor-led orchestration types;
-their private representation is not a persistence or interchange format.
-
-## Reliability and privacy
-
-Backup, restore, integrity, and status operations delegate to
-`radroots_storage::StorageReliability` and return its native versioned plans,
-manifests, revisions, stages, and status values. Restore is staged and must be
-explicitly finalized. Client shutdown is explicit and asynchronous.
-
-Public farm/listing events contain only the coarse locality represented by the
-canonical event model. Exact coordinates, private trade terms, protected
-content, and key references remain behind the private-artifact and secrets
-SPIs. Diagnostics contain only capability and canonical storage status. Public
-errors and daemon failures use stable, redacted classifications while retaining
-private source chains for local diagnostics.
-
-Signer material and bearer credentials are caller-owned capabilities. The SDK
-does not read a keyring, persist secrets, or include credentials
-in `Debug`, `Display`, diagnostics, receipts, or public error text. Hosts remain
-responsible for protecting source chains and any lower-level logs they choose
-to expose. When explicitly requested, `signing::Slot::generate` creates one
-ephemeral key and immediately hands its only persistence representation to the
-host for secure custody.
-
-## Daemon execution
-
-The `radrootsd` feature compiles a private HTTP/RPC adapter using the versioned
-`radroots_protocol::radrootsd::transport_publish::v5` contract. Constructing
-`transport::DaemonDelivery` is inert. Network contact occurs only when the host
-invokes `deliver`; bearer credentials are redacted, HTTP error bodies are not
-surfaced, and the response must match the signed event and requested policies.
-
-## Release posture
-
-This package is enabled for package-realistic validation only. Actual crates.io
-publication remains blocked pending the approval packet and a separately
-authorized operator step. The crate is licensed under `MIT OR Apache-2.0`.
-
-The reviewed all-features public API baseline is recorded at
-[`docs/api/radroots_sdk-0.1.0-alpha.txt`](../../docs/api/radroots_sdk-0.1.0-alpha.txt).
diff --git a/crates/sdk/examples/safe_memory_client.rs b/crates/sdk/examples/safe_memory_client.rs
@@ -1,20 +0,0 @@
-//! Builds and explicitly closes the safe in-process client.
-
-use radroots_sdk::{ClientBuilder, capability::CapabilityId};
-use radroots_storage::event::SourceGeneration;
-
-#[tokio::main]
-async fn main() -> Result<(), Box<dyn std::error::Error>> {
- let generation = SourceGeneration::new([1; 32])?;
- let client = ClientBuilder::memory(generation).build()?;
-
- assert!(
- client
- .capabilities()
- .get(CapabilityId::CANONICAL_STORAGE)
- .is_some()
- );
-
- client.close().await?;
- Ok(())
-}
diff --git a/crates/sdk/examples/transport_profile.rs b/crates/sdk/examples/transport_profile.rs
@@ -1,11 +0,0 @@
-//! Selects local-only behavior without constructing or probing a transport.
-
-use radroots_sdk::transport::Profile;
-
-fn main() {
- let profile = Profile::local_only();
-
- assert!(profile.is_local_only());
- assert!(profile.targets().is_none());
- assert!(profile.satisfaction().is_none());
-}
diff --git a/crates/sdk/src/adapters/mod.rs b/crates/sdk/src/adapters/mod.rs
@@ -1,2 +0,0 @@
-#[cfg(feature = "radrootsd")]
-pub(crate) mod radrootsd;
diff --git a/crates/sdk/src/adapters/radrootsd.rs b/crates/sdk/src/adapters/radrootsd.rs
@@ -1,352 +0,0 @@
-use core::fmt;
-use core::time::Duration;
-
-use radroots_event::draft::SignedEvent;
-use radroots_protocol::radrootsd::transport_publish::v5::{
- DeliveryPolicy as TransportPublishDeliveryPolicy, Error as TransportPublishProtocolError,
- EventRequest as TransportPublishEventRequest, EventResponse as TransportPublishEventResponse,
- METHOD_EVENT, TargetPolicy as TransportPublishTargetPolicy,
-};
-use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue};
-use serde::{Deserialize, Serialize, de::DeserializeOwned};
-use serde_json::{Value, json};
-
-pub const SDK_RADROOTSD_PUBLISH_REQUEST_ID: &str = "radroots-sdk-transport-publish-event";
-pub const SDK_RADROOTSD_PUBLISH_MAX_TARGETS: usize = 20;
-
-#[derive(Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
-pub enum RadrootsdAuth {
- #[default]
- None,
- BearerToken(String),
-}
-
-impl fmt::Debug for RadrootsdAuth {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::None => f.write_str("None"),
- Self::BearerToken(_) => f.write_str("BearerToken(<redacted>)"),
- }
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsdPublishConfig {
- pub endpoint: String,
- pub auth: RadrootsdAuth,
- pub timeout: Duration,
-}
-
-impl RadrootsdPublishConfig {
- pub fn new(endpoint: impl Into<String>) -> Self {
- Self {
- endpoint: endpoint.into(),
- auth: RadrootsdAuth::None,
- timeout: Duration::from_secs(10),
- }
- }
-
- pub fn with_auth(mut self, auth: RadrootsdAuth) -> Self {
- self.auth = auth;
- self
- }
-
- pub fn with_timeout(mut self, timeout: Duration) -> Self {
- self.timeout = timeout;
- self
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsdPublishAdapter {
- config: RadrootsdPublishConfig,
-}
-
-impl RadrootsdPublishAdapter {
- pub fn new(config: RadrootsdPublishConfig) -> Self {
- Self { config }
- }
-
- #[cfg(test)]
- pub fn config(&self) -> &RadrootsdPublishConfig {
- &self.config
- }
-
- pub async fn publish_signed_event(
- &self,
- request: RadrootsdPublishRequest,
- ) -> Result<TransportPublishEventResponse, RadrootsdError> {
- let event_identity =
- RadrootsdPublishEventIdentity::from_signed_event(&request.signed_event);
- let request = request.into_protocol_request();
- request
- .validate(SDK_RADROOTSD_PUBLISH_MAX_TARGETS)
- .map_err(RadrootsdError::from_protocol)?;
- let response = publish_event(
- self.config.endpoint.as_str(),
- &self.config.auth,
- &request,
- self.config.timeout,
- )
- .await?;
- validate_transport_publish_response_for_request(&request, &event_identity, &response)?;
- Ok(response)
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsdPublishRequest {
- pub signed_event: SignedEvent,
- pub target_policy: TransportPublishTargetPolicy,
- pub delivery_policy: TransportPublishDeliveryPolicy,
- pub idempotency_key: Option<String>,
- pub timeout_ms: Option<u64>,
-}
-
-impl RadrootsdPublishRequest {
- fn into_protocol_request(self) -> TransportPublishEventRequest {
- TransportPublishEventRequest {
- raw_event_json: self.signed_event.raw_json().to_owned(),
- target_policy: self.target_policy,
- delivery_policy: self.delivery_policy,
- idempotency_key: self.idempotency_key,
- timeout_ms: self.timeout_ms,
- }
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-struct RadrootsdPublishEventIdentity {
- event_id: String,
- pubkey: String,
- kind: u32,
-}
-
-impl RadrootsdPublishEventIdentity {
- fn from_signed_event(event: &SignedEvent) -> Self {
- Self {
- event_id: event.id_str().to_owned(),
- pubkey: event.pubkey().to_hex().to_owned(),
- kind: event.kind(),
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RadrootsdError {
- InvalidAuthHeader(String),
- InvalidRequest(String),
- Http(String),
- JsonRpc { code: i64, message: String },
- MalformedResponse(String),
-}
-
-impl RadrootsdError {
- fn from_protocol(error: TransportPublishProtocolError) -> Self {
- Self::InvalidRequest(error.to_string())
- }
-}
-
-impl fmt::Display for RadrootsdError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::InvalidAuthHeader(value) => {
- write!(f, "invalid radrootsd bearer token header: {value}")
- }
- Self::InvalidRequest(value) => f.write_str(value),
- Self::Http(value) => f.write_str(value),
- Self::MalformedResponse(value) => f.write_str(value),
- Self::JsonRpc { code, message } => {
- write!(f, "radrootsd jsonrpc failed {code}: {message}")
- }
- }
- }
-}
-
-impl std::error::Error for RadrootsdError {}
-
-#[derive(Debug, Deserialize)]
-struct JsonRpcEnvelope<T> {
- jsonrpc: Option<String>,
- id: Option<Value>,
- result: Option<T>,
- error: Option<JsonRpcError>,
-}
-
-#[derive(Debug, Deserialize)]
-struct JsonRpcError {
- code: i64,
- message: String,
-}
-
-pub async fn publish_event(
- endpoint: &str,
- auth: &RadrootsdAuth,
- request: &TransportPublishEventRequest,
- timeout: Duration,
-) -> Result<TransportPublishEventResponse, RadrootsdError> {
- jsonrpc_call(
- endpoint,
- auth,
- SDK_RADROOTSD_PUBLISH_REQUEST_ID,
- METHOD_EVENT,
- request,
- timeout,
- )
- .await
-}
-
-fn auth_headers(auth: &RadrootsdAuth) -> Result<HeaderMap, RadrootsdError> {
- let mut headers = HeaderMap::new();
- match auth {
- RadrootsdAuth::None => Ok(headers),
- RadrootsdAuth::BearerToken(token) => {
- let header = format!("Bearer {token}");
- let value = HeaderValue::from_str(header.as_str())
- .map_err(|err| RadrootsdError::InvalidAuthHeader(err.to_string()))?;
- headers.insert(AUTHORIZATION, value);
- Ok(headers)
- }
- }
-}
-
-#[cfg(test)]
-pub fn publish_event_request_json(
- request: &TransportPublishEventRequest,
-) -> Result<Value, RadrootsdError> {
- Ok(serde_json::to_value(request).expect("radrootsd transport publish request serializes"))
-}
-
-fn http_status_error(status: reqwest::StatusCode, body: &str) -> RadrootsdError {
- let body_summary = if body.is_empty() {
- "response body empty".to_owned()
- } else {
- format!("response body omitted ({} bytes)", body.len())
- };
- RadrootsdError::Http(format!(
- "radrootsd returned http {}: {}",
- status.as_u16(),
- body_summary
- ))
-}
-
-fn decode_jsonrpc_response<R>(
- method: &str,
- expected_id: &str,
- body: &str,
-) -> Result<R, RadrootsdError>
-where
- R: DeserializeOwned,
-{
- let envelope: JsonRpcEnvelope<R> = serde_json::from_str(body).map_err(|err| {
- RadrootsdError::MalformedResponse(format!("decode radrootsd {method} response: {err}"))
- })?;
- if envelope.jsonrpc.as_deref() != Some("2.0") {
- return Err(RadrootsdError::MalformedResponse(format!(
- "radrootsd {method} returned invalid jsonrpc version"
- )));
- }
- let expected_id_value = Value::String(expected_id.to_owned());
- if envelope.id.as_ref() != Some(&expected_id_value) {
- return Err(RadrootsdError::MalformedResponse(format!(
- "radrootsd {method} returned mismatched jsonrpc id"
- )));
- }
- match (envelope.result, envelope.error) {
- (Some(result), None) => Ok(result),
- (None, Some(error)) => Err(RadrootsdError::JsonRpc {
- code: error.code,
- message: error.message,
- }),
- (Some(_), Some(error)) => Err(RadrootsdError::MalformedResponse(format!(
- "radrootsd {method} returned result and error: {} {}",
- error.code, error.message
- ))),
- (None, None) => Err(RadrootsdError::MalformedResponse(format!(
- "radrootsd {method} returned neither result nor error"
- ))),
- }
-}
-
-async fn jsonrpc_call<P, R>(
- endpoint: &str,
- auth: &RadrootsdAuth,
- request_id: &str,
- method: &str,
- params: &P,
- timeout: Duration,
-) -> Result<R, RadrootsdError>
-where
- P: Serialize + ?Sized,
- R: DeserializeOwned,
-{
- let client = reqwest::Client::builder()
- .timeout(timeout)
- .build()
- .map_err(|err| RadrootsdError::Http(format!("build radrootsd client: {err}")))?;
- let mut request_builder = client
- .post(endpoint)
- .headers(auth_headers(auth)?)
- .json(&json!({
- "jsonrpc": "2.0",
- "id": request_id,
- "method": method,
- "params": params,
- }));
-
- request_builder = request_builder.header(CONTENT_TYPE, "application/json");
-
- let response = request_builder
- .send()
- .await
- .map_err(|err| RadrootsdError::Http(format!("send radrootsd {method} request: {err}")))?;
- let status = response.status();
- let body = response
- .text()
- .await
- .map_err(|err| RadrootsdError::Http(format!("read radrootsd response body: {err}")))?;
-
- if !status.is_success() {
- return Err(http_status_error(status, body.as_str()));
- }
-
- decode_jsonrpc_response(method, request_id, body.as_str())
-}
-
-fn validate_transport_publish_response_for_request(
- request: &TransportPublishEventRequest,
- event_identity: &RadrootsdPublishEventIdentity,
- response: &TransportPublishEventResponse,
-) -> Result<(), RadrootsdError> {
- response.job.validate().map_err(|error| {
- RadrootsdError::MalformedResponse(format!(
- "radrootsd transport publish response invalid: {error}"
- ))
- })?;
- if response.job.event_id != event_identity.event_id {
- return Err(response_mismatch("event_id"));
- }
- if response.job.pubkey != event_identity.pubkey {
- return Err(response_mismatch("pubkey"));
- }
- if response.job.event_kind != event_identity.kind {
- return Err(response_mismatch("event_kind"));
- }
- if response.job.delivery_policy != request.delivery_policy {
- return Err(response_mismatch("delivery_policy"));
- }
- if response.job.target_policy != request.target_policy {
- return Err(response_mismatch("target_policy"));
- }
- Ok(())
-}
-
-fn response_mismatch(field: &str) -> RadrootsdError {
- RadrootsdError::MalformedResponse(format!(
- "radrootsd transport publish response {field} does not match request"
- ))
-}
-
-#[cfg(test)]
-#[path = "../../tests/unit/adapters_radrootsd_tests.rs"]
-mod tests;
diff --git a/crates/sdk/src/capability.rs b/crates/sdk/src/capability.rs
@@ -1,423 +0,0 @@
-//! Side-effect-free client capability reporting.
-
-use std::collections::{BTreeMap, BTreeSet};
-
-/// Stable runtime identity for an SDK capability.
-///
-/// These values intentionally describe behavior rather than Cargo features.
-/// Construction is private so every reported ID comes from the governed
-/// catalog below.
-#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
-pub struct CapabilityId(&'static str);
-
-impl CapabilityId {
- /// Canonical storage operations.
- pub const CANONICAL_STORAGE: Self = Self("storage.canonical");
- /// Persistent storage operations.
- pub const PERSISTENT_STORAGE: Self = Self("storage.persistent");
- /// Storage backup and restore operations.
- pub const BACKUP_RESTORE: Self = Self("storage.backup-restore");
- /// Local signing.
- pub const LOCAL_SIGNING: Self = Self("signing.local");
- /// NIP-46 remote signing.
- pub const NIP46_SIGNING: Self = Self("signing.nip46");
- /// Nostr event fetch.
- pub const NOSTR_FETCH: Self = Self("transport.nostr.fetch");
- /// Nostr event delivery.
- pub const NOSTR_DELIVERY: Self = Self("transport.nostr.delivery");
- /// Reticulum event fetch preview.
- pub const RETICULUM_FETCH: Self = Self("transport.reticulum.fetch");
- /// Reticulum event delivery preview.
- pub const RETICULUM_DELIVERY: Self = Self("transport.reticulum.delivery");
- /// Mesh transport preview.
- pub const MESH_TRANSPORT: Self = Self("transport.mesh");
- /// SimpleX transport experiment.
- pub const SIMPLEX_TRANSPORT: Self = Self("transport.simplex");
- /// Daemon-mediated event delivery.
- pub const DAEMON_DELIVERY: Self = Self("transport.daemon.delivery");
- /// Inbound synchronization.
- pub const SYNC_PULL: Self = Self("sync.pull");
- /// Outbound synchronization.
- pub const SYNC_PUSH: Self = Self("sync.push");
- /// Farm event publication.
- pub const FARM_PUBLICATION: Self = Self("product.farm.publish");
- /// Listing event publication.
- pub const LISTING_PUBLICATION: Self = Self("product.listing.publish");
- /// Trade command execution.
- pub const TRADE_COMMANDS: Self = Self("product.trade.command");
- /// Trade queries.
- pub const TRADE_QUERIES: Self = Self("product.trade.query");
- /// Knowledge event support.
- pub const KNOWLEDGE_EVENTS: Self = Self("event.knowledge");
-
- /// Returns the stable presentation-independent identity.
- #[must_use]
- pub const fn as_str(self) -> &'static str {
- self.0
- }
-}
-
-impl std::fmt::Display for CapabilityId {
- fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- formatter.write_str(self.0)
- }
-}
-
-/// Product maturity independent of runtime availability.
-#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
-pub enum Maturity {
- /// Supported Release V1 behavior.
- Stable,
- /// Preserved pre-stable behavior with an explicit compatibility warning.
- Preview,
- /// Exploratory behavior without a compatibility commitment.
- Experimental,
-}
-
-/// Current runtime availability independent of compilation and configuration.
-#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
-pub enum Availability {
- /// The configured capability is ready.
- Available,
- /// The configured capability is usable with reduced functionality.
- Degraded,
- /// The capability is compiled but not currently usable.
- Unavailable,
- /// The capability is not supported by this build.
- Unsupported,
-}
-
-/// One immutable capability observation.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct CapabilityStatus {
- id: CapabilityId,
- compiled: bool,
- configured: bool,
- availability: Availability,
- maturity: Maturity,
-}
-
-impl CapabilityStatus {
- /// Returns the stable runtime identity.
- #[must_use]
- pub const fn id(self) -> CapabilityId {
- self.id
- }
-
- /// Returns whether support was compiled into this package.
- #[must_use]
- pub const fn is_compiled(self) -> bool {
- self.compiled
- }
-
- /// Returns whether the host configured the capability for this client.
- #[must_use]
- pub const fn is_configured(self) -> bool {
- self.configured
- }
-
- /// Returns the current side-effect-free availability observation.
- #[must_use]
- pub const fn availability(self) -> Availability {
- self.availability
- }
-
- /// Returns the independent product maturity classification.
- #[must_use]
- pub const fn maturity(self) -> Maturity {
- self.maturity
- }
-}
-
-/// Complete deterministic capability report for one client observation.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct CapabilityReport {
- statuses: Vec<CapabilityStatus>,
-}
-
-impl CapabilityReport {
- /// Returns all known capabilities in stable catalog order.
- pub fn iter(&self) -> impl ExactSizeIterator<Item = &CapabilityStatus> {
- self.statuses.iter()
- }
-
- /// Finds one capability by stable runtime identity.
- #[must_use]
- pub fn get(&self, id: CapabilityId) -> Option<CapabilityStatus> {
- self.statuses.iter().copied().find(|status| status.id == id)
- }
-}
-
-#[derive(Clone, Copy)]
-struct Definition {
- id: CapabilityId,
- maturity: Maturity,
- compiled: bool,
-}
-
-const CATALOG: &[Definition] = &[
- Definition::stable(CapabilityId::CANONICAL_STORAGE, true),
- Definition::stable(CapabilityId::PERSISTENT_STORAGE, cfg!(feature = "sqlite")),
- Definition::stable(CapabilityId::BACKUP_RESTORE, true),
- Definition::stable(CapabilityId::LOCAL_SIGNING, cfg!(feature = "local-signing")),
- Definition::stable(CapabilityId::NIP46_SIGNING, cfg!(feature = "nip46")),
- Definition::stable(CapabilityId::NOSTR_FETCH, cfg!(feature = "nostr")),
- Definition::stable(CapabilityId::NOSTR_DELIVERY, cfg!(feature = "nostr")),
- Definition::preview(CapabilityId::RETICULUM_FETCH),
- Definition::preview(CapabilityId::RETICULUM_DELIVERY),
- Definition::experimental(CapabilityId::MESH_TRANSPORT),
- Definition::experimental(CapabilityId::SIMPLEX_TRANSPORT),
- Definition::stable(CapabilityId::DAEMON_DELIVERY, cfg!(feature = "radrootsd")),
- Definition::stable(CapabilityId::SYNC_PULL, cfg!(feature = "sync")),
- Definition::stable(CapabilityId::SYNC_PUSH, cfg!(feature = "sync")),
- Definition::stable(CapabilityId::FARM_PUBLICATION, true),
- Definition::stable(CapabilityId::LISTING_PUBLICATION, true),
- Definition::stable(CapabilityId::TRADE_COMMANDS, true),
- Definition::stable(CapabilityId::TRADE_QUERIES, true),
- Definition::stable(CapabilityId::KNOWLEDGE_EVENTS, cfg!(feature = "knowledge")),
-];
-
-impl Definition {
- const fn stable(id: CapabilityId, compiled: bool) -> Self {
- Self {
- id,
- maturity: Maturity::Stable,
- compiled,
- }
- }
-
- const fn preview(id: CapabilityId) -> Self {
- Self {
- id,
- maturity: Maturity::Preview,
- compiled: false,
- }
- }
-
- const fn experimental(id: CapabilityId) -> Self {
- Self {
- id,
- maturity: Maturity::Experimental,
- compiled: false,
- }
- }
-}
-
-pub(crate) struct Context<'a> {
- pub(crate) storage: bool,
- pub(crate) signer: bool,
- pub(crate) source: bool,
- pub(crate) sink: bool,
- pub(crate) sync: bool,
- pub(crate) lifecycle_availability: Availability,
- pub(crate) explicitly_configured: &'a BTreeSet<CapabilityId>,
- pub(crate) overrides: &'a BTreeMap<CapabilityId, Availability>,
-}
-
-pub(crate) fn report(context: Context<'_>) -> CapabilityReport {
- let statuses = CATALOG
- .iter()
- .map(|definition| {
- let configured = configured(definition.id, &context);
- let availability = if !definition.compiled {
- Availability::Unsupported
- } else if !configured {
- Availability::Unavailable
- } else if context.lifecycle_availability != Availability::Available {
- context.lifecycle_availability
- } else {
- context
- .overrides
- .get(&definition.id)
- .copied()
- .unwrap_or(context.lifecycle_availability)
- };
- CapabilityStatus {
- id: definition.id,
- compiled: definition.compiled,
- configured,
- availability,
- maturity: definition.maturity,
- }
- })
- .collect();
- CapabilityReport { statuses }
-}
-
-fn configured(id: CapabilityId, context: &Context<'_>) -> bool {
- match id {
- CapabilityId::CANONICAL_STORAGE
- | CapabilityId::BACKUP_RESTORE
- | CapabilityId::TRADE_QUERIES => context.storage,
- CapabilityId::SYNC_PULL => (context.sync, context.source) == (true, true),
- CapabilityId::SYNC_PUSH => (context.sync, context.sink) == (true, true),
- CapabilityId::FARM_PUBLICATION
- | CapabilityId::LISTING_PUBLICATION
- | CapabilityId::TRADE_COMMANDS => (context.signer, context.sink) == (true, true),
- CapabilityId::KNOWLEDGE_EVENTS => cfg!(feature = "knowledge"),
- CapabilityId::RETICULUM_FETCH
- | CapabilityId::RETICULUM_DELIVERY
- | CapabilityId::MESH_TRANSPORT
- | CapabilityId::SIMPLEX_TRANSPORT => false,
- _ => context.explicitly_configured.contains(&id),
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn catalog_ids_are_unique_and_not_feature_names() {
- for (index, definition) in CATALOG.iter().enumerate() {
- assert!(
- !CATALOG[..index]
- .iter()
- .any(|candidate| candidate.id == definition.id),
- "duplicate capability {}",
- definition.id
- );
- assert!(definition.id.as_str().contains('.'));
- }
- }
-
- #[test]
- fn maturity_and_unsupported_preview_states_are_independent() {
- let overrides = BTreeMap::new();
- let explicitly_configured = BTreeSet::new();
- let report = report(Context {
- storage: true,
- signer: false,
- source: false,
- sink: false,
- sync: false,
- lifecycle_availability: Availability::Available,
- explicitly_configured: &explicitly_configured,
- overrides: &overrides,
- });
- let storage = report
- .get(CapabilityId::CANONICAL_STORAGE)
- .expect("storage");
- assert!(storage.is_compiled());
- assert!(storage.is_configured());
- assert_eq!(storage.availability(), Availability::Available);
- assert_eq!(storage.maturity(), Maturity::Stable);
-
- let knowledge = report
- .get(CapabilityId::KNOWLEDGE_EVENTS)
- .expect("knowledge");
- assert_eq!(knowledge.is_compiled(), cfg!(feature = "knowledge"));
- assert_eq!(knowledge.is_configured(), cfg!(feature = "knowledge"));
-
- let reticulum = report
- .get(CapabilityId::RETICULUM_FETCH)
- .expect("reticulum");
- assert!(!reticulum.is_compiled());
- assert!(!reticulum.is_configured());
- assert_eq!(reticulum.availability(), Availability::Unsupported);
- assert_eq!(reticulum.maturity(), Maturity::Preview);
-
- let mesh = report.get(CapabilityId::MESH_TRANSPORT).expect("mesh");
- assert_eq!(mesh.maturity(), Maturity::Experimental);
- }
-
- #[test]
- fn configuration_and_availability_matrix_covers_every_decision_path() {
- let explicitly_configured = BTreeSet::from([
- CapabilityId::PERSISTENT_STORAGE,
- CapabilityId::NIP46_SIGNING,
- CapabilityId::NOSTR_FETCH,
- ]);
- let overrides = BTreeMap::from([
- (CapabilityId::PERSISTENT_STORAGE, Availability::Degraded),
- (CapabilityId::NOSTR_FETCH, Availability::Unavailable),
- ]);
-
- let complete = report(Context {
- storage: false,
- signer: true,
- source: true,
- sink: true,
- sync: true,
- lifecycle_availability: Availability::Available,
- explicitly_configured: &explicitly_configured,
- overrides: &overrides,
- });
- assert!(
- complete
- .get(CapabilityId::SYNC_PULL)
- .expect("pull")
- .is_configured()
- );
- assert!(
- complete
- .get(CapabilityId::SYNC_PUSH)
- .expect("push")
- .is_configured()
- );
- assert!(
- complete
- .get(CapabilityId::FARM_PUBLICATION)
- .expect("farm")
- .is_configured()
- );
- assert_eq!(
- complete
- .get(CapabilityId::NOSTR_FETCH)
- .expect("fetch")
- .availability(),
- if cfg!(feature = "nostr") {
- Availability::Unavailable
- } else {
- Availability::Unsupported
- }
- );
-
- let partial = report(Context {
- storage: false,
- signer: true,
- source: false,
- sink: false,
- sync: true,
- lifecycle_availability: Availability::Degraded,
- explicitly_configured: &explicitly_configured,
- overrides: &overrides,
- });
- assert!(
- !partial
- .get(CapabilityId::SYNC_PULL)
- .expect("pull")
- .is_configured()
- );
- assert!(
- !partial
- .get(CapabilityId::SYNC_PUSH)
- .expect("push")
- .is_configured()
- );
- assert!(
- !partial
- .get(CapabilityId::TRADE_COMMANDS)
- .expect("trade")
- .is_configured()
- );
- if partial
- .get(CapabilityId::NIP46_SIGNING)
- .expect("nip46")
- .is_compiled()
- {
- assert_eq!(
- partial
- .get(CapabilityId::NIP46_SIGNING)
- .expect("nip46")
- .availability(),
- Availability::Degraded
- );
- }
- assert_eq!(complete.iter().len(), CATALOG.len());
- assert_eq!(
- CapabilityId::NOSTR_FETCH.to_string(),
- "transport.nostr.fetch"
- );
- }
-}
diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs
@@ -1,1668 +0,0 @@
-//! Client construction and lifecycle.
-
-use std::{
- collections::{BTreeMap, BTreeSet},
- sync::{
- Arc,
- atomic::{AtomicU8, Ordering},
- },
-};
-
-use radroots_signing::Signer;
-use radroots_storage::Storage;
-#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
-use radroots_storage::authored_delivery::AuthoredDeliveryState;
-#[cfg(feature = "memory")]
-use radroots_storage::{event::SourceGeneration, memory::MemoryStorage};
-use radroots_transport::{EventSink, EventSource};
-
-use crate::{
- Error, Result,
- capability::{Availability, CapabilityId, CapabilityReport},
-};
-
-/// Cloneable handle to a composed Radroots client.
-#[derive(Clone)]
-pub struct Client {
- inner: Arc<ClientInner>,
-}
-
-/// Explicit composition boundary for a [`Client`].
-#[derive(Default)]
-pub struct ClientBuilder {
- storage: Option<Arc<dyn Storage>>,
- #[cfg(feature = "sync")]
- sync_storage: Option<Arc<dyn radroots_sync::policy::SyncStorage>>,
- signer: Option<Arc<dyn Signer>>,
- source: Option<Arc<dyn EventSource>>,
- sink: Option<Arc<dyn EventSink>>,
- #[cfg(feature = "sync")]
- sync: Option<radroots_sync::Engine>,
- #[cfg(feature = "sync")]
- host_sync: Option<crate::sync::HostPolicy>,
- #[cfg(feature = "local-signing")]
- signing_slot: Option<crate::signing::Slot>,
- #[cfg(feature = "nostr")]
- nostr_slot: Option<crate::transport::NostrSlot>,
- capability_availability: BTreeMap<CapabilityId, Availability>,
- explicitly_configured_capabilities: BTreeSet<CapabilityId>,
-}
-
-struct ClientInner {
- storage: Arc<dyn Storage>,
- signer: Option<Arc<dyn Signer>>,
- source: Option<Arc<dyn EventSource>>,
- sink: Option<Arc<dyn EventSink>>,
- #[cfg(feature = "sync")]
- sync: Option<radroots_sync::Engine>,
- #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
- signing_slot: Option<crate::signing::Slot>,
- #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
- nostr_slot: Option<crate::transport::NostrSlot>,
- capability_availability: BTreeMap<CapabilityId, Availability>,
- explicitly_configured_capabilities: BTreeSet<CapabilityId>,
- lifecycle: AtomicU8,
-}
-
-const OPEN: u8 = 0;
-const CLOSING: u8 = 1;
-const CLOSE_RETRY_REQUIRED: u8 = 2;
-const CLOSED: u8 = 3;
-
-/// Host-authored, media-free profile replacement.
-#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct ProfileDraft {
- name: String,
- display_name: Option<String>,
- about: Option<String>,
- nip05: Option<String>,
- bot: Option<bool>,
-}
-
-#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
-impl ProfileDraft {
- /// Creates a complete replacement with the required canonical name.
- #[must_use]
- pub fn new(name: impl Into<String>) -> Self {
- Self {
- name: name.into(),
- display_name: None,
- about: None,
- nip05: None,
- bot: None,
- }
- }
-
- /// Sets the optional display name.
- #[must_use]
- pub fn with_display_name(mut self, value: impl Into<String>) -> Self {
- self.display_name = Some(value.into());
- self
- }
-
- /// Sets the optional profile description.
- #[must_use]
- pub fn with_about(mut self, value: impl Into<String>) -> Self {
- self.about = Some(value.into());
- self
- }
-
- /// Sets a syntax-checked NIP-05 identifier at publish time.
- #[must_use]
- pub fn with_nip05(mut self, value: impl Into<String>) -> Self {
- self.nip05 = Some(value.into());
- self
- }
-
- /// Sets the optional NIP-05 bot marker.
- #[must_use]
- pub const fn with_bot(mut self, value: bool) -> Self {
- self.bot = Some(value);
- self
- }
-}
-
-/// One verified, durably ingested profile observation.
-#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct ProfileEvent {
- event_id: String,
- author: String,
- created_at: u64,
- name: Option<String>,
- display_name: Option<String>,
- about: Option<String>,
- picture: Option<String>,
- banner: Option<String>,
- nip05: Option<String>,
- bot: Option<bool>,
-}
-
-#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
-impl ProfileEvent {
- /// Returns the canonical event identifier.
- pub fn event_id(&self) -> &str {
- self.event_id.as_str()
- }
- /// Returns the canonical author public key.
- pub fn author(&self) -> &str {
- self.author.as_str()
- }
- /// Returns the event timestamp in Unix seconds.
- pub const fn created_at(&self) -> u64 {
- self.created_at
- }
- /// Returns the projected profile name.
- pub fn name(&self) -> Option<&str> {
- self.name.as_deref()
- }
- /// Returns the projected display name.
- pub fn display_name(&self) -> Option<&str> {
- self.display_name.as_deref()
- }
- /// Returns the projected description.
- pub fn about(&self) -> Option<&str> {
- self.about.as_deref()
- }
- /// Returns the unverified inbound picture reference.
- pub fn picture(&self) -> Option<&str> {
- self.picture.as_deref()
- }
- /// Returns the unverified inbound banner reference.
- pub fn banner(&self) -> Option<&str> {
- self.banner.as_deref()
- }
- /// Returns the syntax-checked, unresolved NIP-05 identifier.
- pub fn nip05(&self) -> Option<&str> {
- self.nip05.as_deref()
- }
- /// Returns the optional bot marker.
- pub const fn bot(&self) -> Option<bool> {
- self.bot
- }
-}
-
-/// One verified, durably ingested kind-1 social event.
-#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct PostEvent {
- event_id: String,
- author: String,
- created_at: u64,
- content: String,
-}
-
-#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
-impl PostEvent {
- /// Returns the canonical event identifier.
- pub fn event_id(&self) -> &str {
- self.event_id.as_str()
- }
- /// Returns the canonical author public key.
- pub fn author(&self) -> &str {
- self.author.as_str()
- }
- /// Returns the event timestamp in Unix seconds.
- pub const fn created_at(&self) -> u64 {
- self.created_at
- }
- /// Returns the canonical event content.
- pub fn content(&self) -> &str {
- self.content.as_str()
- }
-}
-
-/// Result of one explicit local commit followed by one delivery pass.
-#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct PublishReceipt {
- event_id: String,
- replay: bool,
- delivery_state: AuthoredDeliveryState,
-}
-
-#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
-impl PublishReceipt {
- /// Returns the canonical signed event identifier committed locally.
- pub fn event_id(&self) -> &str {
- self.event_id.as_str()
- }
- /// Returns whether preparation replayed an identical durable operation.
- pub const fn is_replay(&self) -> bool {
- self.replay
- }
- /// Returns the complete durable delivery state after this explicit pass.
- pub const fn delivery_state(&self) -> AuthoredDeliveryState {
- self.delivery_state
- }
- /// Returns whether this explicit pass satisfied the delivery policy.
- pub const fn is_delivered(&self) -> bool {
- matches!(self.delivery_state, AuthoredDeliveryState::Satisfied)
- }
- /// Returns whether durable local intent remains eligible for delivery.
- pub const fn is_delivery_pending(&self) -> bool {
- matches!(
- self.delivery_state,
- AuthoredDeliveryState::Pending | AuthoredDeliveryState::Retryable
- )
- }
-}
-
-/// Passive status of the configured shared Nostr source and sink.
-#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct TransportHealth {
- configured: bool,
- source_available: bool,
- sink_available: bool,
-}
-
-#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
-impl TransportHealth {
- /// Returns whether a validated relay set is installed.
- pub const fn is_configured(&self) -> bool {
- self.configured
- }
- /// Returns whether passive source status is fully available.
- pub const fn is_source_available(&self) -> bool {
- self.source_available
- }
- /// Returns whether passive sink status is fully available.
- pub const fn is_sink_available(&self) -> bool {
- self.sink_available
- }
-}
-
-/// Borrowed high-level social operations over one shared SDK engine.
-#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
-#[derive(Clone, Copy)]
-pub struct SocialOperations<'a> {
- client: &'a Client,
-}
-
-#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
-enum SocialDraft {
- Profile(Box<radroots_event::profile::AuthoredProfile>),
- Update(radroots_event::post::AuthoredUpdate),
- Reply(radroots_event::post::reply::AuthoredNip10Reply),
-}
-
-impl ClientBuilder {
- /// Creates an empty builder with no hidden storage, network, signing, or
- /// runtime side effects.
- #[must_use]
- pub fn new() -> Self {
- Self::default()
- }
-
- /// Creates a builder backed by deterministic in-process memory storage.
- #[cfg(feature = "memory")]
- #[must_use]
- pub fn memory(generation: SourceGeneration) -> Self {
- let storage = Arc::new(MemoryStorage::new(generation));
- let builder = Self::new().storage(storage.clone());
- #[cfg(feature = "sync")]
- {
- let mut builder = builder;
- builder.sync_storage = Some(storage);
- builder
- }
- #[cfg(not(feature = "sync"))]
- builder
- }
-
- /// Creates the ordinary deterministic in-process memory configuration.
- ///
- /// This performs no I/O and is intended for ephemeral local clients whose
- /// source-generation identity does not need to survive the process. Hosts
- /// that persist cursors should call [`Self::memory`] with their own
- /// generation instead.
- #[cfg(feature = "memory")]
- #[must_use]
- pub fn memory_default() -> Self {
- let storage = Arc::new(MemoryStorage::default());
- let builder = Self::new().storage(storage.clone());
- #[cfg(feature = "sync")]
- {
- let mut builder = builder;
- builder.sync_storage = Some(storage);
- builder
- }
- #[cfg(not(feature = "sync"))]
- builder
- }
-
- /// Explicitly opens canonical SQLite storage from validated host-owned
- /// configuration and returns a builder containing only the storage SPI.
- #[cfg(feature = "sqlite")]
- pub async fn sqlite(options: crate::storage::SqliteOptions) -> Result<Self> {
- let storage = radroots_storage_sqlite::SqliteStorage::open(options)
- .await
- .map_err(Error::storage_open_failed)?;
- let storage = Arc::new(storage);
- let builder = Self::new()
- .storage(storage.clone())
- .capability_availability(CapabilityId::PERSISTENT_STORAGE, Availability::Available);
- #[cfg(feature = "sync")]
- {
- let mut builder = builder;
- builder.sync_storage = Some(storage);
- Ok(builder)
- }
- #[cfg(not(feature = "sync"))]
- Ok(builder)
- }
-
- /// Injects the canonical storage capability.
- #[must_use]
- pub fn storage(mut self, storage: Arc<dyn Storage>) -> Self {
- self.storage = Some(storage);
- self
- }
-
- /// Injects an optional canonical signer capability.
- #[must_use]
- pub fn signer(mut self, signer: Arc<dyn Signer>) -> Self {
- self.signer = Some(signer);
- self
- }
-
- /// Installs a module-scoped signer provider over the canonical SPI and
- /// records its presentation-independent runtime capability.
- #[must_use]
- pub fn signing(mut self, provider: crate::signing::Provider) -> Self {
- let capability = match provider.mode() {
- crate::signing::Mode::Local => Some(CapabilityId::LOCAL_SIGNING),
- crate::signing::Mode::Nip46 => Some(CapabilityId::NIP46_SIGNING),
- crate::signing::Mode::Host => None,
- };
- #[cfg(feature = "local-signing")]
- {
- let (signer, slot) = provider.into_parts();
- self.signer = Some(signer);
- self.signing_slot = slot;
- }
- #[cfg(not(feature = "local-signing"))]
- {
- self.signer = Some(provider.into_signer());
- }
- if let Some(capability) = capability {
- self.explicitly_configured_capabilities.insert(capability);
- self.capability_availability
- .insert(capability, Availability::Available);
- }
- self
- }
-
- /// Injects an optional inbound event source.
- #[must_use]
- pub fn source(mut self, source: Arc<dyn EventSource>) -> Self {
- self.source = Some(source);
- self
- }
-
- /// Injects an optional outbound event sink.
- #[must_use]
- pub fn sink(mut self, sink: Arc<dyn EventSink>) -> Self {
- self.sink = Some(sink);
- self
- }
-
- /// Installs one host-reconfigurable Nostr source and sink.
- #[cfg(feature = "nostr")]
- #[must_use]
- pub fn nostr(mut self, slot: crate::transport::NostrSlot) -> Self {
- self.source = Some(Arc::new(slot.clone()));
- self.sink = Some(Arc::new(slot.clone()));
- self.nostr_slot = Some(slot);
- self
- }
-
- /// Injects an explicitly composed synchronization engine.
- #[cfg(feature = "sync")]
- #[must_use]
- pub fn sync_engine(mut self, sync: radroots_sync::Engine) -> Self {
- self.sync = Some(sync);
- self
- }
-
- /// Requests one SDK-composed engine using explicit native host policy.
- ///
- /// This is available only for SDK-created memory or SQLite storage, whose
- /// complete synchronization capability is known without downcasting.
- #[cfg(feature = "sync")]
- #[must_use]
- pub fn host_sync(mut self, policy: crate::sync::HostPolicy) -> Self {
- self.host_sync = Some(policy);
- self
- }
-
- /// Marks a specialized capability as configured and records its
- /// host-observed initial availability without probing resources.
- ///
- /// Reports ignore this observation for capabilities that are not compiled
- /// or configured. Runtime IDs are independent from Cargo feature names.
- #[must_use]
- pub fn capability_availability(mut self, id: CapabilityId, availability: Availability) -> Self {
- self.explicitly_configured_capabilities.insert(id);
- self.capability_availability.insert(id, availability);
- self
- }
-
- /// Validates the selected capabilities and creates a client handle.
- #[allow(unused_mut)]
- pub fn build(mut self) -> Result<Client> {
- let storage = self.storage.ok_or_else(Error::missing_storage)?;
- if (self.signer.is_some(), self.sink.is_some()) == (true, false) {
- return Err(Error::signer_without_sink());
- }
- #[cfg(feature = "sync")]
- if let Some(policy) = self.host_sync {
- let sync_storage = self
- .sync_storage
- .take()
- .ok_or_else(Error::shared_operation_unavailable)?;
- let (clock, ids, deadlines) = policy.composition();
- let mut builder = radroots_sync::Engine::builder(sync_storage, clock, ids, deadlines);
- if let Some(source) = self.source.as_ref() {
- builder = builder.source(Arc::clone(source));
- }
- if let Some(sink) = self.sink.as_ref() {
- builder = builder.sink(Arc::clone(sink));
- }
- if let Some(signer) = self.signer.as_ref() {
- builder = builder.signer(Arc::clone(signer));
- }
- self.sync = Some(builder.build().map_err(Error::invalid_host_configuration)?);
- }
- Ok(Client {
- inner: Arc::new(ClientInner {
- storage,
- signer: self.signer,
- source: self.source,
- sink: self.sink,
- #[cfg(feature = "sync")]
- sync: self.sync,
- #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
- signing_slot: self.signing_slot,
- #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
- nostr_slot: self.nostr_slot,
- capability_availability: self.capability_availability,
- explicitly_configured_capabilities: self.explicitly_configured_capabilities,
- lifecycle: AtomicU8::new(OPEN),
- }),
- })
- }
-}
-
-impl Client {
- /// Returns a deterministic capability report without probing resources or
- /// performing filesystem, network, signing, or storage operations.
- #[must_use]
- pub fn capabilities(&self) -> CapabilityReport {
- let lifecycle_availability = match self.inner.lifecycle.load(Ordering::Acquire) {
- OPEN => Availability::Available,
- CLOSING | CLOSE_RETRY_REQUIRED => Availability::Degraded,
- _ => Availability::Unavailable,
- };
- crate::capability::report(crate::capability::Context {
- storage: true,
- signer: self.inner.signer.is_some(),
- source: self.inner.source.is_some(),
- sink: self.inner.sink.is_some(),
- sync: self.sync_is_configured(),
- lifecycle_availability,
- explicitly_configured: &self.inner.explicitly_configured_capabilities,
- overrides: &self.inner.capability_availability,
- })
- }
-
- /// Returns canonical backend status without exposing a backend handle.
- pub async fn storage_status(&self) -> Result<crate::storage::Status> {
- let storage = self.storage()?;
- radroots_storage::BackupSource::status(storage)
- .await
- .map_err(Error::storage_inspection_failed)
- }
-
- /// Runs canonical integrity inspection without exposing backend internals.
- pub async fn storage_integrity(&self) -> Result<crate::storage::IntegrityStatus> {
- let storage = self.storage()?;
- radroots_storage::BackupSource::integrity(storage)
- .await
- .map_err(Error::storage_inspection_failed)
- }
-
- /// Returns the injected canonical storage capability.
- pub fn storage(&self) -> Result<&dyn Storage> {
- self.require_open()?;
- Ok(self.inner.storage.as_ref())
- }
-
- /// Returns backend-neutral backup, restore, status, and integrity operations.
- pub fn storage_operations(&self) -> Result<crate::storage::Operations<'_>> {
- Ok(crate::storage::Operations::new(self.storage()?))
- }
-
- /// Returns the injected signer, when outbound authoring is enabled.
- pub fn signer(&self) -> Result<Option<&dyn Signer>> {
- self.require_open()?;
- Ok(self.inner.signer.as_deref())
- }
-
- /// Returns the injected inbound source, when pull is enabled.
- pub fn source(&self) -> Result<Option<&dyn EventSource>> {
- self.require_open()?;
- Ok(self.inner.source.as_deref())
- }
-
- /// Returns the injected outbound sink, when delivery is enabled.
- pub fn sink(&self) -> Result<Option<&dyn EventSink>> {
- self.require_open()?;
- Ok(self.inner.sink.as_deref())
- }
-
- /// Returns client-scoped canonical synchronization operations, when configured.
- #[cfg(feature = "sync")]
- pub fn sync(&self) -> Result<Option<crate::sync::Operations<'_>>> {
- self.require_open()?;
- Ok(self.inner.sync.as_ref().map(crate::sync::Operations::new))
- }
-
- /// Returns farm commit operations when canonical synchronization is configured.
- #[cfg(feature = "sync")]
- pub fn farm(&self) -> Result<Option<crate::farm::Operations<'_>>> {
- Ok(self.sync()?.map(crate::farm::Operations::new))
- }
-
- /// Returns listing commit operations when canonical synchronization is configured.
- #[cfg(feature = "sync")]
- pub fn listing(&self) -> Result<Option<crate::listing::Operations<'_>>> {
- Ok(self.sync()?.map(crate::listing::Operations::new))
- }
-
- /// Returns trade operations when canonical synchronization is configured.
- #[cfg(feature = "sync")]
- pub fn trade(&self) -> Result<Option<crate::trade::Operations<'_>>> {
- let storage = self.storage()?;
- Ok(self
- .sync()?
- .map(|sync| crate::trade::Operations::new(storage, sync)))
- }
-
- /// Returns high-level shared social operations when the required explicit
- /// signer, transport, and synchronization composition is present.
- #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
- pub fn social(&self) -> Result<SocialOperations<'_>> {
- self.require_open()?;
- let social_composition = (
- self.inner.sync.is_some(),
- self.inner.signing_slot.is_some(),
- self.inner.nostr_slot.is_some(),
- );
- if social_composition != (true, true, true) {
- return Err(Error::shared_operation_unavailable());
- }
- Ok(SocialOperations { client: self })
- }
-
- /// Returns whether explicit close completed successfully or reached the
- /// lower storage commit point.
- #[must_use]
- pub fn is_closed(&self) -> bool {
- self.inner.lifecycle.load(Ordering::Acquire) == CLOSED
- }
-
- /// Explicitly closes active storage resources across every client clone.
- ///
- /// Dropping this future before its first poll has no effect. Cancellation
- /// after close begins leaves the client unavailable and permits an
- /// explicit retry; it never reports rollback. Repeated completed close is
- /// idempotent. No worker, executor, or blocking `Drop` path is installed.
- pub async fn close(&self) -> Result<()> {
- loop {
- match self.inner.lifecycle.load(Ordering::Acquire) {
- CLOSED => return Ok(()),
- CLOSING => return Err(Error::close_in_progress()),
- state @ (OPEN | CLOSE_RETRY_REQUIRED) => {
- if self
- .inner
- .lifecycle
- .compare_exchange(state, CLOSING, Ordering::AcqRel, Ordering::Acquire)
- .is_ok()
- {
- break;
- }
- }
- _ => return Err(Error::client_closed()),
- }
- }
-
- let attempt = CloseAttempt::new(Arc::clone(&self.inner));
- let close_result = radroots_storage::BackupSource::close(self.inner.storage.as_ref()).await;
- attempt.complete();
- close_result
- .map(|_| ())
- .map_err(Error::storage_close_failed)
- }
-
- fn require_open(&self) -> Result<()> {
- match self.inner.lifecycle.load(Ordering::Acquire) {
- OPEN => Ok(()),
- CLOSING | CLOSE_RETRY_REQUIRED => Err(Error::client_closing()),
- _ => Err(Error::client_closed()),
- }
- }
-
- #[cfg(feature = "sync")]
- fn sync_is_configured(&self) -> bool {
- self.inner.sync.is_some()
- }
-
- #[cfg(not(feature = "sync"))]
- fn sync_is_configured(&self) -> bool {
- false
- }
-}
-
-#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
-impl SocialOperations<'_> {
- /// Observes both transport directions without initiating relay work.
- pub async fn transport_health(&self) -> Result<TransportHealth> {
- use radroots_transport::capability::Availability as TransportAvailability;
- let slot = self.nostr()?;
- let configured = slot.targets().is_some();
- let source = radroots_transport::EventSource::status(slot)
- .await
- .map_err(|_| Error::shared_operation_failed_without_source())?;
- let sink = radroots_transport::EventSink::status(slot)
- .await
- .map_err(|_| Error::shared_operation_failed_without_source())?;
- Ok(TransportHealth {
- configured,
- source_available: source.availability() == TransportAvailability::Available,
- sink_available: sink.availability() == TransportAvailability::Available,
- })
- }
-
- /// Fetches, verifies, and durably ingests the latest profile for the active signer.
- pub async fn fetch_profile_for_signer(&self) -> Result<Option<ProfileEvent>> {
- let identity = self.identity()?;
- let selector = radroots_transport::source::FetchSelector::all()
- .with_kinds(vec![radroots_event::envelope::kind::KIND_PROFILE])
- .and_then(|selector| selector.with_authors(vec![identity.public_key()]))
- .map_err(|_| Error::invalid_host_configuration_without_source())?;
- let events = self.fetch(32, selector).await?;
- let mut profiles = events
- .into_iter()
- .filter_map(|event| profile_event(&event).ok())
- .collect::<Vec<_>>();
- profiles.sort_by_key(|event| std::cmp::Reverse(event.created_at));
- Ok(profiles.into_iter().next())
- }
-
- /// Fetches, verifies, and durably ingests a bounded kind-1 page.
- pub async fn fetch_posts(
- &self,
- limit: u16,
- since_unix_seconds: Option<u64>,
- ) -> Result<Vec<PostEvent>> {
- let mut selector = radroots_transport::source::FetchSelector::all()
- .with_kinds(vec![radroots_event::envelope::kind::KIND_POST])
- .map_err(|_| Error::invalid_host_configuration_without_source())?;
- if let Some(since) = since_unix_seconds {
- selector = selector
- .with_since_unix_seconds(since)
- .map_err(|_| Error::invalid_host_configuration_without_source())?;
- }
- let mut posts = self
- .fetch(limit, selector)
- .await?
- .into_iter()
- .map(|event| PostEvent {
- event_id: event.id_hex(),
- author: event.pubkey().to_hex(),
- created_at: event.created_at(),
- content: event.content().to_owned(),
- })
- .collect::<Vec<_>>();
- posts.sort_by_key(|event| std::cmp::Reverse(event.created_at));
- Ok(posts)
- }
-
- /// Publishes a complete media-free profile replacement.
- pub async fn publish_profile(&self, draft: ProfileDraft) -> Result<PublishReceipt> {
- let mut profile = radroots_event::profile::AuthoredProfile::new(draft.name)
- .map_err(Error::invalid_host_configuration)?;
- if let Some(value) = draft.display_name {
- profile = profile.with_display_name(value);
- }
- if let Some(value) = draft.about {
- profile = profile.with_about(value);
- }
- if let Some(value) = draft.nip05 {
- profile = profile.with_nip05(
- radroots_event::profile::Nip05Identifier::parse(value.as_str())
- .map_err(Error::invalid_host_configuration)?,
- );
- }
- if let Some(value) = draft.bot {
- profile = profile.with_bot(value);
- }
- self.publish(SocialDraft::Profile(Box::new(profile))).await
- }
-
- /// Publishes one strict root kind-1 update.
- pub async fn publish_text(&self, content: impl Into<String>) -> Result<PublishReceipt> {
- let update = radroots_event::post::AuthoredUpdate::new(content)
- .map_err(Error::invalid_host_configuration)?;
- self.publish(SocialDraft::Update(update)).await
- }
-
- /// Publishes one strict direct NIP-10 reply.
- pub async fn publish_reply(
- &self,
- content: impl Into<String>,
- root_event_id: &str,
- root_author: &str,
- relay_hint: Option<&str>,
- ) -> Result<PublishReceipt> {
- let reference = radroots_event::post::reply::Nip10ReplyReference::parse(
- root_event_id,
- root_author,
- relay_hint,
- )
- .map_err(Error::invalid_host_configuration)?;
- let reply = radroots_event::post::reply::AuthoredNip10Reply::direct(content, reference)
- .map_err(Error::invalid_host_configuration)?;
- self.publish(SocialDraft::Reply(reply)).await
- }
-
- async fn fetch(
- &self,
- limit: u16,
- selector: radroots_transport::source::FetchSelector,
- ) -> Result<Vec<radroots_event::SignedEvent>> {
- let slot = self.nostr()?;
- let targets = slot
- .targets()
- .ok_or_else(Error::shared_operation_unavailable)?;
- let request_id = format!("sdk-fetch-{}", uuid::Uuid::new_v4());
- let deadline = now_unix_ms()?.saturating_add(30_000);
- let request = radroots_transport::FetchRequest::new(
- request_id,
- targets,
- radroots_transport::source::FetchBounds::new(limit, deadline)
- .map_err(|_| Error::invalid_host_configuration_without_source())?,
- )
- .map_err(|_| Error::invalid_host_configuration_without_source())?
- .with_selector(selector);
- let page = radroots_transport::EventSource::fetch(slot, request)
- .await
- .map_err(|_| Error::shared_operation_failed_without_source())?;
- let observed = page.events().to_vec();
- let receipt = self
- .client
- .sync()?
- .ok_or_else(Error::shared_operation_unavailable)?
- .ingest_batch(
- observed.clone(),
- &radroots_sync::ingest::RegistryPolicy::verified(),
- )
- .await;
- Ok(observed
- .into_iter()
- .zip(receipt.outcomes())
- .filter_map(|(observed, outcome)| {
- outcome.as_ref().ok().map(|_| observed.event().clone())
- })
- .collect())
- }
-
- async fn publish(&self, authored: SocialDraft) -> Result<PublishReceipt> {
- use radroots_event::contract::AuthorRole;
- use radroots_event_codec::authoring::AuthoredEventPlan;
- use radroots_signing::{Actor, actor::ActorSource, request::CancellationPolicy};
- use radroots_storage::journal::IdempotencyKey;
- use radroots_sync::{PushRequest, policy::SyncId};
- use radroots_transport::policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy};
-
- let identity = self.identity()?;
- let targets = self
- .nostr()?
- .targets()
- .ok_or_else(Error::shared_operation_unavailable)?;
- let operation_uuid = uuid::Uuid::new_v4();
- let operation_id =
- SyncId::new(*operation_uuid.as_bytes()).map_err(Error::invalid_host_configuration)?;
- let now = now_unix_ms()?;
- let created_at = now / 1_000;
- let plan = match authored {
- SocialDraft::Profile(profile) => {
- AuthoredEventPlan::from_profile(&profile, created_at, identity.public_key_hex())
- }
- SocialDraft::Update(update) => {
- AuthoredEventPlan::from_update(&update, created_at, identity.public_key_hex())
- }
- SocialDraft::Reply(reply) => {
- AuthoredEventPlan::from_nip10_reply(&reply, created_at, identity.public_key_hex())
- }
- }
- .map_err(Error::invalid_host_configuration)?;
- let actor = Actor::new(
- identity.public_key(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Any],
- )
- .map_err(Error::invalid_host_configuration)?;
- let request = PushRequest::new(
- operation_id,
- IdempotencyKey::parse(format!("sdk-{operation_uuid}"))
- .map_err(Error::invalid_host_configuration)?,
- actor,
- plan,
- targets,
- SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::any()),
- now.checked_add(30_000)
- .ok_or_else(Error::invalid_host_configuration_without_source)?,
- CancellationPolicy::PreservePublishedRequest,
- )
- .map_err(Error::invalid_host_configuration)?;
- let sync = self
- .client
- .sync()?
- .ok_or_else(Error::shared_operation_unavailable)?;
- let preparation = sync
- .prepare_push(request.clone())
- .await
- .map_err(Error::shared_operation_failed)?;
- sync.sign_prepared(request)
- .await
- .map_err(Error::shared_operation_failed)?;
- sync.admit_signed(operation_id)
- .await
- .map_err(Error::shared_operation_failed)?;
- let status = sync
- .push_status(operation_id)
- .await
- .map_err(Error::shared_operation_failed)?
- .ok_or_else(Error::shared_operation_failed_without_source)?;
- let event_id = status
- .artifact()
- .signed()
- .ok_or_else(Error::shared_operation_failed_without_source)?
- .event()
- .id_hex();
- let delivery = sync
- .deliver_push(operation_id)
- .await
- .map_err(Error::shared_operation_failed)?;
- Ok(PublishReceipt {
- event_id,
- replay: preparation.is_replay(),
- delivery_state: delivery.plan().state(),
- })
- }
-
- fn identity(&self) -> Result<crate::signing::LocalIdentity> {
- self.client
- .inner
- .signing_slot
- .as_ref()
- .and_then(crate::signing::Slot::identity)
- .ok_or_else(Error::shared_operation_unavailable)
- }
-
- fn nostr(&self) -> Result<&crate::transport::NostrSlot> {
- self.client
- .inner
- .nostr_slot
- .as_ref()
- .ok_or_else(Error::shared_operation_unavailable)
- }
-}
-
-#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
-fn now_unix_ms() -> Result<u64> {
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .ok()
- .and_then(|duration| u64::try_from(duration.as_millis()).ok())
- .filter(|value| *value != 0)
- .ok_or_else(Error::shared_operation_unavailable)
-}
-
-#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
-fn profile_event(
- event: &radroots_event::SignedEvent,
-) -> std::result::Result<
- ProfileEvent,
- radroots_event_codec::decode::profile::RadrootsProfileMetadataParseError,
-> {
- let profile =
- radroots_event_codec::decode::profile::parse_inbound_profile_metadata(event.content())?;
- Ok(ProfileEvent {
- event_id: event.id_hex(),
- author: event.pubkey().to_hex(),
- created_at: event.created_at(),
- name: profile.name().map(str::to_owned),
- display_name: profile.display_name().map(str::to_owned),
- about: profile.about().map(str::to_owned),
- picture: profile.picture().map(|value| value.as_str().to_owned()),
- banner: profile.banner().map(|value| value.as_str().to_owned()),
- nip05: profile.nip05().map(|value| value.as_str().to_owned()),
- bot: profile.bot(),
- })
-}
-
-struct CloseAttempt {
- inner: Arc<ClientInner>,
- completed: bool,
-}
-
-impl CloseAttempt {
- fn new(inner: Arc<ClientInner>) -> Self {
- Self {
- inner,
- completed: false,
- }
- }
-
- fn complete(mut self) {
- self.inner.lifecycle.store(CLOSED, Ordering::Release);
- self.completed = true;
- }
-}
-
-impl Drop for CloseAttempt {
- fn drop(&mut self) {
- if !self.completed {
- self.inner
- .lifecycle
- .store(CLOSE_RETRY_REQUIRED, Ordering::Release);
- }
- }
-}
-
-impl std::fmt::Debug for Client {
- fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- formatter
- .debug_struct("Client")
- .field("signer", &self.inner.signer.is_some())
- .field("source", &self.inner.source.is_some())
- .field("sink", &self.inner.sink.is_some())
- .field("closed", &self.is_closed())
- .finish_non_exhaustive()
- }
-}
-
-impl std::fmt::Debug for ClientBuilder {
- fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- formatter
- .debug_struct("ClientBuilder")
- .field("storage", &self.storage.is_some())
- .field("signer", &self.signer.is_some())
- .field("source", &self.source.is_some())
- .field("sink", &self.sink.is_some())
- .finish_non_exhaustive()
- }
-}
-
-#[cfg(all(test, feature = "memory"))]
-mod tests {
- use super::*;
- use radroots_signing::{
- Error as SigningError, SignReceipt, SignRequest, SignerStatus, error::Kind,
- signer::BoxFuture as SigningFuture,
- };
- use radroots_transport::{
- DeliveryReceipt, DeliveryRequest, Error as TransportError, FetchPage, FetchRequest,
- SinkFailure, SinkStatus, SourceStatus, outcome::Retryability,
- source::BoxFuture as TransportFuture,
- };
- use std::{
- future::Future,
- sync::Arc,
- task::{Context, Poll, Wake, Waker},
- };
-
- struct TestSource;
- struct TestSink;
- struct TestSigner;
-
- impl EventSource for TestSource {
- fn status(&self) -> TransportFuture<'_, std::result::Result<SourceStatus, TransportError>> {
- Box::pin(async { Err(TransportError::UnsupportedOperation) })
- }
-
- fn fetch(
- &self,
- _request: FetchRequest,
- ) -> TransportFuture<'_, std::result::Result<FetchPage, TransportError>> {
- Box::pin(async { Err(TransportError::UnsupportedOperation) })
- }
- }
-
- impl EventSink for TestSink {
- fn status(&self) -> TransportFuture<'_, std::result::Result<SinkStatus, TransportError>> {
- Box::pin(async { Err(TransportError::UnsupportedOperation) })
- }
-
- fn deliver(
- &self,
- request: DeliveryRequest,
- ) -> TransportFuture<'_, std::result::Result<DeliveryReceipt, SinkFailure>> {
- Box::pin(async move {
- Err(SinkFailure::for_request(
- &request,
- "test_sink_unavailable",
- Retryability::Terminal,
- None,
- None,
- Vec::new(),
- )
- .expect("test sink failure"))
- })
- }
- }
-
- impl Signer for TestSigner {
- fn status(&self) -> SigningFuture<'_, std::result::Result<SignerStatus, SigningError>> {
- Box::pin(async { Err(SigningError::new(Kind::InternalError)) })
- }
-
- fn sign(
- &self,
- _request: SignRequest,
- ) -> SigningFuture<'_, std::result::Result<SignReceipt, SigningError>> {
- Box::pin(async { Err(SigningError::new(Kind::InternalError)) })
- }
- }
-
- fn generation() -> SourceGeneration {
- SourceGeneration::new([1; 32]).expect("non-zero generation")
- }
-
- #[test]
- fn missing_storage_and_signer_without_sink_fail_closed() {
- assert!(matches!(
- ClientBuilder::new().build(),
- Err(error) if error.kind() == crate::error::ErrorKind::MissingStorage
- ));
- assert!(matches!(
- ClientBuilder::memory(generation())
- .signer(Arc::new(TestSigner))
- .build(),
- Err(error) if error.kind() == crate::error::ErrorKind::SignerWithoutSink
- ));
- }
-
- #[test]
- fn memory_local_source_only_and_sink_only_compositions_are_explicit() {
- let local = ClientBuilder::memory(generation()).build().expect("local");
- assert!(local.source().expect("source capability").is_none());
- assert!(local.sink().expect("sink capability").is_none());
- assert!(local.signer().expect("signer capability").is_none());
-
- let source = ClientBuilder::memory(generation())
- .source(Arc::new(TestSource))
- .build()
- .expect("source-only");
- assert!(source.source().expect("source capability").is_some());
- assert!(source.sink().expect("sink capability").is_none());
-
- let sink = ClientBuilder::memory(generation())
- .sink(Arc::new(TestSink))
- .build()
- .expect("sink-only");
- assert!(sink.source().expect("source capability").is_none());
- assert!(sink.sink().expect("sink capability").is_some());
- }
-
- #[test]
- fn signer_with_sink_is_valid_and_diagnostics_are_capability_only() {
- let client = ClientBuilder::memory(generation())
- .sink(Arc::new(TestSink))
- .signer(Arc::new(TestSigner))
- .build()
- .expect("outbound client");
- assert!(client.signer().expect("signer capability").is_some());
- assert_eq!(
- format!("{client:?}"),
- "Client { signer: true, source: false, sink: true, closed: false, .. }"
- );
- }
-
- #[cfg(all(
- feature = "sync",
- feature = "nostr",
- feature = "local-signing",
- feature = "nip46"
- ))]
- #[test]
- fn curated_models_accessors_and_builder_modes_are_complete() {
- let profile_draft = ProfileDraft::new("farm")
- .with_display_name("Farm")
- .with_about("Local food")
- .with_nip05("farm@example.test")
- .with_bot(false);
- assert_eq!(profile_draft.name, "farm");
- assert_eq!(profile_draft.display_name.as_deref(), Some("Farm"));
- assert_eq!(profile_draft.about.as_deref(), Some("Local food"));
- assert_eq!(profile_draft.nip05.as_deref(), Some("farm@example.test"));
- assert_eq!(profile_draft.bot, Some(false));
-
- let profile = ProfileEvent {
- event_id: "event".to_owned(),
- author: "author".to_owned(),
- created_at: 7,
- name: Some("farm".to_owned()),
- display_name: Some("Farm".to_owned()),
- about: Some("Local food".to_owned()),
- picture: Some("https://example.test/picture".to_owned()),
- banner: Some("https://example.test/banner".to_owned()),
- nip05: Some("farm@example.test".to_owned()),
- bot: Some(false),
- };
- assert_eq!(profile.event_id(), "event");
- assert_eq!(profile.author(), "author");
- assert_eq!(profile.created_at(), 7);
- assert_eq!(profile.name(), Some("farm"));
- assert_eq!(profile.display_name(), Some("Farm"));
- assert_eq!(profile.about(), Some("Local food"));
- assert_eq!(profile.picture(), Some("https://example.test/picture"));
- assert_eq!(profile.banner(), Some("https://example.test/banner"));
- assert_eq!(profile.nip05(), Some("farm@example.test"));
- assert_eq!(profile.bot(), Some(false));
-
- let post = PostEvent {
- event_id: "post".to_owned(),
- author: "author".to_owned(),
- created_at: 8,
- content: "content".to_owned(),
- };
- assert_eq!(post.event_id(), "post");
- assert_eq!(post.author(), "author");
- assert_eq!(post.created_at(), 8);
- assert_eq!(post.content(), "content");
-
- for (delivery_state, delivered, pending) in [
- (AuthoredDeliveryState::Pending, false, true),
- (AuthoredDeliveryState::Retryable, false, true),
- (AuthoredDeliveryState::Satisfied, true, false),
- (AuthoredDeliveryState::Exhausted, false, false),
- (AuthoredDeliveryState::FailedTerminal, false, false),
- (AuthoredDeliveryState::Cancelled, false, false),
- ] {
- let receipt = PublishReceipt {
- event_id: "published".to_owned(),
- replay: true,
- delivery_state,
- };
- assert_eq!(receipt.event_id(), "published");
- assert!(receipt.is_replay());
- assert_eq!(receipt.delivery_state(), delivery_state);
- assert_eq!(receipt.is_delivered(), delivered);
- assert_eq!(receipt.is_delivery_pending(), pending);
- }
-
- let health = TransportHealth {
- configured: true,
- source_available: true,
- sink_available: false,
- };
- assert!(health.is_configured());
- assert!(health.is_source_available());
- assert!(!health.is_sink_available());
-
- let builder = ClientBuilder::memory_default()
- .source(Arc::new(TestSource))
- .host_sync(crate::sync::HostPolicy::default());
- assert!(format!("{builder:?}").contains("storage: true"));
- let client = builder.build().expect("sync client");
- assert!(client.sync().expect("sync").is_some());
- assert!(client.farm().expect("farm").is_some());
- assert!(client.listing().expect("listing").is_some());
- assert!(client.trade().expect("trade").is_some());
- assert!(client.social().is_err());
- assert!(
- format!(
- "{:?}",
- client.storage_operations().expect("storage operations")
- )
- .contains("borrowed canonical storage")
- );
- assert!(
- format!("{:?}", client.sync().expect("sync").expect("operations"))
- .contains("borrowed canonical engine")
- );
-
- let host = ClientBuilder::memory_default()
- .sink(Arc::new(TestSink))
- .signing(crate::signing::Provider::host(Arc::new(TestSigner)))
- .build()
- .expect("host signer");
- assert!(
- !host
- .capabilities()
- .get(CapabilityId::NIP46_SIGNING)
- .expect("nip46")
- .is_configured()
- );
-
- let nip46 = ClientBuilder::memory_default()
- .sink(Arc::new(TestSink))
- .signing(crate::signing::Provider::nip46(Arc::new(TestSigner)))
- .build()
- .expect("nip46 signer");
- assert!(
- nip46
- .capabilities()
- .get(CapabilityId::NIP46_SIGNING)
- .expect("nip46")
- .is_configured()
- );
- }
-
- #[cfg(feature = "local-signing")]
- #[test]
- fn module_scoped_signing_provider_configures_the_matching_capability() {
- let signer = radroots_nostr::signing::LocalSigner::generate().expect("local signer");
- let client = ClientBuilder::memory(generation())
- .sink(Arc::new(TestSink))
- .signing(crate::signing::Provider::local(signer))
- .build()
- .expect("client");
- let status = client
- .capabilities()
- .get(CapabilityId::LOCAL_SIGNING)
- .expect("local signing");
- assert!(status.is_compiled());
- assert!(status.is_configured());
- assert_eq!(status.availability(), Availability::Available);
- }
-
- #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
- #[tokio::test]
- async fn shared_mobile_composition_is_single_client_explicit_and_fail_closed() {
- let signing = crate::signing::Slot::new();
- let nostr = crate::transport::NostrSlot::new(crate::transport::RelayUrlPolicy::Local);
- let client = ClientBuilder::memory(generation())
- .signing(crate::signing::Provider::slot(signing.clone()))
- .nostr(nostr.clone())
- .host_sync(crate::sync::HostPolicy::standard())
- .build()
- .expect("shared client");
-
- let health = client
- .social()
- .expect("social composition")
- .transport_health()
- .await
- .expect("passive health");
- assert!(!health.is_configured());
- assert!(!health.is_source_available());
- assert!(!health.is_sink_available());
- assert!(matches!(
- client
- .social()
- .expect("social composition")
- .fetch_posts(1, None)
- .await,
- Err(error) if error.kind() == crate::error::ErrorKind::SharedOperationUnavailable
- ));
- assert!(
- client
- .social()
- .expect("social composition")
- .fetch_profile_for_signer()
- .await
- .is_err()
- );
-
- let (_secret, identity) = signing.generate().expect("host key handoff");
- assert_eq!(signing.identity(), Some(identity));
- let social = client.social().expect("social composition");
- assert!(social.fetch_profile_for_signer().await.is_err());
- assert!(social.fetch_posts(2, Some(1)).await.is_err());
- assert!(
- social
- .publish_profile(
- ProfileDraft::new("farm")
- .with_display_name("Farm")
- .with_about("Local food")
- .with_nip05("farm@example.test")
- .with_bot(false),
- )
- .await
- .is_err()
- );
- assert!(
- social
- .publish_profile(ProfileDraft::new("farm"))
- .await
- .is_err()
- );
- assert!(social.publish_text("local update").await.is_err());
- assert!(
- social
- .publish_reply(
- "local reply",
- "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- Some("ws://127.0.0.1:7447"),
- )
- .await
- .is_err()
- );
- assert!(
- social
- .publish_reply("local reply", "invalid", "invalid", None)
- .await
- .is_err()
- );
- nostr
- .configure(["ws://127.0.0.1:7447"])
- .expect("relay selection");
- assert!(nostr.targets().is_some());
- let social = client.social().expect("social composition");
- assert!(
- social
- .transport_health()
- .await
- .expect("configured passive health")
- .is_configured()
- );
- let fetch = tokio::time::timeout(
- core::time::Duration::from_secs(3),
- social.fetch_posts(2, Some(1)),
- )
- .await
- .expect("localhost fetch remains bounded");
- assert!(fetch.is_err() || fetch.is_ok_and(|events| events.is_empty()));
- let profile_fetch = tokio::time::timeout(
- core::time::Duration::from_secs(3),
- social.fetch_profile_for_signer(),
- )
- .await
- .expect("localhost profile fetch remains bounded");
- assert!(profile_fetch.is_err() || profile_fetch.is_ok_and(|event| event.is_none()));
- let publish = tokio::time::timeout(
- core::time::Duration::from_secs(3),
- social.publish_text("configured local update"),
- )
- .await
- .expect("localhost publish remains bounded");
- match publish {
- Ok(receipt) => {
- assert_eq!(receipt.event_id().len(), 64);
- assert!(!receipt.is_replay());
- }
- Err(error) => assert_eq!(error.kind(), crate::error::ErrorKind::SharedOperationFailed),
- }
- let profile_publish = tokio::time::timeout(
- core::time::Duration::from_secs(3),
- social.publish_profile(
- ProfileDraft::new("configured-farm")
- .with_display_name("Configured Farm")
- .with_about("Local food"),
- ),
- )
- .await
- .expect("localhost profile publish remains bounded");
- assert!(
- profile_publish.is_ok()
- || matches!(
- profile_publish,
- Err(error)
- if error.kind() == crate::error::ErrorKind::SharedOperationFailed
- )
- );
- let reply_publish = tokio::time::timeout(
- core::time::Duration::from_secs(3),
- social.publish_reply(
- "configured reply",
- "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- None,
- ),
- )
- .await
- .expect("localhost reply publish remains bounded");
- assert!(
- reply_publish.is_ok()
- || matches!(
- reply_publish,
- Err(error)
- if error.kind() == crate::error::ErrorKind::SharedOperationFailed
- )
- );
- nostr.clear();
- assert!(nostr.targets().is_none());
- signing.clear();
- assert!(signing.identity().is_none());
- assert_eq!(
- radroots_signing::Signer::status(&signing)
- .await
- .expect("empty slot status")
- .availability(),
- radroots_signing::status::SignerAvailability::Unavailable
- );
- }
-
- #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
- #[test]
- fn verified_profile_projection_preserves_the_curated_public_fields() {
- use radroots_event::wire::v1::Nip01EventWire;
-
- let author = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
- let content = r#"{"name":"farm","display_name":"Farm","about":"Local food","nip05":"farm@example.test","bot":false}"#;
- let id = radroots_event::draft::compute_nip01_event_id(author, 7, 0, &[], content)
- .expect("canonical profile id")
- .to_hex();
- let raw = serde_json::json!({
- "id": id,
- "pubkey": author,
- "created_at": 7,
- "kind": 0,
- "tags": [],
- "content": content,
- "sig": "d".repeat(128),
- })
- .to_string();
- let wire = Nip01EventWire::parse_json(&raw).expect("profile wire");
- let event = radroots_event::SignedEvent::from_wire_verified_id(wire, raw)
- .expect("verified profile event");
- let profile = profile_event(&event).expect("profile projection");
- assert_eq!(profile.event_id(), id);
- assert_eq!(profile.author(), author);
- assert_eq!(profile.created_at(), 7);
- assert_eq!(profile.name(), Some("farm"));
- assert_eq!(profile.display_name(), Some("Farm"));
- assert_eq!(profile.about(), Some("Local food"));
- assert_eq!(profile.nip05(), Some("farm@example.test"));
- assert_eq!(profile.bot(), Some(false));
- assert_eq!(profile.picture(), None);
- assert_eq!(profile.banner(), None);
- }
-
- #[test]
- fn close_is_clone_shared_idempotent_and_rejects_later_capability_access() {
- let client = ClientBuilder::memory(generation()).build().expect("client");
- let clone = client.clone();
- assert!(!client.is_closed());
- block_on(client.close()).expect("first close");
- assert!(clone.is_closed());
- block_on(clone.close()).expect("repeated close");
- assert!(matches!(
- clone.storage(),
- Err(error) if error.kind() == crate::error::ErrorKind::ClientClosed
- ));
- assert!(matches!(
- client.source(),
- Err(error) if error.kind() == crate::error::ErrorKind::ClientClosed
- ));
- }
-
- #[test]
- fn close_cancellation_boundaries_are_explicit_and_retryable() {
- let client = ClientBuilder::memory(generation()).build().expect("client");
- let unpolled = client.close();
- drop(unpolled);
- assert!(client.storage().is_ok());
-
- client.inner.lifecycle.store(CLOSING, Ordering::Release);
- let attempt = CloseAttempt::new(Arc::clone(&client.inner));
- drop(attempt);
- assert!(matches!(
- client.storage(),
- Err(error) if error.kind() == crate::error::ErrorKind::ClientClosing
- ));
- block_on(client.close()).expect("retry close");
- assert!(client.is_closed());
- }
-
- #[test]
- fn concurrent_clones_converge_on_one_closed_state() {
- let client = ClientBuilder::memory(generation()).build().expect("client");
- let first = client.clone();
- let second = client.clone();
- let outcomes = std::thread::scope(|scope| {
- let first_close = scope.spawn(move || block_on(first.close()));
- let second_close = scope.spawn(move || block_on(second.close()));
- [
- first_close.join().expect("first thread"),
- second_close.join().expect("second thread"),
- ]
- });
- assert!(outcomes.iter().all(|outcome| {
- outcome.is_ok()
- || matches!(
- outcome,
- Err(error)
- if error.kind() == crate::error::ErrorKind::CloseInProgress
- )
- }));
- if !client.is_closed() {
- block_on(client.close()).expect("finish close");
- }
- assert!(client.is_closed());
- }
-
- #[test]
- fn capability_reports_separate_configuration_degradation_and_lifecycle() {
- let local = ClientBuilder::memory(generation())
- .capability_availability(CapabilityId::CANONICAL_STORAGE, Availability::Degraded)
- .build()
- .expect("client");
- let report = local.capabilities();
- let storage = report
- .get(CapabilityId::CANONICAL_STORAGE)
- .expect("storage");
- assert!(storage.is_compiled());
- assert!(storage.is_configured());
- assert_eq!(storage.availability(), Availability::Degraded);
-
- let signing = report.get(CapabilityId::LOCAL_SIGNING).expect("signing");
- assert!(!signing.is_configured());
- assert!(matches!(
- signing.availability(),
- Availability::Unavailable | Availability::Unsupported
- ));
-
- block_on(local.close()).expect("close");
- assert_eq!(
- local
- .capabilities()
- .get(CapabilityId::BACKUP_RESTORE)
- .expect("backup")
- .availability(),
- Availability::Unavailable
- );
- }
-
- #[test]
- fn memory_storage_status_integrity_and_lifecycle_use_native_contracts() {
- use radroots_storage::status::{
- IntegrityHealth, ShutdownState, StorageBackend, StorageOpenMode, WriterPolicy,
- };
-
- let client = ClientBuilder::memory(generation()).build().expect("client");
- let status = block_on(client.storage_status()).expect("status");
- assert_eq!(status.backend(), StorageBackend::Memory);
- assert_eq!(status.open_mode(), StorageOpenMode::Create);
- assert_eq!(status.writer_policy(), WriterPolicy::NoWriter);
- assert_eq!(status.shutdown(), ShutdownState::Open);
- assert_eq!(
- block_on(client.storage_integrity())
- .expect("integrity")
- .health(),
- IntegrityHealth::Healthy
- );
- block_on(client.close()).expect("close");
- assert_eq!(
- block_on(client.storage_status())
- .expect_err("closed")
- .kind(),
- crate::error::ErrorKind::ClientClosed
- );
- }
-
- #[cfg(feature = "sqlite")]
- #[tokio::test]
- async fn sqlite_builder_exposes_native_status_integrity_and_lifecycle() {
- use radroots_storage::status::{
- IntegrityHealth, ShutdownState, StorageBackend, StorageOpenMode, WriterPolicy,
- };
-
- let directory = tempfile::tempdir().expect("temporary directory");
- let paths = crate::storage::SqlitePaths::from_directory(directory.path()).expect("paths");
- let options =
- crate::storage::SqliteOptions::new(paths, crate::storage::SqliteOpenMode::Create)
- .with_source_generation(generation(), 1)
- .expect("source generation");
- let client = ClientBuilder::sqlite(options)
- .await
- .expect("open builder")
- .build()
- .expect("client");
- let status = client.storage_status().await.expect("status");
- assert_eq!(status.backend(), StorageBackend::Sqlite);
- assert_eq!(status.open_mode(), StorageOpenMode::Create);
- assert_eq!(status.writer_policy(), WriterPolicy::AdvisoryProcessLock);
- assert_eq!(status.shutdown(), ShutdownState::Open);
- assert!(status.wal_enabled());
- assert_ne!(status.busy_timeout_ms(), 0);
- assert_eq!(
- client
- .storage_integrity()
- .await
- .expect("integrity")
- .health(),
- IntegrityHealth::Unknown
- );
- client.close().await.expect("close");
- assert!(client.is_closed());
- }
-
- struct ThreadWaker;
-
- impl Wake for ThreadWaker {
- fn wake(self: Arc<Self>) {
- std::thread::current().unpark();
- }
- }
-
- fn block_on<F: Future>(future: F) -> F::Output {
- let waker = Waker::from(Arc::new(ThreadWaker));
- let mut context = Context::from_waker(&waker);
- let mut future = Box::pin(future);
- loop {
- match future.as_mut().poll(&mut context) {
- Poll::Ready(output) => return output,
- Poll::Pending => std::thread::park(),
- }
- }
- }
-}
diff --git a/crates/sdk/src/diagnostics.rs b/crates/sdk/src/diagnostics.rs
@@ -1,59 +0,0 @@
-//! Redacted client diagnostics composed from canonical capability and storage status.
-
-/// One passive diagnostics snapshot containing only lower-owned status types.
-#[derive(Clone, Debug)]
-pub struct Report {
- capabilities: crate::capability::CapabilityReport,
- storage: radroots_storage::StorageStatus,
-}
-
-impl Report {
- /// Returns the side-effect-free SDK capability report.
- #[must_use]
- pub const fn capabilities(&self) -> &crate::capability::CapabilityReport {
- &self.capabilities
- }
-
- /// Returns the canonical backend status without implementation details.
- #[must_use]
- pub const fn storage(&self) -> radroots_storage::StorageStatus {
- self.storage
- }
-}
-
-/// Captures passive, secret-safe diagnostics without filesystem paths, SQL,
-/// connection handles, private artifacts, or recovery side effects.
-pub async fn inspect(client: &crate::Client) -> crate::Result<Report> {
- Ok(Report {
- capabilities: client.capabilities(),
- storage: client.storage_status().await?,
- })
-}
-
-#[cfg(all(test, feature = "memory"))]
-mod tests {
- use std::sync::Arc;
-
- use radroots_storage::{
- event::SourceGeneration, memory::MemoryStorage, status::StorageBackend,
- };
-
- use crate::ClientBuilder;
-
- #[tokio::test]
- async fn report_is_passive_native_and_redacted() {
- let client = ClientBuilder::new()
- .storage(Arc::new(MemoryStorage::new(
- SourceGeneration::new([9; 32]).expect("generation"),
- )))
- .build()
- .expect("client");
- let report = super::inspect(&client).await.expect("report");
-
- assert_eq!(report.storage().backend(), StorageBackend::Memory);
- assert!(report.capabilities().iter().all(|status| {
- !status.id().as_str().contains('/') && !status.id().as_str().contains('\\')
- }));
- assert!(!format!("{report:?}").contains("sqlite"));
- }
-}
diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs
@@ -1,465 +0,0 @@
-//! SDK-owned native errors and secret-safe protocol conversion.
-
-use std::{error, fmt};
-
-use radroots_protocol::{
- error::v1::{
- CapabilityId as ProtocolCapabilityId, Class, ErrorReport, KnownCode, RecoveryAction,
- SafeDetails, SafeMessage,
- },
- runtime::v1::OperationId,
-};
-
-use crate::capability::CapabilityId;
-
-macro_rules! error_catalog {
- ($(
- $kind:ident => {
- code: $code:ident,
- operation: $operation:expr,
- capability: $capability:expr,
- message: $message:literal,
- safe_detail_keys: [$($detail_key:literal),* $(,)?]
- }
- ),+ $(,)?) => {
- /// Stable native SDK error category.
- #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
- #[non_exhaustive]
- pub enum ErrorKind {
- $($kind,)+
- }
-
- impl ErrorKind {
- /// Every SDK error category in catalog order.
- pub const ALL: &'static [Self] = &[$(Self::$kind),+];
-
- /// Returns metadata generated from the single SDK authority.
- #[must_use]
- pub const fn descriptor(self) -> ErrorDescriptor {
- match self {
- $(Self::$kind => ErrorDescriptor {
- kind: Self::$kind,
- code: KnownCode::$code,
- operation: $operation,
- capability: $capability,
- message: $message,
- safe_detail_keys: &[$($detail_key),*],
- },)+
- }
- }
- }
-
- /// Complete SDK-native error metadata catalog.
- pub const CATALOG: &[ErrorDescriptor] = &[
- $(ErrorDescriptor {
- kind: ErrorKind::$kind,
- code: KnownCode::$code,
- operation: $operation,
- capability: $capability,
- message: $message,
- safe_detail_keys: &[$($detail_key),*],
- },)+
- ];
- };
-}
-
-error_catalog! {
- MissingStorage => {
- code: MissingStorage,
- operation: None,
- capability: Some(CapabilityId::CANONICAL_STORAGE),
- message: "SDK storage capability is not configured",
- safe_detail_keys: []
- },
- SignerWithoutSink => {
- code: SignerWithoutSink,
- operation: None,
- capability: None,
- message: "SDK signer requires an outbound event sink",
- safe_detail_keys: []
- },
- CloseInProgress => {
- code: ClientCloseInProgress,
- operation: None,
- capability: Some(CapabilityId::CANONICAL_STORAGE),
- message: "SDK client close is in progress",
- safe_detail_keys: []
- },
- ClientClosing => {
- code: ClientClosing,
- operation: None,
- capability: Some(CapabilityId::CANONICAL_STORAGE),
- message: "SDK client close requires completion or retry",
- safe_detail_keys: []
- },
- ClientClosed => {
- code: ClientClosed,
- operation: None,
- capability: Some(CapabilityId::CANONICAL_STORAGE),
- message: "SDK client is closed",
- safe_detail_keys: []
- },
- StorageCloseFailed => {
- code: StorageCloseFailed,
- operation: None,
- capability: Some(CapabilityId::CANONICAL_STORAGE),
- message: "SDK storage close failed",
- safe_detail_keys: []
- },
- StorageOpenFailed => {
- code: InternalError,
- operation: None,
- capability: Some(CapabilityId::PERSISTENT_STORAGE),
- message: "SDK persistent storage open failed",
- safe_detail_keys: []
- },
- StorageInspectionFailed => {
- code: StorageIntegrityFailed,
- operation: None,
- capability: Some(CapabilityId::CANONICAL_STORAGE),
- message: "SDK storage inspection failed",
- safe_detail_keys: []
- },
- InvalidHostConfiguration => {
- code: InvalidArgument,
- operation: None,
- capability: None,
- message: "SDK host configuration is invalid",
- safe_detail_keys: []
- },
- SharedOperationUnavailable => {
- code: TransportOperationUnavailable,
- operation: None,
- capability: None,
- message: "SDK shared network operation is unavailable",
- safe_detail_keys: []
- },
- SharedOperationFailed => {
- code: SyncPartial,
- operation: None,
- capability: None,
- message: "SDK shared network operation failed",
- safe_detail_keys: []
- },
-}
-
-/// Stable metadata for one native SDK failure.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct ErrorDescriptor {
- kind: ErrorKind,
- code: KnownCode,
- operation: Option<OperationId>,
- capability: Option<CapabilityId>,
- message: &'static str,
- safe_detail_keys: &'static [&'static str],
-}
-
-impl ErrorDescriptor {
- /// Returns the native category.
- #[must_use]
- pub const fn kind(self) -> ErrorKind {
- self.kind
- }
-
- /// Returns the stable protocol code.
- #[must_use]
- pub const fn code(self) -> KnownCode {
- self.code
- }
-
- /// Returns the class from the generated protocol authority.
- #[must_use]
- pub const fn class(self) -> Class {
- self.code.descriptor().class
- }
-
- /// Returns retryability from the generated protocol authority.
- #[must_use]
- pub const fn retryable(self) -> bool {
- self.code.descriptor().retryable
- }
-
- /// Returns recovery actions from the generated protocol authority.
- #[must_use]
- pub const fn recovery_actions(self) -> &'static [RecoveryAction] {
- self.code.descriptor().recovery_actions
- }
-
- /// Returns the related operation when the protocol catalog defines one.
- #[must_use]
- pub const fn operation(self) -> Option<OperationId> {
- self.operation
- }
-
- /// Returns the related runtime capability.
- #[must_use]
- pub const fn capability(self) -> Option<CapabilityId> {
- self.capability
- }
-
- /// Returns the secret-safe native display message.
- #[must_use]
- pub const fn message(self) -> &'static str {
- self.message
- }
-
- /// Returns the only structured detail keys permitted for this category.
- #[must_use]
- pub const fn safe_detail_keys(self) -> &'static [&'static str] {
- self.safe_detail_keys
- }
-}
-
-/// Native SDK failure retaining an optional private source chain.
-pub struct Error {
- kind: ErrorKind,
- source: Option<Box<dyn error::Error + Send + Sync>>,
-}
-
-impl Error {
- pub(crate) fn missing_storage() -> Self {
- Self::without_source(ErrorKind::MissingStorage)
- }
-
- pub(crate) fn signer_without_sink() -> Self {
- Self::without_source(ErrorKind::SignerWithoutSink)
- }
-
- pub(crate) fn close_in_progress() -> Self {
- Self::without_source(ErrorKind::CloseInProgress)
- }
-
- pub(crate) fn client_closing() -> Self {
- Self::without_source(ErrorKind::ClientClosing)
- }
-
- pub(crate) fn client_closed() -> Self {
- Self::without_source(ErrorKind::ClientClosed)
- }
-
- pub(crate) fn storage_close_failed(source: radroots_storage::Error) -> Self {
- Self {
- kind: ErrorKind::StorageCloseFailed,
- source: Some(Box::new(source)),
- }
- }
-
- #[cfg(feature = "sqlite")]
- pub(crate) fn storage_open_failed(source: radroots_storage_sqlite::Error) -> Self {
- Self {
- kind: ErrorKind::StorageOpenFailed,
- source: Some(Box::new(source)),
- }
- }
-
- pub(crate) fn storage_inspection_failed(source: radroots_storage::Error) -> Self {
- Self {
- kind: ErrorKind::StorageInspectionFailed,
- source: Some(Box::new(source)),
- }
- }
-
- #[cfg(any(feature = "sync", feature = "nostr"))]
- pub(crate) fn invalid_host_configuration(
- source: impl error::Error + Send + Sync + 'static,
- ) -> Self {
- Self {
- kind: ErrorKind::InvalidHostConfiguration,
- source: Some(Box::new(source)),
- }
- }
-
- #[cfg(feature = "nostr")]
- pub(crate) fn invalid_host_configuration_without_source() -> Self {
- Self::without_source(ErrorKind::InvalidHostConfiguration)
- }
-
- #[cfg(any(feature = "sync", feature = "nostr"))]
- pub(crate) fn shared_operation_unavailable() -> Self {
- Self::without_source(ErrorKind::SharedOperationUnavailable)
- }
-
- #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
- pub(crate) fn shared_operation_failed(
- source: impl error::Error + Send + Sync + 'static,
- ) -> Self {
- Self {
- kind: ErrorKind::SharedOperationFailed,
- source: Some(Box::new(source)),
- }
- }
-
- #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
- pub(crate) fn shared_operation_failed_without_source() -> Self {
- Self::without_source(ErrorKind::SharedOperationFailed)
- }
-
- fn without_source(kind: ErrorKind) -> Self {
- Self { kind, source: None }
- }
-
- /// Returns the stable native category.
- #[must_use]
- pub const fn kind(&self) -> ErrorKind {
- self.kind
- }
-
- /// Returns metadata from the single SDK catalog.
- #[must_use]
- pub const fn descriptor(&self) -> ErrorDescriptor {
- self.kind.descriptor()
- }
-
- /// Converts to the V1 secret-safe protocol boundary.
- ///
- /// Native source messages are deliberately excluded. Catalog validation
- /// tests guarantee that static messages and capability IDs are valid; this
- /// conversion still fails closed to redacted text or no capability if a
- /// future catalog edit violates those invariants.
- #[must_use]
- pub fn to_report(&self) -> ErrorReport {
- let descriptor = self.descriptor();
- let capability = descriptor
- .capability()
- .and_then(|id| ProtocolCapabilityId::parse(id.as_str().to_owned()).ok());
- let message = SafeMessage::parse(descriptor.message().to_owned())
- .unwrap_or_else(|_| SafeMessage::redacted());
- ErrorReport::known(
- descriptor.code(),
- descriptor.operation(),
- capability,
- message,
- SafeDetails::default(),
- )
- }
-}
-
-impl fmt::Display for Error {
- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- formatter.write_str(self.descriptor().message())
- }
-}
-
-impl fmt::Debug for Error {
- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- formatter
- .debug_struct("Error")
- .field("kind", &self.kind)
- .field("code", &self.descriptor().code())
- .finish_non_exhaustive()
- }
-}
-
-impl error::Error for Error {
- fn source(&self) -> Option<&(dyn error::Error + 'static)> {
- self.source
- .as_deref()
- .map(|source| source as &(dyn error::Error + 'static))
- }
-}
-
-/// SDK result alias.
-pub type Result<T> = std::result::Result<T, Error>;
-
-#[cfg(test)]
-mod tests {
- use std::{collections::BTreeSet, error::Error as _};
-
- use super::*;
-
- #[test]
- fn catalog_is_exhaustive_unique_and_protocol_valid() {
- assert_eq!(CATALOG.len(), ErrorKind::ALL.len());
- let mut kinds = BTreeSet::new();
- let mut codes = BTreeSet::new();
- for (index, descriptor) in CATALOG.iter().copied().enumerate() {
- assert!(kinds.insert(descriptor.kind()));
- assert!(codes.insert(descriptor.code().as_str()));
- assert_eq!(descriptor.kind(), ErrorKind::ALL[index]);
- assert_eq!(descriptor, descriptor.kind().descriptor());
- assert!(!descriptor.recovery_actions().is_empty());
- assert!(SafeMessage::parse(descriptor.message()).is_ok());
- if let Some(capability) = descriptor.capability() {
- assert!(ProtocolCapabilityId::parse(capability.as_str()).is_ok());
- }
- assert!(descriptor.safe_detail_keys().is_empty());
- Error::without_source(descriptor.kind())
- .to_report()
- .validate()
- .expect("protocol report");
- }
- }
-
- #[test]
- fn native_source_is_preserved_but_protocol_report_is_redacted_from_it() {
- let error = Error::storage_close_failed(radroots_storage::Error::BackendUnavailable);
- assert!(error.source().is_some());
- assert_eq!(error.kind(), ErrorKind::StorageCloseFailed);
- assert_eq!(error.to_string(), "SDK storage close failed");
- let report = error.to_report();
- assert_eq!(report.code().as_str(), "storage_close_failed");
- assert_eq!(report.message().as_str(), "SDK storage close failed");
- assert!(!report.message().as_str().contains("backend"));
- assert!(format!("{error:?}").contains("StorageCloseFailed"));
- assert!(!format!("{error:?}").contains("BackendUnavailable"));
- }
-
- #[test]
- fn native_constructor_and_descriptor_surface_is_complete() {
- let source_free = [
- Error::missing_storage(),
- Error::signer_without_sink(),
- Error::close_in_progress(),
- Error::client_closing(),
- Error::client_closed(),
- ];
- for error in source_free {
- assert!(error.source().is_none());
- let descriptor = error.descriptor();
- assert_eq!(descriptor.kind(), error.kind());
- assert_eq!(descriptor.class(), descriptor.code().descriptor().class);
- assert_eq!(
- descriptor.retryable(),
- descriptor.code().descriptor().retryable
- );
- assert_eq!(descriptor.operation(), None);
- assert_eq!(descriptor.message(), error.to_string());
- }
-
- let inspection =
- Error::storage_inspection_failed(radroots_storage::Error::BackendUnavailable);
- assert_eq!(inspection.kind(), ErrorKind::StorageInspectionFailed);
- assert!(inspection.source().is_some());
-
- #[cfg(any(feature = "sync", feature = "nostr"))]
- {
- let host = Error::invalid_host_configuration(std::io::Error::other("private"));
- assert_eq!(host.kind(), ErrorKind::InvalidHostConfiguration);
- assert!(host.source().is_some());
- assert!(!host.to_string().contains("private"));
- assert_eq!(
- Error::shared_operation_unavailable().kind(),
- ErrorKind::SharedOperationUnavailable
- );
- }
-
- #[cfg(feature = "nostr")]
- assert!(
- Error::invalid_host_configuration_without_source()
- .source()
- .is_none()
- );
-
- #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
- {
- let failed = Error::shared_operation_failed(std::io::Error::other("private"));
- assert_eq!(failed.kind(), ErrorKind::SharedOperationFailed);
- assert!(failed.source().is_some());
- assert!(
- Error::shared_operation_failed_without_source()
- .source()
- .is_none()
- );
- }
- }
-}
diff --git a/crates/sdk/src/farm.rs b/crates/sdk/src/farm.rs
@@ -1,549 +0,0 @@
-//! Side-effect-free farm planning and canonical durable enqueue operations.
-
-use std::{error, fmt};
-
-use radroots_event::{
- GenericEventDraft,
- contract::AuthorRole,
- envelope::kind::KIND_FARM,
- farm::Farm,
- id::{AddressableCoordinate, ParseError},
-};
-use radroots_event_codec::{
- authoring::AuthoredEventPlan, encode::EventEncodeError, encode::farm::to_wire_parts,
-};
-use radroots_signing::Actor;
-
-const FARM_PROFILE_CONTRACT_ID: &str = "radroots.farm.profile.v1";
-
-/// Pure inputs for one frozen farm profile plan.
-#[derive(Clone, Debug)]
-pub struct PrepareRequest {
- actor: Actor,
- farm: Farm,
- created_at_unix: u64,
-}
-
-impl PrepareRequest {
- /// Creates explicit canonical planning inputs.
- #[must_use]
- pub const fn new(actor: Actor, farm: Farm, created_at_unix: u64) -> Self {
- Self {
- actor,
- farm,
- created_at_unix,
- }
- }
-}
-
-/// Frozen, replay-stable farm publication plan.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct Plan {
- actor: Actor,
- coordinate: AddressableCoordinate,
- authored_event: AuthoredEventPlan,
-}
-
-impl Plan {
- /// Returns the exact authorized actor carried into signing.
- #[must_use]
- pub const fn actor(&self) -> &Actor {
- &self.actor
- }
-
- /// Returns the canonical addressable farm coordinate.
- #[must_use]
- pub const fn coordinate(&self) -> &AddressableCoordinate {
- &self.coordinate
- }
-
- /// Returns the immutable canonical authored event plan.
- #[must_use]
- pub const fn authored_event(&self) -> &AuthoredEventPlan {
- &self.authored_event
- }
-}
-
-/// Farm plan validation stage.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-#[non_exhaustive]
-pub enum PrepareErrorKind {
- /// The actor does not claim the required farm author role.
- UnauthorizedActor,
- /// The canonical farm codec rejected the native farm model.
- Encode,
- /// The canonical addressable coordinate rejected the farm identity.
- Coordinate,
- /// The canonical event draft rejected the encoded parts.
- Draft,
-}
-
-/// One secret-safe farm planning failure retaining its lower source.
-pub struct PrepareError {
- kind: PrepareErrorKind,
- source: Option<Box<dyn error::Error + Send + Sync>>,
-}
-
-impl PrepareError {
- /// Returns the stable client-level planning stage.
- #[must_use]
- pub const fn kind(&self) -> PrepareErrorKind {
- self.kind
- }
-
- fn unauthorized_actor() -> Self {
- Self {
- kind: PrepareErrorKind::UnauthorizedActor,
- source: None,
- }
- }
-
- fn encode(source: EventEncodeError) -> Self {
- Self::with_source(PrepareErrorKind::Encode, source)
- }
-
- fn coordinate(source: ParseError) -> Self {
- Self::with_source(PrepareErrorKind::Coordinate, source)
- }
-
- fn draft(source: radroots_event::draft::DraftError) -> Self {
- Self::with_source(PrepareErrorKind::Draft, source)
- }
-
- fn with_source(
- kind: PrepareErrorKind,
- source: impl error::Error + Send + Sync + 'static,
- ) -> Self {
- Self {
- kind,
- source: Some(Box::new(source)),
- }
- }
-}
-
-impl fmt::Display for PrepareError {
- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- formatter.write_str(match self.kind {
- PrepareErrorKind::UnauthorizedActor => "farm actor is not authorized",
- PrepareErrorKind::Encode => "farm model is invalid",
- PrepareErrorKind::Coordinate => "farm coordinate is invalid",
- PrepareErrorKind::Draft => "farm event draft is invalid",
- })
- }
-}
-
-impl fmt::Debug for PrepareError {
- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- formatter
- .debug_struct("PrepareError")
- .field("kind", &self.kind)
- .finish_non_exhaustive()
- }
-}
-
-impl error::Error for PrepareError {
- fn source(&self) -> Option<&(dyn error::Error + 'static)> {
- self.source
- .as_deref()
- .map(|source| source as &(dyn error::Error + 'static))
- }
-}
-
-/// Validates and freezes one farm profile without storage, signing, or network work.
-///
-/// `Farm` contains only public profile locality. Exact coordinates and other
-/// private farm artifacts are deliberately not accepted here; hosts persist
-/// their typed references and metadata through
-/// `radroots_storage::private_artifact::PrivateArtifactStore`.
-pub fn prepare(request: PrepareRequest) -> Result<Plan, PrepareError> {
- if !request.actor.satisfies(AuthorRole::Farmer) {
- return Err(PrepareError::unauthorized_actor());
- }
- let parts = to_wire_parts(&request.farm).map_err(PrepareError::encode)?;
- let coordinate = AddressableCoordinate::parse(format!(
- "{KIND_FARM}:{}:{}",
- request.actor.public_key(),
- request.farm.d_tag
- ))
- .map_err(PrepareError::coordinate)?;
- let authored_event = AuthoredEventPlan::from_generic(
- GenericEventDraft::new(
- FARM_PROFILE_CONTRACT_ID,
- parts.kind,
- request.created_at_unix,
- parts.tags,
- parts.content,
- request.actor.public_key().to_hex(),
- )
- .map_err(PrepareError::draft)?,
- )
- .map_err(PrepareError::draft)?;
- Ok(Plan {
- actor: request.actor,
- coordinate,
- authored_event,
- })
-}
-
-#[cfg(feature = "sync")]
-use radroots_signing::request::CancellationPolicy;
-#[cfg(feature = "sync")]
-use radroots_storage::journal::IdempotencyKey;
-#[cfg(feature = "sync")]
-use radroots_sync::{
- policy::{Error as SyncError, SyncId},
- push::PushStatus,
-};
-
-/// Explicit commit inputs for one prepared farm publication.
-#[cfg(feature = "sync")]
-#[derive(Clone, Debug)]
-pub struct EnqueueRequest {
- operation_id: SyncId,
- idempotency_key: IdempotencyKey,
- plan: Plan,
- profile: crate::transport::Profile,
- delivery_deadline_unix_ms: u64,
- cancellation: CancellationPolicy,
-}
-
-#[cfg(feature = "sync")]
-impl EnqueueRequest {
- /// Creates an enqueue request whose transport selection has no fallback.
- #[must_use]
- pub const fn new(
- operation_id: SyncId,
- idempotency_key: IdempotencyKey,
- plan: Plan,
- profile: crate::transport::Profile,
- delivery_deadline_unix_ms: u64,
- cancellation: CancellationPolicy,
- ) -> Self {
- Self {
- operation_id,
- idempotency_key,
- plan,
- profile,
- delivery_deadline_unix_ms,
- cancellation,
- }
- }
-}
-
-/// Borrowed farm commit operations over the canonical sync engine.
-#[cfg(feature = "sync")]
-#[derive(Clone, Copy, Debug)]
-pub struct Operations<'a> {
- sync: crate::sync::Operations<'a>,
-}
-
-#[cfg(feature = "sync")]
-impl<'a> Operations<'a> {
- pub(crate) const fn new(sync: crate::sync::Operations<'a>) -> Self {
- Self { sync }
- }
-
- /// Durably prepares, signs, and locally admits a farm publication.
- ///
- /// Once preparation commits, cancellation cannot erase the authored
- /// intent. Replay with identical operation and idempotency inputs resumes
- /// from its durable signing or admission phase.
- pub async fn enqueue(&self, request: EnqueueRequest) -> Result<PushStatus, SyncError> {
- let targets = request
- .profile
- .targets()
- .cloned()
- .ok_or(SyncError::InvalidPushRequest)?;
- let satisfaction = request
- .profile
- .satisfaction()
- .cloned()
- .ok_or(SyncError::InvalidPushRequest)?;
- self.sync
- .submit_push(radroots_sync::PushRequest::new(
- request.operation_id,
- request.idempotency_key,
- request.plan.actor,
- request.plan.authored_event,
- targets,
- satisfaction,
- request.delivery_deadline_unix_ms,
- request.cancellation,
- )?)
- .await
- }
-}
-
-#[cfg(test)]
-mod tests {
- use radroots_event::farm::FarmPublicLocation;
- use radroots_signing::actor::ActorSource;
-
- use super::*;
-
- const PUBLIC_KEY: &str = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
-
- fn actor(role: AuthorRole) -> Actor {
- Actor::from_public_key_hex(PUBLIC_KEY, ActorSource::ExplicitPublicKey, [role])
- .expect("actor")
- }
-
- fn farm() -> Farm {
- Farm {
- d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(),
- name: "Moss Street Farm".to_owned(),
- about: Some("seasonal vegetables".to_owned()),
- website: None,
- picture: None,
- banner: None,
- location: Some(FarmPublicLocation {
- primary: "Santa Cruz, California".to_owned(),
- city: Some("Santa Cruz".to_owned()),
- region: Some("California".to_owned()),
- country: Some("US".to_owned()),
- geohash: "9q8yy".to_owned(),
- }),
- tags: Some(vec!["vegetables".to_owned()]),
- }
- }
-
- #[test]
- fn prepare_is_pure_deterministic_and_uses_canonical_public_types() {
- let request = PrepareRequest::new(actor(AuthorRole::Farmer), farm(), 1_800_000_000);
- let first = prepare(request.clone()).expect("first plan");
- let second = prepare(request).expect("second plan");
-
- assert_eq!(first, second);
- assert_eq!(
- first
- .authored_event()
- .body()
- .contract()
- .contract_id()
- .as_str(),
- FARM_PROFILE_CONTRACT_ID
- );
- assert_eq!(first.authored_event().body().kind(), KIND_FARM);
- assert_eq!(first.authored_event().created_at(), 1_800_000_000);
- assert_eq!(
- first.authored_event().author(),
- &actor(AuthorRole::Farmer).public_key()
- );
- assert_eq!(
- first.coordinate().as_str(),
- format!("{KIND_FARM}:{PUBLIC_KEY}:AAAAAAAAAAAAAAAAAAAAAA")
- );
- assert!(
- first
- .authored_event()
- .body()
- .content()
- .contains("Moss Street Farm")
- );
- assert!(!first.authored_event().body().content().contains("latitude"));
- assert!(
- !first
- .authored_event()
- .body()
- .content()
- .contains("longitude")
- );
- }
-
- #[test]
- fn prepare_maps_authorization_and_lower_validation_once() {
- let unauthorized = prepare(PrepareRequest::new(
- actor(AuthorRole::Buyer),
- farm(),
- 1_800_000_000,
- ))
- .expect_err("unauthorized");
- assert_eq!(unauthorized.kind(), PrepareErrorKind::UnauthorizedActor);
- assert!(std::error::Error::source(&unauthorized).is_none());
-
- let mut invalid = farm();
- invalid.name.clear();
- let encoded = prepare(PrepareRequest::new(
- actor(AuthorRole::Farmer),
- invalid,
- 1_800_000_000,
- ))
- .expect_err("invalid model");
- assert_eq!(encoded.kind(), PrepareErrorKind::Encode);
- assert!(std::error::Error::source(&encoded).is_some());
- assert_eq!(encoded.to_string(), "farm model is invalid");
- assert!(!format!("{encoded:?}").contains("name"));
- }
-
- #[cfg(all(feature = "sync", feature = "memory", feature = "local-signing"))]
- mod enqueue {
- use std::sync::{
- Arc,
- atomic::{AtomicU8, Ordering},
- };
-
- use radroots_storage::{
- Outbox, event::SourceGeneration, journal::IdempotencyKey, memory::MemoryStorage,
- };
- use radroots_sync::{
- Engine,
- policy::{Clock, DeadlinePolicy, Error, IdSource, OperationKind, SyncId, SyncStorage},
- };
- use radroots_transport::{
- DeliveryReceipt, DeliveryRequest, Error as TransportError, EventSink, SinkFailure,
- SinkStatus, Target, TargetSet, TransportId,
- capability::{Availability, Maturity, SinkCapabilities},
- outcome::Retryability,
- policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy},
- };
-
- use crate::{ClientBuilder, transport::Profile};
-
- use super::*;
-
- struct HostClock;
- struct SequenceIds(AtomicU8);
- struct NoopSink;
-
- impl Clock for HostClock {
- fn now_unix_ms(&self) -> Result<u64, Error> {
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .ok()
- .and_then(|duration| u64::try_from(duration.as_millis()).ok())
- .filter(|value| *value != 0)
- .ok_or(Error::ClockUnavailable)
- }
- }
-
- impl IdSource for SequenceIds {
- fn next_id(&self, _operation: OperationKind) -> Result<SyncId, Error> {
- SyncId::new([self.0.fetch_add(1, Ordering::Relaxed); 16])
- }
- }
-
- impl EventSink for NoopSink {
- fn status(
- &self,
- ) -> radroots_transport::BoxFuture<'_, Result<SinkStatus, TransportError>> {
- Box::pin(async {
- Ok(SinkStatus::new(
- TransportId::NOSTR,
- true,
- Maturity::Stable,
- Availability::Available,
- SinkCapabilities::DELIVER,
- "ready",
- ))
- })
- }
-
- fn deliver(
- &self,
- request: DeliveryRequest,
- ) -> radroots_transport::BoxFuture<'_, Result<DeliveryReceipt, SinkFailure>>
- {
- Box::pin(async move {
- Err(SinkFailure::for_request(
- &request,
- "test_sink_unavailable",
- Retryability::Terminal,
- None,
- None,
- Vec::new(),
- )
- .expect("test sink failure"))
- })
- }
- }
-
- #[tokio::test]
- async fn enqueue_preserves_commit_cancellation_idempotency_and_delivery_policy() {
- let storage = Arc::new(MemoryStorage::new(
- SourceGeneration::new([4; 32]).expect("generation"),
- ));
- let signer =
- Arc::new(radroots_nostr::signing::LocalSigner::generate().expect("local signer"));
- let farm_actor = Actor::new(
- signer.public_key(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .expect("actor");
- let plan =
- prepare(PrepareRequest::new(farm_actor, farm(), 1_800_000_000)).expect("plan");
- let targets = TargetSet::new(vec![
- Target::nostr_relay("wss://farm.example").expect("target"),
- ])
- .expect("targets");
- let satisfaction =
- SatisfactionPolicy::new(SatisfactionClass::Delivered, TargetPolicy::all());
- let profile = Profile::delivery(targets.clone(), satisfaction.clone())
- .expect("transport profile");
- let capability: Arc<dyn SyncStorage> = storage.clone();
- let engine = Engine::builder(
- capability,
- Arc::new(HostClock),
- Arc::new(SequenceIds(AtomicU8::new(1))),
- DeadlinePolicy::new(30_000, 30_000, 30_000).expect("deadlines"),
- )
- .sink(Arc::new(NoopSink))
- .signer(signer)
- .build()
- .expect("engine");
- let client = ClientBuilder::new()
- .storage(storage.clone())
- .sync_engine(engine)
- .build()
- .expect("client");
- let operations = client.farm().expect("open").expect("farm operations");
- let request = EnqueueRequest::new(
- SyncId::new([7; 16]).expect("operation id"),
- IdempotencyKey::parse("farm-publish-a").expect("idempotency key"),
- plan,
- profile,
- 2_000_000_001_000,
- CancellationPolicy::PreservePublishedRequest,
- );
-
- drop(operations.enqueue(request.clone()));
- assert_eq!(
- Outbox::status(storage.as_ref())
- .await
- .expect("outbox status")
- .pending,
- 0
- );
-
- let committed = operations
- .enqueue(request.clone())
- .await
- .expect("committed enqueue");
- assert_eq!(committed.delivery_plan().intent().target_set(), &targets);
- assert_eq!(
- committed.delivery_plan().intent().satisfaction(),
- &satisfaction
- );
- let replay = operations.enqueue(request).await.expect("replay");
- assert_eq!(replay, committed);
-
- let unavailable = EnqueueRequest::new(
- SyncId::new([8; 16]).expect("operation id"),
- IdempotencyKey::parse("farm-publish-preview").expect("idempotency key"),
- prepare(PrepareRequest::new(
- actor(AuthorRole::Farmer),
- farm(),
- 1_800_000_000,
- ))
- .expect("plan"),
- Profile::unavailable_preview(TransportId::RETICULUM),
- 2_000_000_001_000,
- CancellationPolicy::PreservePublishedRequest,
- );
- assert_eq!(
- operations.enqueue(unavailable).await,
- Err(Error::InvalidPushRequest)
- );
- }
- }
-}
diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs
@@ -1,44 +0,0 @@
-//! Host-neutral asynchronous client engine for Radroots.
-//!
-//! Advanced hosts compose capabilities through [`ClientBuilder`] and operate
-//! through a cloneable [`Client`]. All fallible root operations use [`Result`]
-//! and the SDK-owned [`Error`] boundary.
-//!
-//! Constructing an empty builder performs no I/O and makes missing composition
-//! explicit:
-//!
-//! ```
-//! use radroots_sdk::ClientBuilder;
-//!
-//! let result = ClientBuilder::new().build();
-//! assert!(result.is_err());
-//! ```
-//!
-//! Native structs are constructor-led and representation-private. Hosts must
-//! not depend on field layout:
-//!
-//! ```compile_fail
-//! use radroots_sdk::ClientBuilder;
-//!
-//! let _ = ClientBuilder { storage: None };
-//! ```
-
-#![forbid(unsafe_code)]
-#![doc = include_str!("../README.md")]
-
-mod adapters;
-
-pub mod capability;
-pub mod client;
-pub mod diagnostics;
-pub mod error;
-pub mod farm;
-pub mod listing;
-pub mod signing;
-pub mod storage;
-pub mod sync;
-pub mod trade;
-pub mod transport;
-
-pub use crate::client::{Client, ClientBuilder};
-pub use crate::error::{Error, Result};
diff --git a/crates/sdk/src/listing.rs b/crates/sdk/src/listing.rs
@@ -1,641 +0,0 @@
-//! Side-effect-free listing planning and canonical durable enqueue operations.
-
-use std::{error, fmt};
-
-use radroots_event::{contract::AuthorRole, id::ClassifiedListingAddress};
-use radroots_event_codec::authoring::AuthoredEventPlan;
-use radroots_signing::Actor;
-
-mod operational_listing;
-
-pub use operational_listing::{
- BinPricingTryExt, RadrootsClassifiedListingAddressParts,
- RadrootsOperationalListingCanonicalEdit, RadrootsOperationalListingEditDocumentV1,
- RadrootsOperationalListingEditError, RadrootsOperationalListingLifecycleState,
- RadrootsOperationalListingMutation, RadrootsOperationalListingMutationError,
- RadrootsOperationalListingSubtotal, RadrootsOperationalListingTotal,
- RadrootsOperationalListingTradeProjection, RadrootsPublicClassifiedListingAddress,
- parse_classified_listing_address, parse_operational_listing_event,
- parse_public_classified_listing_address, validate_operational_listing_event,
- validate_operational_listing_model,
-};
-use operational_listing::{
- build_operational_listing_mutation_plan, canonicalize_operational_listing_edit,
-};
-
-/// Supported public listing mutation intent.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-#[non_exhaustive]
-pub enum Action {
- /// Publish the first public version of a listing.
- Publish,
- /// Replace an existing addressable listing with a new public version.
- Update,
-}
-
-/// Pure inputs for one frozen public listing plan.
-#[derive(Clone, Debug)]
-pub struct PrepareRequest {
- actor: Actor,
- document: RadrootsOperationalListingEditDocumentV1,
- action: Action,
- created_at_unix: u64,
-}
-
-impl PrepareRequest {
- /// Creates a public listing publication request.
- #[must_use]
- pub const fn publish(
- actor: Actor,
- document: RadrootsOperationalListingEditDocumentV1,
- created_at_unix: u64,
- ) -> Self {
- Self {
- actor,
- document,
- action: Action::Publish,
- created_at_unix,
- }
- }
-
- /// Creates a public listing replacement request.
- #[must_use]
- pub const fn update(
- actor: Actor,
- document: RadrootsOperationalListingEditDocumentV1,
- created_at_unix: u64,
- ) -> Self {
- Self {
- actor,
- document,
- action: Action::Update,
- created_at_unix,
- }
- }
-}
-
-/// Frozen, replay-stable public listing mutation plan.
-#[derive(Clone, Debug)]
-pub struct Plan {
- actor: Actor,
- action: Action,
- address: ClassifiedListingAddress,
- lifecycle: RadrootsOperationalListingLifecycleState,
- authored_event: AuthoredEventPlan,
-}
-
-impl Plan {
- /// Returns the exact authorized actor carried into signing.
- #[must_use]
- pub const fn actor(&self) -> &Actor {
- &self.actor
- }
-
- /// Returns the requested listing mutation intent.
- #[must_use]
- pub const fn action(&self) -> Action {
- self.action
- }
-
- /// Returns the canonical addressable listing identity.
- #[must_use]
- pub const fn address(&self) -> &ClassifiedListingAddress {
- &self.address
- }
-
- /// Returns the lower-owned lifecycle state resulting from the mutation.
- #[must_use]
- pub const fn lifecycle(&self) -> RadrootsOperationalListingLifecycleState {
- self.lifecycle
- }
-
- /// Returns the immutable canonical authored event plan.
- #[must_use]
- pub const fn authored_event(&self) -> &AuthoredEventPlan {
- &self.authored_event
- }
-}
-
-/// Listing plan validation stage.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-#[non_exhaustive]
-pub enum PrepareErrorKind {
- /// The actor does not claim the seller author role.
- UnauthorizedActor,
- /// The lower trade boundary rejected the untrusted edit document.
- Edit,
- /// The lower trade/event boundary rejected mutation preparation.
- Mutation,
-}
-
-/// One secret-safe listing planning failure retaining its lower source.
-pub struct PrepareError {
- kind: PrepareErrorKind,
- source: Option<Box<dyn error::Error + Send + Sync>>,
-}
-
-impl PrepareError {
- /// Returns the stable client-level planning stage.
- #[must_use]
- pub const fn kind(&self) -> PrepareErrorKind {
- self.kind
- }
-
- fn unauthorized_actor() -> Self {
- Self {
- kind: PrepareErrorKind::UnauthorizedActor,
- source: None,
- }
- }
-
- fn edit(source: RadrootsOperationalListingEditError) -> Self {
- Self::with_source(PrepareErrorKind::Edit, source)
- }
-
- fn mutation(source: RadrootsOperationalListingMutationError) -> Self {
- Self::with_source(PrepareErrorKind::Mutation, source)
- }
-
- fn with_source(
- kind: PrepareErrorKind,
- source: impl error::Error + Send + Sync + 'static,
- ) -> Self {
- Self {
- kind,
- source: Some(Box::new(source)),
- }
- }
-}
-
-impl fmt::Display for PrepareError {
- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- formatter.write_str(match self.kind {
- PrepareErrorKind::UnauthorizedActor => "listing actor is not authorized",
- PrepareErrorKind::Edit => "listing edit is invalid",
- PrepareErrorKind::Mutation => "listing mutation is invalid",
- })
- }
-}
-
-impl fmt::Debug for PrepareError {
- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- formatter
- .debug_struct("PrepareError")
- .field("kind", &self.kind)
- .finish_non_exhaustive()
- }
-}
-
-impl error::Error for PrepareError {
- fn source(&self) -> Option<&(dyn error::Error + 'static)> {
- self.source
- .as_deref()
- .map(|source| source as &(dyn error::Error + 'static))
- }
-}
-
-/// Validates and freezes one listing mutation without storage, signing, or network work.
-///
-/// The canonical public model permits only coarse public locality. Exact
-/// coordinates and other private artifacts are deliberately absent; hosts
-/// persist those through `radroots_storage::private_artifact::PrivateArtifactStore`.
-pub fn prepare(request: PrepareRequest) -> Result<Plan, PrepareError> {
- if !request.actor.satisfies(AuthorRole::Seller) {
- return Err(PrepareError::unauthorized_actor());
- }
- let canonical =
- canonicalize_operational_listing_edit(request.actor.public_key(), request.document)
- .map_err(PrepareError::edit)?;
- let address = canonical.public_listing_addr().clone();
- let mutation = match request.action {
- Action::Publish => RadrootsOperationalListingMutation::publish(canonical),
- Action::Update => RadrootsOperationalListingMutation::update(canonical),
- };
- let lifecycle = mutation.lifecycle_state().map_err(PrepareError::mutation)?;
- let authored_event =
- build_operational_listing_mutation_plan(&mutation, request.created_at_unix)
- .map_err(PrepareError::mutation)?;
- Ok(Plan {
- actor: request.actor,
- action: request.action,
- address,
- lifecycle,
- authored_event,
- })
-}
-
-#[cfg(feature = "sync")]
-use radroots_signing::request::CancellationPolicy;
-#[cfg(feature = "sync")]
-use radroots_storage::journal::IdempotencyKey;
-#[cfg(feature = "sync")]
-use radroots_sync::{
- policy::{Error as SyncError, SyncId},
- push::PushStatus,
-};
-
-/// Explicit commit inputs for one prepared public listing mutation.
-#[cfg(feature = "sync")]
-#[derive(Clone, Debug)]
-pub struct EnqueueRequest {
- operation_id: SyncId,
- idempotency_key: IdempotencyKey,
- plan: Plan,
- profile: crate::transport::Profile,
- delivery_deadline_unix_ms: u64,
- cancellation: CancellationPolicy,
-}
-
-#[cfg(feature = "sync")]
-impl EnqueueRequest {
- /// Creates an enqueue request whose transport selection has no fallback.
- #[must_use]
- pub const fn new(
- operation_id: SyncId,
- idempotency_key: IdempotencyKey,
- plan: Plan,
- profile: crate::transport::Profile,
- delivery_deadline_unix_ms: u64,
- cancellation: CancellationPolicy,
- ) -> Self {
- Self {
- operation_id,
- idempotency_key,
- plan,
- profile,
- delivery_deadline_unix_ms,
- cancellation,
- }
- }
-}
-
-/// Borrowed listing commit operations over the canonical sync engine.
-#[cfg(feature = "sync")]
-#[derive(Clone, Copy, Debug)]
-pub struct Operations<'a> {
- sync: crate::sync::Operations<'a>,
-}
-
-#[cfg(feature = "sync")]
-impl<'a> Operations<'a> {
- pub(crate) const fn new(sync: crate::sync::Operations<'a>) -> Self {
- Self { sync }
- }
-
- /// Durably prepares, signs, and locally admits a public listing mutation.
- pub async fn enqueue(&self, request: EnqueueRequest) -> Result<PushStatus, SyncError> {
- let targets = request
- .profile
- .targets()
- .cloned()
- .ok_or(SyncError::InvalidPushRequest)?;
- let satisfaction = request
- .profile
- .satisfaction()
- .cloned()
- .ok_or(SyncError::InvalidPushRequest)?;
- self.sync
- .submit_push(radroots_sync::PushRequest::new(
- request.operation_id,
- request.idempotency_key,
- request.plan.actor,
- request.plan.authored_event,
- targets,
- satisfaction,
- request.delivery_deadline_unix_ms,
- request.cancellation,
- )?)
- .await
- }
-}
-
-#[cfg(test)]
-mod tests {
- use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit};
- use radroots_event::{
- envelope::kind::KIND_CLASSIFIED_LISTING,
- farm::FarmRef,
- id::{DTag, InventoryBinId},
- listing::operational::{
- OperationalListing, OperationalListingAvailability, OperationalListingBin,
- OperationalListingDeliveryMethod, OperationalListingProduct,
- OperationalListingPublicLocation, OperationalListingStatus,
- },
- };
- use radroots_signing::actor::ActorSource;
-
- use super::*;
-
- const PUBLIC_KEY: &str = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
-
- fn actor(role: AuthorRole) -> Actor {
- Actor::from_public_key_hex(PUBLIC_KEY, ActorSource::ExplicitPublicKey, [role])
- .expect("actor")
- }
-
- fn listing(seller: &str) -> OperationalListing {
- OperationalListing {
- d_tag: DTag::parse("AAAAAAAAAAAAAAAAAAAAAg").expect("d tag"),
- published_at: None,
- farm: FarmRef {
- pubkey: seller.to_owned(),
- d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(),
- },
- product: OperationalListingProduct {
- key: "coffee".to_owned(),
- title: "Coffee".to_owned(),
- category: "coffee".to_owned(),
- summary: Some("Single origin coffee".to_owned()),
- process: None,
- lot: None,
- location: None,
- profile: None,
- year: None,
- },
- primary_bin_id: InventoryBinId::parse("bin-1").expect("bin id"),
- bins: vec![OperationalListingBin {
- bin_id: InventoryBinId::parse("bin-1").expect("bin id"),
- quantity: Quantity::try_new(Decimal::from(1000_u32), Unit::MassG)
- .expect("quantity"),
- price_per_canonical_unit: QuantityPrice::try_new(
- Money::try_new(Decimal::from(20_u32), Currency::USD).expect("money"),
- Quantity::try_new(Decimal::from(1_u32), Unit::MassG).expect("unit"),
- )
- .expect("price"),
- display_amount: None,
- display_unit: None,
- display_label: None,
- display_price: None,
- display_price_unit: None,
- }],
- resource_area: None,
- plot: None,
- discounts: None,
- inventory_available: Some(Decimal::from(5_u32)),
- availability: Some(OperationalListingAvailability::Status {
- status: OperationalListingStatus::Active,
- }),
- delivery_method: Some(OperationalListingDeliveryMethod::Pickup),
- location: Some(OperationalListingPublicLocation {
- primary: "Santa Cruz, California".to_owned(),
- city: Some("Santa Cruz".to_owned()),
- region: Some("California".to_owned()),
- country: Some("US".to_owned()),
- geohash: "9q8yy".to_owned(),
- }),
- images: None,
- }
- }
-
- fn document(seller: &str) -> RadrootsOperationalListingEditDocumentV1 {
- RadrootsOperationalListingEditDocumentV1::new(listing(seller))
- }
-
- #[test]
- fn prepare_publish_and_update_are_pure_canonical_and_privacy_bounded() {
- let publish_request = PrepareRequest::publish(
- actor(AuthorRole::Seller),
- document(PUBLIC_KEY),
- 1_800_000_000,
- );
- let first = prepare(publish_request.clone()).expect("publish plan");
- let replay = prepare(publish_request).expect("replayed plan");
- let update = prepare(PrepareRequest::update(
- actor(AuthorRole::Seller),
- document(PUBLIC_KEY),
- 1_800_000_001,
- ))
- .expect("update plan");
-
- assert_eq!(first.action(), Action::Publish);
- assert_eq!(update.action(), Action::Update);
- assert_eq!(
- first.lifecycle(),
- RadrootsOperationalListingLifecycleState::Published
- );
- assert_eq!(
- update.lifecycle(),
- RadrootsOperationalListingLifecycleState::Published
- );
- assert_eq!(first.address(), replay.address());
- assert_eq!(first.authored_event(), replay.authored_event());
- assert_eq!(first.address(), update.address());
- assert_eq!(
- first.authored_event().body().kind(),
- KIND_CLASSIFIED_LISTING
- );
- assert_eq!(first.authored_event().created_at(), 1_800_000_000);
- assert!(!first.authored_event().body().content().contains("latitude"));
- assert!(
- !first
- .authored_event()
- .body()
- .content()
- .contains("longitude")
- );
- }
-
- #[test]
- fn prepare_maps_authorization_and_lower_validation_classes_once() {
- let unauthorized = prepare(PrepareRequest::publish(
- actor(AuthorRole::Buyer),
- document(PUBLIC_KEY),
- 1_800_000_000,
- ))
- .expect_err("unauthorized");
- assert_eq!(unauthorized.kind(), PrepareErrorKind::UnauthorizedActor);
- assert!(std::error::Error::source(&unauthorized).is_none());
-
- let mut invalid = listing(PUBLIC_KEY);
- invalid.product.title.clear();
- let edit = prepare(PrepareRequest::publish(
- actor(AuthorRole::Seller),
- RadrootsOperationalListingEditDocumentV1::new(invalid),
- 1_800_000_000,
- ))
- .expect_err("invalid listing");
- assert_eq!(edit.kind(), PrepareErrorKind::Edit);
- assert!(std::error::Error::source(&edit).is_some());
- assert_eq!(edit.to_string(), "listing edit is invalid");
- assert!(!format!("{edit:?}").contains("title"));
-
- let mismatch = prepare(PrepareRequest::update(
- actor(AuthorRole::Seller),
- document("8f"),
- 1_800_000_000,
- ))
- .expect_err("invalid seller identity");
- assert_eq!(mismatch.kind(), PrepareErrorKind::Edit);
- }
-
- #[cfg(all(feature = "sync", feature = "memory", feature = "local-signing"))]
- mod enqueue {
- use std::sync::{
- Arc,
- atomic::{AtomicU8, Ordering},
- };
-
- use radroots_storage::{
- Outbox, event::SourceGeneration, journal::IdempotencyKey, memory::MemoryStorage,
- };
- use radroots_sync::{
- Engine,
- policy::{Clock, DeadlinePolicy, Error, IdSource, OperationKind, SyncId, SyncStorage},
- };
- use radroots_transport::{
- DeliveryReceipt, DeliveryRequest, Error as TransportError, EventSink, SinkFailure,
- SinkStatus, Target, TargetSet, TransportId,
- capability::{Availability, Maturity, SinkCapabilities},
- outcome::Retryability,
- policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy},
- };
-
- use super::*;
- use crate::{ClientBuilder, transport::Profile};
-
- struct HostClock;
- struct SequenceIds(AtomicU8);
- struct NoopSink;
-
- impl Clock for HostClock {
- fn now_unix_ms(&self) -> Result<u64, Error> {
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .ok()
- .and_then(|duration| u64::try_from(duration.as_millis()).ok())
- .filter(|value| *value != 0)
- .ok_or(Error::ClockUnavailable)
- }
- }
- impl IdSource for SequenceIds {
- fn next_id(&self, _operation: OperationKind) -> Result<SyncId, Error> {
- SyncId::new([self.0.fetch_add(1, Ordering::Relaxed); 16])
- }
- }
- impl EventSink for NoopSink {
- fn status(
- &self,
- ) -> radroots_transport::BoxFuture<'_, Result<SinkStatus, TransportError>> {
- Box::pin(async {
- Ok(SinkStatus::new(
- TransportId::NOSTR,
- true,
- Maturity::Stable,
- Availability::Available,
- SinkCapabilities::DELIVER,
- "ready",
- ))
- })
- }
- fn deliver(
- &self,
- request: DeliveryRequest,
- ) -> radroots_transport::BoxFuture<'_, Result<DeliveryReceipt, SinkFailure>>
- {
- Box::pin(async move {
- Err(SinkFailure::for_request(
- &request,
- "test_sink_unavailable",
- Retryability::Terminal,
- None,
- None,
- Vec::new(),
- )
- .expect("test sink failure"))
- })
- }
- }
-
- #[tokio::test]
- async fn enqueue_preserves_commit_cancellation_idempotency_and_transport_outcomes() {
- let storage = Arc::new(MemoryStorage::new(
- SourceGeneration::new([5; 32]).expect("generation"),
- ));
- let signer =
- Arc::new(radroots_nostr::signing::LocalSigner::generate().expect("local signer"));
- let seller = Actor::new(
- signer.public_key(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Seller],
- )
- .expect("actor");
- let plan = prepare(PrepareRequest::publish(
- seller,
- document(&signer.public_key().to_hex()),
- 1_800_000_000,
- ))
- .expect("plan");
- let targets = TargetSet::new(vec![
- Target::nostr_relay("wss://listing.example").expect("target"),
- ])
- .expect("targets");
- let satisfaction =
- SatisfactionPolicy::new(SatisfactionClass::Delivered, TargetPolicy::all());
- let profile = Profile::delivery(targets.clone(), satisfaction.clone())
- .expect("transport profile");
- let capability: Arc<dyn SyncStorage> = storage.clone();
- let engine = Engine::builder(
- capability,
- Arc::new(HostClock),
- Arc::new(SequenceIds(AtomicU8::new(1))),
- DeadlinePolicy::new(30_000, 30_000, 30_000).expect("deadlines"),
- )
- .sink(Arc::new(NoopSink))
- .signer(signer)
- .build()
- .expect("engine");
- let client = ClientBuilder::new()
- .storage(storage.clone())
- .sync_engine(engine)
- .build()
- .expect("client");
- let operations = client.listing().expect("open").expect("listing operations");
- let request = EnqueueRequest::new(
- SyncId::new([9; 16]).expect("operation id"),
- IdempotencyKey::parse("listing-publish-a").expect("idempotency key"),
- plan,
- profile,
- 2_000_000_001_000,
- CancellationPolicy::PreservePublishedRequest,
- );
-
- drop(operations.enqueue(request.clone()));
- assert_eq!(
- Outbox::status(storage.as_ref())
- .await
- .expect("outbox status")
- .pending,
- 0
- );
- let committed = operations
- .enqueue(request.clone())
- .await
- .expect("committed enqueue");
- assert_eq!(committed.delivery_plan().intent().target_set(), &targets);
- assert_eq!(
- committed.delivery_plan().intent().satisfaction(),
- &satisfaction
- );
- let replay = operations.enqueue(request).await.expect("replay");
- assert_eq!(replay, committed);
-
- let unavailable = EnqueueRequest::new(
- SyncId::new([10; 16]).expect("operation id"),
- IdempotencyKey::parse("listing-update-preview").expect("idempotency key"),
- prepare(PrepareRequest::update(
- actor(AuthorRole::Seller),
- document(PUBLIC_KEY),
- 1_800_000_001,
- ))
- .expect("plan"),
- Profile::unavailable_preview(TransportId::RETICULUM),
- 2_000_000_001_000,
- CancellationPolicy::PreservePublishedRequest,
- );
- assert_eq!(
- operations.enqueue(unavailable).await,
- Err(Error::InvalidPushRequest)
- );
- }
- }
-}
diff --git a/crates/sdk/src/listing/operational_listing/draft.rs b/crates/sdk/src/listing/operational_listing/draft.rs
@@ -1,548 +0,0 @@
-//! Canonicalization for Radroots Listing v1 drafts.
-
-#![forbid(unsafe_code)]
-
-use core::fmt;
-
-use std::{format, vec::Vec};
-
-use radroots_event::{
- envelope::kind::KIND_CLASSIFIED_LISTING,
- id::{ClassifiedListingAddress, InventoryBinId, ParseError},
- listing::operational::OperationalListing,
- trade::validation::OperationalListingValidationError,
-};
-use radroots_identity::{Error as PublicKeyError, PublicKey};
-
-use super::validation::validate_operational_listing_model;
-
-#[derive(Clone, Debug)]
-pub struct RadrootsOperationalListingEditDocumentV1 {
- pub listing: OperationalListing,
-}
-
-impl RadrootsOperationalListingEditDocumentV1 {
- pub fn new(listing: OperationalListing) -> Self {
- Self { listing }
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct RadrootsOperationalListingCanonicalEdit {
- listing: OperationalListing,
- seller_pubkey: PublicKey,
- public_listing_addr: ClassifiedListingAddress,
-}
-
-impl RadrootsOperationalListingCanonicalEdit {
- pub fn new(
- mut listing: OperationalListing,
- seller_pubkey: PublicKey,
- ) -> Result<Self, RadrootsOperationalListingEditError> {
- let farm_pubkey = PublicKey::from_hex(listing.farm.pubkey.as_str())
- .map_err(RadrootsOperationalListingEditError::InvalidFarmPubkey)?;
- if farm_pubkey != seller_pubkey {
- return Err(RadrootsOperationalListingEditError::FarmPubkeyMismatch {
- expected_pubkey: seller_pubkey,
- actual_pubkey: farm_pubkey,
- });
- }
- listing.farm.pubkey = farm_pubkey.to_hex();
- validate_listing_bins(&listing)?;
- let listing = validate_operational_listing_model(listing, &seller_pubkey)
- .map_err(RadrootsOperationalListingEditError::InvalidModel)?
- .listing;
-
- let public_listing_addr = listing_addr(
- KIND_CLASSIFIED_LISTING,
- &seller_pubkey,
- listing.d_tag.as_str(),
- );
-
- Ok(Self {
- listing,
- seller_pubkey,
- public_listing_addr,
- })
- }
-
- pub fn listing(&self) -> &OperationalListing {
- &self.listing
- }
-
- pub fn seller_pubkey(&self) -> &PublicKey {
- &self.seller_pubkey
- }
-
- pub fn public_listing_addr(&self) -> &ClassifiedListingAddress {
- &self.public_listing_addr
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum RadrootsOperationalListingEditError {
- InvalidFarmPubkey(PublicKeyError),
- InvalidClassifiedListingAddress(ParseError),
- InvalidModel(OperationalListingValidationError),
- FarmPubkeyMismatch {
- expected_pubkey: PublicKey,
- actual_pubkey: PublicKey,
- },
- MissingPrimaryBin {
- primary_bin_id: InventoryBinId,
- },
- DuplicateBinId {
- bin_id: InventoryBinId,
- },
-}
-
-impl fmt::Display for RadrootsOperationalListingEditError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::InvalidFarmPubkey(error) => {
- write!(f, "invalid listing edit farm pubkey: {error}")
- }
- Self::InvalidClassifiedListingAddress(error) => {
- write!(f, "invalid listing edit address: {error}")
- }
- Self::InvalidModel(error) => {
- write!(f, "invalid listing edit model: {error}")
- }
- Self::FarmPubkeyMismatch { .. } => {
- f.write_str("listing edit farm pubkey does not match seller")
- }
- Self::MissingPrimaryBin { .. } => f.write_str("listing edit primary bin is missing"),
- Self::DuplicateBinId { .. } => f.write_str("listing edit contains duplicate bin ID"),
- }
- }
-}
-
-impl core::error::Error for RadrootsOperationalListingEditError {}
-
-fn validate_listing_bins(
- listing: &OperationalListing,
-) -> Result<(), RadrootsOperationalListingEditError> {
- let primary_bin_id = listing.primary_bin_id.clone();
- let mut seen_bin_ids = Vec::new();
- let mut primary_bin_found = false;
- for bin in &listing.bins {
- if seen_bin_ids
- .iter()
- .any(|seen_bin_id| seen_bin_id == &bin.bin_id)
- {
- return Err(RadrootsOperationalListingEditError::DuplicateBinId {
- bin_id: bin.bin_id.clone(),
- });
- }
- if bin.bin_id == primary_bin_id {
- primary_bin_found = true;
- }
- seen_bin_ids.push(bin.bin_id.clone());
- }
-
- if !primary_bin_found {
- return Err(RadrootsOperationalListingEditError::MissingPrimaryBin { primary_bin_id });
- }
- Ok(())
-}
-
-fn listing_addr(kind: u32, seller_pubkey: &PublicKey, d_tag: &str) -> ClassifiedListingAddress {
- ClassifiedListingAddress::parse(format!("{kind}:{seller_pubkey}:{d_tag}"))
- .expect("typed listing identity must form a listing address")
-}
-
-/// Canonicalizes an untrusted listing edit for an already-authorized seller.
-///
-/// Actor provenance and role authorization belong to the signing/workflow
-/// boundary. This deterministic function validates only the supplied public
-/// identity and listing data and performs no signing or authorization.
-pub fn canonicalize_operational_listing_edit(
- seller_pubkey: PublicKey,
- mut document: RadrootsOperationalListingEditDocumentV1,
-) -> Result<RadrootsOperationalListingCanonicalEdit, RadrootsOperationalListingEditError> {
- let farm_pubkey = document.listing.farm.pubkey.as_str();
- if farm_pubkey.is_empty() {
- document.listing.farm.pubkey = seller_pubkey.to_hex();
- }
-
- RadrootsOperationalListingCanonicalEdit::new(document.listing, seller_pubkey)
-}
-
-#[cfg(test)]
-mod tests {
- const FIXTURE_ALICE_PUBLIC_KEY_HEX: &str =
- "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
- const FIXTURE_BOB_PUBLIC_KEY_HEX: &str =
- "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af";
- use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit};
- use radroots_event::{
- envelope::kind::KIND_CLASSIFIED_LISTING,
- farm::FarmRef,
- id::{ClassifiedListingAddress, DTag, InventoryBinId},
- listing::operational::{
- OperationalListing, OperationalListingAvailability, OperationalListingBin,
- OperationalListingDeliveryMethod, OperationalListingProduct,
- OperationalListingPublicLocation, OperationalListingStatus,
- },
- trade::validation::OperationalListingValidationError,
- };
- use radroots_identity::PublicKey;
-
- use super::{
- RadrootsOperationalListingCanonicalEdit, RadrootsOperationalListingEditDocumentV1,
- RadrootsOperationalListingEditError, canonicalize_operational_listing_edit,
- };
-
- const SELLER: &str = FIXTURE_ALICE_PUBLIC_KEY_HEX;
- const OTHER: &str = FIXTURE_BOB_PUBLIC_KEY_HEX;
-
- fn d_tag(raw: &str) -> DTag {
- DTag::parse(raw).expect("d tag")
- }
-
- fn bin_id(raw: &str) -> InventoryBinId {
- InventoryBinId::parse(raw).expect("bin id")
- }
-
- fn listing() -> OperationalListing {
- OperationalListing {
- d_tag: d_tag("AAAAAAAAAAAAAAAAAAAAAg"),
- published_at: None,
- farm: FarmRef {
- pubkey: SELLER.to_string(),
- d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
- },
- product: OperationalListingProduct {
- key: "coffee".to_string(),
- title: "Coffee".to_string(),
- category: "coffee".to_string(),
- summary: Some("Single origin coffee".to_string()),
- process: None,
- lot: None,
- location: None,
- profile: None,
- year: None,
- },
- primary_bin_id: bin_id("bin-1"),
- bins: vec![OperationalListingBin {
- bin_id: bin_id("bin-1"),
- quantity: Quantity::try_new(Decimal::from(1000u32), Unit::MassG).unwrap(),
- price_per_canonical_unit: QuantityPrice::try_new(
- Money::try_new(Decimal::from(20u32), Currency::USD).unwrap(),
- Quantity::try_new(Decimal::from(1u32), Unit::MassG).unwrap(),
- )
- .unwrap(),
- display_amount: None,
- display_unit: None,
- display_label: None,
- display_price: None,
- display_price_unit: None,
- }],
- resource_area: None,
- plot: None,
- discounts: None,
- inventory_available: Some(Decimal::from(5u32)),
- availability: Some(OperationalListingAvailability::Status {
- status: OperationalListingStatus::Active,
- }),
- delivery_method: Some(OperationalListingDeliveryMethod::Pickup),
- location: Some(OperationalListingPublicLocation {
- primary: "Victoria".to_string(),
- city: Some("Victoria".to_string()),
- region: Some("British Columbia".to_string()),
- country: Some("CA".to_string()),
- geohash: "c287g".to_string(),
- }),
- images: None,
- }
- }
-
- fn seller_pubkey() -> PublicKey {
- PublicKey::from_hex(SELLER).expect("seller public key")
- }
-
- #[test]
- fn draft_document_wraps_listing() {
- let document = RadrootsOperationalListingEditDocumentV1::new(listing());
-
- assert_eq!(document.listing.d_tag.as_str(), "AAAAAAAAAAAAAAAAAAAAAg");
- assert_eq!(document.listing.product.title, "Coffee");
- }
-
- #[cfg(any())]
- #[test]
- fn draft_document_deserializes_as_untrusted_input() {
- let json = serde_json::to_string(&RadrootsOperationalListingEditDocumentV1::new(listing()))
- .expect("serialize document");
-
- let document: RadrootsOperationalListingEditDocumentV1 =
- serde_json::from_str(&json).expect("deserialize document");
- let canonical = canonicalize_operational_listing_edit(seller_pubkey(), document)
- .expect("canonical draft");
-
- assert_eq!(canonical.seller_pubkey().to_hex(), SELLER);
- assert_eq!(canonical.listing().product.title, "Coffee");
- }
-
- #[test]
- fn canonical_draft_carries_seller_listing_and_addresses() {
- let seller_pubkey = PublicKey::from_hex(SELLER).expect("seller");
- let listing = listing();
-
- let canonical = RadrootsOperationalListingCanonicalEdit::new(listing, seller_pubkey)
- .expect("canonical");
-
- assert_eq!(canonical.seller_pubkey(), &seller_pubkey);
- assert_eq!(
- canonical.public_listing_addr().as_str(),
- format!("{KIND_CLASSIFIED_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg")
- );
- assert_eq!(canonical.listing().d_tag.as_str(), "AAAAAAAAAAAAAAAAAAAAAg");
- }
-
- #[test]
- fn listing_edit_error_variants_are_precise() {
- assert!(matches!(
- RadrootsOperationalListingEditError::InvalidFarmPubkey(
- PublicKey::from_hex("bad").unwrap_err()
- ),
- RadrootsOperationalListingEditError::InvalidFarmPubkey(_)
- ));
- assert!(matches!(
- RadrootsOperationalListingEditError::InvalidClassifiedListingAddress(
- ClassifiedListingAddress::parse("bad").unwrap_err()
- ),
- RadrootsOperationalListingEditError::InvalidClassifiedListingAddress(_)
- ));
- }
-
- #[test]
- fn canonicalize_operational_listing_edit_fills_missing_farm_pubkey_and_derives_address() {
- let mut listing = listing();
- listing.farm.pubkey.clear();
- let document = RadrootsOperationalListingEditDocumentV1::new(listing);
-
- let canonical = canonicalize_operational_listing_edit(seller_pubkey(), document)
- .expect("canonical draft");
-
- assert_eq!(canonical.seller_pubkey().to_hex(), SELLER);
- assert_eq!(
- canonical.public_listing_addr().as_str(),
- format!("{KIND_CLASSIFIED_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg")
- );
- assert_eq!(canonical.listing().farm.pubkey, SELLER);
- }
-
- #[test]
- fn canonicalize_operational_listing_edit_rejects_mismatched_farm_pubkey() {
- let mut listing = listing();
- listing.farm.pubkey = OTHER.to_string();
- let document = RadrootsOperationalListingEditDocumentV1::new(listing);
-
- let error = canonicalize_operational_listing_edit(seller_pubkey(), document).unwrap_err();
-
- assert!(matches!(
- error,
- RadrootsOperationalListingEditError::FarmPubkeyMismatch { .. }
- ));
- }
-
- #[test]
- fn canonicalize_operational_listing_edit_rejects_invalid_farm_pubkey() {
- let mut listing = listing();
- listing.farm.pubkey = "bad".to_string();
- let document = RadrootsOperationalListingEditDocumentV1::new(listing);
-
- let error = canonicalize_operational_listing_edit(seller_pubkey(), document).unwrap_err();
-
- assert!(matches!(
- error,
- RadrootsOperationalListingEditError::InvalidFarmPubkey(_)
- ));
- }
-
- #[test]
- fn canonical_draft_new_rejects_mismatched_farm_pubkey() {
- let mut listing = listing();
- listing.farm.pubkey = OTHER.to_string();
-
- let error = RadrootsOperationalListingCanonicalEdit::new(
- listing,
- PublicKey::from_hex(SELLER).expect("seller"),
- )
- .unwrap_err();
-
- assert!(matches!(
- error,
- RadrootsOperationalListingEditError::FarmPubkeyMismatch { .. }
- ));
- }
-
- #[test]
- fn canonical_draft_new_rejects_invalid_farm_pubkey() {
- let mut listing = listing();
- listing.farm.pubkey = "bad".to_string();
-
- let error = RadrootsOperationalListingCanonicalEdit::new(
- listing,
- PublicKey::from_hex(SELLER).expect("seller"),
- )
- .unwrap_err();
-
- assert!(matches!(
- error,
- RadrootsOperationalListingEditError::InvalidFarmPubkey(_)
- ));
- }
-
- #[test]
- fn canonical_draft_new_rejects_empty_farm_pubkey() {
- let mut listing = listing();
- listing.farm.pubkey.clear();
-
- let error = RadrootsOperationalListingCanonicalEdit::new(
- listing,
- PublicKey::from_hex(SELLER).expect("seller"),
- )
- .unwrap_err();
-
- assert!(matches!(
- error,
- RadrootsOperationalListingEditError::InvalidFarmPubkey(_)
- ));
- }
-
- #[test]
- fn canonicalize_operational_listing_edit_rejects_missing_primary_bin() {
- let mut listing = listing();
- listing.primary_bin_id = bin_id("bin-2");
- let document = RadrootsOperationalListingEditDocumentV1::new(listing);
-
- let error = canonicalize_operational_listing_edit(seller_pubkey(), document).unwrap_err();
-
- assert_eq!(
- error,
- RadrootsOperationalListingEditError::MissingPrimaryBin {
- primary_bin_id: bin_id("bin-2")
- }
- );
- }
-
- #[test]
- fn canonical_draft_new_rejects_missing_primary_bin() {
- let mut listing = listing();
- listing.primary_bin_id = bin_id("bin-2");
-
- let error = RadrootsOperationalListingCanonicalEdit::new(
- listing,
- PublicKey::from_hex(SELLER).expect("seller"),
- )
- .unwrap_err();
-
- assert_eq!(
- error,
- RadrootsOperationalListingEditError::MissingPrimaryBin {
- primary_bin_id: bin_id("bin-2")
- }
- );
- }
-
- #[test]
- fn canonicalize_operational_listing_edit_rejects_duplicate_bin_ids() {
- let mut listing = listing();
- listing.bins.push(listing.bins[0].clone());
- let document = RadrootsOperationalListingEditDocumentV1::new(listing);
-
- let error = canonicalize_operational_listing_edit(seller_pubkey(), document).unwrap_err();
-
- assert_eq!(
- error,
- RadrootsOperationalListingEditError::DuplicateBinId {
- bin_id: bin_id("bin-1")
- }
- );
- }
-
- #[test]
- fn canonical_draft_new_rejects_invalid_model() {
- let mut listing = listing();
- listing.inventory_available = None;
-
- let error = RadrootsOperationalListingCanonicalEdit::new(
- listing,
- PublicKey::from_hex(SELLER).expect("seller"),
- )
- .unwrap_err();
-
- assert_eq!(
- error,
- RadrootsOperationalListingEditError::InvalidModel(
- OperationalListingValidationError::MissingInventory
- )
- );
- }
-
- #[test]
- fn canonical_draft_new_rejects_invalid_secondary_bin() {
- let mut invalid_quantity_listing = listing();
- let mut secondary_bin = invalid_quantity_listing.bins[0].clone();
- secondary_bin.bin_id = bin_id("bin-2");
- secondary_bin.quantity = Quantity::try_new(Decimal::ONE, Unit::MassKg).unwrap();
- invalid_quantity_listing.bins.push(secondary_bin);
-
- let error = RadrootsOperationalListingCanonicalEdit::new(
- invalid_quantity_listing,
- PublicKey::from_hex(SELLER).expect("seller"),
- )
- .unwrap_err();
-
- assert_eq!(
- error,
- RadrootsOperationalListingEditError::InvalidModel(
- OperationalListingValidationError::InvalidBin
- )
- );
-
- let mut mismatched_unit_listing = listing();
- let mut secondary_bin = mismatched_unit_listing.bins[0].clone();
- secondary_bin.bin_id = bin_id("bin-2");
- secondary_bin.price_per_canonical_unit = QuantityPrice::try_new(
- secondary_bin.price_per_canonical_unit.amount().clone(),
- Quantity::try_new(Decimal::ONE, Unit::Each).unwrap(),
- )
- .unwrap();
- mismatched_unit_listing.bins.push(secondary_bin);
-
- let error = RadrootsOperationalListingCanonicalEdit::new(
- mismatched_unit_listing,
- PublicKey::from_hex(SELLER).expect("seller"),
- )
- .unwrap_err();
-
- assert_eq!(
- error,
- RadrootsOperationalListingEditError::InvalidModel(
- OperationalListingValidationError::InvalidPrice
- )
- );
- }
-
- #[test]
- fn canonical_draft_new_rejects_duplicate_bin_ids() {
- let mut listing = listing();
- listing.bins.push(listing.bins[0].clone());
-
- let error = RadrootsOperationalListingCanonicalEdit::new(
- listing,
- PublicKey::from_hex(SELLER).expect("seller"),
- )
- .unwrap_err();
-
- assert_eq!(
- error,
- RadrootsOperationalListingEditError::DuplicateBinId {
- bin_id: bin_id("bin-1")
- }
- );
- }
-}
diff --git a/crates/sdk/src/listing/operational_listing/mod.rs b/crates/sdk/src/listing/operational_listing/mod.rs
@@ -1,253 +0,0 @@
-pub mod draft;
-pub mod model;
-pub mod mutation;
-pub mod price_ext;
-pub mod validation;
-
-use radroots_event::{
- envelope::EventEnvelope,
- id::{AddressableCoordinateParts, ClassifiedListingAddress, DTag, ParseError},
- listing::operational::{OperationalListing, OperationalListingParseError},
-};
-use radroots_event_codec::decode::operational_listing::operational_listing_from_nostr_event;
-use radroots_identity::PublicKey;
-
-pub use self::draft::{
- RadrootsOperationalListingCanonicalEdit, RadrootsOperationalListingEditDocumentV1,
- RadrootsOperationalListingEditError, canonicalize_operational_listing_edit,
-};
-pub use self::model::{RadrootsOperationalListingSubtotal, RadrootsOperationalListingTotal};
-pub use self::mutation::build_operational_listing_mutation_plan;
-pub use self::mutation::{
- RadrootsOperationalListingLifecycleState, RadrootsOperationalListingMutation,
- RadrootsOperationalListingMutationError,
-};
-pub use self::price_ext::BinPricingTryExt;
-pub use self::validation::{
- RadrootsOperationalListingTradeProjection, validate_operational_listing_event,
- validate_operational_listing_model,
-};
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsClassifiedListingAddressParts {
- pub address: ClassifiedListingAddress,
- pub kind: u32,
- pub seller_pubkey: PublicKey,
- pub listing_id: DTag,
-}
-
-impl RadrootsClassifiedListingAddressParts {
- pub fn parse(value: impl AsRef<str>) -> Result<Self, ParseError> {
- parse_classified_listing_address(value)
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsPublicClassifiedListingAddress {
- pub address: ClassifiedListingAddress,
- pub kind: u32,
- pub seller_pubkey: PublicKey,
- pub listing_id: DTag,
-}
-
-impl RadrootsPublicClassifiedListingAddress {
- pub fn parse(value: impl AsRef<str>) -> Result<Self, ParseError> {
- parse_public_classified_listing_address(value)
- }
-}
-
-pub fn parse_classified_listing_address(
- value: impl AsRef<str>,
-) -> Result<RadrootsClassifiedListingAddressParts, ParseError> {
- let value = value.as_ref();
- let address = ClassifiedListingAddress::parse(value)?;
- let parts = AddressableCoordinateParts::parse(address.as_str())
- .expect("typed listing address must contain valid coordinate parts");
- Ok(RadrootsClassifiedListingAddressParts {
- address,
- kind: parts.kind,
- seller_pubkey: parts.pubkey,
- listing_id: parts.d_tag,
- })
-}
-
-pub fn parse_public_classified_listing_address(
- value: impl AsRef<str>,
-) -> Result<RadrootsPublicClassifiedListingAddress, ParseError> {
- let parts = parse_classified_listing_address(value)?;
- Ok(RadrootsPublicClassifiedListingAddress {
- address: parts.address,
- kind: parts.kind,
- seller_pubkey: parts.seller_pubkey,
- listing_id: parts.listing_id,
- })
-}
-
-pub fn parse_operational_listing_event(
- event: &EventEnvelope,
-) -> Result<OperationalListing, OperationalListingParseError> {
- operational_listing_from_nostr_event(event)
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- RadrootsClassifiedListingAddressParts, RadrootsPublicClassifiedListingAddress,
- parse_classified_listing_address, parse_operational_listing_event,
- parse_public_classified_listing_address,
- };
- use radroots_event::{
- envelope::EventEnvelope,
- envelope::EventEnvelopeParts,
- envelope::kind::{KIND_CLASSIFIED_LISTING, KIND_PROFILE},
- id::ClassifiedListingAddress,
- listing::operational::OperationalListingParseError,
- };
-
- const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
-
- fn listing_event() -> EventEnvelope {
- EventEnvelope::new(EventEnvelopeParts {
- id: "9".repeat(64),
- author: SELLER.to_string(),
- created_at: 1,
- kind: KIND_CLASSIFIED_LISTING,
- tags: vec![
- vec!["d".into(), "AAAAAAAAAAAAAAAAAAAAAg".into()],
- vec!["p".into(), SELLER.into()],
- vec!["a".into(), format!("30340:{SELLER}:AAAAAAAAAAAAAAAAAAAAAA")],
- vec!["key".into(), "coffee".into()],
- vec!["title".into(), "Coffee".into()],
- vec!["category".into(), "coffee".into()],
- vec!["summary".into(), "Single origin".into()],
- vec!["radroots:primary_bin".into(), "bin-1".into()],
- vec![
- "radroots:bin".into(),
- "bin-1".into(),
- "1000".into(),
- "g".into(),
- ],
- vec![
- "radroots:price".into(),
- "bin-1".into(),
- "20".into(),
- "USD".into(),
- "1".into(),
- "g".into(),
- ],
- ],
- content: String::new(),
- sig: "f".repeat(128),
- })
- .expect("listing event")
- }
-
- #[test]
- fn parse_operational_listing_event_rejects_non_listing_kind() {
- let event = EventEnvelope::new(EventEnvelopeParts {
- id: "8".repeat(64),
- author: SELLER.to_string(),
- created_at: 1,
- kind: KIND_PROFILE,
- tags: vec![],
- content: String::new(),
- sig: "f".repeat(128),
- })
- .expect("profile event");
-
- assert!(matches!(
- parse_operational_listing_event(&event),
- Err(OperationalListingParseError::InvalidKind(KIND_PROFILE))
- ));
- }
-
- #[test]
- fn parse_operational_listing_event_accepts_listing_kind() {
- let listing = parse_operational_listing_event(&listing_event()).expect("listing");
-
- assert_eq!(listing.d_tag.as_str(), "AAAAAAAAAAAAAAAAAAAAAg");
- assert_eq!(listing.farm.pubkey, SELLER);
- assert_eq!(listing.primary_bin_id.as_str(), "bin-1");
- }
-
- #[test]
- fn listing_address_associated_parsers_delegate_to_public_parsers() {
- let raw = format!("{KIND_CLASSIFIED_LISTING}:{SELLER}:listing-1");
-
- let listing =
- RadrootsClassifiedListingAddressParts::parse(raw.clone()).expect("listing address");
- let public = RadrootsPublicClassifiedListingAddress::parse(&raw).expect("public address");
- let typed = parse_public_classified_listing_address(
- ClassifiedListingAddress::parse(&raw).expect("typed addr"),
- )
- .expect("typed public address");
-
- assert_eq!(listing.address.as_str(), raw);
- assert_eq!(public.address.as_str(), raw);
- assert_eq!(typed.address.as_str(), raw);
- assert_eq!(listing.seller_pubkey.to_hex(), SELLER);
- assert_eq!(public.seller_pubkey.to_hex(), SELLER);
- assert_eq!(typed.seller_pubkey.to_hex(), SELLER);
- }
-
- #[test]
- fn parse_public_classified_listing_address_accepts_public_listing_kind() {
- let raw = format!("{KIND_CLASSIFIED_LISTING}:{SELLER}:listing-1");
- let parsed = parse_public_classified_listing_address(&raw).expect("public listing address");
-
- assert_eq!(parsed.address.as_str(), raw);
- assert_eq!(parsed.kind, KIND_CLASSIFIED_LISTING);
- assert_eq!(parsed.seller_pubkey.to_hex(), SELLER);
- assert_eq!(parsed.listing_id.as_str(), "listing-1");
- }
-
- #[test]
- fn parse_classified_listing_address_rejects_retired_listing_kind() {
- let raw = format!("30403:{SELLER}:listing-1");
-
- assert!(matches!(
- parse_classified_listing_address(&raw),
- Err(radroots_event::id::ParseError::UnexpectedKind {
- expected: KIND_CLASSIFIED_LISTING,
- actual: 30403,
- })
- ));
- assert!(matches!(
- parse_public_classified_listing_address(&raw),
- Err(radroots_event::id::ParseError::UnexpectedKind {
- expected: KIND_CLASSIFIED_LISTING,
- actual: 30403,
- })
- ));
- }
-
- #[test]
- fn parse_public_classified_listing_address_maps_invalid_listing_addresses() {
- assert!(matches!(
- parse_public_classified_listing_address("not-an-address"),
- Err(radroots_event::id::ParseError::InvalidFormat)
- ));
-
- let raw = format!("{KIND_PROFILE}:{SELLER}:listing-1");
- assert!(matches!(
- parse_public_classified_listing_address(&raw),
- Err(radroots_event::id::ParseError::UnexpectedKind {
- expected: KIND_CLASSIFIED_LISTING,
- actual: KIND_PROFILE,
- })
- ));
- assert!(ClassifiedListingAddress::parse(&raw).is_err());
- }
-
- #[test]
- fn parse_classified_listing_address_rejects_non_listing_kind() {
- let raw = format!("{KIND_PROFILE}:{SELLER}:listing-1");
-
- assert!(matches!(
- parse_classified_listing_address(&raw),
- Err(radroots_event::id::ParseError::UnexpectedKind {
- expected: KIND_CLASSIFIED_LISTING,
- actual: KIND_PROFILE,
- })
- ));
- }
-}
diff --git a/crates/sdk/src/listing/operational_listing/model.rs b/crates/sdk/src/listing/operational_listing/model.rs
@@ -1,15 +0,0 @@
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsOperationalListingSubtotal {
- pub price_amount: radroots_core::Money,
- pub price_currency: radroots_core::Currency,
- pub quantity_amount: radroots_core::Decimal,
- pub quantity_unit: radroots_core::Unit,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsOperationalListingTotal {
- pub price_amount: radroots_core::Money,
- pub price_currency: radroots_core::Currency,
- pub quantity_amount: radroots_core::Decimal,
- pub quantity_unit: radroots_core::Unit,
-}
diff --git a/crates/sdk/src/listing/operational_listing/mutation.rs b/crates/sdk/src/listing/operational_listing/mutation.rs
@@ -1,510 +0,0 @@
-//! Mutation plan preparation for Radroots Listing v1.
-
-#![forbid(unsafe_code)]
-
-use core::fmt;
-
-use std::string::{String, ToString};
-
-use radroots_event::id::ClassifiedListingAddress;
-use radroots_event::{
- GenericEventDraft, draft::DraftError, envelope::kind::KIND_CLASSIFIED_LISTING,
-};
-use radroots_event_codec::{
- authoring::AuthoredEventPlan, encode::operational_listing::to_wire_parts_with_kind,
-};
-
-use crate::listing::operational_listing::draft::RadrootsOperationalListingCanonicalEdit;
-
-/// Listing v1 mutation intent for draft preparation only.
-///
-/// Publish and update target the public listing event, while local-only draft
-/// persistence and archive are intentionally unsupported as wire events.
-#[derive(Clone, Debug)]
-pub enum RadrootsOperationalListingMutation {
- Publish {
- draft: RadrootsOperationalListingCanonicalEdit,
- },
- Update {
- draft: RadrootsOperationalListingCanonicalEdit,
- },
- SaveDraft {
- draft: RadrootsOperationalListingCanonicalEdit,
- },
- Archive {
- listing_addr: ClassifiedListingAddress,
- },
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum RadrootsOperationalListingLifecycleState {
- Draft,
- Published,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum RadrootsOperationalListingMutationError {
- UnsupportedMutation,
- EncodeListing(String),
- AuthoredPlan(DraftError),
-}
-
-impl fmt::Display for RadrootsOperationalListingMutationError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::UnsupportedMutation => f.write_str("listing mutation is not supported"),
- Self::EncodeListing(error) => {
- write!(f, "failed to encode listing mutation: {error}")
- }
- Self::AuthoredPlan(error) => {
- write!(f, "failed to build listing authored plan: {error}")
- }
- }
- }
-}
-
-impl core::error::Error for RadrootsOperationalListingMutationError {}
-
-const OPERATIONAL_LISTING_PUBLISHED_CONTRACT_ID: &str = "radroots.operational_listing.published.v1";
-
-impl RadrootsOperationalListingMutation {
- pub fn publish(draft: RadrootsOperationalListingCanonicalEdit) -> Self {
- Self::Publish { draft }
- }
-
- pub fn update(draft: RadrootsOperationalListingCanonicalEdit) -> Self {
- Self::Update { draft }
- }
-
- pub fn save_draft(draft: RadrootsOperationalListingCanonicalEdit) -> Self {
- Self::SaveDraft { draft }
- }
-
- pub fn archive(listing_addr: ClassifiedListingAddress) -> Self {
- Self::Archive { listing_addr }
- }
-
- pub fn lifecycle_state(
- &self,
- ) -> Result<RadrootsOperationalListingLifecycleState, RadrootsOperationalListingMutationError>
- {
- match self {
- Self::Publish { .. } | Self::Update { .. } => {
- Ok(RadrootsOperationalListingLifecycleState::Published)
- }
- Self::SaveDraft { .. } => Ok(RadrootsOperationalListingLifecycleState::Draft),
- Self::Archive { .. } => {
- Err(RadrootsOperationalListingMutationError::UnsupportedMutation)
- }
- }
- }
-
- pub fn canonical_draft(
- &self,
- ) -> Result<&RadrootsOperationalListingCanonicalEdit, RadrootsOperationalListingMutationError>
- {
- match self {
- Self::Publish { draft } | Self::Update { draft } | Self::SaveDraft { draft } => {
- Ok(draft)
- }
- Self::Archive { .. } => {
- Err(RadrootsOperationalListingMutationError::UnsupportedMutation)
- }
- }
- }
-
- pub fn listing_addr(
- &self,
- ) -> Result<&ClassifiedListingAddress, RadrootsOperationalListingMutationError> {
- match self {
- Self::Publish { draft } | Self::Update { draft } => Ok(draft.public_listing_addr()),
- Self::SaveDraft { draft } => Ok(draft.public_listing_addr()),
- Self::Archive { .. } => {
- Err(RadrootsOperationalListingMutationError::UnsupportedMutation)
- }
- }
- }
-}
-
-pub fn build_operational_listing_mutation_plan(
- mutation: &RadrootsOperationalListingMutation,
- created_at: u64,
-) -> Result<AuthoredEventPlan, RadrootsOperationalListingMutationError> {
- let (draft, kind, contract_id) = match mutation {
- RadrootsOperationalListingMutation::Publish { draft }
- | RadrootsOperationalListingMutation::Update { draft } => (
- draft,
- KIND_CLASSIFIED_LISTING,
- OPERATIONAL_LISTING_PUBLISHED_CONTRACT_ID,
- ),
- RadrootsOperationalListingMutation::SaveDraft { .. }
- | RadrootsOperationalListingMutation::Archive { .. } => {
- return Err(RadrootsOperationalListingMutationError::UnsupportedMutation);
- }
- };
- let parts = to_wire_parts_with_kind(draft.listing(), kind).map_err(|error| {
- RadrootsOperationalListingMutationError::EncodeListing(error.to_string())
- })?;
- AuthoredEventPlan::from_generic(
- GenericEventDraft::new(
- contract_id,
- parts.kind,
- created_at,
- parts.tags,
- parts.content,
- draft.seller_pubkey().to_hex(),
- )
- .map_err(RadrootsOperationalListingMutationError::AuthoredPlan)?,
- )
- .map_err(RadrootsOperationalListingMutationError::AuthoredPlan)
-}
-
-#[cfg(all(test, feature = "local-signing"))]
-mod tests {
- const FIXTURE_ALICE_SECRET_KEY_HEX: &str =
- "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
- const FIXTURE_ALICE_PUBLIC_KEY_HEX: &str =
- "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
- use nostr::{EventBuilder, JsonUtil, Keys, Kind, Tag, Timestamp};
- use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit};
- use radroots_event::{
- contract::validate_event_contract_shape,
- envelope::kind::KIND_CLASSIFIED_LISTING,
- farm::FarmRef,
- farm::resource_area::ResourceAreaRef,
- id::{ClassifiedListingAddress, DTag, InventoryBinId},
- listing::operational::{
- OperationalListing, OperationalListingAvailability, OperationalListingBin,
- OperationalListingDeliveryMethod, OperationalListingProduct,
- OperationalListingPublicLocation, OperationalListingStatus,
- },
- wire::Nip01EventWire,
- };
- use radroots_event_codec::{authoring::AuthoredEventPlan, verify::verify_nip01_event};
- use radroots_identity::PublicKey;
-
- use crate::listing::operational_listing::draft::RadrootsOperationalListingCanonicalEdit;
- use crate::listing::operational_listing::validation::validate_operational_listing_event;
-
- use super::{
- OPERATIONAL_LISTING_PUBLISHED_CONTRACT_ID, RadrootsOperationalListingLifecycleState,
- RadrootsOperationalListingMutation, RadrootsOperationalListingMutationError,
- build_operational_listing_mutation_plan,
- };
-
- const SELLER: &str = FIXTURE_ALICE_PUBLIC_KEY_HEX;
-
- fn d_tag(raw: &str) -> DTag {
- DTag::parse(raw).expect("d tag")
- }
-
- fn bin_id(raw: &str) -> InventoryBinId {
- InventoryBinId::parse(raw).expect("bin id")
- }
-
- fn sign_plan(plan: &AuthoredEventPlan) -> radroots_event::envelope::EventEnvelope {
- let keys = Keys::parse(FIXTURE_ALICE_SECRET_KEY_HEX).expect("fixture signing key");
- assert_eq!(keys.public_key().to_hex(), plan.author().to_hex());
- let tags = plan
- .body()
- .tags()
- .iter()
- .cloned()
- .map(|tag| Tag::parse(tag).expect("plan tag"))
- .collect::<Vec<_>>();
- let event = EventBuilder::new(
- Kind::Custom(u16::try_from(plan.body().kind()).expect("NIP-01 kind")),
- plan.body().content(),
- )
- .tags(tags)
- .allow_self_tagging()
- .custom_created_at(Timestamp::from_secs(plan.created_at()))
- .sign_with_keys(&keys)
- .expect("signed listing event");
- assert_eq!(event.id.to_hex(), plan.expected_event_id().to_hex());
- let raw_json = event.as_json();
- Nip01EventWire::parse_json(raw_json.as_str())
- .expect("canonical event wire")
- .into_envelope()
- .expect("event envelope")
- }
-
- fn listing() -> OperationalListing {
- OperationalListing {
- d_tag: d_tag("AAAAAAAAAAAAAAAAAAAAAg"),
- published_at: None,
- farm: FarmRef {
- pubkey: SELLER.to_string(),
- d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
- },
- product: OperationalListingProduct {
- key: "coffee".to_string(),
- title: "Coffee".to_string(),
- category: "coffee".to_string(),
- summary: Some("Single origin coffee".to_string()),
- process: None,
- lot: None,
- location: None,
- profile: None,
- year: None,
- },
- primary_bin_id: bin_id("bin-1"),
- bins: vec![OperationalListingBin {
- bin_id: bin_id("bin-1"),
- quantity: Quantity::try_new(Decimal::from(1000u32), Unit::MassG).unwrap(),
- price_per_canonical_unit: QuantityPrice::try_new(
- Money::try_new(Decimal::from(20u32), Currency::USD).unwrap(),
- Quantity::try_new(Decimal::from(1u32), Unit::MassG).unwrap(),
- )
- .unwrap(),
- display_amount: None,
- display_unit: None,
- display_label: None,
- display_price: None,
- display_price_unit: None,
- }],
- resource_area: None,
- plot: None,
- discounts: None,
- inventory_available: Some(Decimal::from(5u32)),
- availability: Some(OperationalListingAvailability::Status {
- status: OperationalListingStatus::Active,
- }),
- delivery_method: Some(OperationalListingDeliveryMethod::Pickup),
- location: Some(OperationalListingPublicLocation {
- primary: "Farm".to_string(),
- city: Some("Town".to_string()),
- region: Some("Region".to_string()),
- country: Some("US".to_string()),
- geohash: "9q8yy".to_string(),
- }),
- images: None,
- }
- }
-
- fn canonical_draft() -> RadrootsOperationalListingCanonicalEdit {
- RadrootsOperationalListingCanonicalEdit::new(
- listing(),
- PublicKey::from_hex(SELLER).expect("seller"),
- )
- .expect("canonical listing edit")
- }
-
- #[test]
- fn supported_mutations_report_lifecycle_states() {
- assert_eq!(
- RadrootsOperationalListingMutation::publish(canonical_draft())
- .lifecycle_state()
- .expect("state"),
- RadrootsOperationalListingLifecycleState::Published
- );
- assert_eq!(
- RadrootsOperationalListingMutation::update(canonical_draft())
- .lifecycle_state()
- .expect("state"),
- RadrootsOperationalListingLifecycleState::Published
- );
- assert_eq!(
- RadrootsOperationalListingMutation::save_draft(canonical_draft())
- .lifecycle_state()
- .expect("state"),
- RadrootsOperationalListingLifecycleState::Draft
- );
- }
-
- #[test]
- fn supported_mutations_expose_canonical_drafts() {
- let publish = RadrootsOperationalListingMutation::publish(canonical_draft());
- let update = RadrootsOperationalListingMutation::update(canonical_draft());
- let save_draft = RadrootsOperationalListingMutation::save_draft(canonical_draft());
-
- assert_eq!(
- publish
- .canonical_draft()
- .expect("draft")
- .seller_pubkey()
- .to_hex(),
- SELLER
- );
- assert_eq!(
- update
- .canonical_draft()
- .expect("draft")
- .seller_pubkey()
- .to_hex(),
- SELLER
- );
- assert_eq!(
- save_draft
- .canonical_draft()
- .expect("draft")
- .seller_pubkey()
- .to_hex(),
- SELLER
- );
- assert_eq!(
- publish
- .canonical_draft()
- .expect("draft")
- .listing()
- .d_tag
- .as_str(),
- "AAAAAAAAAAAAAAAAAAAAAg"
- );
- }
-
- #[test]
- fn supported_mutations_report_listing_addresses() {
- let publish = RadrootsOperationalListingMutation::publish(canonical_draft());
- let update = RadrootsOperationalListingMutation::update(canonical_draft());
- let save_draft = RadrootsOperationalListingMutation::save_draft(canonical_draft());
-
- assert_eq!(
- publish.listing_addr().expect("address").as_str(),
- format!("{KIND_CLASSIFIED_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg")
- );
- assert_eq!(
- update.listing_addr().expect("address").as_str(),
- format!("{KIND_CLASSIFIED_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg")
- );
- assert_eq!(
- save_draft.listing_addr().expect("address").as_str(),
- format!("{KIND_CLASSIFIED_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg")
- );
- }
-
- #[test]
- fn archive_is_explicitly_unsupported() {
- let archive = RadrootsOperationalListingMutation::archive(
- ClassifiedListingAddress::parse(format!(
- "{KIND_CLASSIFIED_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg"
- ))
- .expect("listing address"),
- );
-
- assert_eq!(
- archive.lifecycle_state().unwrap_err(),
- RadrootsOperationalListingMutationError::UnsupportedMutation
- );
- assert_eq!(
- archive.canonical_draft().unwrap_err(),
- RadrootsOperationalListingMutationError::UnsupportedMutation
- );
- assert_eq!(
- archive.listing_addr().unwrap_err(),
- RadrootsOperationalListingMutationError::UnsupportedMutation
- );
- }
-
- #[test]
- fn build_operational_listing_mutation_plan_maps_publish_and_update_to_published_listing() {
- let publish = RadrootsOperationalListingMutation::publish(canonical_draft());
- let update = RadrootsOperationalListingMutation::update(canonical_draft());
-
- let publish_draft =
- build_operational_listing_mutation_plan(&publish, 1_700_000_000).expect("draft");
- let update_draft =
- build_operational_listing_mutation_plan(&update, 1_700_000_000).expect("draft");
-
- assert_eq!(publish_draft.body().kind(), KIND_CLASSIFIED_LISTING);
- assert_eq!(
- publish_draft.body().contract().contract_id().as_str(),
- OPERATIONAL_LISTING_PUBLISHED_CONTRACT_ID
- );
- assert_eq!(publish_draft.author().to_hex(), SELLER);
- assert_eq!(publish_draft.created_at(), 1_700_000_000);
- assert_eq!(
- publish_draft.body().content(),
- "# Coffee\n\nSingle origin coffee"
- );
- assert_eq!(update_draft.body().kind(), KIND_CLASSIFIED_LISTING);
- assert_eq!(
- update_draft.body().contract().contract_id().as_str(),
- OPERATIONAL_LISTING_PUBLISHED_CONTRACT_ID
- );
- assert_eq!(update_draft.author().to_hex(), SELLER);
- }
-
- #[test]
- fn build_operational_listing_mutation_plan_rejects_save_draft() {
- let save_draft = RadrootsOperationalListingMutation::save_draft(canonical_draft());
-
- assert_eq!(
- build_operational_listing_mutation_plan(&save_draft, 1_700_000_000).unwrap_err(),
- RadrootsOperationalListingMutationError::UnsupportedMutation
- );
- }
-
- #[test]
- fn build_operational_listing_mutation_plan_rejects_archive() {
- let archive = RadrootsOperationalListingMutation::archive(
- ClassifiedListingAddress::parse(format!(
- "{KIND_CLASSIFIED_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg"
- ))
- .expect("listing address"),
- );
-
- assert_eq!(
- build_operational_listing_mutation_plan(&archive, 1_700_000_000).unwrap_err(),
- RadrootsOperationalListingMutationError::UnsupportedMutation
- );
- }
-
- #[test]
- fn build_operational_listing_mutation_plan_reports_encode_errors() {
- let mut listing = listing();
- listing.resource_area = Some(ResourceAreaRef {
- pubkey: SELLER.to_string(),
- d_tag: "bad d tag".to_string(),
- });
- let draft = RadrootsOperationalListingCanonicalEdit::new(
- listing,
- PublicKey::from_hex(SELLER).expect("seller"),
- )
- .expect("canonical listing edit");
- let publish = RadrootsOperationalListingMutation::publish(draft);
-
- let err = build_operational_listing_mutation_plan(&publish, 1_700_000_000).unwrap_err();
-
- assert!(matches!(
- err,
- RadrootsOperationalListingMutationError::EncodeListing(_)
- ));
- }
-
- #[test]
- fn build_operational_listing_mutation_plan_event_id_is_stable_for_fixed_input() {
- let publish = RadrootsOperationalListingMutation::publish(canonical_draft());
-
- let first =
- build_operational_listing_mutation_plan(&publish, 1_700_000_000).expect("draft");
- let second =
- build_operational_listing_mutation_plan(&publish, 1_700_000_000).expect("draft");
-
- assert_eq!(first.expected_event_id(), second.expected_event_id());
- assert_eq!(first.expected_event_id().to_hex().len(), 64);
- assert_eq!(first.body().tags(), second.body().tags());
- assert_eq!(first.body().content(), second.body().content());
- }
-
- #[test]
- fn build_operational_listing_mutation_plan_output_validates_as_operational_listing() {
- let publish = RadrootsOperationalListingMutation::publish(canonical_draft());
- let draft =
- build_operational_listing_mutation_plan(&publish, 1_700_000_000).expect("draft");
-
- let signed = sign_plan(&draft);
- validate_event_contract_shape(&signed, OPERATIONAL_LISTING_PUBLISHED_CONTRACT_ID)
- .expect("operational listing contract");
- let verified = verify_nip01_event(signed).expect("verified listing");
- let validated = validate_operational_listing_event(&verified).expect("validated listing");
-
- assert_eq!(validated.seller_pubkey, SELLER);
- assert!(
- validated
- .listing_addr
- .as_str()
- .contains(&format!(":{SELLER}:"))
- );
- }
-}
diff --git a/crates/sdk/src/listing/operational_listing/price_ext.rs b/crates/sdk/src/listing/operational_listing/price_ext.rs
@@ -1,176 +0,0 @@
-use crate::listing::operational_listing::model::{
- RadrootsOperationalListingSubtotal, RadrootsOperationalListingTotal,
-};
-use radroots_core::pricing::{Error as PricingError, QuantityPriceOps};
-use radroots_core::{Decimal, Quantity};
-use radroots_event::listing::operational::OperationalListingBin;
-
-pub trait BinPricingTryExt {
- fn try_subtotal_for_count(
- &self,
- bin_count: u32,
- ) -> Result<RadrootsOperationalListingSubtotal, PricingError>;
- fn try_total_for_count(
- &self,
- bin_count: u32,
- ) -> Result<RadrootsOperationalListingTotal, PricingError>;
-}
-
-#[inline]
-fn effective_quantity(
- bin: &OperationalListingBin,
- bin_count: u32,
-) -> Result<Quantity, PricingError> {
- let amount = bin
- .quantity
- .amount()
- .checked_mul(Decimal::from(bin_count))
- .map_err(|_| PricingError::ArithmeticOverflow)?;
- Quantity::try_new(amount, bin.quantity.unit()).map_err(|_| PricingError::ArithmeticOverflow)
-}
-
-impl BinPricingTryExt for OperationalListingBin {
- fn try_subtotal_for_count(
- &self,
- bin_count: u32,
- ) -> Result<RadrootsOperationalListingSubtotal, PricingError> {
- let effective_qty = effective_quantity(self, bin_count)?;
- let money = self
- .price_per_canonical_unit
- .try_cost_for_rounded(&effective_qty)?;
- let currency = money.currency();
-
- Ok(RadrootsOperationalListingSubtotal {
- price_amount: money,
- price_currency: currency,
- quantity_amount: effective_qty.amount(),
- quantity_unit: effective_qty.unit(),
- })
- }
-
- fn try_total_for_count(
- &self,
- bin_count: u32,
- ) -> Result<RadrootsOperationalListingTotal, PricingError> {
- let sub = self.try_subtotal_for_count(bin_count)?;
- Ok(RadrootsOperationalListingTotal {
- price_amount: sub.price_amount,
- price_currency: sub.price_currency,
- quantity_amount: sub.quantity_amount,
- quantity_unit: sub.quantity_unit,
- })
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::BinPricingTryExt;
- use radroots_core::pricing::Error as PricingError;
- use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit};
- use radroots_event::id::InventoryBinId;
- use radroots_event::listing::operational::OperationalListingBin;
-
- fn bin_id(raw: &str) -> InventoryBinId {
- InventoryBinId::parse(raw).expect("bin id")
- }
-
- fn valid_bin() -> OperationalListingBin {
- OperationalListingBin {
- bin_id: bin_id("bin-1"),
- quantity: Quantity::try_new(Decimal::from(2u32), Unit::MassG).unwrap(),
- price_per_canonical_unit: QuantityPrice::try_new(
- Money::try_new(Decimal::from(5u32), Currency::USD).unwrap(),
- Quantity::try_new(Decimal::from(1u32), Unit::MassG).unwrap(),
- )
- .unwrap(),
- display_amount: None,
- display_unit: None,
- display_label: None,
- display_price: None,
- display_price_unit: None,
- }
- }
-
- #[test]
- fn try_subtotal_for_rejects_unit_mismatch() {
- let bin = OperationalListingBin {
- bin_id: bin_id("bin-1"),
- quantity: Quantity::try_new(Decimal::from(1u32), Unit::MassG).unwrap(),
- price_per_canonical_unit: QuantityPrice::try_new(
- Money::try_new(Decimal::from(10u32), Currency::USD).unwrap(),
- Quantity::try_new(Decimal::from(1u32), Unit::Each).unwrap(),
- )
- .unwrap(),
- display_amount: None,
- display_unit: None,
- display_label: None,
- display_price: None,
- display_price_unit: None,
- };
-
- let err = bin.try_subtotal_for_count(1).unwrap_err();
- assert_eq!(
- err,
- PricingError::UnitMismatch {
- have: Unit::MassG,
- want: Unit::Each,
- }
- );
- }
-
- #[test]
- fn subtotal_and_total_for_count_follow_effective_quantity() {
- let bin = valid_bin();
- let subtotal = bin.try_subtotal_for_count(3).unwrap();
- let total = bin.try_total_for_count(3).unwrap();
-
- assert_eq!(subtotal.quantity_amount, Decimal::from(6u32));
- assert_eq!(subtotal.quantity_unit, Unit::MassG);
- assert_eq!(subtotal.price_amount.amount(), Decimal::from(30u32));
- assert_eq!(subtotal.price_currency, Currency::USD);
-
- assert_eq!(total.quantity_amount, subtotal.quantity_amount);
- assert_eq!(total.quantity_unit, subtotal.quantity_unit);
- assert_eq!(total.price_amount, subtotal.price_amount);
- assert_eq!(total.price_currency, subtotal.price_currency);
- }
-
- #[test]
- fn try_subtotal_and_try_total_match() {
- let bin = valid_bin();
- let subtotal = bin.try_subtotal_for_count(4).expect("subtotal");
- let total = bin.try_total_for_count(4).expect("total");
-
- assert_eq!(subtotal.quantity_amount, Decimal::from(8u32));
- assert_eq!(subtotal.price_amount.amount(), Decimal::from(40u32));
- assert_eq!(total.quantity_amount, subtotal.quantity_amount);
- assert_eq!(total.price_amount, subtotal.price_amount);
- }
-
- #[test]
- fn try_total_for_count_propagates_subtotal_errors() {
- let bin = OperationalListingBin {
- bin_id: bin_id("bin-1"),
- quantity: Quantity::try_new(Decimal::from(1u32), Unit::MassG).unwrap(),
- price_per_canonical_unit: QuantityPrice::try_new(
- Money::try_new(Decimal::from(10u32), Currency::USD).unwrap(),
- Quantity::try_new(Decimal::from(1u32), Unit::Each).unwrap(),
- )
- .unwrap(),
- display_amount: None,
- display_unit: None,
- display_label: None,
- display_price: None,
- display_price_unit: None,
- };
-
- let err = bin.try_total_for_count(1).unwrap_err();
- assert_eq!(
- err,
- PricingError::UnitMismatch {
- have: Unit::MassG,
- want: Unit::Each,
- }
- );
- }
-}
diff --git a/crates/sdk/src/listing/operational_listing/validation.rs b/crates/sdk/src/listing/operational_listing/validation.rs
@@ -1,988 +0,0 @@
-#![forbid(unsafe_code)]
-
-use std::{
- format,
- string::{String, ToString},
-};
-
-use radroots_core::{Decimal, Money, Quantity, Unit};
-use radroots_event::{
- envelope::kind::{KIND_CLASSIFIED_LISTING, is_classified_listing_kind},
- farm::location::{has_textual_locality, is_public_geohash5},
- id::ClassifiedListingAddress,
- listing::classified::{ClassifiedListingPartition, classify_classified_listing_tags},
- listing::operational::{
- OperationalListing, OperationalListingAvailability, OperationalListingBin,
- OperationalListingDeliveryMethod, OperationalListingPublicLocation,
- },
- trade::validation::OperationalListingValidationError,
-};
-use radroots_identity::PublicKey;
-
-use radroots_event_codec::{
- decode::operational_listing::operational_listing_from_nostr_event,
- verify::RadrootsSignatureVerifiedEvent,
-};
-
-#[derive(Clone, Debug)]
-pub struct RadrootsOperationalListingTradeProjection {
- pub listing_id: String,
- pub listing_addr: ClassifiedListingAddress,
- pub seller_pubkey: String,
- pub title: String,
- pub description: String,
- pub product_type: String,
- pub primary_bin_id: String,
- pub bin_quantity: Quantity,
- pub unit: Unit,
- pub unit_price: Money,
- pub inventory_available: Decimal,
- pub availability: OperationalListingAvailability,
- pub location: OperationalListingPublicLocation,
- pub delivery_method: OperationalListingDeliveryMethod,
- pub listing: OperationalListing,
-}
-
-/// Validates a signature-verified Operational Listing event.
-///
-/// A plain envelope cannot cross this boundary:
-///
-/// ```compile_fail
-/// use radroots_event::envelope::EventEnvelope;
-/// use radroots_sdk::listing::validate_operational_listing_event;
-///
-/// fn validate_unverified(event: &EventEnvelope) {
-/// let _ = validate_operational_listing_event(event);
-/// }
-/// ```
-pub fn validate_operational_listing_event(
- verified_event: &RadrootsSignatureVerifiedEvent,
-) -> Result<RadrootsOperationalListingTradeProjection, OperationalListingValidationError> {
- let event = verified_event.event();
- if !is_classified_listing_kind(event.kind_u32()) {
- return Err(OperationalListingValidationError::InvalidKind {
- kind: event.kind_u32(),
- });
- }
- if classify_classified_listing_tags(event.tags())
- != ClassifiedListingPartition::OperationalListing
- {
- return Err(OperationalListingValidationError::InvalidProfile);
- }
-
- let listing = operational_listing_from_nostr_event(event)
- .map_err(|error| OperationalListingValidationError::ParseError { error })?;
- validate_operational_listing_model(listing, event.author())
-}
-
-/// Validates the trade semantics of an unsigned Operational Listing model.
-///
-/// The seller is typed independently from the model because it is the
-/// authority against which the listing's farm identity is checked. This
-/// function does not perform event-kind, profile, decoding, or signature
-/// checks; callers handling Nostr events must use
-/// [`validate_operational_listing_event`] instead.
-pub fn validate_operational_listing_model(
- listing: OperationalListing,
- seller_pubkey: &PublicKey,
-) -> Result<RadrootsOperationalListingTradeProjection, OperationalListingValidationError> {
- let listing_id = listing.d_tag.as_str().trim().to_string();
-
- if listing.farm.pubkey != seller_pubkey.to_hex() {
- return Err(OperationalListingValidationError::InvalidSeller);
- }
- let listing_addr_raw = format!("{KIND_CLASSIFIED_LISTING}:{}:{listing_id}", seller_pubkey);
- let listing_addr = ClassifiedListingAddress::parse(&listing_addr_raw)
- .expect("validated listing identity must form a listing address");
-
- let title = listing.product.title.trim().to_string();
- if title.is_empty() {
- return Err(OperationalListingValidationError::MissingTitle);
- }
-
- let description = listing
- .product
- .summary
- .as_ref()
- .map(|s| s.trim().to_string())
- .unwrap_or_default();
- if description.is_empty() {
- return Err(OperationalListingValidationError::MissingDescription);
- }
-
- let product_type = if !listing.product.category.trim().is_empty() {
- listing.product.category.trim().to_string()
- } else {
- listing.product.key.trim().to_string()
- };
- if product_type.is_empty() {
- return Err(OperationalListingValidationError::MissingProductType);
- }
-
- if listing.bins.is_empty() {
- return Err(OperationalListingValidationError::MissingBins);
- }
- let primary_bin_id = listing.primary_bin_id.as_str().trim().to_string();
- let primary_bin_index = listing
- .bins
- .iter()
- .position(|bin| bin.bin_id.as_str() == primary_bin_id)
- .ok_or(OperationalListingValidationError::MissingPrimaryBin)?;
- for (index, bin) in listing.bins.iter().enumerate() {
- if listing.bins[..index]
- .iter()
- .any(|seen| seen.bin_id == bin.bin_id)
- {
- return Err(OperationalListingValidationError::InvalidBin);
- }
- }
- let primary_bin = &listing.bins[primary_bin_index];
-
- validate_listing_bin(primary_bin)?;
- for (index, bin) in listing.bins.iter().enumerate() {
- if index != primary_bin_index {
- validate_listing_bin(bin)?;
- }
- }
-
- let inventory_available = listing
- .inventory_available
- .ok_or(OperationalListingValidationError::MissingInventory)?;
- if inventory_available.is_sign_negative() {
- return Err(OperationalListingValidationError::InvalidInventory);
- }
-
- let availability = listing
- .availability
- .clone()
- .ok_or(OperationalListingValidationError::MissingAvailability)?;
- let location = listing
- .location
- .clone()
- .ok_or(OperationalListingValidationError::MissingLocation)?;
- if !has_textual_locality(
- &location.primary,
- location.city.as_deref(),
- location.region.as_deref(),
- location.country.as_deref(),
- ) {
- return Err(OperationalListingValidationError::MissingLocationLocality);
- }
- validate_listing_location_geohash(&location.geohash)?;
- let delivery_method = listing
- .delivery_method
- .clone()
- .ok_or(OperationalListingValidationError::MissingDeliveryMethod)?;
-
- Ok(RadrootsOperationalListingTradeProjection {
- listing_id,
- listing_addr,
- seller_pubkey: seller_pubkey.to_hex(),
- title,
- description,
- product_type,
- primary_bin_id: primary_bin_id.clone(),
- bin_quantity: primary_bin.quantity.clone(),
- unit: primary_bin.quantity.unit(),
- unit_price: primary_bin.price_per_canonical_unit.amount().clone(),
- inventory_available,
- availability,
- location,
- delivery_method,
- listing,
- })
-}
-
-fn validate_listing_bin(
- bin: &OperationalListingBin,
-) -> Result<(), OperationalListingValidationError> {
- if bin.quantity.amount().is_sign_negative() || !bin.quantity.is_canonical() {
- return Err(OperationalListingValidationError::InvalidBin);
- }
- if !bin.price_per_canonical_unit.is_price_per_canonical_unit()
- || bin
- .price_per_canonical_unit
- .amount()
- .amount()
- .is_sign_negative()
- || bin.price_per_canonical_unit.quantity().unit() != bin.quantity.unit()
- {
- return Err(OperationalListingValidationError::InvalidPrice);
- }
- Ok(())
-}
-
-fn validate_listing_location_geohash(
- geohash: &str,
-) -> Result<(), OperationalListingValidationError> {
- if geohash.trim().is_empty() {
- return Err(OperationalListingValidationError::MissingLocationGeohash);
- }
- if !is_public_geohash5(geohash) {
- return Err(OperationalListingValidationError::InvalidLocationGeohash);
- }
- Ok(())
-}
-
-#[cfg(all(test, feature = "local-signing"))]
-mod tests {
- const FIXTURE_ALICE_SECRET_KEY_HEX: &str =
- "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
- const FIXTURE_ALICE_PUBLIC_KEY_HEX: &str =
- "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
- const FIXTURE_BOB_SECRET_KEY_HEX: &str =
- "59392e9068f66431b12f70218fb61281cb6b433d7f27c55d61f1a63fe1a96ff8";
- const FIXTURE_BOB_PUBLIC_KEY_HEX: &str =
- "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af";
- use super::{
- OperationalListingValidationError, validate_operational_listing_event,
- validate_operational_listing_model,
- };
- use nostr::{EventBuilder, JsonUtil, Keys, Kind, Tag, Timestamp};
- use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit};
- use radroots_event::{
- envelope::EventEnvelope,
- envelope::EventEnvelopeParts,
- envelope::kind::KIND_CLASSIFIED_LISTING,
- farm::FarmRef,
- id::{DTag, InventoryBinId},
- listing::operational::{
- OperationalListing, OperationalListingAvailability, OperationalListingBin,
- OperationalListingDeliveryMethod, OperationalListingProduct,
- OperationalListingPublicLocation,
- },
- wire::Nip01EventWire,
- };
- use radroots_event_codec::verify::{RadrootsSignatureVerifiedEvent, verify_nip01_event};
- use radroots_identity::PublicKey;
-
- const SELLER: &str = FIXTURE_ALICE_PUBLIC_KEY_HEX;
- const OTHER_SELLER: &str = FIXTURE_BOB_PUBLIC_KEY_HEX;
-
- fn d_tag(raw: &str) -> DTag {
- DTag::parse(raw).expect("d tag")
- }
-
- fn bin_id(raw: &str) -> InventoryBinId {
- InventoryBinId::parse(raw).expect("bin id")
- }
-
- fn seller_pubkey() -> PublicKey {
- PublicKey::from_hex(SELLER).expect("seller pubkey")
- }
-
- fn other_seller_pubkey() -> PublicKey {
- PublicKey::from_hex(OTHER_SELLER).expect("other seller pubkey")
- }
-
- fn base_listing() -> OperationalListing {
- OperationalListing {
- d_tag: d_tag("AAAAAAAAAAAAAAAAAAAAAg"),
- published_at: None,
- farm: FarmRef {
- pubkey: SELLER.into(),
- d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(),
- },
- product: OperationalListingProduct {
- key: "coffee".into(),
- title: "Coffee".into(),
- category: "coffee".into(),
- summary: Some("Single origin coffee".into()),
- process: None,
- lot: None,
- location: None,
- profile: None,
- year: None,
- },
- primary_bin_id: bin_id("bin-1"),
- bins: vec![OperationalListingBin {
- bin_id: bin_id("bin-1"),
- quantity: Quantity::try_new(Decimal::from(1000u32), Unit::MassG).unwrap(),
- price_per_canonical_unit: QuantityPrice::try_new(
- Money::try_new(Decimal::from(20u32), Currency::USD).unwrap(),
- Quantity::try_new(Decimal::from(1u32), Unit::MassG).unwrap(),
- )
- .unwrap(),
- display_amount: None,
- display_unit: None,
- display_label: None,
- display_price: None,
- display_price_unit: None,
- }],
- resource_area: None,
- plot: None,
- discounts: None,
- inventory_available: Some(Decimal::from(5u32)),
- availability: Some(OperationalListingAvailability::Status {
- status: radroots_event::listing::operational::OperationalListingStatus::Active,
- }),
- delivery_method: Some(OperationalListingDeliveryMethod::Pickup),
- location: Some(OperationalListingPublicLocation {
- primary: "Farm".into(),
- city: Some("Town".into()),
- region: Some("Region".into()),
- country: Some("US".into()),
- geohash: "9q8yy".into(),
- }),
- images: None,
- }
- }
-
- fn base_event(listing: &OperationalListing) -> RadrootsSignatureVerifiedEvent {
- let mut tags = vec![
- vec!["d".into(), listing.d_tag.to_string()],
- vec!["p".into(), listing.farm.pubkey.clone()],
- vec![
- "a".into(),
- format!("30340:{}:{}", listing.farm.pubkey, listing.farm.d_tag),
- ],
- vec!["key".into(), listing.product.key.clone()],
- vec!["title".into(), listing.product.title.clone()],
- vec!["category".into(), listing.product.category.clone()],
- vec![
- "summary".into(),
- listing.product.summary.clone().unwrap_or_default(),
- ],
- vec![
- "radroots:primary_bin".into(),
- listing.primary_bin_id.to_string(),
- ],
- ];
- for bin in &listing.bins {
- tags.push(vec![
- "radroots:bin".into(),
- bin.bin_id.to_string(),
- bin.quantity.amount().to_string(),
- bin.quantity.unit().code().to_string(),
- ]);
- tags.push(vec![
- "radroots:price".into(),
- bin.bin_id.to_string(),
- bin.price_per_canonical_unit.amount().amount().to_string(),
- bin.price_per_canonical_unit
- .amount()
- .currency()
- .as_str()
- .to_string(),
- bin.price_per_canonical_unit.quantity().amount().to_string(),
- bin.price_per_canonical_unit
- .quantity()
- .unit()
- .code()
- .to_string(),
- ]);
- }
- if let Some(inventory) = listing.inventory_available {
- tags.push(vec!["inventory".into(), inventory.to_string()]);
- }
- if let Some(availability) = &listing.availability {
- match availability {
- OperationalListingAvailability::Status { status } => tags.push(vec![
- "status".into(),
- match status {
- radroots_event::listing::operational::OperationalListingStatus::Active => {
- "active".into()
- }
- radroots_event::listing::operational::OperationalListingStatus::Sold => {
- "sold".into()
- }
- radroots_event::listing::operational::OperationalListingStatus::Other {
- value,
- } => value.clone(),
- },
- ]),
- OperationalListingAvailability::Window { start, end } => {
- if let Some(start) = start {
- tags.push(vec![
- "radroots:availability_start".into(),
- start.to_string(),
- ]);
- }
- if let Some(end) = end {
- tags.push(vec!["expires_at".into(), end.to_string()]);
- }
- }
- }
- }
- if let Some(delivery) = &listing.delivery_method {
- let mut tag = vec!["delivery".into()];
- match delivery {
- OperationalListingDeliveryMethod::Pickup => tag.push("pickup".into()),
- OperationalListingDeliveryMethod::LocalDelivery => {
- tag.push("local_delivery".into())
- }
- OperationalListingDeliveryMethod::Shipping => tag.push("shipping".into()),
- OperationalListingDeliveryMethod::Other { method } => {
- tag.push("other".into());
- tag.push(method.clone());
- }
- }
- tags.push(tag);
- }
- if let Some(location) = &listing.location {
- tags.push(vec![
- "location".into(),
- location.primary.clone(),
- location.city.clone().unwrap_or_default(),
- location.region.clone().unwrap_or_default(),
- location.country.clone().unwrap_or_default(),
- ]);
- tags.push(vec!["g".into(), location.geohash.clone()]);
- }
-
- event_with_parts(SELLER, KIND_CLASSIFIED_LISTING, tags, String::new())
- }
-
- fn event_with_parts(
- author: &str,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: String,
- ) -> RadrootsSignatureVerifiedEvent {
- let secret = match author {
- SELLER => FIXTURE_ALICE_SECRET_KEY_HEX,
- OTHER_SELLER => FIXTURE_BOB_SECRET_KEY_HEX,
- _ => panic!("test author must be an approved fixture identity"),
- };
- let keys = Keys::parse(secret).expect("fixture signing key");
- let tags = tags
- .into_iter()
- .map(|tag| Tag::parse(tag).expect("test tag"))
- .collect::<Vec<_>>();
- let event = EventBuilder::new(
- Kind::Custom(u16::try_from(kind).expect("test kind")),
- content,
- )
- .tags(tags)
- .allow_self_tagging()
- .custom_created_at(Timestamp::from_secs(1))
- .sign_with_keys(&keys)
- .expect("signed test event");
- let raw_json = event.as_json();
- let envelope = Nip01EventWire::parse_json(raw_json.as_str())
- .expect("canonical event wire")
- .into_envelope()
- .expect("event envelope");
- verify_nip01_event(envelope).expect("verified test event")
- }
-
- fn assert_validation_err(
- listing: OperationalListing,
- expected: OperationalListingValidationError,
- ) {
- let event = base_event(&listing);
- let err = validate_operational_listing_event(&event).unwrap_err();
- assert_eq!(format!("{err}"), format!("{expected}"));
- }
-
- fn assert_secondary_bin_model_error(
- update: impl FnOnce(&mut OperationalListingBin),
- expected: OperationalListingValidationError,
- ) {
- let mut listing = listing_with_secondary_bin();
- update(&mut listing.bins[1]);
-
- assert_eq!(
- validate_operational_listing_model(listing, &seller_pubkey())
- .expect_err("invalid secondary bin"),
- expected
- );
- }
-
- fn listing_with_secondary_bin() -> OperationalListing {
- let mut listing = base_listing();
- let mut secondary_bin = listing.bins[0].clone();
- secondary_bin.bin_id = bin_id("bin-2");
- listing.bins.push(secondary_bin);
- listing
- }
-
- #[test]
- fn validate_listing_ok() {
- let listing = base_listing();
- let event = base_event(&listing);
- assert!(validate_operational_listing_event(&event).is_ok());
- }
-
- #[test]
- #[cfg(any())]
- fn model_and_verified_event_validation_return_the_same_projection() {
- let listing = listing_with_secondary_bin();
- let event_projection =
- validate_operational_listing_event(&base_event(&listing)).expect("event projection");
- let model_projection = validate_operational_listing_model(listing, &seller_pubkey())
- .expect("model projection");
-
- assert_eq!(
- serde_json::to_value(model_projection).expect("model projection JSON"),
- serde_json::to_value(event_projection).expect("event projection JSON")
- );
- }
-
- #[test]
- fn model_and_verified_event_validation_return_the_same_semantic_errors() {
- let mut listing = base_listing();
- listing.inventory_available = None;
- let event_error = validate_operational_listing_event(&base_event(&listing))
- .expect_err("event inventory error");
- let model_error = validate_operational_listing_model(listing, &seller_pubkey())
- .expect_err("model inventory error");
- assert_eq!(model_error, event_error);
-
- let listing = base_listing();
- let event = event_with_parts(
- OTHER_SELLER,
- KIND_CLASSIFIED_LISTING,
- base_event(&listing).event().tags_as_vec(),
- String::new(),
- );
- let event_error =
- validate_operational_listing_event(&event).expect_err("event seller error");
- let model_error = validate_operational_listing_model(listing, &other_seller_pubkey())
- .expect_err("model seller error");
- assert_eq!(model_error, event_error);
- }
-
- #[test]
- fn model_validation_reports_errors_before_event_encoding() {
- let mut listing = base_listing();
- listing.bins.clear();
- assert_eq!(
- validate_operational_listing_model(listing, &seller_pubkey())
- .expect_err("missing bins"),
- OperationalListingValidationError::MissingBins
- );
-
- let mut listing = base_listing();
- listing.primary_bin_id = bin_id("missing");
- assert_eq!(
- validate_operational_listing_model(listing, &seller_pubkey())
- .expect_err("missing primary bin"),
- OperationalListingValidationError::MissingPrimaryBin
- );
-
- let mut listing = base_listing();
- listing.bins.push(listing.bins[0].clone());
- assert_eq!(
- validate_operational_listing_model(listing, &seller_pubkey())
- .expect_err("duplicate bin ID"),
- OperationalListingValidationError::InvalidBin
- );
-
- let mut listing = base_listing();
- listing.location.as_mut().expect("location").geohash = " ".into();
- assert_eq!(
- validate_operational_listing_model(listing, &seller_pubkey())
- .expect_err("missing geohash"),
- OperationalListingValidationError::MissingLocationGeohash
- );
-
- let mut listing = base_listing();
- listing.location.as_mut().expect("location").geohash = "9q8yyz".into();
- assert_eq!(
- validate_operational_listing_model(listing, &seller_pubkey())
- .expect_err("invalid geohash"),
- OperationalListingValidationError::InvalidLocationGeohash
- );
- }
-
- #[test]
- fn model_validation_rejects_invalid_secondary_bin_quantities() {
- assert_secondary_bin_model_error(
- |bin| bin.quantity = Quantity::try_new(Decimal::ONE, Unit::MassKg).unwrap(),
- OperationalListingValidationError::InvalidBin,
- );
- }
-
- #[test]
- fn model_validation_rejects_invalid_secondary_bin_prices() {
- assert_secondary_bin_model_error(
- |bin| {
- bin.price_per_canonical_unit = QuantityPrice::try_new(
- bin.price_per_canonical_unit.amount().clone(),
- Quantity::try_new(Decimal::from(2u32), Unit::MassG).unwrap(),
- )
- .unwrap();
- },
- OperationalListingValidationError::InvalidPrice,
- );
- assert_secondary_bin_model_error(
- |bin| {
- bin.price_per_canonical_unit = QuantityPrice::try_new(
- bin.price_per_canonical_unit.amount().clone(),
- Quantity::try_new(Decimal::ONE, Unit::MassKg).unwrap(),
- )
- .unwrap();
- },
- OperationalListingValidationError::InvalidPrice,
- );
- assert_secondary_bin_model_error(
- |bin| {
- bin.price_per_canonical_unit = QuantityPrice::try_new(
- bin.price_per_canonical_unit.amount().clone(),
- Quantity::try_new(Decimal::ONE, Unit::Each).unwrap(),
- )
- .unwrap();
- },
- OperationalListingValidationError::InvalidPrice,
- );
- }
-
- #[test]
- fn validate_listing_rejects_retired_kind() {
- let listing = base_listing();
- let event = event_with_parts(
- SELLER,
- 30403,
- base_event(&listing).event().tags_as_vec(),
- String::new(),
- );
- let err = validate_operational_listing_event(&event).unwrap_err();
- assert_eq!(
- err,
- OperationalListingValidationError::InvalidKind { kind: 30403 }
- );
- }
-
- #[test]
- fn validate_listing_rejects_missing_d_tag() {
- let event = event_with_parts(SELLER, KIND_CLASSIFIED_LISTING, Vec::new(), String::new());
- let err = validate_operational_listing_event(&event).unwrap_err();
- assert_eq!(err, OperationalListingValidationError::InvalidProfile);
- }
-
- #[test]
- fn validate_listing_rejects_invalid_currency() {
- let event = event_with_parts(
- SELLER,
- KIND_CLASSIFIED_LISTING,
- vec![
- vec!["d".into(), "AAAAAAAAAAAAAAAAAAAAAg".into()],
- vec!["p".into(), SELLER.into()],
- vec!["a".into(), format!("30340:{SELLER}:AAAAAAAAAAAAAAAAAAAAAA")],
- vec!["key".into(), "coffee".into()],
- vec!["title".into(), "Coffee".into()],
- vec!["category".into(), "coffee".into()],
- vec!["summary".into(), "Single origin".into()],
- vec!["radroots:primary_bin".into(), "bin-1".into()],
- vec![
- "quantity".into(),
- "1".into(),
- "lb".into(),
- "bag".into(),
- "5".into(),
- ],
- vec![
- "price".into(),
- "20".into(),
- "US".into(),
- "1".into(),
- "lb".into(),
- ],
- vec![
- "location".into(),
- "Farm".into(),
- "Town".into(),
- "Region".into(),
- ],
- vec!["status".into(), "active".into()],
- vec!["delivery".into(), "pickup".into()],
- ],
- String::new(),
- );
- let err = validate_operational_listing_event(&event).unwrap_err();
- assert!(format!("{err:?}").starts_with("ParseError"));
- }
-
- #[test]
- fn validate_listing_rejects_mismatched_seller() {
- let listing = base_listing();
- let event = event_with_parts(
- OTHER_SELLER,
- KIND_CLASSIFIED_LISTING,
- base_event(&listing).event().tags_as_vec(),
- String::new(),
- );
- let err = validate_operational_listing_event(&event).unwrap_err();
- assert_eq!(err, OperationalListingValidationError::InvalidSeller);
- }
-
- #[test]
- fn validate_listing_rejects_missing_inventory() {
- let mut listing = base_listing();
- listing.inventory_available = None;
- let event = base_event(&listing);
- let err = validate_operational_listing_event(&event).unwrap_err();
- assert_eq!(err, OperationalListingValidationError::MissingInventory);
- }
-
- #[test]
- fn validate_listing_rejects_invalid_kind() {
- let listing = base_listing();
- let event = event_with_parts(
- SELLER,
- 0,
- base_event(&listing).event().tags_as_vec(),
- String::new(),
- );
- let err = validate_operational_listing_event(&event).unwrap_err();
- assert_eq!(
- err,
- OperationalListingValidationError::InvalidKind { kind: 0 }
- );
- }
-
- #[test]
- fn validate_listing_rejects_missing_title() {
- let mut listing = base_listing();
- listing.product.title = " ".into();
- assert_validation_err(listing, OperationalListingValidationError::MissingTitle);
- }
-
- #[test]
- fn tampered_envelope_cannot_reach_operational_validation() {
- let verified = base_event(&base_listing());
- let event = verified.into_event();
- let tampered = EventEnvelope::new(EventEnvelopeParts {
- id: event.id_hex(),
- author: event.author().to_hex().to_owned(),
- created_at: event.created_at_u64(),
- kind: event.kind_u32(),
- tags: event.tags_as_vec(),
- content: "tampered".to_owned(),
- sig: event.signature_hex(),
- })
- .expect("well-shaped tampered envelope");
-
- assert!(verify_nip01_event(tampered).is_err());
- }
-
- #[test]
- fn validate_listing_rejects_missing_description() {
- let mut listing = base_listing();
- listing.product.summary = Some(" ".into());
- assert_validation_err(
- listing,
- OperationalListingValidationError::MissingDescription,
- );
- }
-
- #[test]
- fn validate_listing_rejects_missing_product_type() {
- let mut listing = base_listing();
- listing.product.category = " ".into();
- listing.product.key = " ".into();
- assert_validation_err(
- listing,
- OperationalListingValidationError::MissingProductType,
- );
- }
-
- #[test]
- fn validate_listing_rejects_missing_bins() {
- let mut listing = base_listing();
- listing.bins.clear();
- assert_validation_err(
- listing,
- OperationalListingValidationError::ParseError {
- error:
- radroots_event::listing::operational::OperationalListingParseError::InvalidTag(
- "radroots:primary_bin".into(),
- ),
- },
- );
- }
-
- #[test]
- fn validate_listing_rejects_missing_primary_bin_id() {
- assert!(InventoryBinId::parse(" ").is_err());
- }
-
- #[test]
- fn validate_listing_rejects_primary_bin_not_found() {
- let mut listing = base_listing();
- listing.primary_bin_id = bin_id("missing");
- assert_validation_err(
- listing,
- OperationalListingValidationError::ParseError {
- error:
- radroots_event::listing::operational::OperationalListingParseError::InvalidTag(
- "radroots:primary_bin".into(),
- ),
- },
- );
- }
-
- #[test]
- fn validate_listing_rejects_non_canonical_quantity() {
- let mut listing = base_listing();
- listing.bins[0].quantity = Quantity::try_new(Decimal::ONE, Unit::MassKg).unwrap();
- assert_validation_err(
- listing,
- OperationalListingValidationError::ParseError {
- error:
- radroots_event::listing::operational::OperationalListingParseError::InvalidTag(
- "radroots:bin".into(),
- ),
- },
- );
- }
-
- #[test]
- fn validate_listing_rejects_non_canonical_price_quantity() {
- let mut listing = base_listing();
- listing.bins[0].price_per_canonical_unit = QuantityPrice::try_new(
- listing.bins[0].price_per_canonical_unit.amount().clone(),
- Quantity::try_new(Decimal::ONE, Unit::MassKg).unwrap(),
- )
- .unwrap();
- assert_validation_err(
- listing,
- OperationalListingValidationError::ParseError {
- error:
- radroots_event::listing::operational::OperationalListingParseError::InvalidTag(
- "radroots:price".into(),
- ),
- },
- );
- }
-
- #[test]
- fn validate_listing_rejects_price_unit_mismatch() {
- let mut listing = base_listing();
- listing.bins[0].price_per_canonical_unit = QuantityPrice::try_new(
- listing.bins[0].price_per_canonical_unit.amount().clone(),
- Quantity::try_new(Decimal::ONE, Unit::Each).unwrap(),
- )
- .unwrap();
- assert_validation_err(
- listing,
- OperationalListingValidationError::ParseError {
- error:
- radroots_event::listing::operational::OperationalListingParseError::InvalidTag(
- "radroots:price".into(),
- ),
- },
- );
- }
-
- #[test]
- fn validate_listing_rejects_negative_inventory() {
- let mut listing = base_listing();
- listing.inventory_available = Some("-1".parse().unwrap());
- assert_validation_err(listing, OperationalListingValidationError::InvalidInventory);
- }
-
- #[test]
- fn validate_listing_rejects_missing_availability() {
- let mut listing = base_listing();
- listing.availability = None;
- assert_validation_err(
- listing,
- OperationalListingValidationError::MissingAvailability,
- );
- }
-
- #[test]
- fn validate_listing_rejects_missing_location() {
- let mut listing = base_listing();
- listing.location = None;
- assert_validation_err(listing, OperationalListingValidationError::MissingLocation);
- }
-
- #[test]
- fn validate_listing_rejects_missing_location_locality() {
- let mut listing = base_listing();
- let location = listing.location.as_mut().expect("location");
- location.city = None;
- location.region = None;
- location.country = None;
- assert_validation_err(
- listing,
- OperationalListingValidationError::MissingLocationLocality,
- );
- }
-
- #[test]
- fn validate_listing_rejects_missing_location_geohash() {
- let mut listing = base_listing();
- listing.location.as_mut().expect("location").geohash = " ".into();
- assert_validation_err(
- listing,
- OperationalListingValidationError::ParseError {
- error:
- radroots_event::listing::operational::OperationalListingParseError::InvalidTag(
- "g".to_string(),
- ),
- },
- );
- }
-
- #[test]
- fn validate_listing_rejects_invalid_location_geohash() {
- let mut listing = base_listing();
- listing.location.as_mut().expect("location").geohash = "9q8yyz".into();
- assert_validation_err(
- listing,
- OperationalListingValidationError::ParseError {
- error:
- radroots_event::listing::operational::OperationalListingParseError::InvalidTag(
- "g".to_string(),
- ),
- },
- );
- }
-
- #[test]
- fn validate_listing_rejects_missing_delivery_method() {
- let mut listing = base_listing();
- listing.delivery_method = None;
- assert_validation_err(
- listing,
- OperationalListingValidationError::MissingDeliveryMethod,
- );
- }
-
- #[test]
- fn validation_error_display_covers_all_variants() {
- let errors = vec![
- OperationalListingValidationError::InvalidKind { kind: 9 },
- OperationalListingValidationError::InvalidProfile,
- OperationalListingValidationError::MissingListingId,
- OperationalListingValidationError::ListingEventNotFound {
- listing_addr: "addr".into(),
- },
- OperationalListingValidationError::ListingEventFetchFailed {
- listing_addr: "addr".into(),
- },
- OperationalListingValidationError::ParseError {
- error:
- radroots_event::listing::operational::OperationalListingParseError::InvalidTag(
- "d".into(),
- ),
- },
- OperationalListingValidationError::InvalidSeller,
- OperationalListingValidationError::MissingFarmProfile,
- OperationalListingValidationError::MissingFarmRecord,
- OperationalListingValidationError::MissingTitle,
- OperationalListingValidationError::MissingDescription,
- OperationalListingValidationError::MissingProductType,
- OperationalListingValidationError::MissingBins,
- OperationalListingValidationError::MissingPrimaryBin,
- OperationalListingValidationError::InvalidBin,
- OperationalListingValidationError::MissingPrice,
- OperationalListingValidationError::InvalidPrice,
- OperationalListingValidationError::MissingInventory,
- OperationalListingValidationError::InvalidInventory,
- OperationalListingValidationError::MissingAvailability,
- OperationalListingValidationError::MissingLocation,
- OperationalListingValidationError::MissingLocationLocality,
- OperationalListingValidationError::MissingLocationGeohash,
- OperationalListingValidationError::InvalidLocationGeohash,
- OperationalListingValidationError::MissingDeliveryMethod,
- ];
- for error in errors {
- assert!(!error.to_string().trim().is_empty());
- }
- }
-}
diff --git a/crates/sdk/src/signing.rs b/crates/sdk/src/signing.rs
@@ -1,532 +0,0 @@
-//! Generic signer composition without protocol or relay ownership.
-
-use std::sync::Arc;
-#[cfg(feature = "local-signing")]
-use std::sync::RwLock;
-
-use radroots_signing::{SignReceipt, SignRequest, Signer, SignerStatus};
-
-/// Host-visible signer composition mode.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-#[non_exhaustive]
-pub enum Mode {
- /// A concrete local Nostr adapter owns opaque key material.
- Local,
- /// A host-provided signer drives NIP-46 protocol and relay execution.
- Nip46,
- /// Another host-provided implementation of the generic signer SPI.
- Host,
-}
-
-/// Cloneable SDK composition wrapper around the canonical signer SPI.
-#[derive(Clone)]
-pub struct Provider {
- mode: Mode,
- signer: Arc<dyn Signer>,
- #[cfg(feature = "local-signing")]
- slot: Option<Slot>,
-}
-
-impl Provider {
- /// Wraps any host-provided canonical signer implementation.
- #[must_use]
- pub fn host(signer: Arc<dyn Signer>) -> Self {
- Self {
- mode: Mode::Host,
- signer,
- #[cfg(feature = "local-signing")]
- slot: None,
- }
- }
-
- /// Wraps the concrete local Nostr adapter without exposing its key.
- #[cfg(feature = "local-signing")]
- #[must_use]
- pub fn local(signer: radroots_nostr::signing::LocalSigner) -> Self {
- Self {
- mode: Mode::Local,
- signer: Arc::new(signer),
- slot: None,
- }
- }
-
- /// Wraps a host-controlled local signer slot.
- ///
- /// The slot starts inert and can be populated or cleared without rebuilding
- /// the client. Secret persistence remains entirely host-owned.
- #[cfg(feature = "local-signing")]
- #[must_use]
- pub fn slot(slot: Slot) -> Self {
- Self {
- mode: Mode::Local,
- signer: Arc::new(slot.clone()),
- slot: Some(slot),
- }
- }
-
- /// Marks a host-provided canonical signer as a NIP-46 composition.
- ///
- /// `radroots_nostr_connect` owns protocol state and its transport SPI. The
- /// injected implementation owns that client plus explicit relay execution;
- /// this wrapper does not contact a relay, persist a session, or start work.
- #[cfg(feature = "nip46")]
- #[must_use]
- pub fn nip46(signer: Arc<dyn Signer>) -> Self {
- Self {
- mode: Mode::Nip46,
- signer,
- #[cfg(feature = "local-signing")]
- slot: None,
- }
- }
-
- /// Returns the explicit composition mode.
- #[must_use]
- pub const fn mode(&self) -> Mode {
- self.mode
- }
-
- /// Borrows the canonical signer SPI.
- #[must_use]
- pub fn as_signer(&self) -> &dyn Signer {
- self.signer.as_ref()
- }
-
- /// Reports canonical status without initiating a signing request.
- pub async fn status(&self) -> Result<SignerStatus, radroots_signing::Error> {
- self.signer.status().await
- }
-
- /// Delegates one already-authorized request to the canonical SPI.
- pub async fn sign(&self, request: SignRequest) -> Result<SignReceipt, radroots_signing::Error> {
- self.signer.sign(request).await
- }
-
- #[cfg(feature = "local-signing")]
- pub(crate) fn into_parts(self) -> (Arc<dyn Signer>, Option<Slot>) {
- (self.signer, self.slot)
- }
-
- #[cfg(not(feature = "local-signing"))]
- pub(crate) fn into_signer(self) -> Arc<dyn Signer> {
- self.signer
- }
-}
-
-/// Public identity controlled by an installed local signer.
-#[cfg(feature = "local-signing")]
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct LocalIdentity {
- public_key: radroots_identity::PublicKey,
- npub: String,
-}
-
-#[cfg(feature = "local-signing")]
-impl LocalIdentity {
- fn from_public_key(
- public_key: radroots_identity::PublicKey,
- ) -> Result<Self, radroots_nostr::Error> {
- Ok(Self {
- public_key,
- npub: radroots_nostr::key::public_key_to_npub(public_key)?,
- })
- }
-
- /// Returns the canonical lowercase public-key hexadecimal form.
- #[must_use]
- pub fn public_key_hex(&self) -> String {
- self.public_key.to_hex()
- }
-
- /// Returns the canonical NIP-19 public identity.
- #[must_use]
- pub fn npub(&self) -> &str {
- self.npub.as_str()
- }
-
- #[cfg(any(test, all(feature = "sync", feature = "nostr")))]
- pub(crate) const fn public_key(&self) -> radroots_identity::PublicKey {
- self.public_key
- }
-}
-
-/// Mutable, client-shareable local signer selected by the host.
-///
-/// The slot never persists key material and its debug output never observes
-/// the installed signer. Installing and clearing are explicit host actions.
-#[cfg(feature = "local-signing")]
-#[derive(Clone, Default)]
-pub struct Slot {
- state: Arc<RwLock<Option<SlotState>>>,
-}
-
-#[cfg(feature = "local-signing")]
-struct SlotState {
- signer: Arc<dyn Signer>,
- identity: LocalIdentity,
-}
-
-#[cfg(feature = "local-signing")]
-impl Slot {
- /// Creates an empty signer slot.
- #[must_use]
- pub fn new() -> Self {
- Self::default()
- }
-
- /// Validates and installs one host-supplied hexadecimal or `nsec` secret.
- pub fn install(&self, encoded: &str) -> Result<LocalIdentity, radroots_nostr::Error> {
- let secret = radroots_nostr::key::SecretKey::parse(encoded)?;
- self.install_secret(secret)
- }
-
- /// Generates, installs, and returns one secret for immediate host custody.
- ///
- /// The SDK retains only the opaque signer. The returned `nsec` is the sole
- /// persistence handoff and must be moved into host secure storage.
- pub fn generate(&self) -> Result<(String, LocalIdentity), radroots_nostr::Error> {
- let secret = radroots_nostr::key::SecretKey::generate();
- let encoded = radroots_nostr::key::secret_key_to_nsec(&secret);
- let identity = self.install_secret(secret)?;
- Ok((encoded, identity))
- }
-
- /// Removes the active signer from this process.
- pub fn clear(&self) {
- if let Ok(mut state) = self.state.write() {
- *state = None;
- }
- }
-
- /// Returns the currently installed public identity.
- #[must_use]
- pub fn identity(&self) -> Option<LocalIdentity> {
- self.state
- .read()
- .ok()
- .and_then(|state| state.as_ref().map(|state| state.identity.clone()))
- }
-
- fn install_secret(
- &self,
- secret: radroots_nostr::key::SecretKey,
- ) -> Result<LocalIdentity, radroots_nostr::Error> {
- let public_key = secret.public_key()?;
- let identity = LocalIdentity::from_public_key(public_key)?;
- let signer: Arc<dyn Signer> = Arc::new(radroots_nostr::signing::LocalSigner::new(secret)?);
- if let Ok(mut state) = self.state.write() {
- *state = Some(SlotState {
- signer,
- identity: identity.clone(),
- });
- }
- Ok(identity)
- }
-
- fn signer(&self) -> Result<Arc<dyn Signer>, radroots_signing::Error> {
- self.state
- .read()
- .map_err(|source| {
- radroots_signing::Error::with_source(
- radroots_signing::error::Kind::InternalError,
- LockFailure(source.to_string()),
- )
- })?
- .as_ref()
- .map(|state| Arc::clone(&state.signer))
- .ok_or_else(|| {
- radroots_signing::Error::new(radroots_signing::error::Kind::SignerUnavailable)
- })
- }
-}
-
-#[cfg(feature = "local-signing")]
-impl Signer for Slot {
- fn status(
- &self,
- ) -> radroots_signing::signer::BoxFuture<'_, Result<SignerStatus, radroots_signing::Error>>
- {
- Box::pin(async move {
- match self.signer() {
- Ok(signer) => signer.status().await,
- Err(error) if error.kind() == radroots_signing::error::Kind::SignerUnavailable => {
- Ok(SignerStatus::unavailable())
- }
- Err(error) => Err(error),
- }
- })
- }
-
- fn sign(
- &self,
- request: SignRequest,
- ) -> radroots_signing::signer::BoxFuture<'_, Result<SignReceipt, radroots_signing::Error>> {
- Box::pin(async move { self.signer()?.sign(request).await })
- }
-}
-
-#[cfg(feature = "local-signing")]
-impl std::fmt::Debug for Slot {
- fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- formatter
- .debug_struct("Slot")
- .field("installed", &self.identity().is_some())
- .finish()
- }
-}
-
-#[cfg(feature = "local-signing")]
-#[derive(Debug)]
-struct LockFailure(String);
-
-#[cfg(feature = "local-signing")]
-impl std::fmt::Display for LockFailure {
- fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- formatter.write_str(self.0.as_str())
- }
-}
-
-#[cfg(feature = "local-signing")]
-impl std::error::Error for LockFailure {}
-
-impl std::fmt::Debug for Provider {
- fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- formatter
- .debug_struct("Provider")
- .field("mode", &self.mode)
- .field("signer", &"<opaque>")
- .finish()
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::sync::{
- Arc,
- atomic::{AtomicUsize, Ordering},
- };
-
- use radroots_event::{GenericEventDraft, contract::AuthorRole};
- use radroots_event_codec::authoring::AuthoredEventPlan;
- use radroots_identity::PublicKey;
- use radroots_protocol::runtime::v1::OperationId;
- use radroots_signing::{
- Actor, AuthoredArtifactId, Error, SignReceipt, SignRequest, SignerStatus, SigningIntentId,
- SigningOperationId,
- actor::ActorSource,
- error::Kind,
- request::{CancellationPolicy, SignPolicy},
- signer::BoxFuture,
- };
- #[cfg(any(feature = "local-signing", feature = "nip46"))]
- use radroots_signing::{capability::SignerKind, status::SignerAvailability};
- #[cfg(feature = "nip46")]
- use radroots_signing::{
- capability::{CancellationSupport, SignerCapability},
- recovery::ReplayCapability,
- status::{AuthChallenge, SignProgress},
- };
-
- use super::*;
-
- const PUBLIC_KEY: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
-
- struct ScriptedSigner {
- status: SignerStatus,
- result: Kind,
- polls: Arc<AtomicUsize>,
- }
-
- impl Signer for ScriptedSigner {
- fn status(&self) -> BoxFuture<'_, Result<SignerStatus, Error>> {
- let status = self.status.clone();
- Box::pin(async move { Ok(status) })
- }
-
- fn sign(&self, _request: SignRequest) -> BoxFuture<'_, Result<SignReceipt, Error>> {
- let result = self.result;
- let polls = Arc::clone(&self.polls);
- Box::pin(async move {
- polls.fetch_add(1, Ordering::Relaxed);
- Err(Error::new(result))
- })
- }
- }
-
- fn request() -> SignRequest {
- let actor = Actor::new(
- PublicKey::from_hex(PUBLIC_KEY).expect("public key"),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Any],
- )
- .expect("actor");
- let plan = AuthoredEventPlan::from_generic(
- GenericEventDraft::new(
- "radroots.social.geochat.v1",
- 20_000,
- 1_700_000_000,
- Vec::new(),
- "frozen-content",
- PUBLIC_KEY,
- )
- .expect("draft"),
- )
- .expect("authored plan");
- SignRequest::new(
- OperationId::SyncPush,
- SigningIntentId::new(
- SigningOperationId::new([1; 16]).expect("operation id"),
- AuthoredArtifactId::new([2; 16]).expect("artifact id"),
- ),
- actor,
- plan,
- SignPolicy::new(1_700_000_100, CancellationPolicy::PreservePublishedRequest)
- .expect("policy"),
- )
- .expect("request")
- }
-
- #[cfg(feature = "nip46")]
- fn remote_status(progress: Option<SignProgress>) -> SignerStatus {
- SignerStatus::new(
- SignerAvailability::AwaitingAuthentication,
- vec![SignerCapability::new(
- SignerKind::Remote,
- ReplayCapability::ExactReplayByRequestId,
- CancellationSupport::BeforeAndAfterPublication,
- true,
- true,
- )],
- progress,
- )
- }
-
- #[cfg(feature = "local-signing")]
- #[tokio::test]
- async fn local_provider_uses_the_concrete_lower_adapter() {
- let local = radroots_nostr::signing::LocalSigner::generate().expect("local signer");
- let provider = Provider::local(local);
- let status = provider.status().await.expect("status");
- assert_eq!(provider.mode(), Mode::Local);
- assert_eq!(status.availability(), SignerAvailability::Ready);
- assert_eq!(status.capabilities()[0].kind(), SignerKind::Local);
- }
-
- #[cfg(feature = "local-signing")]
- #[tokio::test]
- async fn local_slot_hands_secret_to_host_and_supports_lock_restore() {
- let slot = Slot::new();
- assert!(slot.identity().is_none());
- assert_eq!(
- slot.status().await.expect("empty status").availability(),
- SignerAvailability::Unavailable
- );
-
- let (secret, generated) = slot.generate().expect("generated identity");
- assert!(secret.starts_with("nsec1"));
- assert_eq!(slot.identity().expect("installed"), generated);
- assert!(!format!("{slot:?}").contains(secret.as_str()));
-
- slot.clear();
- assert!(slot.identity().is_none());
- let restored = slot.install(secret.as_str()).expect("restored identity");
- assert_eq!(restored, generated);
- assert_eq!(restored.public_key_hex(), restored.public_key().to_hex());
- assert!(restored.npub().starts_with("npub1"));
-
- let provider = Provider::slot(slot.clone());
- assert_eq!(provider.mode(), Mode::Local);
- assert!(format!("{provider:?}").contains("<opaque>"));
- let (_signer, provider_slot) = provider.into_parts();
- assert!(provider_slot.is_some());
-
- slot.clear();
- assert_eq!(
- slot.sign(request()).await.expect_err("empty slot").kind(),
- Kind::SignerUnavailable
- );
- }
-
- #[cfg(feature = "local-signing")]
- #[tokio::test]
- async fn poisoned_local_slot_fails_closed_without_exposing_key_state() {
- let slot = Slot::new();
- let state = Arc::clone(&slot.state);
- let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
- let _guard = state.write().expect("write lock");
- panic!("poison signer slot");
- }));
-
- slot.clear();
- assert!(slot.identity().is_none());
- let (_secret, identity) = slot.generate().expect("secret remains host-owned");
- assert!(!identity.public_key_hex().is_empty());
- assert_eq!(
- slot.sign(request())
- .await
- .expect_err("poisoned slot")
- .kind(),
- Kind::InternalError
- );
- }
-
- #[cfg(feature = "nip46")]
- #[tokio::test]
- async fn nip46_provider_preserves_auth_challenge_and_capabilities() {
- let challenge = AuthChallenge::new(
- "https://signer.example/approve",
- 1_700_000_000,
- Some(1_700_000_100),
- )
- .expect("challenge");
- let signer = ScriptedSigner {
- status: remote_status(Some(SignProgress::authentication(challenge))),
- result: Kind::SignerRejected,
- polls: Arc::new(AtomicUsize::new(0)),
- };
- let provider = Provider::nip46(Arc::new(signer));
- let status = provider.status().await.expect("status");
- assert_eq!(provider.mode(), Mode::Nip46);
- assert_eq!(
- status.availability(),
- SignerAvailability::AwaitingAuthentication
- );
- assert!(status.progress().expect("progress").challenge().is_some());
- assert!(status.capabilities()[0].may_require_authentication());
- }
-
- #[tokio::test]
- async fn canonical_errors_preserve_timeout_drift_and_cancellation() {
- for expected in [
- Kind::SignerTimeout,
- Kind::SignerOutputInvalid,
- Kind::SignerCancelled,
- ] {
- let provider = Provider::host(Arc::new(ScriptedSigner {
- status: SignerStatus::unavailable(),
- result: expected,
- polls: Arc::new(AtomicUsize::new(0)),
- }));
- assert_eq!(
- provider.sign(request()).await.expect_err("failure").kind(),
- expected
- );
- assert_eq!(
- provider.as_signer().status().await.expect("status"),
- SignerStatus::unavailable()
- );
- }
- }
-
- #[test]
- fn dropping_unpolled_signing_future_has_no_effect() {
- let polls = Arc::new(AtomicUsize::new(0));
- let provider = Provider::host(Arc::new(ScriptedSigner {
- status: SignerStatus::unavailable(),
- result: Kind::SignerCancelled,
- polls: Arc::clone(&polls),
- }));
- drop(provider.sign(request()));
- assert_eq!(polls.load(Ordering::Relaxed), 0);
- }
-}
diff --git a/crates/sdk/src/storage.rs b/crates/sdk/src/storage.rs
@@ -1,259 +0,0 @@
-//! Canonical storage capability composition.
-
-/// Backend-neutral storage status owned by `radroots_storage`.
-pub type Status = radroots_storage::StorageStatus;
-
-/// Backend-neutral integrity status owned by `radroots_storage`.
-pub type IntegrityStatus = radroots_storage::status::IntegrityStatus;
-
-/// Validated SQLite open configuration; this contains no connection or pool.
-#[cfg(feature = "sqlite")]
-pub type SqliteOptions = radroots_storage_sqlite::OpenOptions;
-
-/// Explicit SQLite lifecycle mode.
-#[cfg(feature = "sqlite")]
-pub type SqliteOpenMode = radroots_storage_sqlite::OpenMode;
-
-/// Validated SQLite-owned paths; this contains no backend handle.
-#[cfg(feature = "sqlite")]
-pub type SqlitePaths = radroots_storage_sqlite::Paths;
-
-use radroots_storage::backup::{
- BackupId, BackupOperation, BackupPlan, BackupTransition, ReliabilityRevision, RestoreOperation,
- RestorePlan, RestoreTransition, StorageReliability,
-};
-
-/// Borrowed reliability operations over the canonical backend-neutral SPI.
-#[derive(Clone, Copy)]
-pub struct Operations<'a> {
- storage: &'a dyn radroots_storage::Storage,
-}
-
-impl<'a> Operations<'a> {
- pub(crate) const fn new(storage: &'a dyn radroots_storage::Storage) -> Self {
- Self { storage }
- }
-
- /// Begins or resumes one idempotent backup plan.
- pub async fn begin_backup(
- &self,
- plan: BackupPlan,
- ) -> Result<BackupOperation, radroots_storage::Error> {
- StorageReliability::begin_backup(self.storage, plan).await
- }
-
- /// Applies one optimistic backup transition.
- pub async fn transition_backup(
- &self,
- backup_id: BackupId,
- expected_revision: ReliabilityRevision,
- transition: BackupTransition,
- at_unix_ms: u64,
- ) -> Result<BackupOperation, radroots_storage::Error> {
- StorageReliability::transition_backup(
- self.storage,
- backup_id,
- expected_revision,
- transition,
- at_unix_ms,
- )
- .await
- }
-
- /// Begins or resumes one staged restore plan.
- pub async fn begin_restore(
- &self,
- plan: RestorePlan,
- ) -> Result<RestoreOperation, radroots_storage::Error> {
- StorageReliability::begin_restore(self.storage, plan).await
- }
-
- /// Applies one optimistic staged restore transition.
- pub async fn transition_restore(
- &self,
- backup_id: BackupId,
- expected_revision: ReliabilityRevision,
- transition: RestoreTransition,
- at_unix_ms: u64,
- ) -> Result<RestoreOperation, radroots_storage::Error> {
- StorageReliability::transition_restore(
- self.storage,
- backup_id,
- expected_revision,
- transition,
- at_unix_ms,
- )
- .await
- }
-
- /// Returns passive backend status without initiating recovery work.
- pub async fn status(&self) -> Result<Status, radroots_storage::Error> {
- StorageReliability::status(self.storage).await
- }
-
- /// Runs backend-owned integrity inspection.
- pub async fn integrity(&self) -> Result<IntegrityStatus, radroots_storage::Error> {
- StorageReliability::integrity(self.storage).await
- }
-}
-
-impl std::fmt::Debug for Operations<'_> {
- fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- formatter
- .debug_struct("Operations")
- .field("storage", &"<borrowed canonical storage>")
- .finish()
- }
-}
-
-#[cfg(all(test, feature = "memory"))]
-mod tests {
- use std::sync::Arc;
-
- use radroots_storage::{
- backup::{
- BackupFormatVersion, BackupManifest, BackupMember, BackupMemberKind,
- BackupSecretPolicy, BackupStage, MemberDigest, MemberVerification, RestoreMemberStatus,
- RestoreStage,
- },
- event::SourceGeneration,
- memory::MemoryStorage,
- status::IntegrityHealth,
- };
-
- use crate::ClientBuilder;
-
- use super::*;
-
- fn manifest(backup_id: BackupId) -> BackupManifest {
- BackupManifest::new(
- BackupFormatVersion::V1,
- backup_id,
- 1_800_000_000_100,
- BackupSecretPolicy::ExcludeProtectedStorage,
- vec![
- BackupMember::new(
- "runtime/events.bin",
- BackupMemberKind::Runtime,
- 16,
- MemberDigest::new([3; 32]),
- )
- .expect("member"),
- ],
- )
- .expect("manifest")
- }
-
- #[tokio::test]
- async fn memory_reliability_preserves_staging_interruption_integrity_and_native_states() {
- let storage = Arc::new(MemoryStorage::new(
- SourceGeneration::new([8; 32]).expect("generation"),
- ));
- let client = ClientBuilder::new()
- .storage(storage)
- .build()
- .expect("client");
- let operations = client.storage_operations().expect("operations");
- let backup_id = BackupId::new([9; 16]).expect("backup id");
- let plan = BackupPlan::new(
- backup_id,
- BackupFormatVersion::V1,
- BackupSecretPolicy::ExcludeProtectedStorage,
- 1_800_000_000_000,
- )
- .expect("plan");
-
- drop(operations.begin_backup(plan.clone()));
- let planned = operations.begin_backup(plan).await.expect("planned");
- assert_eq!(planned.stage(), BackupStage::Planned);
- let captured = operations
- .transition_backup(
- backup_id,
- planned.revision(),
- BackupTransition::Captured(manifest(backup_id)),
- 1_800_000_000_200,
- )
- .await
- .expect("captured");
- let verified = operations
- .transition_backup(
- backup_id,
- captured.revision(),
- BackupTransition::Verified,
- 1_800_000_000_300,
- )
- .await
- .expect("verified");
- let finalized = operations
- .transition_backup(
- backup_id,
- verified.revision(),
- BackupTransition::Finalize,
- 1_800_000_000_400,
- )
- .await
- .expect("finalized");
- assert_eq!(finalized.stage(), BackupStage::Finalized);
-
- let restore_plan = RestorePlan::new(
- manifest(backup_id),
- BackupSecretPolicy::ExcludeProtectedStorage,
- 1_800_000_001_000,
- )
- .expect("restore plan");
- let staging = operations
- .begin_restore(restore_plan.clone())
- .await
- .expect("staging");
- assert_eq!(staging.stage(), RestoreStage::Staging);
- let replayed = operations
- .begin_restore(restore_plan)
- .await
- .expect("resume staging");
- assert_eq!(replayed, staging);
- let verifying = operations
- .transition_restore(
- backup_id,
- staging.revision(),
- RestoreTransition::Staged,
- 1_800_000_001_100,
- )
- .await
- .expect("verifying");
- let finalizing = operations
- .transition_restore(
- backup_id,
- verifying.revision(),
- RestoreTransition::Verified(vec![
- RestoreMemberStatus::new("runtime/events.bin", MemberVerification::Verified)
- .expect("member status"),
- ]),
- 1_800_000_001_200,
- )
- .await
- .expect("finalizing");
- let restored = operations
- .transition_restore(
- backup_id,
- finalizing.revision(),
- RestoreTransition::Finalize,
- 1_800_000_001_300,
- )
- .await
- .expect("restored");
- assert_eq!(restored.stage(), RestoreStage::Finalized);
- assert_eq!(
- operations.integrity().await.expect("integrity").health(),
- IntegrityHealth::Healthy
- );
- assert_eq!(
- operations
- .status()
- .await
- .expect("status")
- .integrity()
- .health(),
- IntegrityHealth::Healthy
- );
- }
-}
diff --git a/crates/sdk/src/sync.rs b/crates/sdk/src/sync.rs
@@ -1,520 +0,0 @@
-//! Client-scoped access to the canonical synchronization engine.
-
-#[cfg(feature = "sync")]
-use std::sync::Arc;
-
-#[cfg(feature = "sync")]
-use radroots_storage::{authored_delivery::AuthoredDeliveryPlan, projection::ProjectionId};
-#[cfg(feature = "sync")]
-use radroots_sync::{
- Engine, PullReceipt, PullRequest, PushRequest, SyncStatus,
- ingest::{AdmissionPolicy, IngestBatchReceipt, IngestReceipt},
- policy::Error,
- projection::{Reducer, RefreshReceipt, RefreshRequest},
- push::{
- AdmissionRunReceipt, DeliveryExecutionReceipt, PushPreparation, PushStatus,
- SigningRunReceipt,
- },
-};
-#[cfg(feature = "sync")]
-use radroots_transport::source::ObservedEvent;
-
-/// Explicit host policy for SDK-composed synchronization.
-///
-/// Selecting this policy opts into the system clock and operating-system
-/// randomness for operation IDs. It creates no executor, timer, or worker.
-#[cfg(feature = "sync")]
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct HostPolicy {
- deadlines: radroots_sync::policy::DeadlinePolicy,
-}
-
-#[cfg(feature = "sync")]
-impl HostPolicy {
- /// Creates a bounded policy for pull, signing, and delivery calls.
- pub fn new(
- pull_timeout_ms: u64,
- sign_timeout_ms: u64,
- delivery_timeout_ms: u64,
- ) -> Result<Self, radroots_sync::policy::Error> {
- Ok(Self {
- deadlines: radroots_sync::policy::DeadlinePolicy::new(
- pull_timeout_ms,
- sign_timeout_ms,
- delivery_timeout_ms,
- )?,
- })
- }
-
- /// Returns the ordinary bounded native-host policy.
- #[must_use]
- pub fn standard() -> Self {
- Self::new(30_000, 30_000, 30_000).expect("static host deadlines are valid")
- }
-
- pub(crate) fn composition(
- self,
- ) -> (
- Arc<dyn radroots_sync::policy::Clock>,
- Arc<dyn radroots_sync::policy::IdSource>,
- radroots_sync::policy::DeadlinePolicy,
- ) {
- (Arc::new(SystemClock), Arc::new(RandomIds), self.deadlines)
- }
-}
-
-#[cfg(feature = "sync")]
-impl Default for HostPolicy {
- fn default() -> Self {
- Self::standard()
- }
-}
-
-#[cfg(feature = "sync")]
-struct SystemClock;
-
-#[cfg(feature = "sync")]
-impl radroots_sync::policy::Clock for SystemClock {
- fn now_unix_ms(&self) -> Result<u64, radroots_sync::policy::Error> {
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .ok()
- .and_then(|duration| u64::try_from(duration.as_millis()).ok())
- .filter(|value| *value != 0)
- .ok_or(radroots_sync::policy::Error::ClockUnavailable)
- }
-}
-
-#[cfg(feature = "sync")]
-struct RandomIds;
-
-#[cfg(feature = "sync")]
-impl radroots_sync::policy::IdSource for RandomIds {
- fn next_id(
- &self,
- _operation: radroots_sync::policy::OperationKind,
- ) -> Result<radroots_sync::policy::SyncId, radroots_sync::policy::Error> {
- radroots_sync::policy::SyncId::new(*uuid::Uuid::new_v4().as_bytes())
- }
-}
-
-/// Borrowed client operations over one explicitly composed sync engine.
-///
-/// This type owns no scheduling, retries, status strings, outbox state, or
-/// projection state. Every method delegates once to the canonical engine and
-/// returns its native receipt or error.
-#[cfg(feature = "sync")]
-#[derive(Clone, Copy)]
-pub struct Operations<'a> {
- engine: &'a Engine,
-}
-
-#[cfg(feature = "sync")]
-impl<'a> Operations<'a> {
- pub(crate) const fn new(engine: &'a Engine) -> Self {
- Self { engine }
- }
-
- /// Runs one caller-bounded pull and canonical ingest sequence.
- pub async fn pull(
- &self,
- request: PullRequest,
- admission: &dyn AdmissionPolicy,
- ) -> Result<PullReceipt, Error> {
- self.engine.pull(request, admission).await
- }
-
- /// Verifies and atomically ingests one observed event.
- pub async fn ingest(
- &self,
- observed: ObservedEvent,
- admission: &dyn AdmissionPolicy,
- ) -> Result<IngestReceipt, Error> {
- self.engine.ingest(observed, admission).await
- }
-
- /// Ingests a bounded caller-owned batch while preserving partial outcomes.
- pub async fn ingest_batch(
- &self,
- observed: Vec<ObservedEvent>,
- admission: &dyn AdmissionPolicy,
- ) -> IngestBatchReceipt {
- self.engine.ingest_batch(observed, admission).await
- }
-
- /// Runs one bounded projection refresh through its owning reducer.
- pub async fn refresh_projection(
- &self,
- request: RefreshRequest,
- reducer: &dyn Reducer,
- ) -> Result<RefreshReceipt, Error> {
- self.engine.refresh_projection(request, reducer).await
- }
-
- /// Atomically persists one complete authored operation before any side effect.
- pub async fn prepare_push(&self, request: PushRequest) -> Result<PushPreparation, Error> {
- self.engine.prepare_push(request).await
- }
-
- /// Returns the complete durable state of one prepared push operation.
- pub async fn push_status(
- &self,
- operation_id: radroots_sync::policy::SyncId,
- ) -> Result<Option<PushStatus>, Error> {
- self.engine.push_status(operation_id).await
- }
-
- /// Runs one bounded signing phase for an exactly prepared operation.
- pub async fn sign_prepared(&self, request: PushRequest) -> Result<SigningRunReceipt, Error> {
- self.engine.sign_prepared(request).await
- }
-
- /// Runs one bounded local-admission phase for a durably signed artifact.
- pub async fn admit_signed(
- &self,
- operation_id: radroots_sync::policy::SyncId,
- ) -> Result<AdmissionRunReceipt, Error> {
- self.engine.admit_signed(operation_id).await
- }
-
- /// Prepares, signs, and locally admits one authored operation.
- ///
- /// Preparation commits the complete recoverable intent before the signer
- /// can be invoked. Delivery remains an explicit caller-driven phase.
- pub async fn submit_push(&self, request: PushRequest) -> Result<PushStatus, Error> {
- let operation_id = request.operation_id();
- self.sign_prepared(request).await?;
- self.admit_signed(operation_id).await?;
- self.push_status(operation_id)
- .await?
- .ok_or(Error::StorageFailed)
- }
-
- /// Runs one bounded delivery attempt for one durable authored plan.
- pub async fn deliver_push(
- &self,
- operation_id: radroots_sync::policy::SyncId,
- ) -> Result<DeliveryExecutionReceipt, Error> {
- self.engine.deliver_push(operation_id).await
- }
-
- /// Returns the native passive sync status without starting recovery work.
- pub async fn status(&self, projections: &[ProjectionId]) -> Result<SyncStatus, Error> {
- self.engine.status(projections).await
- }
-
- /// Returns the native host scheduling decision for one durable plan.
- pub fn retry_decision(
- &self,
- plan: &AuthoredDeliveryPlan,
- now_unix_ms: u64,
- ) -> Result<radroots_protocol::runtime::v1::SyncRetryDecision, Error> {
- self.engine.retry_decision(plan, now_unix_ms)
- }
-}
-
-#[cfg(feature = "sync")]
-impl std::fmt::Debug for Operations<'_> {
- fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- formatter
- .debug_struct("Operations")
- .field("engine", &"<borrowed canonical engine>")
- .finish()
- }
-}
-
-#[cfg(all(test, feature = "sync", feature = "memory"))]
-mod tests {
- use std::sync::{
- Arc,
- atomic::{AtomicU8, Ordering},
- };
-
- use radroots_event::{
- GenericEventDraft, SignedEvent, contract::AuthorRole, wire::Nip01EventWire,
- };
- use radroots_event_codec::authoring::AuthoredEventPlan;
- use radroots_identity::PublicKey;
- use radroots_protocol::runtime::v1::SyncCapabilityState;
- use radroots_signing::{Actor, actor::ActorSource, request::CancellationPolicy};
- use radroots_storage::{
- event::{SourceGeneration, StoredVisibleEvent},
- journal::IdempotencyKey,
- memory::MemoryStorage,
- projection::{
- ProjectionGeneration, ProjectionId, RawSourceDigest, RebuildFailure, RebuildTicketId,
- },
- };
- use radroots_sync::{
- Engine, PullRequest, PushRequest,
- ingest::RegistryPolicy,
- policy::{Clock, DeadlinePolicy, Error, IdSource, OperationKind, SyncId, SyncStorage},
- projection::{Reducer, ReducerError, RefreshRequest, RefreshState},
- pull::PullTermination,
- };
- use radroots_transport::{
- Error as TransportError, EventSource, FetchPage, FetchRequest, SourceStatus, Target,
- TargetSet, TransportId,
- capability::{Availability, Maturity, SourceCapabilities},
- policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy},
- source::{EventProvenance, NextPage, ObservedEvent},
- };
-
- use crate::{ClientBuilder, error::ErrorKind};
-
- const PUBLIC_KEY: &str = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
-
- struct FixedClock;
- struct SequenceIds(AtomicU8);
- struct CancelledSource;
-
- impl Clock for FixedClock {
- fn now_unix_ms(&self) -> Result<u64, Error> {
- Ok(1_700_000_000_000)
- }
- }
-
- impl IdSource for SequenceIds {
- fn next_id(&self, _operation: OperationKind) -> Result<SyncId, Error> {
- let next = self.0.fetch_add(1, Ordering::Relaxed);
- SyncId::new([next; 16])
- }
- }
-
- impl EventSource for CancelledSource {
- fn status(
- &self,
- ) -> radroots_transport::BoxFuture<'_, Result<SourceStatus, TransportError>> {
- Box::pin(async {
- Ok(SourceStatus::new(
- TransportId::NOSTR,
- true,
- Maturity::Stable,
- Availability::Available,
- SourceCapabilities::FETCH,
- "ready",
- ))
- })
- }
-
- fn fetch(
- &self,
- request: FetchRequest,
- ) -> radroots_transport::BoxFuture<'_, Result<FetchPage, TransportError>> {
- Box::pin(async move {
- FetchPage::for_request(
- &request,
- Vec::new(),
- Vec::new(),
- NextPage::Cancelled { resume_from: None },
- )
- })
- }
- }
-
- struct EmptyReducer {
- id: ProjectionId,
- generation: ProjectionGeneration,
- }
-
- impl Reducer for EmptyReducer {
- fn projection_id(&self) -> &ProjectionId {
- &self.id
- }
-
- fn generation(&self) -> ProjectionGeneration {
- self.generation
- }
-
- fn begin_rebuild(
- &self,
- _ticket_id: RebuildTicketId,
- _source_generation: SourceGeneration,
- _source_digest: RawSourceDigest,
- ) -> Result<(), ReducerError> {
- Ok(())
- }
-
- fn reduce(
- &self,
- events: &[StoredVisibleEvent],
- prior_projected_rows: u64,
- _rebuild_ticket: Option<RebuildTicketId>,
- ) -> Result<u64, ReducerError> {
- assert!(events.is_empty());
- Ok(prior_projected_rows)
- }
-
- fn abort_rebuild(
- &self,
- _ticket_id: RebuildTicketId,
- _failure: RebuildFailure,
- ) -> Result<(), ReducerError> {
- Ok(())
- }
- }
-
- fn target() -> Target {
- Target::nostr_relay("wss://sync.example").expect("target")
- }
-
- fn engine(storage: Arc<MemoryStorage>) -> Engine {
- let capability: Arc<dyn SyncStorage> = storage;
- Engine::builder(
- capability,
- Arc::new(FixedClock),
- Arc::new(SequenceIds(AtomicU8::new(1))),
- DeadlinePolicy::new(1_000, 1_000, 1_000).expect("deadlines"),
- )
- .source(Arc::new(CancelledSource))
- .build()
- .expect("engine")
- }
-
- fn invalid_observation() -> ObservedEvent {
- let mut wire = Nip01EventWire {
- id: "0".repeat(64),
- pubkey: PUBLIC_KEY.to_owned(),
- created_at: 1_800_000_100,
- kind: 0,
- tags: vec![],
- content: "invalid signature fixture".to_owned(),
- sig: "42".repeat(64),
- extra: Default::default(),
- };
- wire.id = wire.computed_event_id().expect("event id").to_hex();
- let raw = format!(
- "{{\"id\":\"{}\",\"pubkey\":\"{}\",\"created_at\":{},\"kind\":0,\"tags\":[],\"content\":\"invalid signature fixture\",\"sig\":\"{}\"}}",
- wire.id, wire.pubkey, wire.created_at, wire.sig
- );
- let event = SignedEvent::from_wire_verified_id(wire, raw).expect("signed event");
- let target = target();
- let provenance = EventProvenance::new(
- TransportId::NOSTR,
- target.fingerprint().clone(),
- 1_700_000_000_000,
- )
- .expect("provenance");
- ObservedEvent::new(event, provenance)
- }
-
- fn push_request() -> PushRequest {
- let actor = Actor::new(
- PublicKey::from_hex(PUBLIC_KEY).expect("public key"),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Any],
- )
- .expect("actor");
- let plan = AuthoredEventPlan::from_generic(
- GenericEventDraft::new(
- "radroots.social.geochat.v1",
- 20_000,
- 1_700_000_000,
- Vec::new(),
- "content",
- PUBLIC_KEY,
- )
- .expect("draft"),
- )
- .expect("authored plan");
- PushRequest::new(
- SyncId::new([8; 16]).expect("operation id"),
- IdempotencyKey::parse("sdk-sync-wrapper").expect("idempotency key"),
- actor,
- plan,
- TargetSet::new(vec![target()]).expect("targets"),
- SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::all()),
- 1_700_000_001_000,
- CancellationPolicy::PreservePublishedRequest,
- )
- .expect("push request")
- }
-
- #[tokio::test]
- async fn operations_delegate_native_pull_ingest_projection_push_delivery_and_status() {
- let storage = Arc::new(MemoryStorage::new(
- SourceGeneration::new([3; 32]).expect("generation"),
- ));
- let client = ClientBuilder::new()
- .storage(storage.clone())
- .sync_engine(engine(storage))
- .build()
- .expect("client");
- let operations = client.sync().expect("open client").expect("sync");
- let policy = RegistryPolicy::verified();
-
- let pull = operations
- .pull(
- PullRequest::new(TargetSet::new(vec![target()]).expect("targets"), 10, 1)
- .expect("pull request"),
- &policy,
- )
- .await
- .expect("pull");
- assert_eq!(pull.termination(), PullTermination::Cancelled);
-
- let ingest = operations
- .ingest_batch(vec![invalid_observation(), invalid_observation()], &policy)
- .await;
- assert_eq!(ingest.accepted(), 0);
- assert_eq!(ingest.rejected(), 2);
- assert!(
- ingest
- .outcomes()
- .iter()
- .all(|outcome| matches!(outcome, Err(Error::VerificationFailed)))
- );
-
- let projection_id = ProjectionId::parse("sdk.sync.test").expect("projection id");
- let generation = ProjectionGeneration::new([5; 32]).expect("generation");
- let projection = operations
- .refresh_projection(
- RefreshRequest::new(projection_id.clone(), generation, 10, 1)
- .expect("refresh request"),
- &EmptyReducer {
- id: projection_id.clone(),
- generation,
- },
- )
- .await
- .expect("projection");
- assert_eq!(projection.state(), RefreshState::Complete);
-
- let push = push_request();
- let operation_id = push.operation_id();
- let preparation = operations
- .prepare_push(push.clone())
- .await
- .expect("durable preparation");
- assert!(!preparation.is_replay());
- assert_eq!(
- operations.sign_prepared(push).await,
- Err(Error::MissingSigner)
- );
- assert!(
- operations
- .push_status(operation_id)
- .await
- .expect("push status")
- .is_some()
- );
- assert_eq!(
- operations.deliver_push(operation_id).await,
- Err(Error::MissingSink)
- );
-
- let status = operations
- .status(std::slice::from_ref(&projection_id))
- .await
- .expect("status");
- assert_eq!(status.source().state(), SyncCapabilityState::Available);
- assert_eq!(status.sink().state(), SyncCapabilityState::Unsupported);
- assert_eq!(status.projections().len(), 1);
-
- client.close().await.expect("close");
- assert_eq!(
- client.sync().expect_err("closed").kind(),
- ErrorKind::ClientClosed
- );
- }
-}
diff --git a/crates/sdk/src/trade.rs b/crates/sdk/src/trade.rs
@@ -1,868 +0,0 @@
-//! Canonical trade planning, commit, query, and private-evidence operations.
-
-use std::{error, fmt};
-
-use radroots_event::{
- GenericEventDraft,
- contract::AuthorRole,
- trade::{TradeMutationEnvelopeV1, TradeProtocolError, canonical_trade_mutation_content},
-};
-use radroots_event_codec::{
- authoring::AuthoredEventPlan, encode::EventEncodeError,
- encode::trade::trade_mutation_event_build,
-};
-use radroots_signing::Actor;
-use radroots_trade::{Projection, ReductionInput, WorkflowPlan, reducer::reduce_trade_records};
-
-/// Pure inputs for one frozen trade command.
-#[derive(Clone, Debug)]
-pub struct PrepareRequest {
- actor: Actor,
- mutation: TradeMutationEnvelopeV1,
-}
-
-impl PrepareRequest {
- /// Creates explicit inputs for any canonical proposal, revision, decision,
- /// cancellation, or resumable mutation.
- #[must_use]
- pub const fn new(actor: Actor, mutation: TradeMutationEnvelopeV1) -> Self {
- Self { actor, mutation }
- }
-}
-
-/// Frozen, replay-stable trade workflow plan.
-#[derive(Clone, Debug)]
-pub struct Plan {
- actor: Actor,
- workflow: WorkflowPlan,
- authored_event: AuthoredEventPlan,
-}
-
-impl Plan {
- /// Returns the exact authorized actor carried into signing.
- #[must_use]
- pub const fn actor(&self) -> &Actor {
- &self.actor
- }
-
- /// Returns the lower-owned validated workflow and required host actions.
- pub const fn workflow(&self) -> &WorkflowPlan {
- &self.workflow
- }
-
- /// Returns the immutable canonical authored event plan.
- #[must_use]
- pub const fn authored_event(&self) -> &AuthoredEventPlan {
- &self.authored_event
- }
-}
-
-/// Trade planning failure stage.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-#[non_exhaustive]
-pub enum PrepareErrorKind {
- /// The actor identity or role cannot author the supplied mutation.
- UnauthorizedActor,
- /// Event-domain canonicalization rejected the mutation.
- CanonicalMutation,
- /// The lower trade workflow rejected the canonical mutation.
- Workflow,
- /// The canonical event codec rejected the mutation.
- Encode,
- /// The canonical event draft rejected the encoded mutation.
- Draft,
-}
-
-/// One secret-safe trade planning failure retaining its lower source.
-pub struct PrepareError {
- kind: PrepareErrorKind,
- source: Option<Box<dyn error::Error + Send + Sync>>,
-}
-
-impl PrepareError {
- /// Returns the stable client-level planning stage.
- #[must_use]
- pub const fn kind(&self) -> PrepareErrorKind {
- self.kind
- }
-
- fn unauthorized_actor() -> Self {
- Self {
- kind: PrepareErrorKind::UnauthorizedActor,
- source: None,
- }
- }
-
- fn canonical(source: TradeProtocolError) -> Self {
- Self::with_source(PrepareErrorKind::CanonicalMutation, source)
- }
-
- fn workflow(source: radroots_trade::Error) -> Self {
- Self::with_source(PrepareErrorKind::Workflow, source)
- }
-
- fn encode(source: EventEncodeError) -> Self {
- Self::with_source(PrepareErrorKind::Encode, source)
- }
-
- fn draft(source: radroots_event::draft::DraftError) -> Self {
- Self::with_source(PrepareErrorKind::Draft, source)
- }
-
- fn with_source(
- kind: PrepareErrorKind,
- source: impl error::Error + Send + Sync + 'static,
- ) -> Self {
- Self {
- kind,
- source: Some(Box::new(source)),
- }
- }
-}
-
-impl fmt::Display for PrepareError {
- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- formatter.write_str(match self.kind {
- PrepareErrorKind::UnauthorizedActor => "trade actor is not authorized",
- PrepareErrorKind::CanonicalMutation => "trade mutation is not canonical",
- PrepareErrorKind::Workflow => "trade workflow is invalid",
- PrepareErrorKind::Encode => "trade event encoding failed",
- PrepareErrorKind::Draft => "trade event draft is invalid",
- })
- }
-}
-
-impl fmt::Debug for PrepareError {
- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- formatter
- .debug_struct("PrepareError")
- .field("kind", &self.kind)
- .finish_non_exhaustive()
- }
-}
-
-impl error::Error for PrepareError {
- fn source(&self) -> Option<&(dyn error::Error + 'static)> {
- self.source
- .as_deref()
- .map(|source| source as &(dyn error::Error + 'static))
- }
-}
-
-/// Canonicalizes, authorizes, validates, and freezes one trade command.
-///
-/// This operation performs no signing, persistence, private-artifact access,
-/// scheduling, or delivery. Every proposal, revision, decision, cancellation,
-/// and resumed command uses the same lower-owned `TradeId` and workflow law.
-pub fn prepare(request: PrepareRequest) -> Result<Plan, PrepareError> {
- let canonical = canonical_trade_mutation_content(request.mutation)
- .map_err(PrepareError::canonical)?
- .envelope;
- let required_role = match (
- canonical.author_pubkey == canonical.buyer_pubkey,
- canonical.author_pubkey == canonical.seller_pubkey,
- ) {
- (true, _) => AuthorRole::Buyer,
- (false, true) => AuthorRole::Seller,
- (false, false) => return Err(PrepareError::unauthorized_actor()),
- };
- if (
- request.actor.public_key() == canonical.author_pubkey,
- request.actor.satisfies(required_role),
- ) != (true, true)
- {
- return Err(PrepareError::unauthorized_actor());
- }
- let workflow = WorkflowPlan::prepare(canonical.clone()).map_err(PrepareError::workflow)?;
- let parts = trade_mutation_event_build(canonical.clone()).map_err(PrepareError::encode)?;
- let authored_event = AuthoredEventPlan::from_generic(
- GenericEventDraft::new(
- canonical.contract_id.clone(),
- parts.kind,
- canonical.authored_at_unix_s,
- parts.tags,
- parts.content,
- canonical.author_pubkey.to_hex(),
- )
- .map_err(PrepareError::draft)?,
- )
- .map_err(PrepareError::draft)?;
- Ok(Plan {
- actor: request.actor,
- workflow,
- authored_event,
- })
-}
-
-/// Deterministically reduces caller-supplied canonical evidence.
-#[must_use]
-pub fn project(input: ReductionInput) -> Projection {
- reduce_trade_records(input)
-}
-
-#[cfg(feature = "sync")]
-use radroots_signing::request::CancellationPolicy;
-#[cfg(feature = "sync")]
-use radroots_storage::{
- event::{EventPage, EventQuery, StoredVisibleEvent},
- journal::IdempotencyKey,
- private_artifact::{PrivateArtifactId, PrivateArtifactMetadata, PrivateArtifactStage},
-};
-#[cfg(feature = "sync")]
-use radroots_sync::{
- policy::{Error as SyncError, SyncId},
- push::PushStatus,
-};
-
-/// Explicit commit inputs for one prepared trade command.
-#[cfg(feature = "sync")]
-#[derive(Clone, Debug)]
-pub struct EnqueueRequest {
- operation_id: SyncId,
- idempotency_key: IdempotencyKey,
- plan: Plan,
- profile: crate::transport::Profile,
- delivery_deadline_unix_ms: u64,
- cancellation: CancellationPolicy,
-}
-
-#[cfg(feature = "sync")]
-impl EnqueueRequest {
- /// Creates a command request whose transport selection has no fallback.
- #[must_use]
- pub const fn new(
- operation_id: SyncId,
- idempotency_key: IdempotencyKey,
- plan: Plan,
- profile: crate::transport::Profile,
- delivery_deadline_unix_ms: u64,
- cancellation: CancellationPolicy,
- ) -> Self {
- Self {
- operation_id,
- idempotency_key,
- plan,
- profile,
- delivery_deadline_unix_ms,
- cancellation,
- }
- }
-}
-
-/// Private-term metadata verification failure.
-#[cfg(feature = "sync")]
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-#[non_exhaustive]
-pub enum PrivateTermsError {
- /// Canonical storage failed to inspect the requested metadata.
- Storage,
- /// The workflow does not require private terms.
- NotRequired,
- /// Metadata is absent, inactive, or does not match the public commitment.
- EvidenceMismatch,
-}
-
-/// Borrowed trade operations over canonical storage and sync capabilities.
-#[cfg(feature = "sync")]
-#[derive(Clone, Copy)]
-pub struct Operations<'a> {
- storage: &'a dyn radroots_storage::Storage,
- sync: crate::sync::Operations<'a>,
-}
-
-#[cfg(feature = "sync")]
-impl fmt::Debug for Operations<'_> {
- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- formatter
- .debug_struct("Operations")
- .field("storage", &"<borrowed canonical storage>")
- .field("sync", &self.sync)
- .finish()
- }
-}
-
-#[cfg(feature = "sync")]
-impl<'a> Operations<'a> {
- pub(crate) const fn new(
- storage: &'a dyn radroots_storage::Storage,
- sync: crate::sync::Operations<'a>,
- ) -> Self {
- Self { storage, sync }
- }
-
- /// Durably prepares, signs, and locally admits a command. Reusing the same
- /// idempotency input is the canonical resume/replay operation.
- pub async fn enqueue(&self, request: EnqueueRequest) -> Result<PushStatus, SyncError> {
- let targets = request
- .profile
- .targets()
- .cloned()
- .ok_or(SyncError::InvalidPushRequest)?;
- let satisfaction = request
- .profile
- .satisfaction()
- .cloned()
- .ok_or(SyncError::InvalidPushRequest)?;
- self.sync
- .submit_push(radroots_sync::PushRequest::new(
- request.operation_id,
- request.idempotency_key,
- request.plan.actor,
- request.plan.authored_event,
- targets,
- satisfaction,
- request.delivery_deadline_unix_ms,
- request.cancellation,
- )?)
- .await
- }
-
- /// Returns one native, bounded, generation-bound page of visible evidence.
- pub async fn query_visible(
- &self,
- query: EventQuery,
- ) -> Result<EventPage<StoredVisibleEvent>, radroots_storage::Error> {
- radroots_storage::event::EventStore::query_visible(self.storage, query).await
- }
-
- /// Returns native private-artifact metadata without reading secret material.
- pub async fn private_artifact(
- &self,
- artifact_id: PrivateArtifactId,
- ) -> Result<Option<PrivateArtifactMetadata>, radroots_storage::Error> {
- radroots_storage::private_artifact::PrivateArtifactStore::metadata(
- self.storage,
- artifact_id,
- )
- .await
- }
-
- /// Verifies that canonical active metadata matches a plan's public schema
- /// and ciphertext commitment. Plaintext, ciphertext, and keys never cross
- /// the SDK boundary.
- pub async fn verify_private_terms(
- &self,
- plan: &Plan,
- artifact_id: PrivateArtifactId,
- ) -> Result<PrivateArtifactMetadata, PrivateTermsError> {
- let expected = plan
- .workflow
- .private_terms()
- .ok_or(PrivateTermsError::NotRequired)?;
- let metadata = self
- .private_artifact(artifact_id)
- .await
- .map_err(|_| PrivateTermsError::Storage)?
- .ok_or(PrivateTermsError::EvidenceMismatch)?;
- let commitment = hex_lower(metadata.commitment().as_bytes());
- let evidence_matches = [
- metadata.stage() == PrivateArtifactStage::Active,
- metadata.schema_id().as_str() == expected.schema_id(),
- commitment == expected.ciphertext_commitment(),
- ];
- if evidence_matches != [true; 3] {
- return Err(PrivateTermsError::EvidenceMismatch);
- }
- Ok(metadata)
- }
-}
-
-#[cfg(feature = "sync")]
-fn hex_lower(bytes: &[u8]) -> String {
- const HEX: &[u8; 16] = b"0123456789abcdef";
- let mut encoded = String::with_capacity(bytes.len() * 2);
- for byte in bytes {
- encoded.push(char::from(HEX[usize::from(byte >> 4)]));
- encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
- }
- encoded
-}
-
-#[cfg(test)]
-mod tests {
- use radroots_event::{
- id::{ClassifiedListingAddress, DTag, EventId, InventoryBinId, MutationId, TradeId},
- trade::{
- FulfillmentProfileV1, RADROOTS_TRADE_CANCELLATION_CONTRACT_ID,
- RADROOTS_TRADE_DECISION_CONTRACT_ID, RADROOTS_TRADE_PROPOSAL_CONTRACT_ID,
- RADROOTS_TRADE_REVISION_DECISION_CONTRACT_ID,
- RADROOTS_TRADE_REVISION_PROPOSAL_CONTRACT_ID, RADROOTS_TRADE_SCHEMA_VERSION,
- TradeCancellationProfileV1, TradeCandidateLineV1, TradeCandidateTermsV1,
- TradeDecisionV1, TradeEconomicAdjustmentV1, TradeEconomicsProfileV1,
- TradeLineTombstoneV1, TradeMutationBodyV1, TradeMutationKindV1, TradePrivateTermsRefV1,
- },
- };
- use radroots_identity::PublicKey;
- use radroots_signing::actor::ActorSource;
- use radroots_trade::workflow::WorkflowAction;
-
- use super::*;
-
- const BUYER: &str = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
- const SELLER: &str = "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af";
-
- fn pubkey(value: &str) -> PublicKey {
- PublicKey::from_hex(value).expect("public key")
- }
-
- fn actor(public_key: &str, role: AuthorRole) -> Actor {
- Actor::from_public_key_hex(public_key, ActorSource::ExplicitPublicKey, [role])
- .expect("actor")
- }
-
- fn mutation_id(marker: char) -> MutationId {
- MutationId::parse(std::iter::repeat_n(marker, 64).collect::<String>()).expect("mutation id")
- }
-
- fn candidate(suffix: &str) -> TradeCandidateTermsV1 {
- TradeCandidateTermsV1 {
- candidate_id: None,
- schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
- base_candidate_id: None,
- supersession_intent: None,
- buyer_pubkey: pubkey(BUYER),
- seller_pubkey: pubkey(SELLER),
- farm_id: DTag::parse("farm-1").expect("farm id"),
- lines: vec![TradeCandidateLineV1 {
- line_id: DTag::parse(format!("line-{suffix}")).expect("line id"),
- listing_addr: ClassifiedListingAddress::parse(format!(
- "30402:{SELLER}:listing-{suffix}"
- ))
- .expect("listing address"),
- listing_event_id: EventId::parse("cc".repeat(32)).expect("event id"),
- listing_snapshot_sha256: "dd".repeat(32),
- product_id: format!("carrots-{suffix}"),
- option_id: None,
- bin_id: InventoryBinId::parse(format!("bin-{suffix}")).expect("bin id"),
- quantity_mantissa: "2".into(),
- quantity_scale: 0,
- unit_code: "count".into(),
- unit_profile: "mvp-count".into(),
- unit_price_mantissa: "500".into(),
- currency_code: "USD".into(),
- line_subtotal_mantissa: "1000".into(),
- replaces_line_id: None,
- }],
- line_tombstones: Vec::<TradeLineTombstoneV1>::new(),
- economics: TradeEconomicsProfileV1 {
- profile_id: "mvp-fixed".into(),
- currency_code: "USD".into(),
- currency_exponent: 2,
- rounding_profile: "half-even".into(),
- subtotal_mantissa: "1000".into(),
- discount_total_mantissa: "0".into(),
- adjustment_total_mantissa: "0".into(),
- total_mantissa: "1000".into(),
- adjustments: Vec::<TradeEconomicAdjustmentV1>::new(),
- },
- fulfillment: FulfillmentProfileV1 {
- profile_id: "market-pickup".into(),
- method: "pickup".into(),
- starts_at_unix_s: 1_800_000_000,
- ends_at_unix_s: 1_800_003_600,
- timezone: "America/New_York".into(),
- utc_offset_seconds: -18_000,
- fold: 0,
- location_class: "farmstand".into(),
- requires_private_terms: true,
- },
- cancellation: TradeCancellationProfileV1 {
- profile_id: "buyer-pre-agreement".into(),
- buyer_pre_agreement: true,
- post_agreement_cutoff_unix_s: None,
- },
- private_terms: Some(TradePrivateTermsRefV1 {
- artifact_id: "artifact-1".into(),
- schema_id: "radroots.private.fulfillment.v1".into(),
- ciphertext_commitment: "ee".repeat(32),
- required_acknowledgement: true,
- }),
- proposal_expires_at_unix_s: 1_800_010_000,
- }
- }
-
- fn envelope(contract_id: &str, body: TradeMutationBodyV1) -> TradeMutationEnvelopeV1 {
- let initial = body.mutation_kind() == TradeMutationKindV1::Proposal;
- TradeMutationEnvelopeV1 {
- mutation_id: None,
- contract_id: contract_id.into(),
- schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
- trade_id: TradeId::parse("11".repeat(16)).expect("trade id"),
- root_mutation_id: (!initial).then(|| mutation_id('1')),
- buyer_pubkey: pubkey(BUYER),
- seller_pubkey: pubkey(SELLER),
- farm_id: DTag::parse("farm-1").expect("farm id"),
- parent_mutation_ids: if initial {
- vec![]
- } else {
- vec![mutation_id('1')]
- },
- author_pubkey: pubkey(BUYER),
- counterparty_pubkey: pubkey(SELLER),
- authored_at_unix_s: 1_800_000_000,
- body,
- }
- }
-
- fn all_commands() -> Vec<TradeMutationEnvelopeV1> {
- let proposal = canonical_trade_mutation_content(envelope(
- RADROOTS_TRADE_PROPOSAL_CONTRACT_ID,
- TradeMutationBodyV1::Proposal {
- candidate: candidate("1"),
- },
- ))
- .expect("proposal")
- .envelope;
- let proposal_id = proposal.mutation_id.expect("proposal id");
- let candidate_id = match &proposal.body {
- TradeMutationBodyV1::Proposal { candidate } => {
- candidate.candidate_id.expect("candidate id")
- }
- _ => unreachable!(),
- };
- vec![
- proposal,
- envelope(
- RADROOTS_TRADE_REVISION_PROPOSAL_CONTRACT_ID,
- TradeMutationBodyV1::RevisionProposal {
- candidate: candidate("2"),
- },
- ),
- envelope(
- RADROOTS_TRADE_DECISION_CONTRACT_ID,
- TradeMutationBodyV1::Decision {
- proposal_mutation_id: proposal_id,
- candidate_id,
- decision: TradeDecisionV1::Declined {
- reason: "unavailable".into(),
- },
- },
- ),
- envelope(
- RADROOTS_TRADE_REVISION_DECISION_CONTRACT_ID,
- TradeMutationBodyV1::RevisionDecision {
- proposal_mutation_id: mutation_id('2'),
- candidate_id,
- decision: TradeDecisionV1::Declined {
- reason: "unavailable".into(),
- },
- },
- ),
- envelope(
- RADROOTS_TRADE_CANCELLATION_CONTRACT_ID,
- TradeMutationBodyV1::Cancellation {
- target_candidate_id: Some(candidate_id),
- target_claim_mutation_id: None,
- reason: "cancelled".into(),
- },
- ),
- ]
- }
-
- #[test]
- fn prepare_covers_every_command_with_one_trade_identity_and_private_plan() {
- let plans = all_commands()
- .into_iter()
- .map(|mutation| {
- prepare(PrepareRequest::new(
- actor(BUYER, AuthorRole::Buyer),
- mutation,
- ))
- })
- .collect::<Result<Vec<_>, _>>()
- .expect("plans");
-
- assert_eq!(
- plans
- .iter()
- .map(|plan| plan.workflow().kind())
- .collect::<Vec<_>>(),
- [
- TradeMutationKindV1::Proposal,
- TradeMutationKindV1::RevisionProposal,
- TradeMutationKindV1::Decision,
- TradeMutationKindV1::RevisionDecision,
- TradeMutationKindV1::Cancellation,
- ]
- );
- assert!(
- plans
- .iter()
- .all(|plan| plan.workflow().trade_id() == plans[0].workflow().trade_id())
- );
- assert_eq!(
- plans[0].workflow().required_actions()[0],
- WorkflowAction::VerifyPrivateTerms
- );
- assert_eq!(
- plans[0]
- .workflow()
- .private_terms()
- .expect("private terms")
- .artifact_id(),
- "artifact-1"
- );
- for plan in plans {
- assert_eq!(
- plan.authored_event()
- .body()
- .contract()
- .contract_id()
- .as_str(),
- plan.workflow().kind().contract_id()
- );
- assert_eq!(
- plan.authored_event().body().kind(),
- plan.workflow().kind().nostr_kind()
- );
- }
- }
-
- #[test]
- fn prepare_rejects_wrong_identity_role_and_invalid_protocol_once() {
- let mutation = all_commands().remove(0);
- let wrong_role = prepare(PrepareRequest::new(
- actor(BUYER, AuthorRole::Seller),
- mutation.clone(),
- ))
- .expect_err("wrong role");
- assert_eq!(wrong_role.kind(), PrepareErrorKind::UnauthorizedActor);
- assert!(std::error::Error::source(&wrong_role).is_none());
-
- let wrong_identity = prepare(PrepareRequest::new(
- actor(SELLER, AuthorRole::Buyer),
- mutation,
- ))
- .expect_err("wrong identity");
- assert_eq!(wrong_identity.kind(), PrepareErrorKind::UnauthorizedActor);
-
- let mut invalid = all_commands().remove(0);
- invalid.contract_id = "radroots.trade.cancellation.v1".into();
- let canonical = prepare(PrepareRequest::new(
- actor(BUYER, AuthorRole::Buyer),
- invalid,
- ))
- .expect_err("invalid contract");
- assert_eq!(canonical.kind(), PrepareErrorKind::CanonicalMutation);
- assert!(std::error::Error::source(&canonical).is_some());
- assert!(!format!("{canonical:?}").contains("artifact-1"));
-
- let mut seller_authored = all_commands().remove(0);
- seller_authored.mutation_id = None;
- seller_authored.author_pubkey = pubkey(SELLER);
- seller_authored.counterparty_pubkey = pubkey(BUYER);
- let seller_plan = prepare(PrepareRequest::new(
- actor(SELLER, AuthorRole::Seller),
- seller_authored,
- ))
- .expect("seller-authored command");
- assert_eq!(
- seller_plan.workflow().trade_id(),
- &TradeId::parse("11".repeat(16)).unwrap()
- );
-
- let outsider = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
- let mut unauthorized = all_commands().remove(0);
- unauthorized.mutation_id = None;
- unauthorized.author_pubkey = pubkey(outsider);
- assert_eq!(
- prepare(PrepareRequest::new(
- actor(outsider, AuthorRole::Any),
- unauthorized,
- ))
- .expect_err("author must be a governed party")
- .kind(),
- PrepareErrorKind::UnauthorizedActor
- );
- }
-
- #[test]
- fn query_projection_returns_the_lower_projection_and_conflict_evidence_types() {
- let trade_id = TradeId::parse("22".repeat(16)).expect("trade id");
- let projection = project(ReductionInput::new(trade_id));
- assert_eq!(projection.trade_id(), &trade_id);
- assert!(projection.candidate_heads().is_empty());
- assert!(!projection.projection_digest().is_empty());
- }
-
- #[cfg(all(feature = "sync", feature = "memory", feature = "local-signing"))]
- mod operations {
- use std::sync::{
- Arc,
- atomic::{AtomicU8, Ordering},
- };
-
- use radroots_nostr::key::SecretKey;
- use radroots_signing::request::CancellationPolicy;
- use radroots_storage::{
- Outbox,
- event::{EventQuery, EventQueryBounds, SourceGeneration},
- journal::IdempotencyKey,
- memory::MemoryStorage,
- private_artifact::{
- ArtifactCommitment, ArtifactKind, ArtifactSchemaId, DurableSecretReference,
- PrivateArtifactId, PrivateArtifactMetadata, PrivateArtifactStore, RetentionPolicy,
- },
- };
- use radroots_sync::{
- Engine,
- policy::{Clock, DeadlinePolicy, Error, IdSource, OperationKind, SyncId, SyncStorage},
- };
- use radroots_transport::{
- DeliveryReceipt, DeliveryRequest, Error as TransportError, EventSink, SinkFailure,
- SinkStatus, Target, TargetSet, TransportId,
- capability::{Availability, Maturity, SinkCapabilities},
- outcome::Retryability,
- policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy},
- };
-
- use super::*;
- use crate::{ClientBuilder, transport::Profile};
-
- const BUYER_SECRET: &str =
- "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
-
- struct HostClock;
- struct SequenceIds(AtomicU8);
- struct NoopSink;
-
- impl Clock for HostClock {
- fn now_unix_ms(&self) -> Result<u64, Error> {
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .ok()
- .and_then(|duration| u64::try_from(duration.as_millis()).ok())
- .filter(|value| *value != 0)
- .ok_or(Error::ClockUnavailable)
- }
- }
- impl IdSource for SequenceIds {
- fn next_id(&self, _operation: OperationKind) -> Result<SyncId, Error> {
- SyncId::new([self.0.fetch_add(1, Ordering::Relaxed); 16])
- }
- }
- impl EventSink for NoopSink {
- fn status(
- &self,
- ) -> radroots_transport::BoxFuture<'_, Result<SinkStatus, TransportError>> {
- Box::pin(async {
- Ok(SinkStatus::new(
- TransportId::NOSTR,
- true,
- Maturity::Stable,
- Availability::Available,
- SinkCapabilities::DELIVER,
- "ready",
- ))
- })
- }
- fn deliver(
- &self,
- request: DeliveryRequest,
- ) -> radroots_transport::BoxFuture<'_, Result<DeliveryReceipt, SinkFailure>>
- {
- Box::pin(async move {
- Err(SinkFailure::for_request(
- &request,
- "test_sink_unavailable",
- Retryability::Terminal,
- None,
- None,
- Vec::new(),
- )
- .expect("test sink failure"))
- })
- }
- }
-
- #[tokio::test]
- async fn operations_cover_private_evidence_pagination_commit_replay_and_cancellation() {
- let storage = Arc::new(MemoryStorage::new(
- SourceGeneration::new([6; 32]).expect("generation"),
- ));
- let artifact_id = PrivateArtifactId::new([7; 16]).expect("artifact id");
- let metadata = PrivateArtifactMetadata::new(
- artifact_id,
- ArtifactKind::parse("trade.private_terms").expect("kind"),
- ArtifactSchemaId::parse("radroots.private.fulfillment.v1").expect("schema"),
- ArtifactCommitment::new([0xee; 32]),
- 64,
- DurableSecretReference::new("memory", "trade-artifact-1", 1).expect("reference"),
- RetentionPolicy::indefinite(),
- 1_800_000_000_000,
- )
- .expect("metadata");
- PrivateArtifactStore::put_metadata(storage.as_ref(), metadata)
- .await
- .expect("store metadata");
-
- let signer = Arc::new(
- radroots_nostr::signing::LocalSigner::new(
- SecretKey::parse(BUYER_SECRET).expect("secret"),
- )
- .expect("signer"),
- );
- let capability: Arc<dyn SyncStorage> = storage.clone();
- let engine = Engine::builder(
- capability,
- Arc::new(HostClock),
- Arc::new(SequenceIds(AtomicU8::new(1))),
- DeadlinePolicy::new(30_000, 30_000, 30_000).expect("deadlines"),
- )
- .sink(Arc::new(NoopSink))
- .signer(signer)
- .build()
- .expect("engine");
- let client = ClientBuilder::new()
- .storage(storage.clone())
- .sync_engine(engine)
- .build()
- .expect("client");
- let operations = client.trade().expect("open").expect("trade operations");
- let plan = prepare(PrepareRequest::new(
- actor(BUYER, AuthorRole::Buyer),
- all_commands().remove(0),
- ))
- .expect("plan");
-
- let verified = operations
- .verify_private_terms(&plan, artifact_id)
- .await
- .expect("private evidence");
- assert_eq!(verified.artifact_id(), artifact_id);
- let page = operations
- .query_visible(EventQuery::all(EventQueryBounds::first(1).expect("bounds")))
- .await
- .expect("page");
- assert!(page.items().is_empty());
- assert!(page.next_cursor().is_none());
-
- let targets = TargetSet::new(vec![
- Target::nostr_relay("wss://trade.example").expect("target"),
- ])
- .expect("targets");
- let satisfaction =
- SatisfactionPolicy::new(SatisfactionClass::Delivered, TargetPolicy::all());
- let request = EnqueueRequest::new(
- SyncId::new([11; 16]).expect("operation id"),
- IdempotencyKey::parse("trade-proposal-a").expect("idempotency"),
- plan,
- Profile::delivery(targets.clone(), satisfaction.clone()).expect("profile"),
- 2_000_000_001_000,
- CancellationPolicy::PreservePublishedRequest,
- );
- drop(operations.enqueue(request.clone()));
- assert_eq!(
- Outbox::status(storage.as_ref())
- .await
- .expect("status")
- .pending,
- 0
- );
- let committed = operations.enqueue(request.clone()).await.expect("commit");
- assert_eq!(committed.delivery_plan().intent().target_set(), &targets);
- let replay = operations.enqueue(request).await.expect("resume replay");
- assert_eq!(replay, committed);
- }
- }
-}
diff --git a/crates/sdk/src/transport.rs b/crates/sdk/src/transport.rs
@@ -1,742 +0,0 @@
-//! Explicit user-facing transport profile composition.
-//!
-//! Profiles retain canonical `radroots_transport` identities, targets,
-//! policies, and statuses. They select no adapter implicitly and never replace
-//! an unavailable selection with another transport.
-
-use radroots_transport::{
- Error, SinkStatus, SourceStatus, TargetSet, TransportId,
- capability::{Availability, Maturity, SinkCapabilities, SourceCapabilities},
- policy::SatisfactionPolicy,
-};
-#[cfg(feature = "nostr")]
-use std::sync::{Arc, RwLock};
-
-#[cfg(feature = "nostr")]
-pub use radroots_transport_nostr::RelayUrlPolicy;
-
-const PREVIEW_UNAVAILABLE_MESSAGE: &str = "preview transport is unavailable in this SDK release";
-
-/// A side-effect-free transport selection for a client operation.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct Profile {
- selection: Selection,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-enum Selection {
- LocalOnly,
- Delivery {
- targets: TargetSet,
- satisfaction: SatisfactionPolicy,
- },
- UnavailablePreview {
- source: SourceStatus,
- sink: SinkStatus,
- },
-}
-
-impl Profile {
- /// Selects local persistence only, with no transport target or fallback.
- #[must_use]
- pub const fn local_only() -> Self {
- Self {
- selection: Selection::LocalOnly,
- }
- }
-
- /// Selects an exact bounded target set and canonical satisfaction policy.
- ///
- /// Impossible quorum and required-target policies are rejected here by the
- /// owning transport contract. Construction performs no network operation.
- pub fn delivery(targets: TargetSet, satisfaction: SatisfactionPolicy) -> Result<Self, Error> {
- satisfaction.validate_for(&targets)?;
- Ok(Self {
- selection: Selection::Delivery {
- targets,
- satisfaction,
- },
- })
- }
-
- /// Describes a preview transport that is intentionally not selectable.
- ///
- /// Both canonical capability directions remain explicitly unconfigured
- /// and unavailable. The profile has no targets and therefore cannot fall
- /// back to local, Nostr, daemon, or another transport.
- #[must_use]
- pub fn unavailable_preview(transport_id: TransportId) -> Self {
- Self {
- selection: Selection::UnavailablePreview {
- source: SourceStatus::new(
- transport_id,
- false,
- Maturity::Preview,
- Availability::Unavailable,
- SourceCapabilities::NONE,
- PREVIEW_UNAVAILABLE_MESSAGE,
- ),
- sink: SinkStatus::new(
- transport_id,
- false,
- Maturity::Preview,
- Availability::Unavailable,
- SinkCapabilities::NONE,
- PREVIEW_UNAVAILABLE_MESSAGE,
- ),
- },
- }
- }
-
- /// Returns whether this profile authorizes no transport operation.
- #[must_use]
- pub const fn is_local_only(&self) -> bool {
- matches!(self.selection, Selection::LocalOnly)
- }
-
- /// Returns the exact selected targets, if delivery is authorized.
- #[must_use]
- pub const fn targets(&self) -> Option<&TargetSet> {
- match &self.selection {
- Selection::Delivery { targets, .. } => Some(targets),
- Selection::LocalOnly | Selection::UnavailablePreview { .. } => None,
- }
- }
-
- /// Returns the exact selected satisfaction policy, if delivery is authorized.
- #[must_use]
- pub const fn satisfaction(&self) -> Option<&SatisfactionPolicy> {
- match &self.selection {
- Selection::Delivery { satisfaction, .. } => Some(satisfaction),
- Selection::LocalOnly | Selection::UnavailablePreview { .. } => None,
- }
- }
-
- /// Returns canonical source status for an unavailable preview.
- #[must_use]
- pub const fn source_status(&self) -> Option<&SourceStatus> {
- match &self.selection {
- Selection::UnavailablePreview { source, .. } => Some(source),
- Selection::LocalOnly | Selection::Delivery { .. } => None,
- }
- }
-
- /// Returns canonical sink status for an unavailable preview.
- #[must_use]
- pub const fn sink_status(&self) -> Option<&SinkStatus> {
- match &self.selection {
- Selection::UnavailablePreview { sink, .. } => Some(sink),
- Selection::LocalOnly | Selection::Delivery { .. } => None,
- }
- }
-}
-
-impl Default for Profile {
- fn default() -> Self {
- Self::local_only()
- }
-}
-
-/// Host-configured, client-shareable Nostr transport slot.
-///
-/// Reconfiguration validates the complete relay set before atomically
-/// replacing the active adapter. Construction, clearing, and target
-/// inspection perform no network I/O.
-#[cfg(feature = "nostr")]
-#[derive(Clone)]
-pub struct NostrSlot {
- policy: RelayUrlPolicy,
- state: Arc<RwLock<Option<NostrState>>>,
-}
-
-#[cfg(feature = "nostr")]
-#[derive(Clone)]
-struct NostrState {
- transport: Arc<radroots_transport_nostr::NostrTransport>,
- targets: TargetSet,
-}
-
-#[cfg(feature = "nostr")]
-impl NostrSlot {
- /// Creates an inert slot with an explicit destination policy.
- #[must_use]
- pub fn new(policy: RelayUrlPolicy) -> Self {
- Self {
- policy,
- state: Arc::new(RwLock::new(None)),
- }
- }
-
- /// Validates and atomically installs the complete relay selection.
- pub fn configure<I, S>(&self, relays: I) -> crate::Result<()>
- where
- I: IntoIterator<Item = S>,
- S: AsRef<str>,
- {
- let config = radroots_transport_nostr::Config::new(self.policy, relays)
- .map_err(crate::Error::invalid_host_configuration)?;
- let targets = TargetSet::new(
- config
- .relays()
- .iter()
- .map(radroots_transport_nostr::RelayUrl::to_target)
- .collect::<Result<Vec<_>, _>>()
- .map_err(crate::Error::invalid_host_configuration)?,
- )
- .map_err(|_| crate::Error::invalid_host_configuration_without_source())?;
- let state = NostrState {
- transport: Arc::new(radroots_transport_nostr::NostrTransport::new(config)),
- targets,
- };
- let mut current = self
- .state
- .write()
- .map_err(|_| crate::Error::shared_operation_unavailable())?;
- *current = Some(state);
- Ok(())
- }
-
- /// Removes the active adapter without starting or stopping background work.
- pub fn clear(&self) {
- if let Ok(mut state) = self.state.write() {
- *state = None;
- }
- }
-
- /// Returns the currently selected canonical targets.
- #[must_use]
- pub fn targets(&self) -> Option<TargetSet> {
- self.snapshot().map(|state| state.targets)
- }
-
- fn snapshot(&self) -> Option<NostrState> {
- self.state.read().ok().and_then(|state| state.clone())
- }
-}
-
-#[cfg(feature = "nostr")]
-impl radroots_transport::EventSource for NostrSlot {
- fn status(
- &self,
- ) -> radroots_transport::BoxFuture<'_, Result<SourceStatus, radroots_transport::Error>> {
- Box::pin(async move {
- match self.snapshot() {
- Some(state) => {
- radroots_transport::EventSource::status(state.transport.as_ref()).await
- }
- None => Ok(SourceStatus::new(
- TransportId::NOSTR,
- false,
- Maturity::Stable,
- Availability::Unavailable,
- SourceCapabilities::FETCH,
- "Nostr transport is not configured",
- )),
- }
- })
- }
-
- fn fetch(
- &self,
- request: radroots_transport::FetchRequest,
- ) -> radroots_transport::BoxFuture<
- '_,
- Result<radroots_transport::FetchPage, radroots_transport::Error>,
- > {
- Box::pin(async move {
- let state = self
- .snapshot()
- .ok_or(radroots_transport::Error::UnsupportedOperation)?;
- radroots_transport::EventSource::fetch(state.transport.as_ref(), request).await
- })
- }
-}
-
-#[cfg(feature = "nostr")]
-impl radroots_transport::EventSink for NostrSlot {
- fn status(
- &self,
- ) -> radroots_transport::BoxFuture<'_, Result<SinkStatus, radroots_transport::Error>> {
- Box::pin(async move {
- match self.snapshot() {
- Some(state) => {
- radroots_transport::EventSink::status(state.transport.as_ref()).await
- }
- None => Ok(SinkStatus::new(
- TransportId::NOSTR,
- false,
- Maturity::Stable,
- Availability::Unavailable,
- SinkCapabilities::DELIVER,
- "Nostr transport is not configured",
- )),
- }
- })
- }
-
- fn deliver(
- &self,
- request: radroots_transport::DeliveryRequest,
- ) -> radroots_transport::BoxFuture<
- '_,
- Result<radroots_transport::DeliveryReceipt, radroots_transport::SinkFailure>,
- > {
- Box::pin(async move {
- let Some(state) = self.snapshot() else {
- return Err(radroots_transport::SinkFailure::for_request(
- &request,
- "nostr_transport_not_configured",
- radroots_transport::outcome::Retryability::Terminal,
- None,
- None,
- Vec::new(),
- )
- .expect("static unconfigured sink failure is valid"));
- };
- radroots_transport::EventSink::deliver(state.transport.as_ref(), request).await
- })
- }
-}
-
-#[cfg(feature = "nostr")]
-impl std::fmt::Debug for NostrSlot {
- fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- formatter
- .debug_struct("NostrSlot")
- .field("policy", &self.policy)
- .field("configured", &self.targets().is_some())
- .finish()
- }
-}
-
-/// Explicit daemon adapter authentication configuration.
-#[cfg(feature = "radrootsd")]
-#[derive(Clone, Eq, PartialEq)]
-#[non_exhaustive]
-pub enum DaemonAuth {
- /// Sends no authorization header.
- None,
- /// Sends the supplied bearer credential only when delivery is invoked.
- BearerToken(String),
-}
-
-#[cfg(feature = "radrootsd")]
-impl std::fmt::Debug for DaemonAuth {
- fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::None => formatter.write_str("None"),
- Self::BearerToken(_) => formatter.write_str("BearerToken(<redacted>)"),
- }
- }
-}
-
-/// Explicit daemon endpoint and request deadline configuration.
-#[cfg(feature = "radrootsd")]
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct DaemonConfig {
- endpoint: String,
- auth: DaemonAuth,
- timeout: core::time::Duration,
-}
-
-#[cfg(feature = "radrootsd")]
-impl DaemonConfig {
- /// Creates inert configuration; no client is built and no request is sent.
- #[must_use]
- pub fn new(endpoint: impl Into<String>) -> Self {
- Self {
- endpoint: endpoint.into(),
- auth: DaemonAuth::None,
- timeout: core::time::Duration::from_secs(10),
- }
- }
-
- /// Selects explicit authentication for later invocation.
- #[must_use]
- pub fn with_auth(mut self, auth: DaemonAuth) -> Self {
- self.auth = auth;
- self
- }
-
- /// Selects the complete HTTP/RPC request deadline.
- #[must_use]
- pub const fn with_timeout(mut self, timeout: core::time::Duration) -> Self {
- self.timeout = timeout;
- self
- }
-}
-
-/// Stable secret-safe daemon execution failure class.
-#[cfg(feature = "radrootsd")]
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-#[non_exhaustive]
-pub enum DaemonErrorKind {
- /// The explicit authentication value cannot be represented safely.
- Authentication,
- /// The versioned protocol rejected the request.
- InvalidRequest,
- /// HTTP transport or timeout failed.
- Transport,
- /// The daemon returned a JSON-RPC error.
- Rpc,
- /// The response was malformed or did not match the request.
- InvalidResponse,
-}
-
-/// One redacted daemon failure retaining a private source chain.
-#[cfg(feature = "radrootsd")]
-pub struct DaemonError {
- kind: DaemonErrorKind,
- source: crate::adapters::radrootsd::RadrootsdError,
-}
-
-#[cfg(feature = "radrootsd")]
-impl DaemonError {
- /// Returns the stable failure class.
- #[must_use]
- pub const fn kind(&self) -> DaemonErrorKind {
- self.kind
- }
-
- fn from_private(source: crate::adapters::radrootsd::RadrootsdError) -> Self {
- use crate::adapters::radrootsd::RadrootsdError;
- let kind = match &source {
- RadrootsdError::InvalidAuthHeader(_) => DaemonErrorKind::Authentication,
- RadrootsdError::InvalidRequest(_) => DaemonErrorKind::InvalidRequest,
- RadrootsdError::Http(_) => DaemonErrorKind::Transport,
- RadrootsdError::JsonRpc { .. } => DaemonErrorKind::Rpc,
- RadrootsdError::MalformedResponse(_) => DaemonErrorKind::InvalidResponse,
- };
- Self { kind, source }
- }
-}
-
-#[cfg(feature = "radrootsd")]
-impl std::fmt::Display for DaemonError {
- fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- formatter.write_str(match self.kind {
- DaemonErrorKind::Authentication => "daemon authentication configuration is invalid",
- DaemonErrorKind::InvalidRequest => "daemon delivery request is invalid",
- DaemonErrorKind::Transport => "daemon transport failed",
- DaemonErrorKind::Rpc => "daemon RPC failed",
- DaemonErrorKind::InvalidResponse => "daemon response is invalid",
- })
- }
-}
-
-#[cfg(feature = "radrootsd")]
-impl std::fmt::Debug for DaemonError {
- fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- formatter
- .debug_struct("DaemonError")
- .field("kind", &self.kind)
- .finish_non_exhaustive()
- }
-}
-
-#[cfg(feature = "radrootsd")]
-impl std::error::Error for DaemonError {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- Some(&self.source)
- }
-}
-
-/// Explicitly configured daemon execution adapter.
-///
-/// Construction is inert. Network contact occurs only in [`Self::deliver`].
-#[cfg(feature = "radrootsd")]
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct DaemonDelivery {
- adapter: crate::adapters::radrootsd::RadrootsdPublishAdapter,
-}
-
-#[cfg(feature = "radrootsd")]
-impl DaemonDelivery {
- /// Creates an inert adapter from explicit host configuration.
- #[must_use]
- pub fn new(config: DaemonConfig) -> Self {
- let auth = match config.auth {
- DaemonAuth::None => crate::adapters::radrootsd::RadrootsdAuth::None,
- DaemonAuth::BearerToken(token) => {
- crate::adapters::radrootsd::RadrootsdAuth::BearerToken(token)
- }
- };
- Self {
- adapter: crate::adapters::radrootsd::RadrootsdPublishAdapter::new(
- crate::adapters::radrootsd::RadrootsdPublishConfig::new(config.endpoint)
- .with_auth(auth)
- .with_timeout(config.timeout),
- ),
- }
- }
-
- /// Invokes the generation-5 daemon transport-publish contract.
- pub async fn deliver(
- &self,
- signed_event: radroots_event::SignedEvent,
- target_policy: radroots_protocol::radrootsd::transport_publish::v5::TargetPolicy,
- delivery_policy: radroots_protocol::radrootsd::transport_publish::v5::DeliveryPolicy,
- idempotency_key: Option<String>,
- timeout_ms: Option<u64>,
- ) -> Result<radroots_protocol::radrootsd::transport_publish::v5::EventResponse, DaemonError>
- {
- self.adapter
- .publish_signed_event(crate::adapters::radrootsd::RadrootsdPublishRequest {
- signed_event,
- target_policy,
- delivery_policy,
- idempotency_key,
- timeout_ms,
- })
- .await
- .map_err(DaemonError::from_private)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use radroots_transport::{
- Error, TARGET_SET_MAX_ITEMS, Target,
- capability::{Availability, Maturity},
- policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy},
- target::TargetFingerprint,
- };
-
- use super::*;
-
- fn target(index: usize) -> Target {
- Target::nostr_relay(format!("wss://relay-{index}.example")).expect("target")
- }
-
- #[cfg(feature = "nostr")]
- fn signed_event() -> radroots_event::SignedEvent {
- let raw = r#"{"id":"56bfc78223bb2221bad82b539efdec1ade0f56d0eb0e1f592fd387df4b2ceee0","pubkey":"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df","created_at":1700000001,"kind":0,"tags":[],"content":"{}","sig":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"}"#;
- let wire = radroots_event::wire::v1::Nip01EventWire::parse_json(raw).expect("wire event");
- radroots_event::SignedEvent::from_wire_verified_id(wire, raw).expect("signed event")
- }
-
- #[test]
- fn delivery_profile_preserves_canonical_targets_and_policy() {
- let targets = TargetSet::new(vec![target(1), target(2)]).expect("target set");
- let policy = SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::all());
- let profile = Profile::delivery(targets.clone(), policy.clone()).expect("profile");
-
- assert_eq!(profile.targets(), Some(&targets));
- assert_eq!(profile.satisfaction(), Some(&policy));
- assert!(!profile.is_local_only());
- assert!(profile.source_status().is_none());
- assert!(profile.sink_status().is_none());
- }
-
- #[test]
- fn canonical_target_and_policy_bounds_fail_during_profile_construction() {
- assert_eq!(TargetSet::new(Vec::new()), Err(Error::EmptyTargetSet));
- assert_eq!(
- TargetSet::new((0..=TARGET_SET_MAX_ITEMS).map(target).collect()),
- Err(Error::TargetSetTooLarge)
- );
-
- let targets = TargetSet::new(vec![target(1)]).expect("target set");
- let quorum = SatisfactionPolicy::new(
- SatisfactionClass::Delivered,
- TargetPolicy::quorum(2).expect("non-zero quorum"),
- );
- assert_eq!(
- Profile::delivery(targets.clone(), quorum),
- Err(Error::InvalidSatisfactionPolicy)
- );
-
- let missing =
- TargetFingerprint::from_target(target(2).kind(), target(2).uri(), target(2).scope());
- let required = SatisfactionPolicy::new(
- SatisfactionClass::Accepted,
- TargetPolicy::required(vec![missing]).expect("required policy"),
- );
- assert_eq!(
- Profile::delivery(targets, required),
- Err(Error::RequiredTargetNotRequested)
- );
- }
-
- #[test]
- fn preview_transport_is_explicitly_unavailable_and_unselectable() {
- let profile = Profile::unavailable_preview(TransportId::RETICULUM);
- let source = profile.source_status().expect("source status");
- let sink = profile.sink_status().expect("sink status");
-
- assert_eq!(source.transport_id(), TransportId::RETICULUM);
- assert_eq!(sink.transport_id(), TransportId::RETICULUM);
- assert!(!source.is_configured());
- assert!(!sink.is_configured());
- assert_eq!(source.maturity(), Maturity::Preview);
- assert_eq!(sink.maturity(), Maturity::Preview);
- assert_eq!(source.availability(), Availability::Unavailable);
- assert_eq!(sink.availability(), Availability::Unavailable);
- assert!(!source.capabilities().can_fetch());
- assert!(!sink.capabilities().can_deliver());
- assert!(profile.targets().is_none());
- assert!(profile.satisfaction().is_none());
- }
-
- #[test]
- fn local_and_preview_profiles_never_substitute_fallback_targets() {
- let local = Profile::local_only();
- let preview = Profile::unavailable_preview(TransportId::RETICULUM);
- assert!(local.is_local_only());
- assert!(local.targets().is_none());
- assert!(preview.targets().is_none());
-
- let selected = TargetSet::new(vec![target(7)]).expect("selected targets");
- let profile = Profile::delivery(
- selected.clone(),
- SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::any()),
- )
- .expect("profile");
- assert_eq!(profile.targets(), Some(&selected));
- assert!(
- profile
- .targets()
- .expect("targets")
- .targets()
- .iter()
- .all(|target| *target.kind() == TransportId::NOSTR)
- );
- }
-
- #[test]
- fn default_profile_is_local_only() {
- assert_eq!(Profile::default(), Profile::local_only());
- }
-
- #[cfg(feature = "radrootsd")]
- #[test]
- fn daemon_configuration_is_inert_explicit_and_redacted() {
- let config = DaemonConfig::new("http://127.0.0.1:1/rpc")
- .with_auth(DaemonAuth::BearerToken("secret-token".to_owned()))
- .with_timeout(core::time::Duration::from_millis(5));
- let adapter = DaemonDelivery::new(config);
-
- let debug = format!("{adapter:?}");
- assert!(!debug.contains("secret-token"));
- assert!(!debug.contains("reqwest"));
- assert_eq!(format!("{:?}", DaemonAuth::None), "None");
- assert_eq!(
- format!("{:?}", DaemonAuth::BearerToken("private".to_owned())),
- "BearerToken(<redacted>)"
- );
- }
-
- #[cfg(feature = "radrootsd")]
- #[test]
- fn daemon_errors_are_stably_classified_and_redacted() {
- use std::error::Error as _;
-
- use crate::adapters::radrootsd::RadrootsdError;
-
- let cases = [
- (
- RadrootsdError::InvalidAuthHeader("private".to_owned()),
- DaemonErrorKind::Authentication,
- "daemon authentication configuration is invalid",
- ),
- (
- RadrootsdError::InvalidRequest("private".to_owned()),
- DaemonErrorKind::InvalidRequest,
- "daemon delivery request is invalid",
- ),
- (
- RadrootsdError::Http("private".to_owned()),
- DaemonErrorKind::Transport,
- "daemon transport failed",
- ),
- (
- RadrootsdError::JsonRpc {
- code: -1,
- message: "private".to_owned(),
- },
- DaemonErrorKind::Rpc,
- "daemon RPC failed",
- ),
- (
- RadrootsdError::MalformedResponse("private".to_owned()),
- DaemonErrorKind::InvalidResponse,
- "daemon response is invalid",
- ),
- ];
- for (private, kind, display) in cases {
- let error = DaemonError::from_private(private);
- assert_eq!(error.kind(), kind);
- assert_eq!(error.to_string(), display);
- assert!(error.source().is_some());
- assert!(!format!("{error:?}").contains("private"));
- }
- }
-
- #[cfg(feature = "nostr")]
- #[test]
- fn nostr_slot_reconfiguration_is_validated_atomic_and_inert() {
- let slot = NostrSlot::new(RelayUrlPolicy::Local);
- assert!(slot.targets().is_none());
- assert!(slot.configure(["ws://127.0.0.1:7447"]).is_ok());
- let original = slot.targets().expect("configured targets");
- assert!(slot.configure(Vec::<String>::new()).is_err());
- assert_eq!(slot.targets(), Some(original));
- slot.clear();
- assert!(slot.targets().is_none());
- assert!(format!("{slot:?}").contains("configured: false"));
- }
-
- #[cfg(feature = "nostr")]
- #[test]
- fn poisoned_nostr_slot_fails_closed_for_every_host_operation() {
- let slot = NostrSlot::new(RelayUrlPolicy::Local);
- let state = Arc::clone(&slot.state);
- let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
- let _guard = state.write().expect("write lock");
- panic!("poison transport slot");
- }));
-
- slot.clear();
- assert!(slot.targets().is_none());
- assert!(slot.configure(["ws://127.0.0.1:7447"]).is_err());
- }
-
- #[cfg(feature = "nostr")]
- #[tokio::test]
- async fn empty_nostr_slot_reports_unavailable_and_rejects_operations() {
- use radroots_transport::{
- DeliveryRequest, EventSink as _, EventSource as _, FetchRequest, sink::DeliveryPayload,
- source::FetchBounds,
- };
-
- let slot = NostrSlot::new(RelayUrlPolicy::Local);
- let source = radroots_transport::EventSource::status(&slot)
- .await
- .expect("source status");
- assert_eq!(source.availability(), Availability::Unavailable);
-
- let targets = TargetSet::new(vec![target(1)]).expect("targets");
- let fetch = FetchRequest::new(
- "fetch",
- targets.clone(),
- FetchBounds::new(1, 1).expect("bounds"),
- )
- .expect("fetch");
- assert_eq!(slot.fetch(fetch).await, Err(Error::UnsupportedOperation));
-
- let sink = radroots_transport::EventSink::status(&slot)
- .await
- .expect("sink status");
- assert_eq!(sink.availability(), Availability::Unavailable);
- let deliver = DeliveryRequest::new(
- "deliver",
- DeliveryPayload::new(signed_event()),
- targets,
- SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::any()),
- 1,
- )
- .expect("delivery");
- let failure = slot.deliver(deliver).await.expect_err("unconfigured sink");
- assert_eq!(failure.code(), "nostr_transport_not_configured");
- }
-}
diff --git a/crates/sdk/tests/lifecycle.rs b/crates/sdk/tests/lifecycle.rs
@@ -1,156 +0,0 @@
-#[cfg(feature = "memory")]
-use radroots_sdk::{ClientBuilder, capability::CapabilityId, error::ErrorKind};
-
-const CLIENT_SOURCE: &str = include_str!("../src/client.rs");
-const MANIFEST: &str = include_str!("../Cargo.toml");
-
-#[test]
-fn clean_default_path_contains_no_implicit_resource_or_worker_authority() {
- let production = CLIENT_SOURCE
- .split_once("#[cfg(all(test, feature = \"memory\"))]")
- .expect("production client boundary")
- .0;
- for forbidden in [
- "keyring",
- "reqwest",
- "radrootsd",
- "std::fs",
- "tokio::fs",
- "tokio::spawn",
- "std::thread::spawn",
- "Runtime::new",
- "File::create",
- "impl Drop for Client",
- ] {
- assert!(
- !production.contains(forbidden),
- "clean client path contains implicit authority `{forbidden}`"
- );
- }
- assert!(MANIFEST.contains("default = [\"memory\"]"));
- assert!(!MANIFEST.contains("default = [\"native\"]"));
- assert!(!MANIFEST.contains("default = [\"full\"]"));
-}
-
-#[cfg(feature = "memory")]
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn memory_client_is_passive_clone_shared_and_explicitly_closed() {
- let generation =
- radroots_storage::event::SourceGeneration::new([41; 32]).expect("non-zero generation");
- let client = ClientBuilder::memory(generation).build().expect("client");
-
- let first_report = client.capabilities();
- let second_report = client.capabilities();
- assert_eq!(first_report, second_report);
- let storage = first_report
- .get(CapabilityId::CANONICAL_STORAGE)
- .expect("canonical storage status");
- assert!(storage.is_compiled());
- assert!(storage.is_configured());
-
- let unpolled_close = client.close();
- drop(unpolled_close);
- assert!(client.storage().is_ok());
-
- let first = client.clone();
- let second = client.clone();
- let first_close = tokio::spawn(async move { first.close().await });
- let second_close = tokio::spawn(async move { second.close().await });
- let outcomes = [
- first_close.await.expect("first close task"),
- second_close.await.expect("second close task"),
- ];
- assert!(outcomes.iter().all(|outcome| {
- outcome.is_ok()
- || outcome
- .as_ref()
- .is_err_and(|error| error.kind() == ErrorKind::CloseInProgress)
- }));
- if !client.is_closed() {
- client.close().await.expect("complete close");
- }
-
- assert!(client.is_closed());
- assert_eq!(
- client
- .storage()
- .err()
- .expect("closed client rejects storage")
- .kind(),
- ErrorKind::ClientClosed
- );
- assert_eq!(
- client
- .capabilities()
- .get(CapabilityId::CANONICAL_STORAGE)
- .expect("closed storage status")
- .availability(),
- radroots_sdk::capability::Availability::Unavailable
- );
-}
-
-#[cfg(all(feature = "memory", feature = "sqlite"))]
-#[tokio::test]
-async fn sqlite_resources_exist_only_after_explicit_open_and_close_across_clones() {
- use radroots_sdk::storage::{SqliteOpenMode, SqliteOptions, SqlitePaths};
- use radroots_storage::status::{ShutdownState, StorageBackend};
-
- let directory = tempfile::tempdir().expect("temporary directory");
- let empty_builder = ClientBuilder::new();
- assert!(
- directory
- .path()
- .read_dir()
- .expect("read tempdir")
- .next()
- .is_none()
- );
- drop(empty_builder);
-
- let paths = SqlitePaths::from_directory(directory.path()).expect("SQLite paths");
- let generation =
- radroots_storage::event::SourceGeneration::new([42; 32]).expect("non-zero generation");
- let options = SqliteOptions::new(paths, SqliteOpenMode::Create)
- .with_source_generation(generation, 1)
- .expect("source generation");
- assert!(
- directory
- .path()
- .read_dir()
- .expect("read tempdir")
- .next()
- .is_none()
- );
-
- let client = ClientBuilder::sqlite(options)
- .await
- .expect("explicit SQLite open")
- .build()
- .expect("client");
- assert!(
- directory
- .path()
- .read_dir()
- .expect("read tempdir")
- .next()
- .is_some()
- );
- let status = client.storage_status().await.expect("storage status");
- assert_eq!(status.backend(), StorageBackend::Sqlite);
- assert_eq!(status.shutdown(), ShutdownState::Open);
-
- let clone = client.clone();
- let unpolled_close = clone.close();
- drop(unpolled_close);
- assert!(client.storage().is_ok());
- client.close().await.expect("explicit close");
- assert!(clone.is_closed());
- assert_eq!(
- clone
- .storage_status()
- .await
- .expect_err("closed clone rejects inspection")
- .kind(),
- ErrorKind::ClientClosed
- );
-}
diff --git a/crates/sdk/tests/package_boundary.rs b/crates/sdk/tests/package_boundary.rs
@@ -1,554 +0,0 @@
-use std::collections::BTreeSet;
-
-const MANIFEST: &str = include_str!("../Cargo.toml");
-const ROOT: &str = include_str!("../src/lib.rs");
-const CLIENT: &str = include_str!("../src/client.rs");
-const FARM: &str = include_str!("../src/farm.rs");
-const LISTING: &str = include_str!("../src/listing.rs");
-const TRADE: &str = include_str!("../src/trade.rs");
-const STORAGE: &str = include_str!("../src/storage.rs");
-const DIAGNOSTICS: &str = include_str!("../src/diagnostics.rs");
-const ADAPTERS: &str = include_str!("../src/adapters/mod.rs");
-const RADROOTSD: &str = include_str!("../src/adapters/radrootsd.rs");
-const SYNC: &str = include_str!("../src/sync.rs");
-const TRANSPORT: &str = include_str!("../src/transport.rs");
-const PUBLICATION: &str = include_str!("../../../contracts/releases/publication.toml");
-
-#[test]
-fn manifest_has_final_identity_and_dependency_boundary() {
- assert!(MANIFEST.contains("name = \"radroots_sdk\""));
- assert!(MANIFEST.contains("publish = [\"crates-io\"]"));
- assert!(MANIFEST.contains("version.workspace = true"));
- assert!(MANIFEST.contains("name = \"package_boundary\""));
-
- let dependencies = dependency_names(MANIFEST);
- let expected = BTreeSet::from([
- "radroots_core",
- "radroots_event",
- "radroots_event_codec",
- "radroots_geonames",
- "radroots_identity",
- "radroots_nostr",
- "radroots_nostr_connect",
- "radroots_protocol",
- "radroots_secrets",
- "radroots_signing",
- "radroots_storage",
- "radroots_storage_sqlite",
- "radroots_sync",
- "radroots_trade",
- "radroots_transport",
- "radroots_transport_nostr",
- ]);
- assert_eq!(dependencies, expected);
- for forbidden in [
- "radroots_event_store",
- "radroots_nostr_signer",
- "radroots_outbox",
- "radroots_protected_store",
- "radroots_runtime_paths",
- "radroots_secret_vault",
- "radroots_transport_reticulum",
- ] {
- assert!(
- !dependencies.contains(forbidden),
- "SDK depends on private or superseded package `{forbidden}`"
- );
- }
-}
-
-#[test]
-fn manifest_has_exact_feature_vocabulary_and_explicit_optional_activation() {
- let features = MANIFEST
- .split_once("[features]")
- .expect("feature section")
- .1
- .split_once("[dependencies]")
- .expect("dependency section")
- .0
- .lines()
- .filter_map(|line| line.split_once(" = ").map(|(name, _)| name.trim()))
- .collect::<BTreeSet<_>>();
- assert_eq!(
- features,
- BTreeSet::from([
- "default",
- "full",
- "geonames",
- "knowledge",
- "local-signing",
- "memory",
- "native",
- "nip46",
- "nostr",
- "radrootsd",
- "sqlite",
- "sync",
- ])
- );
- for activation in [
- "dep:radroots_storage_sqlite",
- "dep:radroots_sync",
- "dep:radroots_nostr",
- "dep:radroots_transport_nostr",
- "dep:radroots_nostr_connect",
- "dep:radroots_secrets",
- "dep:reqwest",
- "dep:serde",
- "dep:serde_json",
- "dep:radroots_geonames",
- ] {
- assert!(
- MANIFEST.contains(activation),
- "missing activation `{activation}`"
- );
- }
- for retired in [
- "runtime =",
- "local-runtime",
- "signer-adapters",
- "transport-nostr-runtime",
- "transport-nostr-client",
- "fixtures =",
- ] {
- assert!(!MANIFEST.contains(retired), "retired feature `{retired}`");
- }
-}
-
-#[test]
-fn root_declares_exact_final_module_skeleton() {
- let actual = ROOT
- .lines()
- .filter_map(|line| line.trim().strip_prefix("pub mod "))
- .map(|module| module.trim_end_matches(';'))
- .collect::<BTreeSet<_>>();
- let expected = BTreeSet::from([
- "capability",
- "client",
- "diagnostics",
- "error",
- "farm",
- "listing",
- "signing",
- "storage",
- "sync",
- "trade",
- "transport",
- ]);
- assert_eq!(actual, expected);
- assert!(!ROOT.contains("no_std"));
- assert_eq!(ROOT.matches("pub use crate::").count(), 2);
- assert!(ROOT.contains("pub use crate::client::{Client, ClientBuilder};"));
- assert!(ROOT.contains("pub use crate::error::{Error, Result};"));
- assert!(!ROOT.contains("pub use radroots_"));
-}
-
-#[test]
-fn package_contains_only_reachable_sources_and_registered_targets() {
- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
- assert_eq!(
- rust_files(&root.join("src")),
- BTreeSet::from([
- "adapters/mod.rs".to_owned(),
- "adapters/radrootsd.rs".to_owned(),
- "capability.rs".to_owned(),
- "client.rs".to_owned(),
- "diagnostics.rs".to_owned(),
- "error.rs".to_owned(),
- "farm.rs".to_owned(),
- "lib.rs".to_owned(),
- "listing.rs".to_owned(),
- "listing/operational_listing/draft.rs".to_owned(),
- "listing/operational_listing/mod.rs".to_owned(),
- "listing/operational_listing/model.rs".to_owned(),
- "listing/operational_listing/mutation.rs".to_owned(),
- "listing/operational_listing/price_ext.rs".to_owned(),
- "listing/operational_listing/validation.rs".to_owned(),
- "signing.rs".to_owned(),
- "storage.rs".to_owned(),
- "sync.rs".to_owned(),
- "trade.rs".to_owned(),
- "transport.rs".to_owned(),
- ])
- );
- assert_eq!(
- rust_files(&root.join("tests")),
- BTreeSet::from([
- "lifecycle.rs".to_owned(),
- "package_boundary.rs".to_owned(),
- "public_api.rs".to_owned(),
- "unit/adapters_radrootsd_tests.rs".to_owned(),
- ])
- );
- assert_eq!(
- rust_files(&root.join("examples")),
- BTreeSet::from([
- "safe_memory_client.rs".to_owned(),
- "transport_profile.rs".to_owned(),
- ])
- );
-}
-
-#[test]
-fn compatibility_packages_are_removed() {
- assert!(PUBLICATION.contains("retired = []"));
- let approved = PUBLICATION
- .split_once("approved_packages = [")
- .expect("approved package list")
- .1
- .split_once("\n]")
- .expect("approved package list terminator")
- .0;
- assert!(!approved.contains("radroots_runtime_contract_v1"));
- let workspace = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
- .parent()
- .and_then(std::path::Path::parent)
- .expect("workspace root");
- assert!(!workspace.join("crates/runtime_contract_v1").exists());
- assert!(!workspace.join("crates/event_index_bindings").exists());
-}
-
-#[test]
-fn root_types_have_the_required_std_contracts() {
- fn assert_client<T: Clone + Send + Sync>() {}
- fn assert_builder<T: Send + Sync>() {}
- fn assert_error<T: std::error::Error + Send + Sync + 'static>() {}
-
- assert_client::<radroots_sdk::Client>();
- assert_builder::<radroots_sdk::ClientBuilder>();
- assert_error::<radroots_sdk::Error>();
- let result: radroots_sdk::Result<()> = Ok(());
- assert!(result.is_ok());
-}
-
-#[cfg(feature = "sqlite")]
-#[test]
-fn sqlite_backend_implements_the_canonical_storage_capability() {
- fn assert_storage<T: radroots_storage::Storage>() {}
- assert_storage::<radroots_storage_sqlite::SqliteStorage>();
-}
-
-#[cfg(feature = "nostr")]
-#[test]
-fn nostr_adapter_implements_independent_source_and_sink_capabilities() {
- fn assert_source<T: radroots_transport::EventSource>() {}
- fn assert_sink<T: radroots_transport::EventSink>() {}
- assert_source::<radroots_transport_nostr::NostrTransport>();
- assert_sink::<radroots_transport_nostr::NostrTransport>();
-}
-
-#[test]
-fn lifecycle_source_owns_no_hidden_worker_runtime_or_blocking_drop() {
- for forbidden in [
- "tokio::spawn",
- "std::thread::spawn",
- "Runtime::new",
- "block_on(self.close",
- "impl Drop for Client",
- ] {
- assert!(
- !CLIENT.contains(forbidden),
- "forbidden lifecycle source `{forbidden}`"
- );
- }
- assert!(CLIENT.contains("pub async fn close(&self)"));
- assert!(CLIENT.contains("impl Drop for CloseAttempt"));
-}
-
-#[test]
-fn sdk_source_contains_no_studio_storage_surface() {
- let source_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
- let mut pending = vec![source_root];
- while let Some(path) = pending.pop() {
- for entry in std::fs::read_dir(path).expect("read SDK source") {
- let entry = entry.expect("source entry");
- let path = entry.path();
- if path.is_dir() {
- pending.push(path);
- } else if path.extension().and_then(|value| value.to_str()) == Some("rs") {
- let source = std::fs::read_to_string(&path).expect("read source file");
- for forbidden in [
- "studio.sqlite",
- "SdkStudioStore",
- "sdk_studio_state",
- "studio_store",
- ] {
- assert!(
- !source.contains(forbidden),
- "{} contains retired Studio storage marker {forbidden}",
- path.display()
- );
- }
- }
- }
- }
-}
-
-#[test]
-fn transport_profiles_reuse_canonical_types_and_forbid_fallback() {
- assert!(TRANSPORT.contains("use radroots_transport::{"));
- assert!(TRANSPORT.contains("satisfaction.validate_for(&targets)?"));
- assert!(TRANSPORT.contains("Selection::UnavailablePreview"));
- for duplicate in [
- "pub struct TargetSet",
- "pub enum TargetPolicy",
- "pub enum SatisfactionPolicy",
- "pub struct SourceStatus",
- "pub struct SinkStatus",
- "DefaultProfile",
- ] {
- assert!(
- !TRANSPORT.contains(duplicate),
- "SDK transport source contains forbidden duplicate or fallback `{duplicate}`"
- );
- }
-}
-
-#[test]
-fn sync_operations_only_delegate_to_the_canonical_engine() {
- for delegation in [
- "self.engine.pull(request, admission).await",
- "self.engine.ingest(observed, admission).await",
- "self.engine.ingest_batch(observed, admission).await",
- "self.engine.refresh_projection(request, reducer).await",
- "self.engine.prepare_push(request).await",
- "self.engine.push_status(operation_id).await",
- "self.engine.sign_prepared(request).await",
- "self.engine.admit_signed(operation_id).await",
- "self.engine.deliver_push(operation_id).await",
- "self.engine.status(projections).await",
- "self.engine.retry_decision(plan, now_unix_ms)",
- ] {
- assert!(
- SYNC.contains(delegation),
- "missing sync delegation `{delegation}`"
- );
- }
- for duplicate in [
- "pub struct SyncTransport",
- "pub struct SyncOutbox",
- "pub struct SyncProjection",
- "pub struct SyncStatus",
- "pub struct PushOutbox",
- ] {
- assert!(
- !SYNC.contains(duplicate),
- "SDK sync source contains duplicate lower model `{duplicate}`"
- );
- }
- assert!(
- !std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/sync_runtime.rs")
- .exists()
- );
-}
-
-#[test]
-fn authored_submission_is_one_protocol_neutral_boundary_for_all_typed_contracts() {
- use radroots_event_codec::authoring::REGISTRY_V7_TYPED_AUTHORING_CONTRACT_IDS;
-
- assert_eq!(REGISTRY_V7_TYPED_AUTHORING_CONTRACT_IDS.len(), 8);
- assert!(SYNC.contains("pub async fn submit_push"));
- assert!(SYNC.contains("request: PushRequest"));
- assert!(SYNC.contains("Result<PushStatus, Error>"));
- for contract_id in REGISTRY_V7_TYPED_AUTHORING_CONTRACT_IDS {
- assert!(
- !SYNC.contains(contract_id),
- "SDK submission must not branch on typed contract `{contract_id}`"
- );
- }
-}
-
-#[test]
-fn farm_operations_preserve_pure_planning_commit_and_privacy_boundaries() {
- for required in [
- "encode::farm::to_wire_parts",
- "AddressableCoordinate",
- "GenericEventDraft",
- "AuthoredEventPlan",
- "radroots_sync::PushRequest::new",
- "self.sync",
- ".submit_push(",
- "PrivateArtifactStore",
- ] {
- assert!(
- FARM.contains(required),
- "missing farm boundary `{required}`"
- );
- }
- for forbidden in [
- "SdkExactLocation",
- "SdkPublicLocality",
- "latitude:",
- "longitude:",
- "enqueue_signed_workflow",
- "local_event_seq",
- "outbox_event_id",
- ] {
- assert!(
- !FARM.contains(forbidden),
- "farm source contains retired SDK or private representation `{forbidden}`"
- );
- }
- let source_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
- assert!(!source_root.join("farms_runtime.rs").exists());
-}
-
-#[test]
-fn listing_operations_reuse_trade_event_sync_and_privacy_boundaries() {
- for required in [
- "canonicalize_operational_listing_edit",
- "RadrootsOperationalListingMutation",
- "RadrootsOperationalListingLifecycleState",
- "build_operational_listing_mutation_plan",
- "radroots_sync::PushRequest::new",
- "PrivateArtifactStore",
- ] {
- assert!(
- LISTING.contains(required),
- "missing listing boundary `{required}`"
- );
- }
- for forbidden in [
- "SdkMutationState",
- "ListingEnqueueReceipt",
- "enqueue_signed_workflow",
- "local_event_seq",
- "outbox_event_id",
- "latitude:",
- "longitude:",
- ] {
- assert!(
- !LISTING.contains(forbidden),
- "listing source contains retired duplicate or private representation `{forbidden}`"
- );
- }
- assert!(
- !std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/listings_runtime.rs")
- .exists()
- );
-}
-
-#[test]
-fn trade_operations_use_canonical_workflow_storage_sync_and_projection_types() {
- for required in [
- "WorkflowPlan",
- "TradeMutationEnvelopeV1",
- "ReductionInput",
- "Projection",
- "reduce_trade_records",
- "EventQuery",
- "EventPage<StoredVisibleEvent>",
- "PrivateArtifactMetadata",
- "radroots_sync::PushRequest::new",
- ] {
- assert!(
- TRADE.contains(required),
- "missing trade boundary `{required}`"
- );
- }
- for forbidden in [
- "sqlx::",
- "QueryBuilder",
- "TradeStatusView",
- "SdkPrivateTradeArtifact",
- "SdkMutationState",
- "TradeCommandReceipt",
- "struct Page",
- "struct TradeId",
- ] {
- assert!(
- !TRADE.contains(forbidden),
- "trade source contains retired duplicate `{forbidden}`"
- );
- }
- assert!(
- !std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src/trade_runtime.rs")
- .exists()
- );
-}
-
-#[test]
-fn reliability_and_diagnostics_return_canonical_storage_contracts() {
- for required in [
- "StorageReliability::begin_backup",
- "StorageReliability::transition_backup",
- "StorageReliability::begin_restore",
- "StorageReliability::transition_restore",
- "StorageReliability::integrity",
- "StorageReliability::status",
- ] {
- assert!(
- STORAGE.contains(required),
- "missing storage delegation `{required}`"
- );
- }
- for forbidden in [
- "struct BackupManifest",
- "struct IntegrityStatus",
- "Studio",
- "sqlx::",
- ] {
- assert!(
- !STORAGE.contains(forbidden),
- "storage source duplicates `{forbidden}`"
- );
- }
- assert!(DIAGNOSTICS.contains("radroots_storage::StorageStatus"));
- for forbidden in ["path:", "SqlitePool", "private_artifact"] {
- assert!(
- !DIAGNOSTICS.contains(forbidden),
- "diagnostics leaks `{forbidden}`"
- );
- }
-}
-
-#[test]
-fn radrootsd_adapter_is_private_explicit_versioned_and_redacted() {
- assert!(ROOT.contains("mod adapters;"));
- assert!(!ROOT.contains("pub mod adapters"));
- assert!(ADAPTERS.contains("cfg(feature = \"radrootsd\")"));
- assert!(ADAPTERS.contains("pub(crate) mod radrootsd"));
- assert!(RADROOTSD.contains("transport_publish::v5"));
- assert!(RADROOTSD.contains("reqwest::Client::builder"));
- assert!(RADROOTSD.contains("BearerToken(<redacted>)"));
- assert!(!RADROOTSD.contains("tokio::spawn"));
-}
-
-fn dependency_names(manifest: &str) -> BTreeSet<&str> {
- let dependencies = manifest
- .split_once("[dependencies]")
- .expect("dependency section")
- .1
- .split_once("[[test]]")
- .expect("test section")
- .0;
- dependencies
- .lines()
- .filter_map(|line| line.split_once(" = ").map(|(name, _)| name.trim()))
- .filter(|name| name.starts_with("radroots_"))
- .collect()
-}
-
-fn rust_files(root: &std::path::Path) -> BTreeSet<String> {
- let mut files = BTreeSet::new();
- let mut pending = vec![root.to_path_buf()];
- while let Some(directory) = pending.pop() {
- for entry in std::fs::read_dir(&directory).expect("read package source directory") {
- let path = entry.expect("read package source entry").path();
- if path.is_dir() {
- pending.push(path);
- } else if path.extension().and_then(|value| value.to_str()) == Some("rs") {
- files.insert(
- path.strip_prefix(root)
- .expect("source remains below root")
- .to_string_lossy()
- .replace(std::path::MAIN_SEPARATOR, "/"),
- );
- }
- }
- }
- files
-}
diff --git a/crates/sdk/tests/public_api.rs b/crates/sdk/tests/public_api.rs
@@ -1,172 +0,0 @@
-use std::{any::type_name, collections::BTreeSet};
-
-const ACTIVE_MODULES: &[(&str, &str)] = &[
- ("capability", include_str!("../src/capability.rs")),
- ("client", include_str!("../src/client.rs")),
- ("diagnostics", include_str!("../src/diagnostics.rs")),
- ("error", include_str!("../src/error.rs")),
- ("farm", include_str!("../src/farm.rs")),
- ("listing", include_str!("../src/listing.rs")),
- ("signing", include_str!("../src/signing.rs")),
- ("storage", include_str!("../src/storage.rs")),
- ("sync", include_str!("../src/sync.rs")),
- ("trade", include_str!("../src/trade.rs")),
- ("transport", include_str!("../src/transport.rs")),
-];
-
-#[test]
-fn public_native_type_snapshot_uses_contextual_names() {
- let actual = BTreeSet::from([
- type_name::<radroots_sdk::Client>(),
- type_name::<radroots_sdk::ClientBuilder>(),
- type_name::<radroots_sdk::capability::Availability>(),
- type_name::<radroots_sdk::capability::CapabilityId>(),
- type_name::<radroots_sdk::capability::CapabilityReport>(),
- type_name::<radroots_sdk::capability::CapabilityStatus>(),
- type_name::<radroots_sdk::capability::Maturity>(),
- type_name::<radroots_sdk::diagnostics::Report>(),
- type_name::<radroots_sdk::error::Error>(),
- type_name::<radroots_sdk::error::ErrorDescriptor>(),
- type_name::<radroots_sdk::error::ErrorKind>(),
- type_name::<radroots_sdk::farm::Plan>(),
- type_name::<radroots_sdk::farm::PrepareError>(),
- type_name::<radroots_sdk::farm::PrepareErrorKind>(),
- type_name::<radroots_sdk::farm::PrepareRequest>(),
- type_name::<radroots_sdk::listing::Action>(),
- type_name::<radroots_sdk::listing::Plan>(),
- type_name::<radroots_sdk::listing::PrepareError>(),
- type_name::<radroots_sdk::listing::PrepareErrorKind>(),
- type_name::<radroots_sdk::listing::PrepareRequest>(),
- type_name::<radroots_sdk::signing::Mode>(),
- type_name::<radroots_sdk::signing::Provider>(),
- type_name::<radroots_sdk::storage::Operations<'static>>(),
- type_name::<radroots_sdk::trade::Plan>(),
- type_name::<radroots_sdk::trade::PrepareError>(),
- type_name::<radroots_sdk::trade::PrepareErrorKind>(),
- type_name::<radroots_sdk::trade::PrepareRequest>(),
- type_name::<radroots_sdk::transport::Profile>(),
- ]);
- #[cfg(feature = "sync")]
- let actual = actual
- .into_iter()
- .chain([
- type_name::<radroots_sdk::farm::EnqueueRequest>(),
- type_name::<radroots_sdk::farm::Operations<'static>>(),
- type_name::<radroots_sdk::listing::EnqueueRequest>(),
- type_name::<radroots_sdk::listing::Operations<'static>>(),
- type_name::<radroots_sdk::sync::Operations<'static>>(),
- type_name::<radroots_sdk::sync::HostPolicy>(),
- type_name::<radroots_sdk::trade::EnqueueRequest>(),
- type_name::<radroots_sdk::trade::Operations<'static>>(),
- type_name::<radroots_sdk::trade::PrivateTermsError>(),
- ])
- .collect::<BTreeSet<_>>();
- #[cfg(feature = "local-signing")]
- let actual = actual
- .into_iter()
- .chain([
- type_name::<radroots_sdk::signing::LocalIdentity>(),
- type_name::<radroots_sdk::signing::Slot>(),
- ])
- .collect::<BTreeSet<_>>();
- #[cfg(feature = "nostr")]
- let actual = actual
- .into_iter()
- .chain([type_name::<radroots_sdk::transport::NostrSlot>()])
- .collect::<BTreeSet<_>>();
- #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
- let actual = actual
- .into_iter()
- .chain([
- type_name::<radroots_sdk::client::PostEvent>(),
- type_name::<radroots_sdk::client::ProfileDraft>(),
- type_name::<radroots_sdk::client::ProfileEvent>(),
- type_name::<radroots_sdk::client::PublishReceipt>(),
- type_name::<radroots_sdk::client::SocialOperations<'static>>(),
- type_name::<radroots_sdk::client::TransportHealth>(),
- ])
- .collect::<BTreeSet<_>>();
- #[cfg(feature = "radrootsd")]
- let actual = actual
- .into_iter()
- .chain([
- type_name::<radroots_sdk::transport::DaemonAuth>(),
- type_name::<radroots_sdk::transport::DaemonConfig>(),
- type_name::<radroots_sdk::transport::DaemonDelivery>(),
- type_name::<radroots_sdk::transport::DaemonError>(),
- type_name::<radroots_sdk::transport::DaemonErrorKind>(),
- ])
- .collect::<BTreeSet<_>>();
-
- assert!(actual.iter().all(|name| !name.contains("RadrootsSdk")));
- assert!(actual.iter().all(|name| {
- name.rsplit("::")
- .next()
- .is_some_and(|item| !item.starts_with("Sdk"))
- }));
- assert_eq!(actual.len(), expected_public_type_count());
-}
-
-#[test]
-fn active_native_api_has_no_sdk_owned_traits_or_public_field_layout() {
- for (module, source) in ACTIVE_MODULES {
- for line in source.lines() {
- let line = line.trim_start();
- assert!(
- !line.starts_with("pub trait "),
- "{module} must reuse lower host SPIs instead of exposing an SDK-owned trait"
- );
- for declaration in ["pub struct ", "pub enum ", "pub trait ", "pub type "] {
- if let Some(item) = line.strip_prefix(declaration) {
- let item = item
- .split(|character: char| {
- character == '<'
- || character == '('
- || character == '{'
- || character == ';'
- || character.is_whitespace()
- })
- .next()
- .expect("public item name");
- assert!(
- !item.starts_with("RadrootsSdk") && !item.starts_with("Sdk"),
- "{module} exposes representation-prefixed item {item}"
- );
- }
- }
-
- let public_field = line.starts_with("pub ")
- && line.contains(':')
- && ![
- "pub async fn ",
- "pub const ",
- "pub enum ",
- "pub fn ",
- "pub mod ",
- "pub static ",
- "pub struct ",
- "pub trait ",
- "pub type ",
- "pub use ",
- ]
- .iter()
- .any(|prefix| line.starts_with(prefix));
- assert!(!public_field, "{module} exposes native field `{line}`");
- }
- }
-}
-
-const fn expected_public_type_count() -> usize {
- let count = 28;
- #[cfg(feature = "sync")]
- let count = count + 9;
- #[cfg(feature = "local-signing")]
- let count = count + 2;
- #[cfg(feature = "nostr")]
- let count = count + 1;
- #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
- let count = count + 6;
- #[cfg(feature = "radrootsd")]
- let count = count + 5;
- count
-}
diff --git a/crates/sdk/tests/unit/adapters_radrootsd_tests.rs b/crates/sdk/tests/unit/adapters_radrootsd_tests.rs
@@ -1,940 +0,0 @@
-use super::*;
-use radroots_event::wire::Nip01EventWire;
-use radroots_protocol::radrootsd::transport_publish::v5::{
- DeliveryPolicy as TransportPublishDeliveryPolicy, EventRequest as TransportPublishEventRequest,
- EventResponse as TransportPublishEventResponse, Job as TransportPublishJobView,
- JobStatus as TransportPublishJobStatus,
- NostrTargetSourcePolicy as NostrPublishTargetSourcePolicy,
- OutcomeKind as TransportPublishOutcomeKind,
- ReticulumBehavior as TransportPublishReticulumBehavior, Target as TransportPublishTarget,
- TargetOutcome as TransportPublishTargetOutcome, TargetPolicy as TransportPublishTargetPolicy,
- TargetSource as TransportPublishTargetSource,
-};
-const RADROOTS_RETICULUM_ENDPOINT_URI: &str = "reticulum:local";
-use std::io::{Read, Write};
-use std::net::TcpListener;
-use std::thread::JoinHandle;
-
-const SIGNED_EVENT_PUBLIC_KEY: &str =
- "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
-
-struct RecordedHttpRequest {
- request_line: String,
- headers: Vec<(String, String)>,
- body: String,
-}
-
-fn spawn_http_server(
- status: &str,
- response_body: &str,
-) -> (String, JoinHandle<RecordedHttpRequest>) {
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server");
- let endpoint = format!("http://{}/rpc", listener.local_addr().expect("addr"));
- let status = status.to_owned();
- let response_body = response_body.to_owned();
- let content_length = response_body.len();
- let handle = std::thread::spawn(move || {
- let (mut stream, _) = listener.accept().expect("accept");
- let mut request = Vec::new();
- let mut buffer = [0u8; 1024];
- loop {
- let read = stream.read(&mut buffer).expect("read request");
- if read == 0 {
- break;
- }
- request.extend_from_slice(&buffer[..read]);
- if request.windows(4).any(|window| window == b"\r\n\r\n") {
- let headers_end = request
- .windows(4)
- .position(|window| window == b"\r\n\r\n")
- .expect("headers end")
- + 4;
- let header_text = String::from_utf8_lossy(&request[..headers_end]);
- let request_content_length = header_text
- .lines()
- .find_map(|line| {
- let (name, value) = line.split_once(':')?;
- name.eq_ignore_ascii_case("content-length")
- .then(|| value.trim().parse::<usize>().expect("content length"))
- })
- .unwrap_or(0);
- while request.len() < headers_end + request_content_length {
- let read = stream.read(&mut buffer).expect("read body");
- if read == 0 {
- break;
- }
- request.extend_from_slice(&buffer[..read]);
- }
- break;
- }
- }
- let request_text = String::from_utf8_lossy(&request);
- let (headers_text, body) = request_text.split_once("\r\n\r\n").expect("request body");
- let mut header_lines = headers_text.lines();
- let request_line = header_lines.next().expect("request line").to_owned();
- let headers = header_lines
- .filter_map(|line| {
- let (name, value) = line.split_once(':')?;
- Some((name.to_ascii_lowercase(), value.trim().to_owned()))
- })
- .collect::<Vec<_>>();
- let response = format!(
- "HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {content_length}\r\nconnection: close\r\n\r\n{response_body}",
- );
- stream
- .write_all(response.as_bytes())
- .expect("write response");
- RecordedHttpRequest {
- request_line,
- headers,
- body: body.to_owned(),
- }
- });
- (endpoint, handle)
-}
-
-fn signed_event() -> SignedEvent {
- let mut wire = Nip01EventWire {
- id: String::new(),
- pubkey: SIGNED_EVENT_PUBLIC_KEY.to_owned(),
- created_at: 1_700_000_000,
- kind: 30_402,
- tags: vec![vec!["d".to_owned(), "listing-1".to_owned()]],
- content: "{\"name\":\"carrots\"}".to_owned(),
- sig: "c".repeat(128),
- extra: Default::default(),
- };
- wire.id = wire.computed_event_id().expect("event id").into_string();
- let raw_json = serde_json::to_string(&wire).expect("raw event json");
- SignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event")
-}
-
-fn publish_request() -> TransportPublishEventRequest {
- TransportPublishEventRequest {
- raw_event_json: signed_event().raw_json().to_owned(),
- target_policy: TransportPublishTargetPolicy::nostr(
- NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault,
- vec!["wss://relay.example.com".to_owned()],
- ),
- delivery_policy: TransportPublishDeliveryPolicy::Any,
- idempotency_key: Some("idem-1".to_owned()),
- timeout_ms: Some(10_000),
- }
-}
-
-fn signed_event_id() -> String {
- signed_event().id_str().to_owned()
-}
-
-fn signed_event_pubkey() -> String {
- signed_event().pubkey().to_hex().to_owned()
-}
-
-fn job_status_for_outcome(outcome_kind: TransportPublishOutcomeKind) -> TransportPublishJobStatus {
- if outcome_kind.counts_toward_accepted_delivery() {
- TransportPublishJobStatus::DeliverySatisfied
- } else if outcome_kind.is_retryable() {
- TransportPublishJobStatus::DeliveryUnsatisfiedRetryable
- } else if outcome_kind.is_terminal_failure() {
- TransportPublishJobStatus::DeliveryUnsatisfiedTerminal
- } else if outcome_kind == TransportPublishOutcomeKind::DeferredUntilImplemented {
- TransportPublishJobStatus::DeliveryDeferred
- } else {
- TransportPublishJobStatus::DeliveryUnsatisfiedRetryable
- }
-}
-
-fn job(outcome_kind: TransportPublishOutcomeKind) -> TransportPublishJobView {
- let status = job_status_for_outcome(outcome_kind);
- TransportPublishJobView {
- job_id: "job-1".to_owned(),
- status,
- terminal: matches!(
- status,
- TransportPublishJobStatus::DeliverySatisfied
- | TransportPublishJobStatus::DeliveryUnsatisfiedTerminal
- | TransportPublishJobStatus::DeliveryDeferred
- | TransportPublishJobStatus::DeliveryDeferredUntilImplemented
- | TransportPublishJobStatus::Rejected
- ),
- delivery_satisfied: status == TransportPublishJobStatus::DeliverySatisfied,
- event_id: signed_event_id(),
- pubkey: signed_event_pubkey(),
- event_kind: 30_402,
- target_policy: TransportPublishTargetPolicy::nostr(
- NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault,
- vec!["wss://relay.example.com".to_owned()],
- ),
- delivery_policy: TransportPublishDeliveryPolicy::Any,
- target_count: 1,
- acknowledged_count: usize::from(outcome_kind.counts_toward_accepted_delivery()),
- retryable_count: usize::from(outcome_kind.is_retryable()),
- terminal_count: usize::from(outcome_kind.is_terminal_failure()),
- requested_at_ms: 1_700_000_000_000,
- completed_at_ms: Some(1_700_000_000_100),
- last_error: None,
- targets: vec![TransportPublishTargetOutcome {
- transport_kind: "nostr".to_owned(),
- endpoint_uri: "wss://relay.example.com".to_owned(),
- target_scope: None,
- target_label: None,
- source: TransportPublishTargetSource::Request,
- attempted: true,
- outcome_kind,
- message: Some("relay outcome".to_owned()),
- latency_ms: Some(7),
- }],
- }
-}
-
-fn explicit_nostr_job(
- endpoints: Vec<String>,
- delivery_policy: TransportPublishDeliveryPolicy,
-) -> TransportPublishJobView {
- let targets = endpoints
- .iter()
- .map(|endpoint| TransportPublishTargetOutcome {
- transport_kind: "nostr".to_owned(),
- endpoint_uri: endpoint.clone(),
- target_scope: None,
- target_label: None,
- source: TransportPublishTargetSource::Request,
- attempted: true,
- outcome_kind: TransportPublishOutcomeKind::Accepted,
- message: Some("relay outcome".to_owned()),
- latency_ms: Some(7),
- })
- .collect::<Vec<_>>();
- TransportPublishJobView {
- job_id: "job-explicit-nostr".to_owned(),
- status: TransportPublishJobStatus::DeliverySatisfied,
- terminal: true,
- delivery_satisfied: true,
- event_id: signed_event_id(),
- pubkey: signed_event_pubkey(),
- event_kind: 30_402,
- target_policy: TransportPublishTargetPolicy::explicit_targets(
- endpoints
- .iter()
- .map(|endpoint| TransportPublishTarget::nostr(endpoint.as_str()))
- .collect::<Vec<_>>(),
- ),
- delivery_policy,
- target_count: targets.len(),
- acknowledged_count: targets.len(),
- retryable_count: 0,
- terminal_count: 0,
- requested_at_ms: 1_700_000_000_000,
- completed_at_ms: Some(1_700_000_000_100),
- last_error: None,
- targets,
- }
-}
-
-fn reticulum_deferred_job() -> TransportPublishJobView {
- TransportPublishJobView {
- job_id: "job-reticulum".to_owned(),
- status: TransportPublishJobStatus::DeliveryDeferred,
- terminal: true,
- delivery_satisfied: false,
- event_id: signed_event_id(),
- pubkey: signed_event_pubkey(),
- event_kind: 30_402,
- target_policy: TransportPublishTargetPolicy::explicit_targets(vec![
- TransportPublishTarget::reticulum(
- TransportPublishReticulumBehavior::DeferDeliveryPlans,
- ),
- ]),
- delivery_policy: TransportPublishDeliveryPolicy::Any,
- target_count: 1,
- acknowledged_count: 0,
- retryable_count: 0,
- terminal_count: 0,
- requested_at_ms: 1_700_000_000_000,
- completed_at_ms: Some(1_700_000_000_100),
- last_error: Some("delivery_deferred_until_implemented".to_owned()),
- targets: vec![TransportPublishTargetOutcome {
- transport_kind: "reticulum".to_owned(),
- endpoint_uri: RADROOTS_RETICULUM_ENDPOINT_URI.to_owned(),
- target_scope: None,
- target_label: None,
- source: TransportPublishTargetSource::Reticulum,
- attempted: false,
- outcome_kind: TransportPublishOutcomeKind::DeferredUntilImplemented,
- message: Some("reticulum deferred until implemented".to_owned()),
- latency_ms: None,
- }],
- }
-}
-
-fn publish_response_json_for_job(job: TransportPublishJobView) -> String {
- serde_json::json!({
- "jsonrpc": "2.0",
- "id": SDK_RADROOTSD_PUBLISH_REQUEST_ID,
- "result": {
- "deduplicated": false,
- "job": job
- }
- })
- .to_string()
-}
-
-fn publish_response_json() -> String {
- publish_response_json_for_job(job(TransportPublishOutcomeKind::Accepted))
-}
-
-fn reticulum_deferred_response_json() -> String {
- serde_json::json!({
- "jsonrpc": "2.0",
- "id": SDK_RADROOTSD_PUBLISH_REQUEST_ID,
- "result": {
- "deduplicated": false,
- "job": reticulum_deferred_job()
- }
- })
- .to_string()
-}
-
-fn assert_message(error: RadrootsdError, fragment: &str) {
- let message = error.to_string();
- assert!(
- message.contains(fragment),
- "expected {message:?} to contain {fragment:?}"
- );
-}
-
-#[test]
-fn auth_headers_omit_or_redact_bearer_authorization() {
- let none = auth_headers(&RadrootsdAuth::None).expect("none auth");
- assert!(!none.contains_key(AUTHORIZATION));
- assert_eq!(format!("{:?}", RadrootsdAuth::None), "None");
-
- let bearer = auth_headers(&RadrootsdAuth::BearerToken("sdk-token".into())).expect("bearer");
- assert_eq!(
- bearer
- .get(AUTHORIZATION)
- .expect("authorization")
- .to_str()
- .expect("authorization str"),
- "Bearer sdk-token"
- );
-
- let error = auth_headers(&RadrootsdAuth::BearerToken("bad\ntoken".into())).expect_err("error");
- assert!(matches!(error, RadrootsdError::InvalidAuthHeader(_)));
- assert_eq!(
- format!("{:?}", RadrootsdAuth::BearerToken("token-secret".into())),
- "BearerToken(<redacted>)"
- );
- assert!(
- RadrootsdError::InvalidAuthHeader("bad header".to_owned())
- .to_string()
- .contains("invalid radrootsd bearer token header")
- );
- assert_eq!(
- RadrootsdError::InvalidRequest("invalid request".to_owned()).to_string(),
- "invalid request"
- );
- assert_eq!(
- RadrootsdError::Http("http failed".to_owned()).to_string(),
- "http failed"
- );
- assert_eq!(
- RadrootsdError::MalformedResponse("bad envelope".to_owned()).to_string(),
- "bad envelope"
- );
-}
-
-#[test]
-fn radrootsd_publish_config_builders_preserve_typed_runtime_options() {
- let config = RadrootsdPublishConfig::new("http://127.0.0.1:8080/rpc")
- .with_auth(RadrootsdAuth::BearerToken("sdk-token".to_owned()))
- .with_timeout(Duration::from_millis(250));
- let adapter = RadrootsdPublishAdapter::new(config.clone());
-
- assert_eq!(adapter.config(), &config);
- assert_eq!(adapter.config().endpoint, "http://127.0.0.1:8080/rpc");
- assert_eq!(
- adapter.config().auth,
- RadrootsdAuth::BearerToken("sdk-token".to_owned())
- );
- assert_eq!(adapter.config().timeout, Duration::from_millis(250));
-}
-
-#[test]
-fn publish_event_request_json_uses_signed_event_contract() {
- let value = publish_event_request_json(&publish_request()).expect("request json");
-
- let raw_event_json = value["raw_event_json"].as_str().expect("raw event json");
- let raw_event: serde_json::Value = serde_json::from_str(raw_event_json).expect("raw event");
- assert_eq!(raw_event["id"], signed_event_id());
- assert_eq!(raw_event["pubkey"], SIGNED_EVENT_PUBLIC_KEY);
- assert_eq!(raw_event["kind"], 30_402);
- assert_eq!(value["target_policy"]["kind"], "nostr");
- assert_eq!(
- value["target_policy"]["source_policy"],
- "request_then_author_write_then_daemon_default"
- );
- assert_eq!(
- value["target_policy"]["relay_urls"][0],
- "wss://relay.example.com"
- );
- assert_eq!(value["delivery_policy"]["mode"], "any");
- assert_eq!(value["idempotency_key"], "idem-1");
- let rendered = value.to_string();
- assert!(!rendered.contains("signer_session_id"));
- assert!(!rendered.contains("bridge."));
-}
-
-#[test]
-fn decode_jsonrpc_response_validates_envelope_and_errors() {
- let response: TransportPublishEventResponse = decode_jsonrpc_response(
- METHOD_EVENT,
- SDK_RADROOTSD_PUBLISH_REQUEST_ID,
- publish_response_json().as_str(),
- )
- .expect("response");
- assert_eq!(response.job.event_id, signed_event_id());
-
- let error = decode_jsonrpc_response::<TransportPublishEventResponse>(
- METHOD_EVENT,
- SDK_RADROOTSD_PUBLISH_REQUEST_ID,
- r#"{"jsonrpc":"2.0","id":"radroots-sdk-transport-publish-event","error":{"code":-32001,"message":"principal unauthorized"}}"#,
- )
- .expect_err("jsonrpc error");
- assert!(matches!(
- error,
- RadrootsdError::JsonRpc { code: -32001, .. }
- ));
- assert_message(error, "principal unauthorized");
-
- assert!(matches!(
- decode_jsonrpc_response::<serde_json::Value>(METHOD_EVENT, "expected", "not json"),
- Err(RadrootsdError::MalformedResponse(_))
- ));
- assert!(matches!(
- decode_jsonrpc_response::<serde_json::Value>(
- METHOD_EVENT,
- "expected",
- r#"{"jsonrpc":"2.0","id":"other","result":{}}"#
- ),
- Err(RadrootsdError::MalformedResponse(_))
- ));
- assert!(matches!(
- decode_jsonrpc_response::<serde_json::Value>(
- METHOD_EVENT,
- "expected",
- r#"{"jsonrpc":"2.0","id":"expected"}"#
- ),
- Err(RadrootsdError::MalformedResponse(_))
- ));
- assert!(matches!(
- decode_jsonrpc_response::<serde_json::Value>(
- METHOD_EVENT,
- "expected",
- r#"{"jsonrpc":"1.0","id":"expected","result":{}}"#
- ),
- Err(RadrootsdError::MalformedResponse(_))
- ));
- assert!(matches!(
- decode_jsonrpc_response::<serde_json::Value>(
- METHOD_EVENT,
- "expected",
- r#"{"jsonrpc":"2.0","id":"expected","result":{},"error":{"code":-32002,"message":"both"}}"#
- ),
- Err(RadrootsdError::MalformedResponse(_))
- ));
-}
-
-#[tokio::test]
-async fn publish_event_posts_transport_publish_jsonrpc() {
- let (endpoint, handle) = spawn_http_server("200 OK", publish_response_json().as_str());
-
- let receipt = publish_event(
- endpoint.as_str(),
- &RadrootsdAuth::BearerToken("sdk-token".into()),
- &publish_request(),
- Duration::from_secs(2),
- )
- .await
- .expect("publish");
-
- assert_eq!(receipt.job.event_id, signed_event_id());
- let recorded = handle.join().expect("server thread");
- assert_eq!(recorded.request_line, "POST /rpc HTTP/1.1");
- assert!(
- recorded
- .headers
- .iter()
- .any(|(name, value)| name == "authorization" && value == "Bearer sdk-token")
- );
- let body: serde_json::Value = serde_json::from_str(recorded.body.as_str()).expect("body");
- assert_eq!(body["method"], METHOD_EVENT);
- assert_eq!(body["id"], SDK_RADROOTSD_PUBLISH_REQUEST_ID);
- let raw_event_json = body["params"]["raw_event_json"]
- .as_str()
- .expect("raw event json");
- let raw_event: serde_json::Value = serde_json::from_str(raw_event_json).expect("raw event");
- assert_eq!(raw_event["content"], "{\"name\":\"carrots\"}");
- assert_eq!(body["params"]["target_policy"]["kind"], "nostr");
- assert_eq!(
- body["params"]["target_policy"]["relay_urls"][0],
- "wss://relay.example.com"
- );
-}
-
-#[tokio::test]
-async fn publish_signed_event_posts_typed_radrootsd_request() {
- let mut response_job = explicit_nostr_job(
- vec!["wss://relay.example.com".to_owned()],
- TransportPublishDeliveryPolicy::All,
- );
- response_job.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![
- TransportPublishTarget::nostr("wss://relay.example.com")
- .with_scope("farm.local")
- .with_label("Farm relay"),
- ]);
- response_job.targets[0].target_scope = Some("farm.local".to_owned());
- response_job.targets[0].target_label = Some("Farm relay".to_owned());
- let response_json = publish_response_json_for_job(response_job);
- let (endpoint, handle) = spawn_http_server("200 OK", response_json.as_str());
- let adapter = RadrootsdPublishAdapter::new(
- RadrootsdPublishConfig::new(endpoint)
- .with_auth(RadrootsdAuth::BearerToken("sdk-token".into())),
- );
-
- let receipt = adapter
- .publish_signed_event(RadrootsdPublishRequest {
- signed_event: signed_event(),
- target_policy: TransportPublishTargetPolicy::explicit_targets(vec![
- TransportPublishTarget::nostr("wss://relay.example.com")
- .with_scope("farm.local")
- .with_label("Farm relay"),
- ]),
- delivery_policy: TransportPublishDeliveryPolicy::All,
- idempotency_key: Some("idem-typed".to_owned()),
- timeout_ms: Some(7_000),
- })
- .await
- .expect("typed publish");
-
- assert!(receipt.job.delivery_satisfied);
- assert_eq!(
- receipt.job.targets[0].target_scope.as_deref(),
- Some("farm.local")
- );
- assert_eq!(
- receipt.job.targets[0].target_label.as_deref(),
- Some("Farm relay")
- );
- let recorded = handle.join().expect("server thread");
- assert!(
- recorded
- .headers
- .iter()
- .any(|(name, value)| name == "authorization" && value == "Bearer sdk-token")
- );
- let body: serde_json::Value = serde_json::from_str(recorded.body.as_str()).expect("body");
- assert_eq!(body["params"]["delivery_policy"]["mode"], "all");
- assert_eq!(body["params"]["target_policy"]["kind"], "explicit_targets");
- assert_eq!(
- body["params"]["target_policy"]["targets"][0]["endpoint_uri"],
- "wss://relay.example.com"
- );
- assert_eq!(
- body["params"]["target_policy"]["targets"][0]["target_scope"],
- "farm.local"
- );
- assert_eq!(
- body["params"]["target_policy"]["targets"][0]["target_label"],
- "Farm relay"
- );
- assert_eq!(body["params"]["idempotency_key"], "idem-typed");
- assert_eq!(body["params"]["timeout_ms"], 7_000);
-}
-
-#[tokio::test]
-async fn publish_signed_event_preserves_typed_reticulum_behavior() {
- let response_json = reticulum_deferred_response_json();
- let (endpoint, handle) = spawn_http_server("200 OK", response_json.as_str());
- let adapter = RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new(endpoint));
-
- let response = adapter
- .publish_signed_event(RadrootsdPublishRequest {
- signed_event: signed_event(),
- target_policy: TransportPublishTargetPolicy::explicit_targets(vec![
- TransportPublishTarget::reticulum(
- TransportPublishReticulumBehavior::DeferDeliveryPlans,
- ),
- ]),
- delivery_policy: TransportPublishDeliveryPolicy::Any,
- idempotency_key: Some("idem-reticulum".to_owned()),
- timeout_ms: None,
- })
- .await
- .expect("typed Reticulum publish request");
- assert_eq!(
- response.job.status,
- TransportPublishJobStatus::DeliveryDeferred
- );
- assert!(!response.job.delivery_satisfied);
-
- let recorded = handle.join().expect("server thread");
- let body: serde_json::Value = serde_json::from_str(recorded.body.as_str()).expect("body");
- assert_eq!(body["params"]["target_policy"]["kind"], "explicit_targets");
- assert_eq!(
- body["params"]["target_policy"]["targets"][0]["transport_kind"],
- "reticulum"
- );
- assert_eq!(
- body["params"]["target_policy"]["targets"][0]["endpoint_uri"],
- RADROOTS_RETICULUM_ENDPOINT_URI
- );
- assert_eq!(
- body["params"]["target_policy"]["targets"][0]["reticulum_behavior"],
- "defer_delivery_plans"
- );
-}
-
-#[tokio::test]
-async fn publish_signed_event_rejects_mismatched_daemon_event_identity() {
- let mut response_job = job(TransportPublishOutcomeKind::Accepted);
- response_job.event_id = "0".repeat(64);
- let response_json = publish_response_json_for_job(response_job);
- let (endpoint, _handle) = spawn_http_server("200 OK", response_json.as_str());
- let adapter = RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new(endpoint));
-
- let error = adapter
- .publish_signed_event(RadrootsdPublishRequest {
- signed_event: signed_event(),
- target_policy: TransportPublishTargetPolicy::nostr(
- NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault,
- vec!["wss://relay.example.com".to_owned()],
- ),
- delivery_policy: TransportPublishDeliveryPolicy::Any,
- idempotency_key: Some("idem-mismatch".to_owned()),
- timeout_ms: None,
- })
- .await
- .expect_err("mismatched response");
-
- assert!(matches!(error, RadrootsdError::MalformedResponse(_)));
- assert_message(error, "event_id");
-}
-
-#[tokio::test]
-async fn publish_signed_event_rejects_mismatched_daemon_pubkey_and_kind() {
- for (field, response_job) in [
- {
- let mut response_job = job(TransportPublishOutcomeKind::Accepted);
- response_job.pubkey = "0".repeat(64);
- ("pubkey", response_job)
- },
- {
- let mut response_job = job(TransportPublishOutcomeKind::Accepted);
- response_job.event_kind = 30_403;
- ("event_kind", response_job)
- },
- ] {
- let response_json = publish_response_json_for_job(response_job);
- let (endpoint, _handle) = spawn_http_server("200 OK", response_json.as_str());
- let adapter = RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new(endpoint));
-
- let error = adapter
- .publish_signed_event(RadrootsdPublishRequest {
- signed_event: signed_event(),
- target_policy: TransportPublishTargetPolicy::nostr(
- NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault,
- vec!["wss://relay.example.com".to_owned()],
- ),
- delivery_policy: TransportPublishDeliveryPolicy::Any,
- idempotency_key: Some(format!("idem-mismatch-{field}")),
- timeout_ms: None,
- })
- .await
- .expect_err("mismatched response");
-
- assert!(matches!(error, RadrootsdError::MalformedResponse(_)));
- assert_message(error, field);
- }
-}
-
-#[tokio::test]
-async fn publish_signed_event_rejects_mismatched_daemon_delivery_policy() {
- let mut response_job = job(TransportPublishOutcomeKind::Accepted);
- response_job.delivery_policy = TransportPublishDeliveryPolicy::All;
- let response_json = publish_response_json_for_job(response_job);
- let (endpoint, _handle) = spawn_http_server("200 OK", response_json.as_str());
- let adapter = RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new(endpoint));
-
- let error = adapter
- .publish_signed_event(RadrootsdPublishRequest {
- signed_event: signed_event(),
- target_policy: TransportPublishTargetPolicy::nostr(
- NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault,
- vec!["wss://relay.example.com".to_owned()],
- ),
- delivery_policy: TransportPublishDeliveryPolicy::Any,
- idempotency_key: Some("idem-delivery-policy-mismatch".to_owned()),
- timeout_ms: None,
- })
- .await
- .expect_err("delivery policy mismatch");
-
- assert!(matches!(error, RadrootsdError::MalformedResponse(_)));
- assert_message(error, "delivery_policy");
-}
-
-#[tokio::test]
-async fn publish_signed_event_rejects_mismatched_explicit_target_response() {
- let (endpoint, _handle) = spawn_http_server("200 OK", publish_response_json().as_str());
- let adapter = RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new(endpoint));
-
- let error = adapter
- .publish_signed_event(RadrootsdPublishRequest {
- signed_event: signed_event(),
- target_policy: TransportPublishTargetPolicy::explicit_targets(vec![
- TransportPublishTarget::reticulum(
- TransportPublishReticulumBehavior::DeferDeliveryPlans,
- ),
- ]),
- delivery_policy: TransportPublishDeliveryPolicy::Any,
- idempotency_key: Some("idem-target-mismatch".to_owned()),
- timeout_ms: None,
- })
- .await
- .expect_err("target mismatch");
-
- assert!(matches!(error, RadrootsdError::MalformedResponse(_)));
- assert_message(error, "target_policy");
-}
-
-#[tokio::test]
-async fn publish_signed_event_accepts_reordered_explicit_target_outcomes() {
- let mut response_job = explicit_nostr_job(
- vec![
- "wss://relay-a.example.com".to_owned(),
- "wss://relay-b.example.com".to_owned(),
- ],
- TransportPublishDeliveryPolicy::Any,
- );
- response_job.targets.reverse();
- let response_json = publish_response_json_for_job(response_job);
- let (endpoint, _handle) = spawn_http_server("200 OK", response_json.as_str());
- let adapter = RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new(endpoint));
-
- let response = adapter
- .publish_signed_event(RadrootsdPublishRequest {
- signed_event: signed_event(),
- target_policy: TransportPublishTargetPolicy::explicit_targets(vec![
- TransportPublishTarget::nostr("wss://relay-a.example.com"),
- TransportPublishTarget::nostr("wss://relay-b.example.com"),
- ]),
- delivery_policy: TransportPublishDeliveryPolicy::Any,
- idempotency_key: Some("idem-explicit-reordered-outcomes".to_owned()),
- timeout_ms: None,
- })
- .await
- .expect("reordered explicit target outcomes");
-
- assert!(response.job.delivery_satisfied);
- assert_eq!(
- response.job.targets[0].endpoint_uri,
- "wss://relay-b.example.com"
- );
- assert_eq!(
- response.job.targets[1].endpoint_uri,
- "wss://relay-a.example.com"
- );
-}
-
-#[tokio::test]
-async fn publish_signed_event_rejects_mismatched_explicit_target_outcomes() {
- let mut response_job = explicit_nostr_job(
- vec!["wss://relay.example.com".to_owned()],
- TransportPublishDeliveryPolicy::Any,
- );
- response_job.targets[0].endpoint_uri = "wss://relay-other.example.com".to_owned();
- let response_json = publish_response_json_for_job(response_job);
- let (endpoint, _handle) = spawn_http_server("200 OK", response_json.as_str());
- let adapter = RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new(endpoint));
-
- let error = adapter
- .publish_signed_event(RadrootsdPublishRequest {
- signed_event: signed_event(),
- target_policy: TransportPublishTargetPolicy::explicit_targets(vec![
- TransportPublishTarget::nostr("wss://relay.example.com"),
- ]),
- delivery_policy: TransportPublishDeliveryPolicy::Any,
- idempotency_key: Some("idem-explicit-outcome-mismatch".to_owned()),
- timeout_ms: None,
- })
- .await
- .expect_err("explicit target outcome mismatch");
-
- assert!(matches!(error, RadrootsdError::MalformedResponse(_)));
- assert_message(error, "explicit target policy");
-}
-
-#[tokio::test]
-async fn publish_signed_event_rejects_mismatched_scoped_explicit_target_outcomes() {
- let mut response_job = explicit_nostr_job(
- vec!["wss://relay.example.com".to_owned()],
- TransportPublishDeliveryPolicy::Any,
- );
- response_job.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![
- TransportPublishTarget::nostr("wss://relay.example.com").with_scope("farm.local"),
- ]);
- response_job.targets[0].target_scope = Some("farm.remote".to_owned());
- let response_json = publish_response_json_for_job(response_job);
- let (endpoint, _handle) = spawn_http_server("200 OK", response_json.as_str());
- let adapter = RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new(endpoint));
-
- let error = adapter
- .publish_signed_event(RadrootsdPublishRequest {
- signed_event: signed_event(),
- target_policy: TransportPublishTargetPolicy::explicit_targets(vec![
- TransportPublishTarget::nostr("wss://relay.example.com").with_scope("farm.local"),
- ]),
- delivery_policy: TransportPublishDeliveryPolicy::Any,
- idempotency_key: Some("idem-scoped-explicit-outcome-mismatch".to_owned()),
- timeout_ms: None,
- })
- .await
- .expect_err("scoped explicit target outcome mismatch");
-
- assert!(matches!(error, RadrootsdError::MalformedResponse(_)));
- assert_message(error, "explicit target policy");
-}
-
-#[tokio::test]
-async fn publish_event_http_errors_omit_body_and_token_material() {
- let body = "{\"error\":\"token-secret content carrots\"}";
- let (endpoint, _handle) = spawn_http_server("503 Service Unavailable", body);
-
- let error = publish_event(
- endpoint.as_str(),
- &RadrootsdAuth::BearerToken("token-secret".into()),
- &publish_request(),
- Duration::from_secs(2),
- )
- .await
- .expect_err("http error");
- let message = error.to_string();
-
- assert!(message.contains("503"));
- assert!(message.contains("response body omitted"));
- assert!(!message.contains("token-secret"));
- assert!(!message.contains("carrots"));
-}
-
-#[tokio::test]
-async fn publish_event_empty_http_error_reports_empty_body() {
- let (endpoint, _handle) = spawn_http_server("500 Internal Server Error", "");
-
- let error = publish_event(
- endpoint.as_str(),
- &RadrootsdAuth::None,
- &publish_request(),
- Duration::from_secs(2),
- )
- .await
- .expect_err("http error");
-
- assert!(error.to_string().contains("response body empty"));
-}
-
-#[tokio::test]
-async fn publish_signed_event_rejects_invalid_target_requests_before_http() {
- let adapter =
- RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new("http://127.0.0.1:9/rpc"));
- let base = RadrootsdPublishRequest {
- signed_event: signed_event(),
- target_policy: TransportPublishTargetPolicy::nostr(
- NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault,
- vec!["wss://relay.example.com".to_owned()],
- ),
- delivery_policy: TransportPublishDeliveryPolicy::Any,
- idempotency_key: Some("idem-1".to_owned()),
- timeout_ms: Some(1_000),
- };
-
- let mut invalid_quorum = base.clone();
- invalid_quorum.delivery_policy = TransportPublishDeliveryPolicy::Quorum { quorum: 0 };
- let mut too_many_targets = base.clone();
- too_many_targets.target_policy = TransportPublishTargetPolicy::nostr(
- NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault,
- (0..=SDK_RADROOTSD_PUBLISH_MAX_TARGETS)
- .map(|index| format!("wss://relay-{index}.example.com"))
- .collect(),
- );
- let mut empty_endpoint_uri = base.clone();
- empty_endpoint_uri.target_policy = TransportPublishTargetPolicy::nostr(
- NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault,
- vec![" ".to_owned()],
- );
- let mut nostr_reticulum_behavior = base.clone();
- nostr_reticulum_behavior.target_policy =
- TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget {
- transport_kind: "nostr".to_owned(),
- endpoint_uri: "wss://relay.example.com".to_owned(),
- target_scope: None,
- target_label: None,
- reticulum_behavior: Some(TransportPublishReticulumBehavior::RejectDeliveryAttempts),
- }]);
- let mut explicit_radrootsd_target = base.clone();
- explicit_radrootsd_target.target_policy =
- TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget {
- transport_kind: "radrootsd".to_owned(),
- endpoint_uri: "radrootsd-execution:publish".to_owned(),
- target_scope: None,
- target_label: None,
- reticulum_behavior: None,
- }]);
- let radrootsd_error = adapter
- .publish_signed_event(explicit_radrootsd_target)
- .await
- .expect_err("explicit radrootsd target");
- assert!(matches!(
- radrootsd_error,
- RadrootsdError::InvalidRequest(message)
- if message.contains("transport target 0 kind must be canonical lowercase")
- ));
- let mut empty_idempotency = base;
- empty_idempotency.idempotency_key = Some(" ".to_owned());
-
- for request in [
- invalid_quorum,
- too_many_targets,
- empty_endpoint_uri,
- nostr_reticulum_behavior,
- empty_idempotency,
- ] {
- assert!(matches!(
- adapter.publish_signed_event(request).await,
- Err(RadrootsdError::InvalidRequest(_))
- ));
- }
-}
-
-#[tokio::test]
-async fn adapter_rejects_invalid_request_before_transport() {
- let adapter =
- RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new("http://127.0.0.1:9/rpc"));
- let request = RadrootsdPublishRequest {
- signed_event: signed_event(),
- target_policy: TransportPublishTargetPolicy::nostr(
- NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault,
- Vec::new(),
- ),
- delivery_policy: TransportPublishDeliveryPolicy::Quorum { quorum: 0 },
- idempotency_key: None,
- timeout_ms: None,
- };
-
- let error = adapter
- .publish_signed_event(request)
- .await
- .expect_err("invalid request");
-
- assert!(matches!(error, RadrootsdError::InvalidRequest(_)));
-}
diff --git a/crates/sql_wasm_runtime/Cargo.toml b/crates/sql_wasm_runtime/Cargo.toml
@@ -1,24 +0,0 @@
-[package]
-name = "radroots_sdk_sql_wasm_runtime"
-publish = false
-version = "0.1.0-alpha"
-edition.workspace = true
-authors = ["Tyson Lupul <tyson@radroots.org>"]
-rust-version.workspace = true
-license.workspace = true
-description = "SQL WASM runtime for Radroots SDK data surfaces"
-repository.workspace = true
-homepage.workspace = true
-
-[lib]
-crate-type = ["rlib"]
-
-[dependencies]
-radroots_sql_core = { workspace = true, features = ["web"] }
-serde = { workspace = true }
-serde_json = { workspace = true }
-serde-wasm-bindgen = { workspace = true }
-wasm-bindgen = { workspace = true }
-
-[lints.rust]
-unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
diff --git a/crates/sql_wasm_runtime/src/lib.rs b/crates/sql_wasm_runtime/src/lib.rs
@@ -1,534 +0,0 @@
-#![forbid(unsafe_code)]
-
-use radroots_sql_core::{ExecOutcome, SqlError, SqlExecutor, utils};
-use serde::de::DeserializeOwned;
-use serde_json::Value;
-use std::cell::Cell;
-use std::sync::atomic::{AtomicBool, Ordering};
-use wasm_bindgen::JsValue;
-
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::prelude::*;
-
-#[cfg(target_arch = "wasm32")]
-#[wasm_bindgen]
-extern "C" {
- #[wasm_bindgen(js_name = __radroots_sql_wasm_exec)]
- fn js_exec(sql: &str, params_json: &str) -> JsValue;
-
- #[wasm_bindgen(js_name = __radroots_sql_wasm_query)]
- fn js_query(sql: &str, params_json: &str) -> JsValue;
-
- #[wasm_bindgen(js_name = __radroots_sql_wasm_export_bytes)]
- fn js_export_bytes() -> JsValue;
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-use std::sync::{Mutex, OnceLock};
-
-#[cfg(all(not(target_arch = "wasm32"), test))]
-use std::collections::VecDeque;
-
-#[cfg(not(target_arch = "wasm32"))]
-type RecordedCall = (String, String);
-
-#[cfg(all(not(target_arch = "wasm32"), test))]
-type NativeHostResult = Result<Value, SqlError>;
-
-#[cfg(not(target_arch = "wasm32"))]
-fn exec_calls() -> &'static Mutex<Vec<RecordedCall>> {
- static EXEC_CALLS: OnceLock<Mutex<Vec<RecordedCall>>> = OnceLock::new();
- EXEC_CALLS.get_or_init(|| Mutex::new(Vec::new()))
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn query_calls() -> &'static Mutex<Vec<RecordedCall>> {
- static QUERY_CALLS: OnceLock<Mutex<Vec<RecordedCall>>> = OnceLock::new();
- QUERY_CALLS.get_or_init(|| Mutex::new(Vec::new()))
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn export_calls() -> &'static Mutex<u64> {
- static EXPORT_CALLS: OnceLock<Mutex<u64>> = OnceLock::new();
- EXPORT_CALLS.get_or_init(|| Mutex::new(0))
-}
-
-#[cfg(all(not(target_arch = "wasm32"), test))]
-fn exec_results() -> &'static Mutex<VecDeque<NativeHostResult>> {
- static EXEC_RESULTS: OnceLock<Mutex<VecDeque<NativeHostResult>>> = OnceLock::new();
- EXEC_RESULTS.get_or_init(|| Mutex::new(VecDeque::new()))
-}
-
-#[cfg(all(not(target_arch = "wasm32"), test))]
-fn query_results() -> &'static Mutex<VecDeque<NativeHostResult>> {
- static QUERY_RESULTS: OnceLock<Mutex<VecDeque<NativeHostResult>>> = OnceLock::new();
- QUERY_RESULTS.get_or_init(|| Mutex::new(VecDeque::new()))
-}
-
-#[cfg(all(not(target_arch = "wasm32"), test))]
-fn push_exec_result(result: NativeHostResult) {
- let mut results = exec_results().lock().expect("exec results lock");
- results.push_back(result);
-}
-
-#[cfg(all(not(target_arch = "wasm32"), test))]
-fn push_query_result(result: NativeHostResult) {
- let mut results = query_results().lock().expect("query results lock");
- results.push_back(result);
-}
-
-#[cfg(all(not(target_arch = "wasm32"), test))]
-fn take_exec_result() -> Option<NativeHostResult> {
- exec_results()
- .lock()
- .expect("exec results lock")
- .pop_front()
-}
-
-#[cfg(all(not(target_arch = "wasm32"), test))]
-fn take_query_result() -> Option<NativeHostResult> {
- query_results()
- .lock()
- .expect("query results lock")
- .pop_front()
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn js_exec(sql: &str, params_json: &str) -> JsValue {
- let mut calls = exec_calls().lock().expect("exec calls lock");
- calls.push((sql.to_string(), params_json.to_string()));
- JsValue::NULL
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn js_query(sql: &str, params_json: &str) -> JsValue {
- let mut calls = query_calls().lock().expect("query calls lock");
- calls.push((sql.to_string(), params_json.to_string()));
- JsValue::NULL
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn js_export_bytes() -> JsValue {
- let mut calls = export_calls().lock().expect("export calls lock");
- *calls += 1;
- JsValue::NULL
-}
-
-const SAVEPOINT: &str = "radroots_schema_tx";
-const EXPORT_LOCK_ERR: &str = "replica db export in progress";
-
-static EXPORT_LOCK_ACTIVE: AtomicBool = AtomicBool::new(false);
-
-thread_local! {
- static EXPORT_LOCK_BYPASS: Cell<bool> = const { Cell::new(false) };
-}
-
-pub struct WasmSqlExecutor;
-
-impl WasmSqlExecutor {
- pub fn new() -> Self {
- Self
- }
-}
-
-impl Default for WasmSqlExecutor {
- fn default() -> Self {
- Self::new()
- }
-}
-
-impl SqlExecutor for WasmSqlExecutor {
- fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> {
- if export_lock_blocked() {
- return Err(SqlError::InvalidArgument(EXPORT_LOCK_ERR.to_string()));
- }
- let v = host_exec_json(sql, params_json)?;
- Ok(exec_outcome_from_json(&v))
- }
-
- fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> {
- let v = host_query_json(sql, params_json)?;
- Ok(query_raw_from_json(&v))
- }
-
- fn begin(&self) -> Result<(), SqlError> {
- if export_lock_blocked() {
- return Err(SqlError::InvalidArgument(EXPORT_LOCK_ERR.to_string()));
- }
- begin_tx();
- Ok(())
- }
-
- fn commit(&self) -> Result<(), SqlError> {
- if export_lock_blocked() {
- return Err(SqlError::InvalidArgument(EXPORT_LOCK_ERR.to_string()));
- }
- commit_tx();
- Ok(())
- }
-
- fn rollback(&self) -> Result<(), SqlError> {
- if export_lock_blocked() {
- return Err(SqlError::InvalidArgument(EXPORT_LOCK_ERR.to_string()));
- }
- rollback_tx();
- Ok(())
- }
-}
-
-pub fn parse_json<T: DeserializeOwned>(s: &str) -> Result<T, SqlError> {
- utils::parse_json(s)
-}
-
-fn host_exec_json(sql: &str, params_json: &str) -> Result<Value, SqlError> {
- let js = exec(sql, params_json);
- #[cfg(all(not(target_arch = "wasm32"), test))]
- if let Some(result) = take_exec_result() {
- return result;
- }
- sql_value_from_js(js)
-}
-
-fn host_query_json(sql: &str, params_json: &str) -> Result<Value, SqlError> {
- let js = query(sql, params_json);
- #[cfg(all(not(target_arch = "wasm32"), test))]
- if let Some(result) = take_query_result() {
- return result;
- }
- sql_value_from_js(js)
-}
-
-#[cfg(target_arch = "wasm32")]
-fn sql_value_from_js(js: JsValue) -> Result<Value, SqlError> {
- serde_wasm_bindgen::from_value(js).map_err(|e| SqlError::SerializationError(e.to_string()))
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn sql_value_from_js(_js: JsValue) -> Result<Value, SqlError> {
- Err(SqlError::UnsupportedPlatform)
-}
-
-fn exec_outcome_from_json(v: &Value) -> ExecOutcome {
- let changes = v.get("changes").and_then(|x| x.as_i64()).unwrap_or(0);
- let last_insert_id = v
- .get("last_insert_id")
- .or_else(|| v.get("lastInsertRowid"))
- .and_then(|x| x.as_i64())
- .unwrap_or(0);
- ExecOutcome {
- changes,
- last_insert_id,
- }
-}
-
-fn query_raw_from_json(v: &Value) -> String {
- v.to_string()
-}
-
-pub fn err_js(err: SqlError) -> JsValue {
- err_js_value(err)
-}
-
-#[cfg(target_arch = "wasm32")]
-fn err_js_value(err: SqlError) -> JsValue {
- match err_js_with_encoder(err, |err| {
- let value = err.to_json();
- serde_wasm_bindgen::to_value(&value).map_err(|_| ())
- }) {
- Ok(value) => value,
- Err(err) => JsValue::from_str(&err.to_string()),
- }
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn err_js_value(err: SqlError) -> JsValue {
- let _ = err.to_json();
- JsValue::NULL
-}
-
-#[cfg(target_arch = "wasm32")]
-fn err_js_with_encoder(
- err: SqlError,
- encode: impl FnOnce(&SqlError) -> Result<JsValue, ()>,
-) -> Result<JsValue, SqlError> {
- encode(&err).map_err(|()| err)
-}
-
-pub fn exec(sql: &str, params_json: &str) -> JsValue {
- js_exec(sql, params_json)
-}
-
-pub fn query(sql: &str, params_json: &str) -> JsValue {
- js_query(sql, params_json)
-}
-
-pub fn export_bytes() -> JsValue {
- js_export_bytes()
-}
-
-pub fn begin_tx() {
- let _ = js_exec(&format!("savepoint {}", SAVEPOINT), "[]");
-}
-
-pub fn commit_tx() {
- let _ = js_exec(&format!("release savepoint {}", SAVEPOINT), "[]");
-}
-
-pub fn rollback_tx() {
- let _ = js_exec(&format!("rollback to savepoint {}", SAVEPOINT), "[]");
- let _ = js_exec(&format!("release savepoint {}", SAVEPOINT), "[]");
-}
-
-pub fn export_lock_begin() -> Result<(), SqlError> {
- let was_active = EXPORT_LOCK_ACTIVE.swap(true, Ordering::SeqCst);
- if was_active {
- return Err(SqlError::InvalidArgument(EXPORT_LOCK_ERR.to_string()));
- }
- Ok(())
-}
-
-pub fn export_lock_end() {
- EXPORT_LOCK_ACTIVE.store(false, Ordering::SeqCst);
-}
-
-pub fn export_lock_active() -> bool {
- EXPORT_LOCK_ACTIVE.load(Ordering::SeqCst)
-}
-
-pub fn with_export_lock_bypass<T>(f: impl FnOnce() -> T) -> T {
- EXPORT_LOCK_BYPASS.with(|flag| {
- let prev = flag.replace(true);
- let out = f();
- flag.set(prev);
- out
- })
-}
-
-fn export_lock_blocked() -> bool {
- if !EXPORT_LOCK_ACTIVE.load(Ordering::SeqCst) {
- return false;
- }
- EXPORT_LOCK_BYPASS.with(|flag| !flag.get())
-}
-
-#[cfg(test)]
-mod tests {
- use std::{
- collections::BTreeMap,
- sync::{Mutex, OnceLock},
- };
-
- use radroots_sql_core::{SqlError, SqlExecutor};
- use serde_json::json;
-
- use super::{
- WasmSqlExecutor, begin_tx, commit_tx, err_js, exec, exec_calls, exec_outcome_from_json,
- exec_results, export_bytes, export_calls, export_lock_active, export_lock_begin,
- export_lock_end, parse_json, push_exec_result, push_query_result, query, query_calls,
- query_raw_from_json, query_results, rollback_tx, with_export_lock_bypass,
- };
-
- fn native_test_lock() -> &'static Mutex<()> {
- static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
- TEST_LOCK.get_or_init(|| Mutex::new(()))
- }
-
- fn reset_native_state() {
- exec_calls().lock().expect("exec calls lock").clear();
- query_calls().lock().expect("query calls lock").clear();
- *export_calls().lock().expect("export calls lock") = 0;
- exec_results().lock().expect("exec results lock").clear();
- query_results().lock().expect("query results lock").clear();
- export_lock_end();
- }
-
- #[test]
- fn parse_json_reports_valid_and_invalid_payloads() {
- let parsed: BTreeMap<String, u64> = parse_json(r#"{"count":2}"#).expect("parse json");
- assert_eq!(parsed.get("count"), Some(&2));
- assert_eq!(
- parse_json::<BTreeMap<String, u64>>("{").unwrap_err().code(),
- "ERR_SERIALIZATION"
- );
- }
-
- #[test]
- fn err_js_accepts_sql_errors() {
- let _ = err_js(SqlError::Internal);
- let _ = err_js(SqlError::UnsupportedPlatform);
- }
-
- #[test]
- fn exec_query_export_delegate_to_js_hooks() {
- let _guard = native_test_lock().lock().expect("native test lock");
- reset_native_state();
-
- let _ = exec("select 1", "[]");
- let _ = query("select 2", "[1]");
- let _ = export_bytes();
-
- let exec_len = exec_calls().lock().map(|calls| calls.len()).unwrap_or(0);
- let query_len = query_calls().lock().map(|calls| calls.len()).unwrap_or(0);
- let export_len = export_calls().lock().map(|calls| *calls).unwrap_or(0);
- assert!(exec_len >= 1);
- assert!(query_len >= 1);
- assert!(export_len >= 1);
- }
-
- #[test]
- fn tx_helpers_emit_expected_savepoint_statements() {
- let _guard = native_test_lock().lock().expect("native test lock");
- reset_native_state();
-
- begin_tx();
- commit_tx();
- rollback_tx();
-
- let calls = exec_calls()
- .lock()
- .map(|calls| calls.clone())
- .unwrap_or_default();
- assert!(
- calls
- .iter()
- .any(|(sql, _)| sql == "savepoint radroots_schema_tx")
- );
- assert!(
- calls
- .iter()
- .any(|(sql, _)| sql == "release savepoint radroots_schema_tx")
- );
- assert!(
- calls
- .iter()
- .any(|(sql, _)| sql == "rollback to savepoint radroots_schema_tx")
- );
- }
-
- #[test]
- fn export_lock_tracks_state() {
- let _guard = native_test_lock().lock().expect("native test lock");
- reset_native_state();
-
- assert!(!export_lock_active());
- export_lock_begin().expect("begin export lock");
- assert!(export_lock_active());
- assert!(with_export_lock_bypass(|| true));
- export_lock_end();
- assert!(!export_lock_active());
- }
-
- #[test]
- fn executor_decodes_exec_outcomes() {
- let _guard = native_test_lock().lock().expect("native test lock");
- reset_native_state();
-
- let executor = WasmSqlExecutor;
- push_exec_result(Ok(json!({"changes": 2, "lastInsertRowid": 99})));
- let outcome = executor
- .exec("insert into listing values (?)", r#"["bin-1"]"#)
- .expect("exec outcome");
- assert_eq!(outcome.changes, 2);
- assert_eq!(outcome.last_insert_id, 99);
-
- push_exec_result(Ok(json!({"changes": 3, "last_insert_id": 101})));
- let outcome = executor
- .exec("update listing set qty = ?", "[1]")
- .expect("exec outcome");
- assert_eq!(outcome.changes, 3);
- assert_eq!(outcome.last_insert_id, 101);
-
- let default_outcome = exec_outcome_from_json(&json!({}));
- assert_eq!(default_outcome.changes, 0);
- assert_eq!(default_outcome.last_insert_id, 0);
-
- let calls = exec_calls().lock().expect("exec calls lock").clone();
- assert!(calls.iter().any(|(sql, params)| {
- sql == "insert into listing values (?)" && params == r#"["bin-1"]"#
- }));
- }
-
- #[test]
- fn executor_decodes_query_results_and_host_errors() {
- let _guard = native_test_lock().lock().expect("native test lock");
- reset_native_state();
-
- let executor = WasmSqlExecutor::new();
- let rows = json!([{"id": "listing-1"}]);
- push_query_result(Ok(rows.clone()));
- assert_eq!(
- executor
- .query_raw("select id from listing", "[]")
- .expect("query rows"),
- query_raw_from_json(&rows)
- );
-
- assert_eq!(
- executor
- .query_raw("select id from listing", "[]")
- .unwrap_err()
- .code(),
- "ERR_UNSUPPORTED_PLATFORM"
- );
-
- push_exec_result(Err(SqlError::SerializationError(
- "host response was not an object".to_string(),
- )));
- assert_eq!(
- executor
- .exec("insert into listing values (?)", "[]")
- .unwrap_err()
- .code(),
- "ERR_SERIALIZATION"
- );
- assert_eq!(
- executor
- .exec("insert into listing values (?)", "[]")
- .unwrap_err()
- .code(),
- "ERR_UNSUPPORTED_PLATFORM"
- );
- }
-
- #[test]
- fn export_lock_rejects_nested_lock_and_write_trait_calls() {
- let _guard = native_test_lock().lock().expect("native test lock");
- reset_native_state();
-
- let executor = WasmSqlExecutor::new();
- export_lock_begin().expect("begin export lock");
- assert_eq!(
- export_lock_begin().unwrap_err().to_string(),
- "invalid argument: replica db export in progress"
- );
- assert_eq!(
- executor
- .exec("insert into listing values (?)", "[]")
- .unwrap_err()
- .code(),
- "ERR_INVALID_ARGUMENT"
- );
- assert_eq!(executor.begin().unwrap_err().code(), "ERR_INVALID_ARGUMENT");
- assert_eq!(
- executor.commit().unwrap_err().code(),
- "ERR_INVALID_ARGUMENT"
- );
- assert_eq!(
- executor.rollback().unwrap_err().code(),
- "ERR_INVALID_ARGUMENT"
- );
-
- with_export_lock_bypass(|| {
- push_exec_result(Ok(json!({"changes": 1, "last_insert_id": 7})));
- let outcome = executor
- .exec("insert into listing values (?)", "[]")
- .expect("bypassed exec");
- assert_eq!(outcome.changes, 1);
- executor.begin().expect("bypassed begin");
- executor.commit().expect("bypassed commit");
- executor.rollback().expect("bypassed rollback");
- });
- assert!(export_lock_active());
- export_lock_end();
- }
-}
diff --git a/crates/trade_bindings/Cargo.toml b/crates/trade_bindings/Cargo.toml
@@ -1,10 +0,0 @@
-[package]
-name = "radroots_trade_bindings"
-description = "Generated bindings for Radroots trade contracts"
-version.workspace = true
-edition.workspace = true
-rust-version.workspace = true
-license.workspace = true
-repository.workspace = true
-homepage.workspace = true
-publish = false
diff --git a/crates/trade_bindings/src/lib.rs b/crates/trade_bindings/src/lib.rs
@@ -1,297 +0,0 @@
-//! Private inventory for canonical native trade bindings.
-//!
-//! The SDK generator authenticates the reviewed mapping against final
-//! `radroots_trade`, `radroots_event`, and `radroots_core` owners. This crate
-//! intentionally does not activate code generation in a public runtime crate.
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum TradeTypeDisposition {
- SourceTradeRoot,
- SourceTradeSupport,
- EventsBindingImport,
- SdkLocalPackageShape,
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct TradeTypeInventoryEntry {
- pub export_name: &'static str,
- pub disposition: TradeTypeDisposition,
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum TradeLargeIntegerPolicy {
- JsonNumberSafeCount,
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct TradeLargeIntegerPolicyEntry {
- pub type_name: &'static str,
- pub field_name: &'static str,
- pub policy: TradeLargeIntegerPolicy,
-}
-
-pub const TRADE_TYPE_INVENTORY: &[TradeTypeInventoryEntry] = &[
- event_import("RadrootsFarmRef"),
- event_import("RadrootsOperationalListing"),
- event_import("RadrootsOperationalListingAvailability"),
- event_import("RadrootsOperationalListingBin"),
- event_import("RadrootsOperationalListingDeliveryMethod"),
- event_import("RadrootsOperationalListingProduct"),
- event_import("RadrootsOperationalListingPublicLocation"),
- event_import("RadrootsOperationalListingStatus"),
- local_shape("RadrootsTradeFacetCount"),
- source_root("RadrootsTradeAgreementStateV1"),
- source_root("RadrootsTradeAttestationStateV1"),
- source_root("RadrootsTradeConflictStateV1"),
- source_root("RadrootsTradeEvidenceStateV1"),
- source_root("RadrootsTradeFulfillmentStateV1"),
- source_root("RadrootsOperationalListingTradeProjection"),
- local_shape("RadrootsTradeListingBackofficeOverlay"),
- local_shape("RadrootsTradeListingBackofficeQuery"),
- local_shape("RadrootsTradeListingBackofficeView"),
- local_shape("RadrootsTradeListingBinProjection"),
- local_shape("RadrootsTradeListingFacets"),
- local_shape("RadrootsTradeListingMarketStatus"),
- local_shape("RadrootsTradeListingProjection"),
- local_shape("RadrootsTradeListingQuery"),
- local_shape("RadrootsTradeListingSort"),
- local_shape("RadrootsTradeListingSortField"),
- source_root("RadrootsOperationalListingSubtotal"),
- source_root("RadrootsOperationalListingTotal"),
- local_shape("RadrootsTradeMarketplaceListingSummary"),
- local_shape("RadrootsTradeModerationFlag"),
- local_shape("RadrootsTradeModerationSeverity"),
- local_shape("RadrootsTradeModerationStatus"),
- source_root("RadrootsTradeNegotiationStateV1"),
- source_root("RadrootsTradePaymentStateV1"),
- source_root("RadrootsTradePrivateTermsStateV1"),
- source_root("RadrootsTradeProjectionV1"),
- local_shape("RadrootsTradeReviewPriority"),
- local_shape("RadrootsTradeReviewQueueEntry"),
- local_shape("RadrootsTradeReviewStatus"),
- local_shape("RadrootsTradeSortDirection"),
-];
-
-pub const TRADE_LARGE_INTEGER_POLICIES: &[TradeLargeIntegerPolicyEntry] = &[
- json_number_safe_count("RadrootsTradeFacetCount", "count"),
- json_number_safe_count(
- "RadrootsTradeListingBackofficeView",
- "open_moderation_flag_count",
- ),
- json_number_safe_count("RadrootsTradeListingProjection", "trade_count"),
- json_number_safe_count("RadrootsTradeListingProjection", "open_trade_count"),
- json_number_safe_count("RadrootsTradeListingProjection", "terminal_trade_count"),
- json_number_safe_count("RadrootsTradeMarketplaceListingSummary", "trade_count"),
- json_number_safe_count("RadrootsTradeMarketplaceListingSummary", "open_trade_count"),
- json_number_safe_count(
- "RadrootsTradeMarketplaceListingSummary",
- "terminal_trade_count",
- ),
-];
-
-const fn source_root(export_name: &'static str) -> TradeTypeInventoryEntry {
- TradeTypeInventoryEntry {
- export_name,
- disposition: TradeTypeDisposition::SourceTradeRoot,
- }
-}
-
-const fn event_import(export_name: &'static str) -> TradeTypeInventoryEntry {
- TradeTypeInventoryEntry {
- export_name,
- disposition: TradeTypeDisposition::EventsBindingImport,
- }
-}
-
-const fn local_shape(export_name: &'static str) -> TradeTypeInventoryEntry {
- TradeTypeInventoryEntry {
- export_name,
- disposition: TradeTypeDisposition::SdkLocalPackageShape,
- }
-}
-
-const fn json_number_safe_count(
- type_name: &'static str,
- field_name: &'static str,
-) -> TradeLargeIntegerPolicyEntry {
- TradeLargeIntegerPolicyEntry {
- type_name,
- field_name,
- policy: TradeLargeIntegerPolicy::JsonNumberSafeCount,
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{TRADE_LARGE_INTEGER_POLICIES, TRADE_TYPE_INVENTORY, TradeTypeDisposition};
-
- #[test]
- fn trade_type_inventory_is_deterministic() {
- let expected = TRADE_TYPE_INVENTORY
- .iter()
- .map(|entry| entry.export_name)
- .collect::<Vec<_>>();
-
- assert_eq!(
- expected,
- [
- "RadrootsFarmRef",
- "RadrootsOperationalListing",
- "RadrootsOperationalListingAvailability",
- "RadrootsOperationalListingBin",
- "RadrootsOperationalListingDeliveryMethod",
- "RadrootsOperationalListingProduct",
- "RadrootsOperationalListingPublicLocation",
- "RadrootsOperationalListingStatus",
- "RadrootsTradeFacetCount",
- "RadrootsTradeAgreementStateV1",
- "RadrootsTradeAttestationStateV1",
- "RadrootsTradeConflictStateV1",
- "RadrootsTradeEvidenceStateV1",
- "RadrootsTradeFulfillmentStateV1",
- "RadrootsOperationalListingTradeProjection",
- "RadrootsTradeListingBackofficeOverlay",
- "RadrootsTradeListingBackofficeQuery",
- "RadrootsTradeListingBackofficeView",
- "RadrootsTradeListingBinProjection",
- "RadrootsTradeListingFacets",
- "RadrootsTradeListingMarketStatus",
- "RadrootsTradeListingProjection",
- "RadrootsTradeListingQuery",
- "RadrootsTradeListingSort",
- "RadrootsTradeListingSortField",
- "RadrootsOperationalListingSubtotal",
- "RadrootsOperationalListingTotal",
- "RadrootsTradeMarketplaceListingSummary",
- "RadrootsTradeModerationFlag",
- "RadrootsTradeModerationSeverity",
- "RadrootsTradeModerationStatus",
- "RadrootsTradeNegotiationStateV1",
- "RadrootsTradePaymentStateV1",
- "RadrootsTradePrivateTermsStateV1",
- "RadrootsTradeProjectionV1",
- "RadrootsTradeReviewPriority",
- "RadrootsTradeReviewQueueEntry",
- "RadrootsTradeReviewStatus",
- "RadrootsTradeSortDirection"
- ]
- );
- }
-
- #[test]
- fn source_owned_trade_support_types_are_marked_for_event_import() {
- for export_name in [
- "RadrootsFarmRef",
- "RadrootsOperationalListing",
- "RadrootsOperationalListingAvailability",
- "RadrootsOperationalListingBin",
- "RadrootsOperationalListingDeliveryMethod",
- "RadrootsOperationalListingProduct",
- "RadrootsOperationalListingPublicLocation",
- "RadrootsOperationalListingStatus",
- ] {
- assert_eq!(
- disposition(export_name),
- TradeTypeDisposition::EventsBindingImport
- );
- }
- }
-
- #[test]
- fn trade_source_roots_are_marked_for_source_registry() {
- let source_roots = TRADE_TYPE_INVENTORY
- .iter()
- .filter(|entry| entry.disposition == TradeTypeDisposition::SourceTradeRoot)
- .map(|entry| entry.export_name)
- .collect::<Vec<_>>();
-
- assert_eq!(
- source_roots,
- [
- "RadrootsTradeAgreementStateV1",
- "RadrootsTradeAttestationStateV1",
- "RadrootsTradeConflictStateV1",
- "RadrootsTradeEvidenceStateV1",
- "RadrootsTradeFulfillmentStateV1",
- "RadrootsOperationalListingTradeProjection",
- "RadrootsOperationalListingSubtotal",
- "RadrootsOperationalListingTotal",
- "RadrootsTradeNegotiationStateV1",
- "RadrootsTradePaymentStateV1",
- "RadrootsTradePrivateTermsStateV1",
- "RadrootsTradeProjectionV1"
- ]
- );
- }
-
- #[test]
- fn trade_source_support_types_are_marked_for_source_registry() {
- assert!(
- TRADE_TYPE_INVENTORY
- .iter()
- .all(|entry| entry.disposition != TradeTypeDisposition::SourceTradeSupport)
- );
- }
-
- #[test]
- fn trade_large_integer_policy_covers_current_count_fields() {
- let actual = TRADE_LARGE_INTEGER_POLICIES
- .iter()
- .map(|entry| (entry.type_name, entry.field_name, entry.policy))
- .collect::<Vec<_>>();
-
- assert_eq!(
- actual,
- [
- (
- "RadrootsTradeFacetCount",
- "count",
- super::TradeLargeIntegerPolicy::JsonNumberSafeCount
- ),
- (
- "RadrootsTradeListingBackofficeView",
- "open_moderation_flag_count",
- super::TradeLargeIntegerPolicy::JsonNumberSafeCount
- ),
- (
- "RadrootsTradeListingProjection",
- "trade_count",
- super::TradeLargeIntegerPolicy::JsonNumberSafeCount
- ),
- (
- "RadrootsTradeListingProjection",
- "open_trade_count",
- super::TradeLargeIntegerPolicy::JsonNumberSafeCount
- ),
- (
- "RadrootsTradeListingProjection",
- "terminal_trade_count",
- super::TradeLargeIntegerPolicy::JsonNumberSafeCount
- ),
- (
- "RadrootsTradeMarketplaceListingSummary",
- "trade_count",
- super::TradeLargeIntegerPolicy::JsonNumberSafeCount
- ),
- (
- "RadrootsTradeMarketplaceListingSummary",
- "open_trade_count",
- super::TradeLargeIntegerPolicy::JsonNumberSafeCount
- ),
- (
- "RadrootsTradeMarketplaceListingSummary",
- "terminal_trade_count",
- super::TradeLargeIntegerPolicy::JsonNumberSafeCount
- ),
- ]
- );
- }
-
- fn disposition(export_name: &str) -> TradeTypeDisposition {
- TRADE_TYPE_INVENTORY
- .iter()
- .find(|entry| entry.export_name == export_name)
- .map(|entry| entry.disposition)
- .expect("inventory entry")
- }
-}
diff --git a/deny.toml b/deny.toml
@@ -1,67 +0,0 @@
-[graph]
-targets = [
- "aarch64-apple-darwin",
- "wasm32-unknown-unknown",
- "x86_64-pc-windows-gnu",
- "x86_64-unknown-linux-gnu",
-]
-all-features = true
-
-[advisories]
-# `nostr` 0.44.7 depends on `url-fork` 3.0.1 and therefore `idna` 0.5.0.
-# The only network-capable public adapter canonicalizes every relay URL with
-# patched `url` >=2.5.4 before the value reaches upstream Nostr code. This
-# exact exception is contract-bound and must be removed with the Nostr 0.45
-# stable upgrade.
-ignore = [
- "RUSTSEC-2024-0384", # `instant` is an unmaintained, non-vulnerability Nostr 0.44 dependency.
- "RUSTSEC-2024-0421",
-]
-
-[licenses]
-allow = [
- "0BSD",
- "Apache-2.0",
- "Apache-2.0 WITH LLVM-exception",
- "BSD-1-Clause",
- "BSD-2-Clause",
- "BSD-3-Clause",
- "BSL-1.0",
- "CC0-1.0",
- "CDLA-Permissive-2.0",
- "ISC",
- "MIT",
- "MIT-0",
- "MPL-2.0",
- "Unicode-3.0",
- "Unlicense",
- "Zlib",
-]
-confidence-threshold = 0.93
-
-[licenses.private]
-ignore = false
-registries = []
-
-[bans]
-multiple-versions = "allow"
-wildcards = "deny"
-highlight = "all"
-workspace-default-features = "allow"
-external-default-features = "allow"
-allow = []
-allow-workspace = true
-deny = []
-skip = []
-skip-tree = []
-
-[sources]
-unknown-registry = "deny"
-unknown-git = "deny"
-allow-registry = ["https://github.com/rust-lang/crates.io-index"]
-allow-git = ["https://github.com/radrootslabs/lib.git"]
-
-[sources.allow-org]
-github = []
-gitlab = []
-bitbucket = []
diff --git a/tools/xtask/Cargo.toml b/tools/xtask/Cargo.toml
@@ -1,33 +0,0 @@
-[package]
-name = "radroots_sdk_xtask"
-description = "Workspace automation for Radroots SDK crates"
-version.workspace = true
-edition.workspace = true
-rust-version.workspace = true
-license.workspace = true
-repository.workspace = true
-homepage.workspace = true
-publish = false
-
-[[bin]]
-name = "radroots_sdk_xtask"
-path = "src/main.rs"
-
-[dependencies]
-dto_bindgen_backend_ts = { workspace = true }
-dto_bindgen_core = { workspace = true }
-radroots_core_bindings = { path = "../../crates/core_bindings", version = "=0.1.0-alpha" }
-radroots_event_bindings = { path = "../../crates/event_bindings", version = "=0.1.0-alpha" }
-radroots_identity = { workspace = true }
-radroots_identity_bindings = { path = "../../crates/identity_bindings", version = "=0.1.0-alpha" }
-radroots_protocol = { workspace = true }
-radroots_replica_schema_bindings = { path = "../../crates/replica_schema_bindings", version = "=0.1.0-alpha" }
-flate2 = "1"
-serde_json = "1"
-serde = { workspace = true, features = ["derive"] }
-semver = "1"
-sha2 = { workspace = true }
-syn = { version = "2", features = ["full", "visit"] }
-tar = "0.4"
-tempfile = { workspace = true }
-toml = "0.8"
diff --git a/tools/xtask/src/api_qualification.rs b/tools/xtask/src/api_qualification.rs
@@ -1,139 +0,0 @@
-use std::{collections::BTreeSet, fs, path::Path, process::Command};
-
-use serde::Deserialize;
-
-#[derive(Debug, Deserialize)]
-struct Contract {
- schema_version: u16,
- baseline_revision: String,
- package_version: String,
- tool: String,
- minimum_tool_version: String,
- policy: String,
- feature_policy: String,
- packages: Vec<String>,
-}
-
-pub fn run(root: &Path) -> Result<(), String> {
- let contract = load(root)?;
- validate(&contract)?;
- verify_tool(&contract)?;
- verify_revision(root, &contract.baseline_revision)?;
- for package in &contract.packages {
- let args = invocation(package);
- eprintln!("cargo {}", args.join(" "));
- let output = Command::new("cargo")
- .args(&args)
- .current_dir(root)
- .output()
- .map_err(|error| format!("failed to start cargo-public-api: {error}"))?;
- if !output.status.success() {
- return Err(format!(
- "public API extraction failed for {package}: {}",
- String::from_utf8_lossy(&output.stderr).trim()
- ));
- }
- let actual = String::from_utf8(output.stdout)
- .map_err(|error| format!("public API output for {package} was not UTF-8: {error}"))?;
- let snapshot_path = root.join(format!(
- "docs/api/{package}-{}.txt",
- contract.package_version
- ));
- let expected = fs::read_to_string(&snapshot_path)
- .map_err(|error| format!("read {}: {error}", snapshot_path.display()))?;
- if actual != expected {
- return Err(format!(
- "reviewed public API snapshot drifted for {package}: {}",
- snapshot_path.display()
- ));
- }
- }
- Ok(())
-}
-
-fn load(root: &Path) -> Result<Contract, String> {
- let path = root.join("contracts/releases/api_semver.toml");
- let raw = fs::read_to_string(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- toml::from_str(&raw).map_err(|error| format!("failed to parse {}: {error}", path.display()))
-}
-
-fn validate(contract: &Contract) -> Result<(), String> {
- let unique = contract.packages.iter().collect::<BTreeSet<_>>();
- if contract.schema_version != 2
- || contract.package_version != "0.1.0-alpha"
- || contract.tool != "cargo-public-api"
- || contract.policy != "reviewed-breaking-snapshot"
- || contract.feature_policy != "all"
- || contract.baseline_revision.len() < 7
- || unique.len() != 2
- || unique.len() != contract.packages.len()
- {
- return Err("invalid SDK public API qualification contract".to_owned());
- }
- Ok(())
-}
-
-fn verify_tool(contract: &Contract) -> Result<(), String> {
- let output = Command::new("cargo")
- .args(["public-api", "--version"])
- .output()
- .map_err(|error| format!("failed to start cargo-public-api: {error}"))?;
- if !output.status.success() {
- return Err("cargo-public-api is required".to_owned());
- }
- let stdout = String::from_utf8_lossy(&output.stdout);
- let installed = stdout
- .split_whitespace()
- .find_map(|value| semver::Version::parse(value).ok())
- .ok_or_else(|| format!("could not parse cargo-public-api version: {stdout}"))?;
- let minimum = semver::Version::parse(&contract.minimum_tool_version)
- .map_err(|error| format!("invalid minimum tool version: {error}"))?;
- if installed < minimum {
- return Err(format!(
- "cargo-public-api {} or newer is required, found {installed}",
- contract.minimum_tool_version
- ));
- }
- Ok(())
-}
-
-fn verify_revision(root: &Path, revision: &str) -> Result<(), String> {
- let status = Command::new("git")
- .args(["cat-file", "-e", &format!("{revision}^{{commit}}")])
- .current_dir(root)
- .status()
- .map_err(|error| format!("failed to inspect API baseline revision: {error}"))?;
- if status.success() {
- Ok(())
- } else {
- Err(format!("API baseline revision {revision} is unavailable"))
- }
-}
-
-fn invocation(package: &str) -> Vec<String> {
- vec![
- "public-api".to_owned(),
- "-p".to_owned(),
- package.to_owned(),
- "--all-features".to_owned(),
- "-sss".to_owned(),
- "--color".to_owned(),
- "never".to_owned(),
- ]
-}
-
-#[cfg(test)]
-mod tests {
- use super::{invocation, load, validate};
-
- #[test]
- fn current_contract_covers_both_front_doors() {
- let root = crate::fs::workspace_root().expect("workspace root");
- let contract = load(&root).expect("contract");
- validate(&contract).expect("valid contract");
- let invocation = invocation("radroots");
- assert!(invocation.contains(&"--all-features".to_owned()));
- assert!(invocation.contains(&"-sss".to_owned()));
- }
-}
diff --git a/tools/xtask/src/architecture.rs b/tools/xtask/src/architecture.rs
@@ -1,1511 +0,0 @@
-use std::{
- collections::{BTreeMap, BTreeSet},
- fs,
- path::{Component, Path},
-};
-
-use serde::Deserialize;
-
-mod api_leakage;
-mod dependency_boundary;
-
-const DEVIATIONS_RELATIVE: &str = "docs/implementation/deviations.toml";
-const ARCHITECTURE_RELATIVE: &str = "docs/specs/radroots_crates_release_v1.toml";
-const ARCHITECTURE_ID: &str = "radroots.crates.release.v1";
-const PUBLIC_HOMEPAGE: &str = "https://radroots.org";
-const PUBLIC_README: &str = "README.md";
-const PUBLIC_AUTHORS: &[&str] = &["Tyson Lupul <tyson@radroots.org>"];
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct DeviationLedger {
- schema_version: u16,
- architecture_id: String,
- deviation: Vec<DeviationRecord>,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct DeviationRecord {
- id: String,
- date: String,
- status: String,
- approval: String,
- affected_steps: Vec<String>,
- spec_anchors: Vec<String>,
- source_evidence: Vec<String>,
- replacement_action: String,
- verification: Vec<String>,
- unresolved_risk: String,
- normative_architecture_change: bool,
- adr_required: bool,
- #[serde(default)]
- closure_evidence: Vec<String>,
-}
-
-#[derive(Debug, Deserialize)]
-struct ArchitectureIdentity {
- spec_id: String,
- edition: String,
- resolver: String,
- rust_version: String,
- license: String,
- canonical_repositories: Vec<String>,
- repositories: BTreeMap<String, ArchitectureRepository>,
- package: Vec<ArchitecturePackage>,
-}
-
-#[derive(Debug, Deserialize)]
-struct ArchitectureRepository {
- url: String,
- version: String,
- packages: Vec<String>,
-}
-
-#[derive(Debug, Deserialize)]
-struct ArchitecturePackage {
- name: String,
-}
-
-#[derive(Debug, Deserialize)]
-struct WorkspaceManifest {
- workspace: WorkspaceMembers,
-}
-
-#[derive(Debug, Deserialize)]
-struct WorkspaceMembershipManifest {
- workspace: WorkspaceMembership,
-}
-
-#[derive(Debug, Deserialize)]
-struct WorkspaceMembership {
- members: Vec<String>,
-}
-
-#[derive(Debug, Deserialize)]
-struct WorkspaceMembers {
- members: Vec<String>,
- resolver: String,
- package: WorkspacePackage,
- metadata: WorkspaceMetadata,
- lints: WorkspaceLints,
-}
-
-#[derive(Debug, Deserialize)]
-struct WorkspaceLints {
- rust: WorkspaceRustLints,
- rustdoc: WorkspaceRustdocLints,
- clippy: WorkspaceClippyLints,
-}
-
-#[derive(Debug, Deserialize)]
-struct WorkspaceRustLints {
- unsafe_code: String,
-}
-
-#[derive(Debug, Deserialize)]
-struct WorkspaceRustdocLints {
- broken_intra_doc_links: String,
-}
-
-#[derive(Debug, Deserialize)]
-struct WorkspaceClippyLints {
- dbg_macro: String,
- todo: String,
- unimplemented: String,
-}
-
-#[derive(Debug, Deserialize)]
-struct WorkspaceMetadata {
- radroots: RadrootsWorkspaceMetadata,
-}
-
-#[derive(Debug, Deserialize)]
-struct RadrootsWorkspaceMetadata {
- #[serde(rename = "public-package")]
- public_package: PublicPackageMetadata,
-}
-
-#[derive(Debug, Deserialize)]
-struct PublicPackageMetadata {
- version: String,
- authors: Vec<String>,
- readme: String,
-}
-
-#[derive(Debug, Deserialize)]
-struct WorkspacePackage {
- version: String,
- edition: String,
- #[serde(rename = "rust-version")]
- rust_version: String,
- license: String,
- repository: String,
- homepage: String,
- readme: String,
- authors: Vec<String>,
-}
-
-pub fn validate(workspace_root: &Path) -> Result<(), String> {
- validate_workspace_members(workspace_root)?;
- let architecture_path = workspace_root.join(ARCHITECTURE_RELATIVE);
- let architecture_raw = fs::read_to_string(&architecture_path)
- .map_err(|error| format!("read {}: {error}", architecture_path.display()))?;
- let architecture = toml::from_str::<ArchitectureIdentity>(&architecture_raw)
- .map_err(|error| format!("parse {}: {error}", architecture_path.display()))?;
-
- let architecture_packages = architecture
- .package
- .iter()
- .map(|package| package.name.clone())
- .collect::<BTreeSet<_>>();
- api_leakage::validate_policy_catalog(workspace_root, &architecture_packages)?;
- dependency_boundary::validate_policy_catalog(workspace_root, &architecture_packages)?;
- validate_workspace_toolchain(workspace_root, &architecture)?;
- validate_public_package_metadata(workspace_root, &architecture)?;
- validate_no_production_sibling_paths(workspace_root)?;
- validate_public_dependency_versions(workspace_root, &architecture)?;
-
- let ledger_path = workspace_root.join(DEVIATIONS_RELATIVE);
- let ledger_raw = fs::read_to_string(&ledger_path)
- .map_err(|error| format!("read {}: {error}", ledger_path.display()))?;
- validate_ledger(workspace_root, &architecture.spec_id, &ledger_raw)
-}
-
-pub fn validate_dependency_boundaries(workspace_root: &Path) -> Result<(), String> {
- validate(workspace_root)?;
- dependency_boundary::validate_resolved_boundaries(workspace_root)
-}
-
-pub fn validate_api_boundaries(workspace_root: &Path) -> Result<(), String> {
- validate(workspace_root)?;
- api_leakage::validate_public_api(workspace_root)
-}
-
-pub fn validate_ci(workspace_root: &Path) -> Result<(), String> {
- validate(workspace_root)?;
- dependency_boundary::validate_resolved_boundaries(workspace_root)?;
- api_leakage::validate_public_api(workspace_root)
-}
-
-fn validate_workspace_toolchain(
- workspace_root: &Path,
- architecture: &ArchitectureIdentity,
-) -> Result<(), String> {
- let manifest_path = workspace_root.join("Cargo.toml");
- let manifest_raw = fs::read_to_string(&manifest_path)
- .map_err(|error| format!("read {}: {error}", manifest_path.display()))?;
- let manifest = toml::from_str::<WorkspaceManifest>(&manifest_raw)
- .map_err(|error| format!("parse {}: {error}", manifest_path.display()))?;
- if manifest.workspace.resolver != architecture.resolver || architecture.resolver != "3" {
- return Err(format!(
- "workspace resolver {} must match architecture resolver {}",
- manifest.workspace.resolver, architecture.resolver
- ));
- }
- if manifest.workspace.package.rust_version != architecture.rust_version
- || architecture.rust_version != "1.97.1"
- {
- return Err(format!(
- "workspace rust-version {} must match architecture rust_version {}",
- manifest.workspace.package.rust_version, architecture.rust_version
- ));
- }
- let workspace_package = &manifest.workspace.package;
- let public_metadata = &manifest.workspace.metadata.radroots.public_package;
- let repository = architecture
- .repositories
- .values()
- .find(|repository| repository.url == workspace_package.repository)
- .ok_or_else(|| "workspace repository has no architecture allocation".to_owned())?;
- if workspace_package.version != repository.version
- || public_metadata.version != repository.version
- {
- return Err(format!(
- "workspace package version {} and public package version source {} must match frozen repository version {}",
- workspace_package.version, public_metadata.version, repository.version
- ));
- }
- for member in &manifest.workspace.members {
- let member_path = workspace_root.join(member).join("Cargo.toml");
- let member_raw = fs::read_to_string(&member_path)
- .map_err(|error| format!("read {}: {error}", member_path.display()))?;
- let member_manifest = member_raw
- .parse::<toml::Value>()
- .map_err(|error| format!("parse {}: {error}", member_path.display()))?;
- let package = member_manifest
- .get("package")
- .and_then(toml::Value::as_table)
- .ok_or_else(|| format!("{} is missing [package]", member_path.display()))?;
- let package_name = package
- .get("name")
- .and_then(toml::Value::as_str)
- .ok_or_else(|| format!("{} is missing package.name", member_path.display()))?;
- let package_version = match package.get("version") {
- Some(toml::Value::String(version)) => version.as_str(),
- Some(toml::Value::Table(source))
- if source.get("workspace").and_then(toml::Value::as_bool) == Some(true) =>
- {
- workspace_package.version.as_str()
- }
- _ => {
- return Err(format!(
- "workspace package {package_name} must declare version 0.1.0-alpha or inherit it from workspace.package"
- ));
- }
- };
- if package_version != repository.version {
- return Err(format!(
- "workspace package {package_name} version {package_version} must match frozen repository version {}",
- repository.version
- ));
- }
- }
- if public_metadata.authors != PUBLIC_AUTHORS {
- return Err("public package authors source must match the workspace convention".to_owned());
- }
- if public_metadata.readme != PUBLIC_README {
- return Err(format!(
- "public package readme source must be {PUBLIC_README}"
- ));
- }
- let lints = &manifest.workspace.lints;
- if lints.rust.unsafe_code != "forbid"
- || lints.rustdoc.broken_intra_doc_links != "deny"
- || lints.clippy.dbg_macro != "deny"
- || lints.clippy.todo != "deny"
- || lints.clippy.unimplemented != "deny"
- {
- return Err(
- "workspace lints must forbid unsafe code and deny the approved rustdoc/Clippy baseline"
- .to_owned(),
- );
- }
- if workspace_package.edition != architecture.edition || architecture.edition != "2024" {
- return Err(format!(
- "workspace edition {} must match architecture edition {}",
- workspace_package.edition, architecture.edition
- ));
- }
- if workspace_package.license != architecture.license {
- return Err(format!(
- "workspace license {} must match architecture license {}",
- workspace_package.license, architecture.license
- ));
- }
- if !architecture
- .canonical_repositories
- .contains(&workspace_package.repository)
- {
- return Err(format!(
- "workspace repository {} is not canonical",
- workspace_package.repository
- ));
- }
- if workspace_package.homepage != PUBLIC_HOMEPAGE {
- return Err(format!(
- "workspace homepage {} must be {PUBLIC_HOMEPAGE}",
- workspace_package.homepage
- ));
- }
- if workspace_package.authors != PUBLIC_AUTHORS {
- return Err("workspace authors must match the public package convention".to_owned());
- }
- if !workspace_root.join(&workspace_package.readme).is_file() {
- return Err(format!(
- "workspace readme {} does not exist",
- workspace_package.readme
- ));
- }
- for license in ["LICENSE-MIT", "LICENSE-APACHE"] {
- if !workspace_root.join(license).is_file() {
- return Err(format!("workspace is missing {license}"));
- }
- }
- let toolchain_path = workspace_root.join("rust-toolchain.toml");
- let toolchain_raw = fs::read_to_string(&toolchain_path)
- .map_err(|error| format!("read {}: {error}", toolchain_path.display()))?;
- let toolchain = toolchain_raw
- .parse::<toml::Value>()
- .map_err(|error| format!("parse {}: {error}", toolchain_path.display()))?;
- let channel = toolchain
- .get("toolchain")
- .and_then(|value| value.get("channel"))
- .and_then(toml::Value::as_str);
- if channel != Some(architecture.rust_version.as_str()) {
- return Err(format!(
- "rust-toolchain.toml channel must match architecture rust_version {}",
- architecture.rust_version
- ));
- }
- Ok(())
-}
-
-fn validate_public_package_metadata(
- workspace_root: &Path,
- architecture: &ArchitectureIdentity,
-) -> Result<(), String> {
- let workspace_raw = fs::read_to_string(workspace_root.join("Cargo.toml"))
- .map_err(|error| format!("read workspace Cargo.toml: {error}"))?;
- let workspace = toml::from_str::<WorkspaceManifest>(&workspace_raw)
- .map_err(|error| format!("parse workspace Cargo.toml: {error}"))?;
- let repository = architecture
- .repositories
- .values()
- .find(|repository| repository.url == workspace.workspace.package.repository)
- .ok_or_else(|| "workspace repository has no architecture allocation".to_owned())?;
- let local_packages = repository.packages.iter().cloned().collect::<BTreeSet<_>>();
- let all_packages = architecture
- .package
- .iter()
- .map(|package| package.name.as_str())
- .collect::<BTreeSet<_>>();
- let mut found_local_packages = BTreeSet::new();
-
- for member in &workspace.workspace.members {
- let manifest_path = workspace_root.join(member).join("Cargo.toml");
- let raw = fs::read_to_string(&manifest_path)
- .map_err(|error| format!("read {}: {error}", manifest_path.display()))?;
- let manifest = raw
- .parse::<toml::Value>()
- .map_err(|error| format!("parse {}: {error}", manifest_path.display()))?;
- let package = manifest
- .get("package")
- .and_then(toml::Value::as_table)
- .ok_or_else(|| format!("{} is missing [package]", manifest_path.display()))?;
- let name = package
- .get("name")
- .and_then(toml::Value::as_str)
- .ok_or_else(|| format!("{} is missing package.name", manifest_path.display()))?;
- if !all_packages.contains(name) {
- continue;
- }
- if !local_packages.contains(name) {
- return Err(format!(
- "public package {name} belongs to a different canonical repository"
- ));
- }
- found_local_packages.insert(name.to_owned());
- validate_public_manifest_field(
- package,
- "version",
- &workspace.workspace.package.version,
- &repository.version,
- name,
- )?;
- validate_public_manifest_field(
- package,
- "edition",
- &workspace.workspace.package.edition,
- &architecture.edition,
- name,
- )?;
- validate_public_manifest_field(
- package,
- "rust-version",
- &workspace.workspace.package.rust_version,
- &architecture.rust_version,
- name,
- )?;
- validate_public_manifest_field(
- package,
- "license",
- &workspace.workspace.package.license,
- &architecture.license,
- name,
- )?;
- validate_public_manifest_field(
- package,
- "repository",
- &workspace.workspace.package.repository,
- &repository.url,
- name,
- )?;
- validate_public_manifest_field(
- package,
- "homepage",
- &workspace.workspace.package.homepage,
- PUBLIC_HOMEPAGE,
- name,
- )?;
- let authors = resolve_public_authors(package, &workspace.workspace.package.authors)
- .ok_or_else(|| format!("public package {name} must declare authors"))?;
- if authors != PUBLIC_AUTHORS {
- return Err(format!(
- "public package {name} authors must match the workspace convention"
- ));
- }
- let readme = package.get("readme").and_then(toml::Value::as_str);
- if readme != Some(PUBLIC_README)
- || !workspace_root.join(member).join(PUBLIC_README).is_file()
- {
- return Err(format!(
- "public package {name} must use an existing crate-local {PUBLIC_README}"
- ));
- }
- let publish_registries = package
- .get("publish")
- .and_then(toml::Value::as_array)
- .ok_or_else(|| format!("public package {name} must set publish = [\"crates-io\"]"))?;
- if publish_registries.len() != 1 || publish_registries[0].as_str() != Some("crates-io") {
- return Err(format!(
- "public package {name} must set publish = [\"crates-io\"]"
- ));
- }
- let inherits_lints = manifest
- .get("lints")
- .and_then(toml::Value::as_table)
- .is_some_and(|lints| {
- lints.len() == 1
- && lints.get("workspace").and_then(toml::Value::as_bool) == Some(true)
- });
- if !inherits_lints {
- return Err(format!(
- "public package {name} must inherit the workspace lint policy"
- ));
- }
- }
- if found_local_packages != local_packages {
- let missing = local_packages
- .difference(&found_local_packages)
- .cloned()
- .collect::<Vec<_>>()
- .join(", ");
- let extra = found_local_packages
- .difference(&local_packages)
- .cloned()
- .collect::<Vec<_>>()
- .join(", ");
- return Err(format!(
- "workspace public package inventory is missing: {missing}; workspace public package inventory has unallocated packages: {extra}"
- ));
- }
- Ok(())
-}
-
-fn validate_public_dependency_versions(
- workspace_root: &Path,
- architecture: &ArchitectureIdentity,
-) -> Result<(), String> {
- let workspace_path = workspace_root.join("Cargo.toml");
- let workspace_raw = fs::read_to_string(&workspace_path)
- .map_err(|error| format!("read {}: {error}", workspace_path.display()))?;
- let workspace = toml::from_str::<WorkspaceManifest>(&workspace_raw)
- .map_err(|error| format!("parse {}: {error}", workspace_path.display()))?;
- let workspace_value = workspace_raw
- .parse::<toml::Value>()
- .map_err(|error| format!("parse {}: {error}", workspace_path.display()))?;
- let workspace_dependencies = workspace_value
- .get("workspace")
- .and_then(toml::Value::as_table)
- .and_then(|workspace| workspace.get("dependencies"))
- .and_then(toml::Value::as_table);
- let repository = architecture
- .repositories
- .values()
- .find(|repository| repository.url == workspace.workspace.package.repository)
- .ok_or_else(|| "workspace repository has no architecture allocation".to_owned())?;
- let local_packages = repository
- .packages
- .iter()
- .map(String::as_str)
- .collect::<BTreeSet<_>>();
- let public_packages = architecture
- .package
- .iter()
- .map(|package| package.name.as_str())
- .collect::<BTreeSet<_>>();
- let package_versions = architecture
- .repositories
- .values()
- .flat_map(|repository| {
- repository
- .packages
- .iter()
- .map(|package| (package.as_str(), repository.version.as_str()))
- })
- .collect::<BTreeMap<_, _>>();
-
- for member in &workspace.workspace.members {
- let manifest_path = workspace_root.join(member).join("Cargo.toml");
- let raw = fs::read_to_string(&manifest_path)
- .map_err(|error| format!("read {}: {error}", manifest_path.display()))?;
- let manifest = raw
- .parse::<toml::Value>()
- .map_err(|error| format!("parse {}: {error}", manifest_path.display()))?;
- let package_name = manifest
- .get("package")
- .and_then(toml::Value::as_table)
- .and_then(|package| package.get("name"))
- .and_then(toml::Value::as_str)
- .ok_or_else(|| format!("{} is missing package.name", manifest_path.display()))?;
- if !local_packages.contains(package_name) {
- continue;
- }
- let policy = PublicDependencyPolicy {
- workspace_root,
- workspace_dependencies,
- public_packages: &public_packages,
- local_packages: &local_packages,
- package_versions: &package_versions,
- };
- validate_public_dependency_sections(member, package_name, &manifest, &policy)?;
- }
- Ok(())
-}
-
-struct PublicDependencyPolicy<'a> {
- workspace_root: &'a Path,
- workspace_dependencies: Option<&'a toml::value::Table>,
- public_packages: &'a BTreeSet<&'a str>,
- local_packages: &'a BTreeSet<&'a str>,
- package_versions: &'a BTreeMap<&'a str, &'a str>,
-}
-
-fn validate_public_dependency_sections(
- member: &str,
- owner: &str,
- manifest: &toml::Value,
- policy: &PublicDependencyPolicy<'_>,
-) -> Result<(), String> {
- let manifest_table = manifest
- .as_table()
- .ok_or_else(|| format!("{member}/Cargo.toml must be a TOML table"))?;
- for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
- if let Some(dependencies) = manifest_table.get(section).and_then(toml::Value::as_table) {
- validate_public_dependency_table(member, owner, section, dependencies, policy)?;
- }
- }
- if let Some(targets) = manifest_table.get("target").and_then(toml::Value::as_table) {
- for (target, target_value) in targets {
- let Some(target_table) = target_value.as_table() else {
- continue;
- };
- for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
- if let Some(dependencies) =
- target_table.get(section).and_then(toml::Value::as_table)
- {
- validate_public_dependency_table(
- member,
- owner,
- &format!("target.{target}.{section}"),
- dependencies,
- policy,
- )?;
- }
- }
- }
- }
- Ok(())
-}
-
-fn validate_public_dependency_table(
- member: &str,
- owner: &str,
- section: &str,
- dependencies: &toml::value::Table,
- policy: &PublicDependencyPolicy<'_>,
-) -> Result<(), String> {
- for (dependency_key, declaration) in dependencies {
- let inherits_workspace = declaration
- .as_table()
- .and_then(|table| table.get("workspace"))
- .and_then(toml::Value::as_bool)
- == Some(true);
- let resolved = if inherits_workspace {
- policy
- .workspace_dependencies
- .and_then(|dependencies| dependencies.get(dependency_key))
- .ok_or_else(|| {
- format!(
- "public package {owner} {section}.{dependency_key} inherits a missing workspace dependency"
- )
- })?
- } else {
- declaration
- };
- let resolved_table = resolved.as_table();
- let dependency_path = resolved_table
- .and_then(|table| table.get("path"))
- .and_then(toml::Value::as_str);
- let declared_package = resolved_table
- .and_then(|table| table.get("package"))
- .and_then(toml::Value::as_str);
- let path_base = if inherits_workspace {
- policy.workspace_root.to_path_buf()
- } else {
- policy.workspace_root.join(member)
- };
- let path_package = if declared_package.is_none() {
- dependency_path
- .map(|path| dependency_package_name(&path_base.join(path)))
- .transpose()?
- } else {
- None
- };
- let dependency_name = declared_package
- .or(path_package.as_deref())
- .unwrap_or(dependency_key.as_str());
- if !policy.public_packages.contains(dependency_name) {
- continue;
- }
- let version = match resolved {
- toml::Value::String(version) => Some(version.as_str()),
- toml::Value::Table(table) => table.get("version").and_then(toml::Value::as_str),
- _ => None,
- };
- let governed_version = policy
- .package_versions
- .get(dependency_name)
- .ok_or_else(|| {
- format!("public dependency {dependency_name} has no repository version authority")
- })?;
- let exact_version = format!("={governed_version}");
- if policy.local_packages.contains(dependency_name)
- && (dependency_path.is_none() || version != Some(exact_version.as_str()))
- {
- return Err(format!(
- "public package {owner} {section}.{dependency_key} dependency on {dependency_name} must declare path and exact version {exact_version}"
- ));
- }
- if !policy.local_packages.contains(dependency_name)
- && (dependency_path.is_some() || version != Some(exact_version.as_str()))
- {
- return Err(format!(
- "public package {owner} {section}.{dependency_key} cross-repository dependency on {dependency_name} must declare exact registry version {exact_version} without a path"
- ));
- }
- }
- Ok(())
-}
-
-fn validate_no_production_sibling_paths(workspace_root: &Path) -> Result<(), String> {
- let canonical_root = fs::canonicalize(workspace_root)
- .map_err(|error| format!("canonicalize {}: {error}", workspace_root.display()))?;
- let workspace_path = workspace_root.join("Cargo.toml");
- let workspace_raw = fs::read_to_string(&workspace_path)
- .map_err(|error| format!("read {}: {error}", workspace_path.display()))?;
- let workspace = toml::from_str::<WorkspaceManifest>(&workspace_raw)
- .map_err(|error| format!("parse {}: {error}", workspace_path.display()))?;
- let workspace_value = workspace_raw
- .parse::<toml::Value>()
- .map_err(|error| format!("parse {}: {error}", workspace_path.display()))?;
- if let Some(dependencies) = workspace_value
- .get("workspace")
- .and_then(toml::Value::as_table)
- .and_then(|workspace| workspace.get("dependencies"))
- .and_then(toml::Value::as_table)
- {
- validate_dependency_path_table(
- &canonical_root,
- &canonical_root,
- "workspace",
- "workspace.dependencies",
- dependencies,
- )?;
- }
- for member in &workspace.workspace.members {
- let manifest_path = workspace_root.join(member).join("Cargo.toml");
- let raw = fs::read_to_string(&manifest_path)
- .map_err(|error| format!("read {}: {error}", manifest_path.display()))?;
- let manifest = raw
- .parse::<toml::Value>()
- .map_err(|error| format!("parse {}: {error}", manifest_path.display()))?;
- validate_manifest_dependency_paths(
- &canonical_root,
- &canonical_root.join(member),
- member,
- &manifest,
- )?;
- }
- Ok(())
-}
-
-fn validate_manifest_dependency_paths(
- workspace_root: &Path,
- package_root: &Path,
- owner: &str,
- manifest: &toml::Value,
-) -> Result<(), String> {
- let Some(manifest_table) = manifest.as_table() else {
- return Err(format!("{owner}/Cargo.toml must be a TOML table"));
- };
- for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
- if let Some(dependencies) = manifest_table.get(section).and_then(toml::Value::as_table) {
- validate_dependency_path_table(
- workspace_root,
- package_root,
- owner,
- section,
- dependencies,
- )?;
- }
- }
- if let Some(targets) = manifest_table.get("target").and_then(toml::Value::as_table) {
- for (target, target_value) in targets {
- let Some(target_table) = target_value.as_table() else {
- continue;
- };
- for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
- if let Some(dependencies) =
- target_table.get(section).and_then(toml::Value::as_table)
- {
- validate_dependency_path_table(
- workspace_root,
- package_root,
- owner,
- &format!("target.{target}.{section}"),
- dependencies,
- )?;
- }
- }
- }
- }
- Ok(())
-}
-
-fn validate_dependency_path_table(
- workspace_root: &Path,
- path_base: &Path,
- owner: &str,
- section: &str,
- dependencies: &toml::value::Table,
-) -> Result<(), String> {
- for (dependency, declaration) in dependencies {
- let Some(path) = declaration
- .as_table()
- .and_then(|table| table.get("path"))
- .and_then(toml::Value::as_str)
- else {
- continue;
- };
- let resolved = fs::canonicalize(path_base.join(path)).map_err(|error| {
- format!(
- "resolve production dependency {owner} {section}.{dependency} path {path}: {error}"
- )
- })?;
- if !resolved.starts_with(workspace_root) {
- return Err(format!(
- "production dependency {owner} {section}.{dependency} path {path} escapes the repository; use a registry version or an external local-development override"
- ));
- }
- }
- Ok(())
-}
-
-fn dependency_package_name(package_root: &Path) -> Result<String, String> {
- let manifest_path = package_root.join("Cargo.toml");
- let raw = fs::read_to_string(&manifest_path).map_err(|error| {
- format!(
- "read dependency manifest {}: {error}",
- manifest_path.display()
- )
- })?;
- let manifest = raw.parse::<toml::Value>().map_err(|error| {
- format!(
- "parse dependency manifest {}: {error}",
- manifest_path.display()
- )
- })?;
- manifest
- .get("package")
- .and_then(toml::Value::as_table)
- .and_then(|package| package.get("name"))
- .and_then(toml::Value::as_str)
- .map(str::to_owned)
- .ok_or_else(|| format!("{} is missing package.name", manifest_path.display()))
-}
-
-fn validate_public_manifest_field(
- package: &toml::value::Table,
- key: &str,
- workspace_value: &str,
- expected: &str,
- package_name: &str,
-) -> Result<(), String> {
- let value = resolve_public_string(package, key, workspace_value)
- .ok_or_else(|| format!("public package {package_name} must declare {key}"))?;
- if value != expected {
- return Err(format!(
- "public package {package_name} {key} {value} must be {expected}"
- ));
- }
- Ok(())
-}
-
-fn resolve_public_string<'a>(
- package: &'a toml::value::Table,
- key: &str,
- workspace_value: &'a str,
-) -> Option<&'a str> {
- match package.get(key)? {
- toml::Value::String(value) => Some(value),
- toml::Value::Table(value)
- if value.get("workspace").and_then(toml::Value::as_bool) == Some(true) =>
- {
- Some(workspace_value)
- }
- _ => None,
- }
-}
-
-fn resolve_public_authors<'a>(
- package: &'a toml::value::Table,
- workspace_authors: &'a [String],
-) -> Option<Vec<&'a str>> {
- match package.get("authors")? {
- toml::Value::Array(values) => values.iter().map(toml::Value::as_str).collect(),
- toml::Value::Table(value)
- if value.get("workspace").and_then(toml::Value::as_bool) == Some(true) =>
- {
- Some(workspace_authors.iter().map(String::as_str).collect())
- }
- _ => None,
- }
-}
-
-fn validate_workspace_members(workspace_root: &Path) -> Result<(), String> {
- let manifest_path = workspace_root.join("Cargo.toml");
- let manifest_raw = fs::read_to_string(&manifest_path)
- .map_err(|error| format!("read {}: {error}", manifest_path.display()))?;
- let manifest = toml::from_str::<WorkspaceMembershipManifest>(&manifest_raw)
- .map_err(|error| format!("parse {}: {error}", manifest_path.display()))?;
- let manifest_value = manifest_raw
- .parse::<toml::Value>()
- .map_err(|error| format!("parse {}: {error}", manifest_path.display()))?;
- validate_manifest_dependency_keys(&manifest_value, "Cargo.toml")?;
- let declared = manifest
- .workspace
- .members
- .into_iter()
- .collect::<BTreeSet<_>>();
- let mut discovered = BTreeSet::new();
- for root in ["crates", "tools"] {
- let directory = workspace_root.join(root);
- for entry in fs::read_dir(&directory)
- .map_err(|error| format!("read {}: {error}", directory.display()))?
- {
- let entry =
- entry.map_err(|error| format!("read {} entry: {error}", directory.display()))?;
- if entry
- .file_type()
- .map_err(|error| format!("read {} type: {error}", entry.path().display()))?
- .is_dir()
- && entry.path().join("Cargo.toml").is_file()
- {
- let directory_name = entry.file_name().to_string_lossy().into_owned();
- require_lower_snake_case(
- &directory_name,
- &format!("{root}/{directory_name} crate directory"),
- )?;
- let package_manifest_path = entry.path().join("Cargo.toml");
- let package_manifest_raw =
- fs::read_to_string(&package_manifest_path).map_err(|error| {
- format!("read {}: {error}", package_manifest_path.display())
- })?;
- let package_manifest =
- package_manifest_raw
- .parse::<toml::Value>()
- .map_err(|error| {
- format!("parse {}: {error}", package_manifest_path.display())
- })?;
- let package_name = package_manifest
- .get("package")
- .and_then(toml::Value::as_table)
- .and_then(|package| package.get("name"))
- .and_then(toml::Value::as_str)
- .ok_or_else(|| {
- format!(
- "{} is missing package.name",
- package_manifest_path.display()
- )
- })?;
- require_lower_snake_case(
- package_name,
- &format!("{} package.name", package_manifest_path.display()),
- )?;
- validate_manifest_dependency_keys(
- &package_manifest,
- &package_manifest_path.display().to_string(),
- )?;
- discovered.insert(format!("{root}/{directory_name}"));
- }
- }
- }
- if declared != discovered {
- let missing = discovered
- .difference(&declared)
- .cloned()
- .collect::<Vec<_>>()
- .join(", ");
- let unknown = declared
- .difference(&discovered)
- .cloned()
- .collect::<Vec<_>>()
- .join(", ");
- return Err(format!(
- "workspace membership is missing local package roots: {missing}; workspace membership has unknown package roots: {unknown}"
- ));
- }
- Ok(())
-}
-
-fn validate_manifest_dependency_keys(manifest: &toml::Value, label: &str) -> Result<(), String> {
- let Some(manifest) = manifest.as_table() else {
- return Err(format!("{label} must be a TOML table"));
- };
- for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
- if let Some(dependencies) = manifest.get(section).and_then(toml::Value::as_table) {
- validate_dependency_keys(dependencies, label, section)?;
- }
- }
- if let Some(workspace_dependencies) = manifest
- .get("workspace")
- .and_then(toml::Value::as_table)
- .and_then(|workspace| workspace.get("dependencies"))
- .and_then(toml::Value::as_table)
- {
- validate_dependency_keys(workspace_dependencies, label, "workspace.dependencies")?;
- }
- if let Some(targets) = manifest.get("target").and_then(toml::Value::as_table) {
- for (target, target_value) in targets {
- let Some(target_table) = target_value.as_table() else {
- continue;
- };
- for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
- if let Some(dependencies) =
- target_table.get(section).and_then(toml::Value::as_table)
- {
- validate_dependency_keys(
- dependencies,
- label,
- &format!("target.{target}.{section}"),
- )?;
- }
- }
- }
- }
- Ok(())
-}
-
-fn validate_dependency_keys(
- dependencies: &toml::value::Table,
- label: &str,
- section: &str,
-) -> Result<(), String> {
- for key in dependencies
- .keys()
- .filter(|key| key.starts_with("radroots"))
- {
- require_lower_snake_case(key, &format!("{label} {section} dependency key"))?;
- }
- Ok(())
-}
-
-fn require_lower_snake_case(value: &str, label: &str) -> Result<(), String> {
- if is_lower_snake_case(value) {
- Ok(())
- } else {
- Err(format!("{label} `{value}` must use lowercase snake case"))
- }
-}
-
-fn is_lower_snake_case(value: &str) -> bool {
- !value.is_empty()
- && value.split('_').all(|segment| {
- segment
- .as_bytes()
- .first()
- .is_some_and(u8::is_ascii_lowercase)
- && segment
- .bytes()
- .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
- })
-}
-
-fn validate_ledger(
- workspace_root: &Path,
- expected_architecture_id: &str,
- raw: &str,
-) -> Result<(), String> {
- let ledger = toml::from_str::<DeviationLedger>(raw)
- .map_err(|error| format!("parse {DEVIATIONS_RELATIVE}: {error}"))?;
- if ledger.schema_version != 1 {
- return Err("deviation ledger schema_version must be 1".to_owned());
- }
- if ledger.architecture_id != expected_architecture_id
- || ledger.architecture_id != ARCHITECTURE_ID
- {
- return Err(format!(
- "deviation ledger architecture_id {} must match {}",
- ledger.architecture_id, expected_architecture_id
- ));
- }
-
- let mut ids = BTreeSet::new();
- for record in &ledger.deviation {
- validate_record(workspace_root, record)?;
- if !ids.insert(record.id.as_str()) {
- return Err(format!("duplicate deviation id {}", record.id));
- }
- }
- Ok(())
-}
-
-fn validate_record(workspace_root: &Path, record: &DeviationRecord) -> Result<(), String> {
- if !is_deviation_id(&record.id) {
- return Err(format!("deviation id {} must use RCRV1-DEV-NNN", record.id));
- }
- if !is_iso_date(&record.date) {
- return Err(format!("deviation {} date must use YYYY-MM-DD", record.id));
- }
- if !matches!(record.status.as_str(), "active" | "closed" | "superseded") {
- return Err(format!(
- "deviation {} status must be active, closed, or superseded",
- record.id
- ));
- }
- require_text(&record.id, "approval", &record.approval)?;
- require_text(&record.id, "replacement_action", &record.replacement_action)?;
- require_text(&record.id, "unresolved_risk", &record.unresolved_risk)?;
- require_nonempty_list(&record.id, "affected_steps", &record.affected_steps)?;
- require_nonempty_list(&record.id, "spec_anchors", &record.spec_anchors)?;
- require_nonempty_list(&record.id, "source_evidence", &record.source_evidence)?;
- require_nonempty_list(&record.id, "verification", &record.verification)?;
-
- for step in &record.affected_steps {
- let valid = step.len() == 3
- && step.bytes().all(|byte| byte.is_ascii_digit())
- && step
- .parse::<u16>()
- .is_ok_and(|value| (1..=315).contains(&value));
- if !valid {
- return Err(format!(
- "deviation {} affected step {} must be in 001..315",
- record.id, step
- ));
- }
- }
- for anchor in &record.spec_anchors {
- validate_spec_anchor(workspace_root, &record.id, anchor)?;
- }
- if record.status == "active" && !record.closure_evidence.is_empty() {
- return Err(format!(
- "active deviation {} must not carry closure_evidence",
- record.id
- ));
- }
- if record.status != "active" {
- require_nonempty_list(&record.id, "closure_evidence", &record.closure_evidence)?;
- }
-
- let _ = (record.normative_architecture_change, record.adr_required);
- Ok(())
-}
-
-fn validate_spec_anchor(
- workspace_root: &Path,
- deviation_id: &str,
- anchor: &str,
-) -> Result<(), String> {
- let (relative, fragment) = anchor
- .split_once('#')
- .map_or((anchor, None), |(path, fragment)| (path, Some(fragment)));
- if relative.trim().is_empty() || fragment.is_some_and(|value| value.trim().is_empty()) {
- return Err(format!(
- "deviation {deviation_id} has invalid spec anchor {anchor}"
- ));
- }
- let path = Path::new(relative);
- if path.components().any(|component| {
- matches!(
- component,
- Component::ParentDir | Component::RootDir | Component::Prefix(_)
- )
- }) {
- return Err(format!(
- "deviation {deviation_id} spec anchor must be repository-relative: {anchor}"
- ));
- }
- if !relative.starts_with("docs/specs/") || !workspace_root.join(path).is_file() {
- return Err(format!(
- "deviation {deviation_id} spec anchor does not resolve to a local spec: {anchor}"
- ));
- }
- Ok(())
-}
-
-fn require_text(deviation_id: &str, field: &str, value: &str) -> Result<(), String> {
- if value.trim().is_empty() {
- return Err(format!(
- "deviation {deviation_id} field {field} must not be empty"
- ));
- }
- Ok(())
-}
-
-fn require_nonempty_list(deviation_id: &str, field: &str, values: &[String]) -> Result<(), String> {
- if values.is_empty() || values.iter().any(|value| value.trim().is_empty()) {
- return Err(format!(
- "deviation {deviation_id} field {field} must contain non-empty values"
- ));
- }
- Ok(())
-}
-
-fn is_deviation_id(value: &str) -> bool {
- value
- .strip_prefix("RCRV1-DEV-")
- .is_some_and(|suffix| suffix.len() == 3 && suffix.bytes().all(|byte| byte.is_ascii_digit()))
-}
-
-fn is_iso_date(value: &str) -> bool {
- value.len() == 10
- && value.as_bytes()[4] == b'-'
- && value.as_bytes()[7] == b'-'
- && value
- .bytes()
- .enumerate()
- .all(|(index, byte)| matches!(index, 4 | 7) || byte.is_ascii_digit())
-}
-
-#[cfg(test)]
-mod tests {
- use std::{
- collections::BTreeMap,
- fs,
- path::PathBuf,
- time::{SystemTime, UNIX_EPOCH},
- };
-
- use super::{
- ArchitectureIdentity, ArchitecturePackage, ArchitectureRepository, validate_ledger,
- validate_no_production_sibling_paths, validate_public_dependency_versions,
- validate_public_package_metadata, validate_workspace_members, validate_workspace_toolchain,
- };
-
- fn test_root(label: &str) -> PathBuf {
- let nonce = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .expect("clock")
- .as_nanos();
- let root = std::env::temp_dir().join(format!("radroots_architecture_{label}_{nonce}"));
- fs::create_dir_all(root.join("docs/specs")).expect("create spec root");
- fs::write(
- root.join("docs/specs/radroots_crates_release_v1.md"),
- "# Architecture\n",
- )
- .expect("write spec");
- root
- }
-
- fn complete_ledger() -> &'static str {
- r#"schema_version = 1
-architecture_id = "radroots.crates.release.v1"
-
-[[deviation]]
-id = "RCRV1-DEV-001"
-date = "2026-07-27"
-status = "active"
-approval = "Explicit user correction dated 2026-07-27."
-affected_steps = ["015", "016"]
-spec_anchors = ["docs/specs/radroots_crates_release_v1.md#repository-topology"]
-source_evidence = ["The approved architecture assigns packages to the existing lib and sdk repositories."]
-replacement_action = "Keep both standalone repositories and verify them independently."
-verification = ["Repository-local architecture validation passes."]
-unresolved_risk = "Remote publication remains separately authorized."
-normative_architecture_change = false
-adr_required = false
-"#
- }
-
- fn architecture() -> ArchitectureIdentity {
- ArchitectureIdentity {
- spec_id: "radroots.crates.release.v1".to_string(),
- edition: "2024".to_string(),
- resolver: "3".to_string(),
- rust_version: "1.97.1".to_string(),
- license: "MIT OR Apache-2.0".to_string(),
- canonical_repositories: vec!["https://github.com/radrootslabs/sdk".to_string()],
- repositories: BTreeMap::from([(
- "sdk".to_string(),
- ArchitectureRepository {
- url: "https://github.com/radrootslabs/sdk".to_string(),
- version: "0.1.0-alpha".to_string(),
- packages: vec!["radroots".to_string()],
- },
- )]),
- package: vec![ArchitecturePackage {
- name: "radroots".to_string(),
- }],
- }
- }
-
- fn complete_workspace_manifest(members: &str) -> String {
- format!(
- "[workspace]\nmembers = [{members}]\nresolver = \"3\"\n\n[workspace.package]\nversion = \"0.1.0-alpha\"\nedition = \"2024\"\nrust-version = \"1.97.1\"\nlicense = \"MIT OR Apache-2.0\"\nrepository = \"https://github.com/radrootslabs/sdk\"\nhomepage = \"https://radroots.org\"\nreadme = \"README\"\nauthors = [\"Tyson Lupul <tyson@radroots.org>\"]\n\n[workspace.metadata.radroots.public-package]\nversion = \"0.1.0-alpha\"\nauthors = [\"Tyson Lupul <tyson@radroots.org>\"]\nreadme = \"README.md\"\n\n[workspace.lints.rust]\nunsafe_code = \"forbid\"\n\n[workspace.lints.rustdoc]\nbroken_intra_doc_links = \"deny\"\n\n[workspace.lints.clippy]\ndbg_macro = \"deny\"\ntodo = \"deny\"\nunimplemented = \"deny\"\n"
- )
- }
-
- #[test]
- fn accepts_complete_active_deviation() {
- let root = test_root("complete");
- validate_ledger(&root, "radroots.crates.release.v1", complete_ledger())
- .expect("complete deviation");
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn rejects_incomplete_active_deviation() {
- let root = test_root("incomplete");
- let incomplete = complete_ledger().replace(
- "spec_anchors = [\"docs/specs/radroots_crates_release_v1.md#repository-topology\"]",
- "spec_anchors = []",
- );
- let error = validate_ledger(&root, "radroots.crates.release.v1", &incomplete)
- .expect_err("missing anchor must fail");
- assert!(error.contains("field spec_anchors must contain non-empty values"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn workspace_membership_requires_every_local_package_root() {
- let root = test_root("workspace_members");
- for path in ["crates/a", "tools/xtask"] {
- fs::create_dir_all(root.join(path)).expect("create package root");
- fs::write(
- root.join(path).join("Cargo.toml"),
- "[package]\nname = \"fixture\"\n",
- )
- .expect("write package manifest");
- }
- fs::write(
- root.join("Cargo.toml"),
- "[workspace]\nmembers = [\"crates/a\", \"tools/xtask\"]\nresolver = \"3\"\n\n[workspace.package]\nrust-version = \"1.97.1\"\n",
- )
- .expect("write complete workspace");
- validate_workspace_members(&root).expect("complete membership");
-
- fs::write(
- root.join("Cargo.toml"),
- "[workspace]\nmembers = [\"tools/xtask\"]\nresolver = \"3\"\n\n[workspace.package]\nrust-version = \"1.97.1\"\n",
- )
- .expect("write incomplete workspace");
- let error = validate_workspace_members(&root).expect_err("missing member must fail");
- assert!(error.contains("missing local package roots: crates/a"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn workspace_identity_rejects_kebab_case_crate_names_and_dependency_keys() {
- let root = test_root("workspace_snake_case_identity");
- fs::create_dir_all(root.join("crates/radroots_probe")).expect("create package root");
- fs::create_dir_all(root.join("tools/xtask")).expect("create xtask root");
- fs::write(
- root.join("Cargo.toml"),
- "[workspace]\nmembers = [\"crates/radroots_probe\", \"tools/xtask\"]\nresolver = \"3\"\n",
- )
- .expect("write workspace manifest");
- fs::write(
- root.join("crates/radroots_probe/Cargo.toml"),
- "[package]\nname = \"radroots-probe\"\n",
- )
- .expect("write kebab package manifest");
- fs::write(
- root.join("tools/xtask/Cargo.toml"),
- "[package]\nname = \"xtask\"\n",
- )
- .expect("write xtask manifest");
-
- let package_error = validate_workspace_members(&root)
- .expect_err("kebab-case Cargo package identity must fail");
- assert!(package_error.contains("package.name"));
- assert!(package_error.contains("lowercase snake case"));
-
- fs::write(
- root.join("crates/radroots_probe/Cargo.toml"),
- "[package]\nname = \"radroots_probe\"\n",
- )
- .expect("write snake package manifest");
- fs::write(
- root.join("Cargo.toml"),
- "[workspace]\nmembers = [\"crates/radroots_probe\", \"tools/xtask\"]\nresolver = \"3\"\n\n[workspace.dependencies]\nradroots-probe = \"0.1.0-alpha\"\n",
- )
- .expect("write kebab dependency key");
-
- let dependency_error = validate_workspace_members(&root)
- .expect_err("kebab-case Radroots dependency key must fail");
- assert!(dependency_error.contains("workspace.dependencies dependency key"));
- assert!(dependency_error.contains("lowercase snake case"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn workspace_toolchain_requires_exact_resolver_and_rust_version() {
- let root = test_root("workspace_toolchain");
- fs::write(root.join("Cargo.toml"), complete_workspace_manifest(""))
- .expect("write workspace manifest");
- for path in ["README", "LICENSE-MIT", "LICENSE-APACHE"] {
- fs::write(root.join(path), "fixture\n").expect("write workspace metadata file");
- }
- fs::write(
- root.join("rust-toolchain.toml"),
- "[toolchain]\nchannel = \"1.97.1\"\n",
- )
- .expect("write toolchain");
- let architecture = architecture();
- validate_workspace_toolchain(&root, &architecture).expect("exact toolchain policy");
-
- fs::write(
- root.join("rust-toolchain.toml"),
- "[toolchain]\nchannel = \"1.97.0\"\n",
- )
- .expect("write mismatched toolchain");
- let error = validate_workspace_toolchain(&root, &architecture)
- .expect_err("mismatched toolchain must fail");
- assert!(error.contains("channel must match"));
-
- fs::write(
- root.join("rust-toolchain.toml"),
- "[toolchain]\nchannel = \"1.97.1\"\n",
- )
- .expect("restore toolchain");
- let manifest = complete_workspace_manifest("").replacen(
- "[workspace.package]\nversion = \"0.1.0-alpha\"",
- "[workspace.package]\nversion = \"0.1.0\"",
- 1,
- );
- fs::write(root.join("Cargo.toml"), manifest).expect("write mismatched version cohort");
- let version_error = validate_workspace_toolchain(&root, &architecture)
- .expect_err("mismatched version cohort must fail");
- assert!(version_error.contains("must match frozen repository version"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn public_package_metadata_requires_canonical_inheritance() {
- let root = test_root("public_package_metadata");
- fs::create_dir_all(root.join("crates/radroots")).expect("create public package");
- fs::write(
- root.join("Cargo.toml"),
- complete_workspace_manifest("\"crates/radroots\""),
- )
- .expect("write workspace manifest");
- let manifest = "[package]\nname = \"radroots\"\nversion.workspace = true\nedition.workspace = true\nrust-version.workspace = true\nlicense.workspace = true\nrepository.workspace = true\nhomepage.workspace = true\nauthors.workspace = true\nreadme = \"README.md\"\npublish = [\"crates-io\"]\n\n[lints]\nworkspace = true\n";
- fs::write(root.join("crates/radroots/Cargo.toml"), manifest)
- .expect("write public manifest");
- fs::write(root.join("crates/radroots/README.md"), "fixture\n")
- .expect("write public readme");
- validate_public_package_metadata(&root, &architecture()).expect("canonical metadata");
-
- fs::write(
- root.join("crates/radroots/Cargo.toml"),
- manifest.replace("[lints]\nworkspace = true\n", ""),
- )
- .expect("write incomplete public manifest");
- let error = validate_public_package_metadata(&root, &architecture())
- .expect_err("missing lint inheritance must fail");
- assert!(error.contains("must inherit the workspace lint policy"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn public_package_inventory_requires_every_local_package() {
- let root = test_root("public_package_inventory");
- fs::write(root.join("Cargo.toml"), complete_workspace_manifest(""))
- .expect("write workspace manifest");
- let error = validate_public_package_metadata(&root, &architecture())
- .expect_err("missing local public package must fail");
- assert!(error.contains("workspace public package inventory is missing: radroots"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn public_dependencies_require_path_and_exact_version() {
- let root = test_root("public_dependency_version");
- fs::create_dir_all(root.join("crates/radroots")).expect("create public package");
- fs::create_dir_all(root.join("crates/dependency")).expect("create dependency package");
- fs::write(
- root.join("Cargo.toml"),
- format!(
- "{}\n[workspace.dependencies]\nradroots_core = {{ package = \"radroots_core\", path = \"crates/dependency\", version = \"=0.1.0-alpha\" }}\n",
- complete_workspace_manifest("\"crates/radroots\"")
- ),
- )
- .expect("write workspace manifest");
- fs::write(
- root.join("crates/radroots/Cargo.toml"),
- "[package]\nname = \"radroots\"\nversion = \"0.1.0-alpha\"\n\n[dependencies]\nradroots_core = { workspace = true }\n",
- )
- .expect("write public manifest");
- fs::write(
- root.join("crates/dependency/Cargo.toml"),
- "[package]\nname = \"radroots_core\"\nversion = \"0.1.0-alpha\"\n",
- )
- .expect("write dependency manifest");
- let mut architecture = architecture();
- architecture.package.push(ArchitecturePackage {
- name: "radroots_core".to_owned(),
- });
- architecture
- .repositories
- .get_mut("sdk")
- .expect("sdk repository")
- .packages
- .push("radroots_core".to_owned());
- validate_public_dependency_versions(&root, &architecture)
- .expect("path plus exact version public dependency");
-
- let workspace_path = root.join("Cargo.toml");
- let workspace = fs::read_to_string(&workspace_path).expect("read workspace manifest");
- fs::write(
- &workspace_path,
- workspace.replace(", version = \"=0.1.0-alpha\"", ""),
- )
- .expect("write path-only dependency");
- let error = validate_public_dependency_versions(&root, &architecture)
- .expect_err("path-only public dependency must fail");
- assert!(error.contains("must declare path and exact version =0.1.0-alpha"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn production_dependencies_reject_sibling_paths() {
- let root = test_root("production_sibling_path");
- let sibling = root.with_extension("sibling");
- fs::create_dir_all(root.join("crates/radroots")).expect("create public package");
- fs::create_dir_all(root.join("crates/dependency")).expect("create local dependency");
- fs::create_dir_all(&sibling).expect("create sibling dependency");
- fs::write(
- root.join("Cargo.toml"),
- complete_workspace_manifest("\"crates/radroots\""),
- )
- .expect("write workspace manifest");
- let sibling_path = sibling.to_string_lossy();
- fs::write(
- root.join("crates/radroots/Cargo.toml"),
- format!(
- "[package]\nname = \"radroots\"\nversion = \"0.1.0-alpha\"\n\n[dependencies]\nprobe = {{ path = \"{sibling_path}\" }}\n"
- ),
- )
- .expect("write sibling dependency");
- let error = validate_no_production_sibling_paths(&root)
- .expect_err("sibling production path must fail");
- assert!(error.contains("escapes the repository"));
-
- fs::write(
- root.join("crates/radroots/Cargo.toml"),
- "[package]\nname = \"radroots\"\nversion = \"0.1.0-alpha\"\n\n[dependencies]\nprobe = { path = \"../dependency\" }\n",
- )
- .expect("write local dependency");
- fs::create_dir_all(root.join(".cargo")).expect("create cargo config directory");
- fs::write(
- root.join(".cargo/config.toml"),
- format!("[patch.crates-io]\nprobe = {{ path = \"{sibling_path}\" }}\n"),
- )
- .expect("write development override");
- validate_no_production_sibling_paths(&root).expect("in-repository path");
- let _ = fs::remove_dir_all(root);
- let _ = fs::remove_dir_all(sibling);
- }
-}
diff --git a/tools/xtask/src/architecture/api_leakage.rs b/tools/xtask/src/architecture/api_leakage.rs
@@ -1,1433 +0,0 @@
-use std::{
- collections::{BTreeMap, BTreeSet},
- fs,
- path::{Component, Path, PathBuf},
- process::Command,
-};
-
-use serde::Deserialize;
-use syn::{
- Expr, ExprLit, Fields, ForeignItem, ImplItem, Item, Lit, Meta, Path as SynPath, TraitItem,
- Type, UseTree, Visibility, visit::Visit,
-};
-
-const API_BOUNDARIES_RELATIVE: &str = "contracts/releases/api_boundaries.toml";
-const SPEC_ID: &str = "radroots.crates.release.v1";
-const POLICY_SCHEMA_VERSION: u16 = 1;
-const CURRENT_STEP: u16 = 25;
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct ApiBoundaryPolicy {
- schema_version: u16,
- spec_id: String,
- forbidden_public_paths: Vec<String>,
- package: Vec<ApiPackagePolicy>,
- #[serde(default)]
- exception: Vec<ApiException>,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct ApiPackagePolicy {
- name: String,
- allowed_public_paths: Vec<String>,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct ApiException {
- id: String,
- package: String,
- source: String,
- forbidden_path: String,
- items: Vec<String>,
- observed_paths: Vec<String>,
- adr: String,
- removal_step: u16,
- rationale: String,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoMetadata {
- packages: Vec<CargoPackage>,
- workspace_members: Vec<String>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoPackage {
- id: String,
- name: String,
- manifest_path: String,
- targets: Vec<CargoTarget>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoTarget {
- kind: Vec<String>,
- src_path: String,
-}
-
-struct ModuleUnit {
- module: Vec<String>,
- parent: Option<Vec<String>>,
- declared_public: bool,
- initially_effective: bool,
- source_relative: String,
- items: Vec<Item>,
-}
-
-#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)]
-struct ApiFinding {
- package: String,
- source: String,
- item: String,
- forbidden_path: String,
- observed_path: String,
-}
-
-#[derive(Debug)]
-enum InternalExport {
- Module(Vec<String>),
- Item(Vec<String>, String),
- External,
-}
-
-pub(super) fn validate_policy_catalog(
- workspace_root: &Path,
- expected_packages: &BTreeSet<String>,
-) -> Result<(), String> {
- let policy = load_policy(workspace_root)?;
- validate_policy(workspace_root, &policy, expected_packages)
-}
-
-pub(super) fn validate_public_api(workspace_root: &Path) -> Result<(), String> {
- let policy = load_policy(workspace_root)?;
- let expected_packages = policy
- .package
- .iter()
- .map(|package| package.name.clone())
- .collect::<BTreeSet<_>>();
- validate_policy(workspace_root, &policy, &expected_packages)?;
- let metadata = load_metadata(workspace_root)?;
- let findings = scan_workspace(workspace_root, &policy, &metadata)?;
- let unapproved = findings
- .into_iter()
- .filter(|finding| !exception_matches(&policy.exception, finding))
- .collect::<Vec<_>>();
- if unapproved.is_empty() {
- Ok(())
- } else {
- Err(format!(
- "forbidden public implementation type leakage:\n{}",
- unapproved
- .iter()
- .map(format_finding)
- .collect::<Vec<_>>()
- .join("\n")
- ))
- }
-}
-
-fn load_policy(workspace_root: &Path) -> Result<ApiBoundaryPolicy, String> {
- let path = workspace_root.join(API_BOUNDARIES_RELATIVE);
- let raw =
- fs::read_to_string(&path).map_err(|error| format!("read {}: {error}", path.display()))?;
- toml::from_str(&raw).map_err(|error| format!("parse {}: {error}", path.display()))
-}
-
-fn validate_policy(
- workspace_root: &Path,
- policy: &ApiBoundaryPolicy,
- expected_packages: &BTreeSet<String>,
-) -> Result<(), String> {
- if policy.schema_version != POLICY_SCHEMA_VERSION {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} schema_version must be {POLICY_SCHEMA_VERSION}"
- ));
- }
- if policy.spec_id != SPEC_ID {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} spec_id must be {SPEC_ID}"
- ));
- }
-
- let forbidden = sorted_unique(
- "forbidden_public_paths",
- &policy.forbidden_public_paths,
- false,
- )?;
- if forbidden.len() != policy.forbidden_public_paths.len() {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} forbidden_public_paths must be unique"
- ));
- }
- for required in [
- "keyring",
- "nostr_sdk",
- "reqwest",
- "sqlx",
- "std::os",
- "tokio",
- ] {
- if !forbidden.contains(required) {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} must forbid public {required} paths"
- ));
- }
- }
-
- let mut package_names = BTreeSet::new();
- for package in &policy.package {
- if !package_names.insert(package.name.as_str()) {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} package {} is declared more than once",
- package.name
- ));
- }
- let allowed = sorted_unique(
- &format!("package {} allowed_public_paths", package.name),
- &package.allowed_public_paths,
- true,
- )?;
- for path in allowed {
- if !forbidden.contains(path) {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} package {} allows {path}, which is not forbidden globally",
- package.name
- ));
- }
- }
- }
- let actual_packages = package_names
- .into_iter()
- .map(str::to_owned)
- .collect::<BTreeSet<_>>();
- if &actual_packages != expected_packages {
- return Err(package_set_mismatch(expected_packages, &actual_packages));
- }
-
- let package_policy = policy
- .package
- .iter()
- .map(|package| (package.name.as_str(), package))
- .collect::<BTreeMap<_, _>>();
- let mut exception_ids = BTreeSet::new();
- let mut exception_keys = BTreeSet::new();
- for exception in &policy.exception {
- if !exception_ids.insert(exception.id.as_str()) {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} exception id {} is duplicated",
- exception.id
- ));
- }
- if !valid_exception_id(&exception.id) {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} exception id {} must match RCRV1-API-NNN",
- exception.id
- ));
- }
- let package = package_policy
- .get(exception.package.as_str())
- .ok_or_else(|| {
- format!(
- "{API_BOUNDARIES_RELATIVE} exception {} names unknown package {}",
- exception.id, exception.package
- )
- })?;
- if !forbidden.contains(exception.forbidden_path.as_str()) {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} exception {} names unknown forbidden path {}",
- exception.id, exception.forbidden_path
- ));
- }
- if package
- .allowed_public_paths
- .iter()
- .any(|allowed| allowed == &exception.forbidden_path)
- {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} exception {} is redundant with package allowance {}",
- exception.id, exception.forbidden_path
- ));
- }
- validate_relative_source(&exception.id, &exception.source)?;
- let items = sorted_unique(
- &format!("exception {} items", exception.id),
- &exception.items,
- false,
- )?;
- if items.len() != exception.items.len() {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} exception {} items must be unique",
- exception.id
- ));
- }
- let observed_paths = sorted_unique(
- &format!("exception {} observed_paths", exception.id),
- &exception.observed_paths,
- false,
- )?;
- if observed_paths.len() != exception.observed_paths.len()
- || observed_paths.iter().any(|path| {
- !(path == &exception.forbidden_path
- || path
- .strip_prefix(&exception.forbidden_path)
- .is_some_and(|suffix| suffix.starts_with("::")))
- })
- {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} exception {} observed_paths must be sorted, unique, and rooted at {}",
- exception.id, exception.forbidden_path
- ));
- }
- if exception.removal_step <= CURRENT_STEP {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} exception {} expired at Step {}",
- exception.id, exception.removal_step
- ));
- }
- if exception.rationale.trim().is_empty() {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} exception {} must include a rationale",
- exception.id
- ));
- }
- validate_adr(workspace_root, exception)?;
- for item in &exception.items {
- let key = (
- exception.package.as_str(),
- exception.source.as_str(),
- item.as_str(),
- exception.forbidden_path.as_str(),
- );
- if !exception_keys.insert(key) {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} exception {} duplicates an item-scoped allowance",
- exception.id
- ));
- }
- }
- }
- Ok(())
-}
-
-fn sorted_unique<'a>(
- label: &str,
- values: &'a [String],
- allow_empty: bool,
-) -> Result<BTreeSet<&'a str>, String> {
- if !allow_empty && values.is_empty() {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} {label} must not be empty"
- ));
- }
- let unique = values.iter().map(String::as_str).collect::<BTreeSet<_>>();
- if unique.iter().copied().ne(values.iter().map(String::as_str)) {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} {label} must be sorted and unique"
- ));
- }
- Ok(unique)
-}
-
-fn valid_exception_id(id: &str) -> bool {
- id.strip_prefix("RCRV1-API-")
- .is_some_and(|suffix| suffix.len() == 3 && suffix.bytes().all(|byte| byte.is_ascii_digit()))
-}
-
-fn validate_relative_source(id: &str, source: &str) -> Result<(), String> {
- let path = Path::new(source);
- if source.is_empty()
- || path.is_absolute()
- || path
- .components()
- .any(|component| !matches!(component, Component::Normal(_)))
- || path.extension().and_then(|extension| extension.to_str()) != Some("rs")
- {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} exception {id} source must be a normalized relative Rust path"
- ));
- }
- Ok(())
-}
-
-fn validate_adr(workspace_root: &Path, exception: &ApiException) -> Result<(), String> {
- let path = Path::new(&exception.adr);
- if path.is_absolute()
- || !exception.adr.starts_with("docs/decisions/")
- || path
- .components()
- .any(|component| !matches!(component, Component::Normal(_)))
- || path.extension().and_then(|extension| extension.to_str()) != Some("md")
- {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} exception {} ADR must be a normalized docs/decisions/*.md path",
- exception.id
- ));
- }
- let full = workspace_root.join(path);
- let raw = fs::read_to_string(&full).map_err(|error| {
- format!(
- "{API_BOUNDARIES_RELATIVE} exception {} ADR {} is not readable: {error}",
- exception.id,
- full.display()
- )
- })?;
- if !raw.contains(&exception.id) {
- return Err(format!(
- "{API_BOUNDARIES_RELATIVE} exception {} ADR {} must cite the exception id",
- exception.id, exception.adr
- ));
- }
- Ok(())
-}
-
-fn package_set_mismatch(expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> String {
- let missing = expected
- .difference(actual)
- .cloned()
- .collect::<Vec<_>>()
- .join(", ");
- let extra = actual
- .difference(expected)
- .cloned()
- .collect::<Vec<_>>()
- .join(", ");
- format!(
- "{API_BOUNDARIES_RELATIVE} package catalog mismatch; missing: {missing}; extra: {extra}"
- )
-}
-
-fn load_metadata(workspace_root: &Path) -> Result<CargoMetadata, String> {
- let output = Command::new("cargo")
- .args(["metadata", "--format-version", "1", "--locked", "--no-deps"])
- .current_dir(workspace_root)
- .output()
- .map_err(|error| format!("run cargo metadata: {error}"))?;
- if !output.status.success() {
- return Err(format!(
- "cargo metadata failed while checking public API boundaries: {}",
- String::from_utf8_lossy(&output.stderr).trim()
- ));
- }
- serde_json::from_slice(&output.stdout)
- .map_err(|error| format!("parse cargo metadata for public API boundaries: {error}"))
-}
-
-fn scan_workspace(
- _workspace_root: &Path,
- policy: &ApiBoundaryPolicy,
- metadata: &CargoMetadata,
-) -> Result<Vec<ApiFinding>, String> {
- let workspace_members = metadata
- .workspace_members
- .iter()
- .map(String::as_str)
- .collect::<BTreeSet<_>>();
- let package_policy = policy
- .package
- .iter()
- .map(|package| (package.name.as_str(), package))
- .collect::<BTreeMap<_, _>>();
- let forbidden = policy
- .forbidden_public_paths
- .iter()
- .map(String::as_str)
- .collect::<BTreeSet<_>>();
- let mut findings = BTreeSet::new();
-
- for package in &metadata.packages {
- if !workspace_members.contains(package.id.as_str()) {
- continue;
- }
- let Some(package_policy) = package_policy.get(package.name.as_str()).copied() else {
- continue;
- };
- let manifest_path = Path::new(&package.manifest_path);
- let package_root = manifest_path.parent().ok_or_else(|| {
- format!(
- "package {} manifest has no parent: {}",
- package.name, package.manifest_path
- )
- })?;
- let allowed = package_policy
- .allowed_public_paths
- .iter()
- .map(String::as_str)
- .collect::<BTreeSet<_>>();
- let library_targets = package
- .targets
- .iter()
- .filter(|target| {
- target
- .kind
- .iter()
- .any(|kind| kind == "lib" || kind == "proc-macro")
- })
- .collect::<Vec<_>>();
- if library_targets.len() != 1 {
- return Err(format!(
- "public package {} must expose exactly one library target for API leakage analysis",
- package.name
- ));
- }
- let source_path = Path::new(&library_targets[0].src_path);
- let source_module_directory = module_directory(source_path)?;
- let mut units = BTreeMap::new();
- collect_module(
- package_root,
- Vec::new(),
- None,
- true,
- true,
- source_path,
- &source_module_directory,
- None,
- &mut units,
- )?;
- findings.extend(scan_package_units(
- &package.name,
- &units,
- &forbidden,
- &allowed,
- )?);
- }
-
- Ok(findings.into_iter().collect())
-}
-
-#[allow(clippy::too_many_arguments)]
-fn collect_module(
- package_root: &Path,
- module: Vec<String>,
- parent: Option<Vec<String>>,
- declared_public: bool,
- initially_effective: bool,
- source_path: &Path,
- module_directory: &Path,
- inline_items: Option<Vec<Item>>,
- units: &mut BTreeMap<Vec<String>, ModuleUnit>,
-) -> Result<(), String> {
- if units.contains_key(&module) {
- return Ok(());
- }
- let (items, source_relative) = if let Some(items) = inline_items {
- let relative = source_path
- .strip_prefix(package_root)
- .map_err(|_| {
- format!(
- "module source {} escapes package {}",
- source_path.display(),
- package_root.display()
- )
- })?
- .to_string_lossy()
- .replace('\\', "/");
- (items, relative)
- } else {
- let raw = fs::read_to_string(source_path)
- .map_err(|error| format!("read {}: {error}", source_path.display()))?;
- let file = syn::parse_file(&raw)
- .map_err(|error| format!("parse {}: {error}", source_path.display()))?;
- let relative = source_path
- .strip_prefix(package_root)
- .map_err(|_| {
- format!(
- "module source {} escapes package {}",
- source_path.display(),
- package_root.display()
- )
- })?
- .to_string_lossy()
- .replace('\\', "/");
- (file.items, relative)
- };
-
- let children = items
- .iter()
- .filter_map(|item| match item {
- Item::Mod(item_mod) => Some(item_mod.clone()),
- _ => None,
- })
- .collect::<Vec<_>>();
- units.insert(
- module.clone(),
- ModuleUnit {
- module: module.clone(),
- parent,
- declared_public,
- initially_effective,
- source_relative,
- items,
- },
- );
-
- for child in children {
- let mut child_module = module.clone();
- child_module.push(child.ident.to_string());
- let child_public = is_public(&child.vis);
- let child_effective = initially_effective && child_public;
- if let Some((_, inline)) = child.content {
- collect_module(
- package_root,
- child_module,
- Some(module.clone()),
- child_public,
- child_effective,
- source_path,
- &module_directory.join(child.ident.to_string()),
- Some(inline),
- units,
- )?;
- } else {
- let child_path = module_source_path(source_path, module_directory, &child)?;
- collect_module(
- package_root,
- child_module,
- Some(module.clone()),
- child_public,
- child_effective,
- &child_path,
- &module_directory.join(child.ident.to_string()),
- None,
- units,
- )?;
- }
- }
- Ok(())
-}
-
-fn module_directory(source_path: &Path) -> Result<PathBuf, String> {
- let parent = source_path
- .parent()
- .ok_or_else(|| format!("module source has no parent: {}", source_path.display()))?;
- let file_name = source_path.file_name().and_then(|name| name.to_str());
- if matches!(file_name, Some("lib.rs" | "main.rs" | "mod.rs")) {
- Ok(parent.to_path_buf())
- } else {
- Ok(parent.join(
- source_path
- .file_stem()
- .ok_or_else(|| format!("module source has no stem: {}", source_path.display()))?,
- ))
- }
-}
-
-fn module_source_path(
- source_path: &Path,
- module_directory: &Path,
- item_mod: &syn::ItemMod,
-) -> Result<PathBuf, String> {
- if let Some(path) = path_attribute(item_mod) {
- return Ok(source_path
- .parent()
- .unwrap_or_else(|| Path::new("."))
- .join(path));
- }
- let name = item_mod.ident.to_string();
- let flat = module_directory.join(format!("{name}.rs"));
- let nested = module_directory.join(&name).join("mod.rs");
- match (flat.is_file(), nested.is_file()) {
- (true, false) => Ok(flat),
- (false, true) => Ok(nested),
- (true, true) => Err(format!(
- "module {} is ambiguous between {} and {}",
- item_mod.ident,
- flat.display(),
- nested.display()
- )),
- (false, false) => Err(format!(
- "module {} source is missing under {}",
- item_mod.ident,
- module_directory.display()
- )),
- }
-}
-
-fn path_attribute(item_mod: &syn::ItemMod) -> Option<String> {
- item_mod.attrs.iter().find_map(|attribute| {
- if !attribute.path().is_ident("path") {
- return None;
- }
- let Meta::NameValue(value) = &attribute.meta else {
- return None;
- };
- let Expr::Lit(ExprLit {
- lit: Lit::Str(path),
- ..
- }) = &value.value
- else {
- return None;
- };
- Some(path.value())
- })
-}
-
-fn scan_package_units(
- package: &str,
- units: &BTreeMap<Vec<String>, ModuleUnit>,
- forbidden: &BTreeSet<&str>,
- allowed: &BTreeSet<&str>,
-) -> Result<Vec<ApiFinding>, String> {
- let mut effective = units
- .values()
- .filter(|unit| unit.initially_effective)
- .map(|unit| unit.module.clone())
- .collect::<BTreeSet<_>>();
- let mut exported_items = BTreeSet::new();
- let mut changed = true;
- while changed {
- changed = propagate_public_modules(units, &mut effective);
- let scopes = effective.iter().cloned().collect::<Vec<_>>();
- for scope in scopes {
- let unit = units
- .get(&scope)
- .ok_or_else(|| format!("missing module unit {}", module_label(&scope)))?;
- for item_use in unit.items.iter().filter_map(|item| match item {
- Item::Use(item_use) if is_public(&item_use.vis) => Some(item_use),
- _ => None,
- }) {
- for (segments, glob) in flatten_use_tree(&item_use.tree) {
- match resolve_internal_export(&scope, &segments, glob, units) {
- InternalExport::Module(module) => {
- changed |= effective.insert(module);
- }
- InternalExport::Item(module, item) => {
- changed |= exported_items.insert((module, item));
- }
- InternalExport::External => {}
- }
- }
- }
- }
- }
- propagate_public_modules(units, &mut effective);
-
- let mut findings = BTreeSet::new();
- for unit in units.values() {
- let imports = import_map(&unit.items);
- let unit_effective = effective.contains(&unit.module);
- let explicitly_exported = exported_items
- .iter()
- .filter(|(module, _)| module == &unit.module)
- .map(|(_, item)| item.as_str())
- .collect::<BTreeSet<_>>();
-
- for item in &unit.items {
- if let Item::Use(item_use) = item {
- if unit_effective && is_public(&item_use.vis) {
- scan_public_use(
- package,
- unit,
- item_use,
- &imports,
- forbidden,
- allowed,
- &mut findings,
- );
- }
- continue;
- }
- if let Item::Impl(item_impl) = item {
- let Some(self_name) = impl_self_name(&item_impl.self_ty) else {
- continue;
- };
- if unit_effective || explicitly_exported.contains(self_name.as_str()) {
- scan_impl(
- package,
- unit,
- item_impl,
- &self_name,
- &imports,
- forbidden,
- allowed,
- &mut findings,
- );
- }
- continue;
- }
- let Some(name) = item_name(item) else {
- continue;
- };
- if (unit_effective && item_is_public(item))
- || explicitly_exported.contains(name.as_str())
- {
- scan_item(
- package,
- unit,
- item,
- &imports,
- forbidden,
- allowed,
- &mut findings,
- );
- }
- }
- }
- Ok(findings.into_iter().collect())
-}
-
-fn propagate_public_modules(
- units: &BTreeMap<Vec<String>, ModuleUnit>,
- effective: &mut BTreeSet<Vec<String>>,
-) -> bool {
- let mut changed = false;
- loop {
- let before = effective.len();
- for unit in units.values() {
- if unit.declared_public
- && unit
- .parent
- .as_ref()
- .is_some_and(|parent| effective.contains(parent))
- {
- effective.insert(unit.module.clone());
- }
- }
- if effective.len() == before {
- break;
- }
- changed = true;
- }
- changed
-}
-
-fn flatten_use_tree(tree: &UseTree) -> Vec<(Vec<String>, bool)> {
- fn visit(tree: &UseTree, prefix: &mut Vec<String>, output: &mut Vec<(Vec<String>, bool)>) {
- match tree {
- UseTree::Path(path) => {
- prefix.push(path.ident.to_string());
- visit(&path.tree, prefix, output);
- prefix.pop();
- }
- UseTree::Name(name) => {
- if name.ident == "self" {
- output.push((prefix.clone(), false));
- } else {
- let mut path = prefix.clone();
- path.push(name.ident.to_string());
- output.push((path, false));
- }
- }
- UseTree::Rename(rename) => {
- let mut path = prefix.clone();
- path.push(rename.ident.to_string());
- output.push((path, false));
- }
- UseTree::Glob(_) => output.push((prefix.clone(), true)),
- UseTree::Group(group) => {
- for item in &group.items {
- visit(item, prefix, output);
- }
- }
- }
- }
-
- let mut output = Vec::new();
- visit(tree, &mut Vec::new(), &mut output);
- output
-}
-
-fn resolve_internal_export(
- current: &[String],
- segments: &[String],
- glob: bool,
- units: &BTreeMap<Vec<String>, ModuleUnit>,
-) -> InternalExport {
- if segments.is_empty() {
- return InternalExport::External;
- }
- let candidates = internal_candidates(current, segments);
- for candidate in candidates {
- if glob && units.contains_key(&candidate) {
- return InternalExport::Module(candidate);
- }
- if units.contains_key(&candidate) {
- return InternalExport::Module(candidate);
- }
- if let Some((item, module)) = candidate.split_last() {
- let module = module.to_vec();
- if units.contains_key(&module) {
- return InternalExport::Item(module, item.clone());
- }
- }
- }
- InternalExport::External
-}
-
-fn internal_candidates(current: &[String], segments: &[String]) -> Vec<Vec<String>> {
- let mut candidates = Vec::new();
- match segments.first().map(String::as_str) {
- Some("crate") => candidates.push(segments[1..].to_vec()),
- Some("self") => {
- let mut path = current.to_vec();
- path.extend_from_slice(&segments[1..]);
- candidates.push(path);
- }
- Some("super") => {
- let mut base = current.to_vec();
- let mut index = 0;
- while segments.get(index).map(String::as_str) == Some("super") {
- base.pop();
- index += 1;
- }
- base.extend_from_slice(&segments[index..]);
- candidates.push(base);
- }
- _ => {
- candidates.push(segments.to_vec());
- let mut local = current.to_vec();
- local.extend_from_slice(segments);
- if !candidates.contains(&local) {
- candidates.push(local);
- }
- }
- }
- candidates
-}
-
-fn import_map(items: &[Item]) -> BTreeMap<String, Vec<String>> {
- fn visit(tree: &UseTree, prefix: &mut Vec<String>, output: &mut BTreeMap<String, Vec<String>>) {
- match tree {
- UseTree::Path(path) => {
- prefix.push(path.ident.to_string());
- visit(&path.tree, prefix, output);
- prefix.pop();
- }
- UseTree::Name(name) => {
- if name.ident == "self" {
- if let Some(local) = prefix.last() {
- output.insert(local.clone(), prefix.clone());
- }
- } else {
- let mut path = prefix.clone();
- path.push(name.ident.to_string());
- output.insert(name.ident.to_string(), path);
- }
- }
- UseTree::Rename(rename) => {
- let mut path = prefix.clone();
- path.push(rename.ident.to_string());
- output.insert(rename.rename.to_string(), path);
- }
- UseTree::Glob(_) => {}
- UseTree::Group(group) => {
- for item in &group.items {
- visit(item, prefix, output);
- }
- }
- }
- }
-
- let mut output = BTreeMap::new();
- for item_use in items.iter().filter_map(|item| match item {
- Item::Use(item_use) => Some(item_use),
- _ => None,
- }) {
- visit(&item_use.tree, &mut Vec::new(), &mut output);
- }
- output
-}
-
-#[allow(clippy::too_many_arguments)]
-fn scan_item(
- package: &str,
- unit: &ModuleUnit,
- item: &Item,
- imports: &BTreeMap<String, Vec<String>>,
- forbidden: &BTreeSet<&str>,
- allowed: &BTreeSet<&str>,
- findings: &mut BTreeSet<ApiFinding>,
-) {
- let name = item_name(item).unwrap_or_else(|| "<item>".to_owned());
- let label = qualified_item(&unit.module, &name);
- let mut visitor =
- LeakageVisitor::new(package, unit, label, imports, forbidden, allowed, findings);
- match item {
- Item::Const(item) => visitor.visit_type(&item.ty),
- Item::Enum(item) => {
- visitor.visit_generics(&item.generics);
- for variant in &item.variants {
- visit_fields(&mut visitor, &variant.fields, true);
- }
- }
- Item::Fn(item) => visitor.visit_signature(&item.sig),
- Item::ForeignMod(item) => {
- for foreign in &item.items {
- if let ForeignItem::Fn(function) = foreign {
- visitor.visit_signature(&function.sig);
- }
- }
- }
- Item::Static(item) => visitor.visit_type(&item.ty),
- Item::Struct(item) => {
- visitor.visit_generics(&item.generics);
- visit_fields(&mut visitor, &item.fields, false);
- }
- Item::Trait(item) => {
- visitor.visit_generics(&item.generics);
- for bound in &item.supertraits {
- visitor.visit_type_param_bound(bound);
- }
- for trait_item in &item.items {
- match trait_item {
- TraitItem::Const(item) => visitor.visit_type(&item.ty),
- TraitItem::Fn(item) => visitor.visit_signature(&item.sig),
- TraitItem::Type(item) => {
- visitor.visit_generics(&item.generics);
- for bound in &item.bounds {
- visitor.visit_type_param_bound(bound);
- }
- if let Some((_, ty)) = &item.default {
- visitor.visit_type(ty);
- }
- }
- _ => {}
- }
- }
- }
- Item::TraitAlias(item) => {
- visitor.visit_generics(&item.generics);
- for bound in &item.bounds {
- visitor.visit_type_param_bound(bound);
- }
- }
- Item::Type(item) => {
- visitor.visit_generics(&item.generics);
- visitor.visit_type(&item.ty);
- }
- Item::Union(item) => {
- visitor.visit_generics(&item.generics);
- for field in &item.fields.named {
- if is_public(&field.vis) {
- visitor.visit_type(&field.ty);
- }
- }
- }
- _ => {}
- }
-}
-
-fn visit_fields(visitor: &mut LeakageVisitor<'_>, fields: &Fields, all_visible: bool) {
- for field in fields {
- if all_visible || is_public(&field.vis) {
- visitor.visit_type(&field.ty);
- }
- }
-}
-
-#[allow(clippy::too_many_arguments)]
-fn scan_impl(
- package: &str,
- unit: &ModuleUnit,
- item_impl: &syn::ItemImpl,
- self_name: &str,
- imports: &BTreeMap<String, Vec<String>>,
- forbidden: &BTreeSet<&str>,
- allowed: &BTreeSet<&str>,
- findings: &mut BTreeSet<ApiFinding>,
-) {
- let trait_impl = item_impl.trait_.is_some();
- for item in &item_impl.items {
- let (name, is_exposed) = match item {
- ImplItem::Const(item) => (item.ident.to_string(), trait_impl || is_public(&item.vis)),
- ImplItem::Fn(item) => (
- item.sig.ident.to_string(),
- trait_impl || is_public(&item.vis),
- ),
- ImplItem::Type(item) => (item.ident.to_string(), trait_impl || is_public(&item.vis)),
- _ => continue,
- };
- if !is_exposed {
- continue;
- }
- let label = format!("{}::{name}", qualified_item(&unit.module, self_name));
- let mut visitor =
- LeakageVisitor::new(package, unit, label, imports, forbidden, allowed, findings);
- if let Some((_, trait_path, _)) = &item_impl.trait_ {
- visitor.visit_path(trait_path);
- }
- match item {
- ImplItem::Const(item) => visitor.visit_type(&item.ty),
- ImplItem::Fn(item) => visitor.visit_signature(&item.sig),
- ImplItem::Type(item) => {
- visitor.visit_generics(&item.generics);
- visitor.visit_type(&item.ty);
- }
- _ => {}
- }
- }
-}
-
-#[allow(clippy::too_many_arguments)]
-fn scan_public_use(
- package: &str,
- unit: &ModuleUnit,
- item_use: &syn::ItemUse,
- imports: &BTreeMap<String, Vec<String>>,
- forbidden: &BTreeSet<&str>,
- allowed: &BTreeSet<&str>,
- findings: &mut BTreeSet<ApiFinding>,
-) {
- for (segments, _) in flatten_use_tree(&item_use.tree) {
- if segments.is_empty() {
- continue;
- }
- let label = format!("{}::pub use", module_label(&unit.module));
- record_path(
- package, unit, &label, &segments, imports, forbidden, allowed, findings,
- );
- }
-}
-
-struct LeakageVisitor<'a> {
- package: &'a str,
- unit: &'a ModuleUnit,
- item: String,
- imports: &'a BTreeMap<String, Vec<String>>,
- forbidden: &'a BTreeSet<&'a str>,
- allowed: &'a BTreeSet<&'a str>,
- findings: &'a mut BTreeSet<ApiFinding>,
-}
-
-impl<'a> LeakageVisitor<'a> {
- #[allow(clippy::too_many_arguments)]
- fn new(
- package: &'a str,
- unit: &'a ModuleUnit,
- item: String,
- imports: &'a BTreeMap<String, Vec<String>>,
- forbidden: &'a BTreeSet<&'a str>,
- allowed: &'a BTreeSet<&'a str>,
- findings: &'a mut BTreeSet<ApiFinding>,
- ) -> Self {
- Self {
- package,
- unit,
- item,
- imports,
- forbidden,
- allowed,
- findings,
- }
- }
-}
-
-impl<'ast> Visit<'ast> for LeakageVisitor<'_> {
- fn visit_path(&mut self, path: &'ast SynPath) {
- let segments = path
- .segments
- .iter()
- .map(|segment| segment.ident.to_string())
- .collect::<Vec<_>>();
- record_path(
- self.package,
- self.unit,
- &self.item,
- &segments,
- self.imports,
- self.forbidden,
- self.allowed,
- self.findings,
- );
- syn::visit::visit_path(self, path);
- }
-}
-
-#[allow(clippy::too_many_arguments)]
-fn record_path(
- package: &str,
- unit: &ModuleUnit,
- item: &str,
- segments: &[String],
- imports: &BTreeMap<String, Vec<String>>,
- forbidden: &BTreeSet<&str>,
- allowed: &BTreeSet<&str>,
- findings: &mut BTreeSet<ApiFinding>,
-) {
- if segments.is_empty() {
- return;
- }
- let resolved = if let Some(imported) = imports.get(&segments[0]) {
- let mut resolved = imported.clone();
- resolved.extend_from_slice(&segments[1..]);
- resolved
- } else {
- segments.to_vec()
- };
- let observed = resolved.join("::");
- for forbidden_path in forbidden {
- if allowed.contains(forbidden_path)
- || !(observed == *forbidden_path
- || observed
- .strip_prefix(forbidden_path)
- .is_some_and(|suffix| suffix.starts_with("::")))
- {
- continue;
- }
- findings.insert(ApiFinding {
- package: package.to_owned(),
- source: unit.source_relative.clone(),
- item: item.to_owned(),
- forbidden_path: (*forbidden_path).to_owned(),
- observed_path: observed.clone(),
- });
- }
-}
-
-fn exception_matches(exceptions: &[ApiException], finding: &ApiFinding) -> bool {
- exceptions.iter().any(|exception| {
- exception.package == finding.package
- && exception.source == finding.source
- && exception.forbidden_path == finding.forbidden_path
- && exception.items.binary_search(&finding.item).is_ok()
- && exception
- .observed_paths
- .binary_search(&finding.observed_path)
- .is_ok()
- })
-}
-
-fn format_finding(finding: &ApiFinding) -> String {
- format!(
- "package={} source={} item={} forbidden_path={} observed_path={}",
- finding.package,
- finding.source,
- finding.item,
- finding.forbidden_path,
- finding.observed_path
- )
-}
-
-fn item_is_public(item: &Item) -> bool {
- match item {
- Item::Const(item) => is_public(&item.vis),
- Item::Enum(item) => is_public(&item.vis),
- Item::Fn(item) => is_public(&item.vis),
- Item::ForeignMod(_) => true,
- Item::Static(item) => is_public(&item.vis),
- Item::Struct(item) => is_public(&item.vis),
- Item::Trait(item) => is_public(&item.vis),
- Item::TraitAlias(item) => is_public(&item.vis),
- Item::Type(item) => is_public(&item.vis),
- Item::Union(item) => is_public(&item.vis),
- _ => false,
- }
-}
-
-fn item_name(item: &Item) -> Option<String> {
- match item {
- Item::Const(item) => Some(item.ident.to_string()),
- Item::Enum(item) => Some(item.ident.to_string()),
- Item::Fn(item) => Some(item.sig.ident.to_string()),
- Item::Static(item) => Some(item.ident.to_string()),
- Item::Struct(item) => Some(item.ident.to_string()),
- Item::Trait(item) => Some(item.ident.to_string()),
- Item::TraitAlias(item) => Some(item.ident.to_string()),
- Item::Type(item) => Some(item.ident.to_string()),
- Item::Union(item) => Some(item.ident.to_string()),
- _ => None,
- }
-}
-
-fn impl_self_name(self_ty: &Type) -> Option<String> {
- let Type::Path(path) = self_ty else {
- return None;
- };
- path.path
- .segments
- .last()
- .map(|segment| segment.ident.to_string())
-}
-
-fn is_public(visibility: &Visibility) -> bool {
- matches!(visibility, Visibility::Public(_))
-}
-
-fn qualified_item(module: &[String], item: &str) -> String {
- if module.is_empty() {
- item.to_owned()
- } else {
- format!("{}::{item}", module.join("::"))
- }
-}
-
-fn module_label(module: &[String]) -> String {
- if module.is_empty() {
- "crate".to_owned()
- } else {
- module.join("::")
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- ApiBoundaryPolicy, ApiException, CargoMetadata, exception_matches, scan_workspace,
- validate_policy,
- };
- use serde::Deserialize;
- use std::{collections::BTreeSet, fs};
-
- const POLICY: &str = include_str!("../../../../contracts/releases/api_boundaries.toml");
- const ARCHITECTURE: &str =
- include_str!("../../../../docs/specs/radroots_crates_release_v1.toml");
- const GENERIC_SQLX: &str = include_str!("../../tests/fixtures/api-leakage/generic-sqlx.rs");
- const GENERIC_RENAMED_TOKIO: &str =
- include_str!("../../tests/fixtures/api-leakage/generic-renamed-tokio.rs");
- const ALLOWED_ADAPTER: &str =
- include_str!("../../tests/fixtures/api-leakage/allowed-concrete-adapter.rs");
- const PRIVATE_IMPLEMENTATION: &str =
- include_str!("../../tests/fixtures/api-leakage/private-implementation.rs");
- const ADR_EXCEPTION: &str = include_str!("../../tests/fixtures/api-leakage/adr-exception.rs");
-
- #[derive(Deserialize)]
- struct ArchitectureCatalog {
- package: Vec<ArchitecturePackage>,
- }
-
- #[derive(Deserialize)]
- struct ArchitecturePackage {
- name: String,
- }
-
- fn policy() -> ApiBoundaryPolicy {
- toml::from_str(POLICY).expect("API boundary policy")
- }
-
- fn expected_packages() -> BTreeSet<String> {
- toml::from_str::<ArchitectureCatalog>(ARCHITECTURE)
- .expect("architecture")
- .package
- .into_iter()
- .map(|package| package.name)
- .collect()
- }
-
- fn fixture_metadata(root: &std::path::Path, package: &str, source: &str) -> CargoMetadata {
- let package_root = root.join(package);
- fs::create_dir_all(package_root.join("src")).expect("create fixture source");
- fs::write(
- package_root.join("Cargo.toml"),
- "[package]\nname='fixture'\n",
- )
- .expect("write fixture manifest");
- fs::write(package_root.join("src/lib.rs"), source).expect("write fixture source");
- let id = format!("fixture#{package}@0.1.0");
- CargoMetadata {
- workspace_members: vec![id.clone()],
- packages: vec![super::CargoPackage {
- id,
- name: package.to_owned(),
- manifest_path: package_root
- .join("Cargo.toml")
- .to_string_lossy()
- .into_owned(),
- targets: vec![super::CargoTarget {
- kind: vec!["lib".to_owned()],
- src_path: package_root
- .join("src/lib.rs")
- .to_string_lossy()
- .into_owned(),
- }],
- }],
- }
- }
-
- fn scan_fixture(package: &str, source: &str) -> Vec<super::ApiFinding> {
- let root = tempfile::TempDir::new().expect("fixture root");
- scan_workspace(
- root.path(),
- &policy(),
- &fixture_metadata(root.path(), package, source),
- )
- .expect("scan fixture")
- }
-
- fn write_baseline_adr(root: &std::path::Path) {
- fs::create_dir_all(root.join("docs/decisions")).expect("baseline ADR directory");
- fs::write(
- root.join("docs/decisions/0001-public-api-leakage-migration-baseline.md"),
- "RCRV1-API-001 RCRV1-API-002 RCRV1-API-003 RCRV1-API-004 RCRV1-API-005 RCRV1-API-006 RCRV1-API-007 RCRV1-API-008\n",
- )
- .expect("baseline ADR");
- }
-
- #[test]
- fn policy_covers_exact_architecture_catalog() {
- let root = tempfile::TempDir::new().expect("policy root");
- write_baseline_adr(root.path());
- validate_policy(root.path(), &policy(), &expected_packages())
- .expect("policy without exceptions");
- }
-
- #[test]
- fn generic_sqlx_public_field_is_forbidden() {
- let findings = scan_fixture("radroots_storage", GENERIC_SQLX);
- assert_eq!(findings.len(), 1);
- assert_eq!(findings[0].item, "StorageHandle");
- assert_eq!(findings[0].forbidden_path, "sqlx");
- assert_eq!(findings[0].observed_path, "sqlx::SqlitePool");
- }
-
- #[test]
- fn renamed_tokio_return_type_is_resolved() {
- let findings = scan_fixture("radroots_sync", GENERIC_RENAMED_TOKIO);
- assert_eq!(findings.len(), 1);
- assert_eq!(findings[0].item, "executor");
- assert_eq!(findings[0].forbidden_path, "tokio");
- assert_eq!(findings[0].observed_path, "tokio::runtime::Handle");
- }
-
- #[test]
- fn specified_nostr_adapter_may_expose_protocol_types() {
- let findings = scan_fixture("radroots_nostr", ALLOWED_ADAPTER);
- assert!(findings.is_empty());
- }
-
- #[test]
- fn private_implementation_types_are_not_public_api() {
- let findings = scan_fixture("radroots_sdk", PRIVATE_IMPLEMENTATION);
- assert!(findings.is_empty());
- }
-
- #[test]
- fn exact_item_exception_requires_resolving_adr() {
- let root = tempfile::TempDir::new().expect("exception root");
- write_baseline_adr(root.path());
- fs::create_dir_all(root.path().join("docs/decisions")).expect("ADR directory");
- fs::write(
- root.path().join("docs/decisions/0001-test.md"),
- "# Test\n\nRCRV1-API-999\n",
- )
- .expect("ADR");
- let mut policy = policy();
- policy.exception.push(ApiException {
- id: "RCRV1-API-999".to_owned(),
- package: "radroots_sdk".to_owned(),
- source: "src/lib.rs".to_owned(),
- forbidden_path: "reqwest".to_owned(),
- items: vec!["temporary_client".to_owned()],
- observed_paths: vec!["reqwest::Client".to_owned()],
- adr: "docs/decisions/0001-test.md".to_owned(),
- removal_step: 226,
- rationale: "test-only exception".to_owned(),
- });
- validate_policy(root.path(), &policy, &expected_packages()).expect("resolving ADR");
- let finding = scan_workspace(
- root.path(),
- &policy,
- &fixture_metadata(root.path(), "radroots_sdk", ADR_EXCEPTION),
- )
- .expect("scan exception fixture")
- .pop()
- .expect("finding");
- assert!(exception_matches(&policy.exception, &finding));
-
- fs::remove_file(root.path().join("docs/decisions/0001-test.md")).expect("remove ADR");
- let error = validate_policy(root.path(), &policy, &expected_packages())
- .expect_err("missing ADR must fail");
- assert!(error.contains("is not readable"));
- }
-}
diff --git a/tools/xtask/src/architecture/dependency_boundary.rs b/tools/xtask/src/architecture/dependency_boundary.rs
@@ -1,718 +0,0 @@
-use std::{
- collections::{BTreeMap, BTreeSet},
- fs,
- path::Path,
- process::Command,
-};
-
-use serde::Deserialize;
-
-const PACKAGE_TIERS_RELATIVE: &str = "contracts/releases/package_tiers.toml";
-const SPEC_ID: &str = "radroots.crates.release.v1";
-const DIRECTION: &str = "same_or_lower";
-const POLICY_SCHEMA_VERSION: u16 = 1;
-const CURRENT_STEP: u16 = 24;
-const EXPECTED_TIERS: &[(&str, u8)] = &[
- ("foundation", 0),
- ("domain", 1),
- ("spi", 2),
- ("adapter", 3),
- ("orchestration", 4),
- ("sdk", 5),
- ("facade", 6),
-];
-const EXPECTED_DEPENDENCY_KINDS: &[&str] = &["build", "dev", "normal"];
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct DependencyBoundaryPolicy {
- schema_version: u16,
- spec_id: String,
- direction: String,
- enforced_dependency_kinds: Vec<String>,
- tier: Vec<PackageTier>,
- #[serde(default)]
- temporary_exception: Vec<TemporaryException>,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct PackageTier {
- id: String,
- rank: u8,
- packages: Vec<String>,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct TemporaryException {
- owner: String,
- dependency: String,
- kind: String,
- target: String,
- features: Vec<String>,
- uses_default_features: bool,
- removal_step: u16,
- rationale: String,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoMetadata {
- packages: Vec<CargoPackage>,
- resolve: Option<CargoResolve>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoPackage {
- id: String,
- name: String,
- #[serde(default)]
- dependencies: Vec<CargoDependency>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoDependency {
- name: String,
- #[serde(default)]
- kind: Option<String>,
- #[serde(default)]
- features: Vec<String>,
- #[serde(default)]
- target: Option<String>,
- #[serde(default = "default_true")]
- uses_default_features: bool,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoResolve {
- nodes: Vec<CargoNode>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoNode {
- id: String,
- #[serde(default)]
- deps: Vec<CargoNodeDependency>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoNodeDependency {
- name: String,
- pkg: String,
- #[serde(default)]
- dep_kinds: Vec<CargoDependencyKind>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoDependencyKind {
- #[serde(default)]
- kind: Option<String>,
- #[serde(default)]
- target: Option<String>,
-}
-
-#[derive(Clone, Copy)]
-struct PackagePlacement<'a> {
- tier: &'a str,
- rank: u8,
-}
-
-fn default_true() -> bool {
- true
-}
-
-pub(super) fn validate_policy_catalog(
- workspace_root: &Path,
- expected_packages: &BTreeSet<String>,
-) -> Result<(), String> {
- let policy = load_policy(workspace_root)?;
- validate_policy(&policy, expected_packages).map(|_| ())
-}
-
-pub(super) fn validate_resolved_boundaries(workspace_root: &Path) -> Result<(), String> {
- let policy = load_policy(workspace_root)?;
- let expected_packages = policy
- .tier
- .iter()
- .flat_map(|tier| tier.packages.iter().cloned())
- .collect::<BTreeSet<_>>();
- let placements = validate_policy(&policy, &expected_packages)?;
- let metadata = load_metadata(workspace_root)?;
- validate_metadata(&policy, &placements, &metadata)
-}
-
-fn load_policy(workspace_root: &Path) -> Result<DependencyBoundaryPolicy, String> {
- let path = workspace_root.join(PACKAGE_TIERS_RELATIVE);
- let raw =
- fs::read_to_string(&path).map_err(|error| format!("read {}: {error}", path.display()))?;
- toml::from_str(&raw).map_err(|error| format!("parse {}: {error}", path.display()))
-}
-
-fn validate_policy<'a>(
- policy: &'a DependencyBoundaryPolicy,
- expected_packages: &BTreeSet<String>,
-) -> Result<BTreeMap<&'a str, PackagePlacement<'a>>, String> {
- if policy.schema_version != POLICY_SCHEMA_VERSION {
- return Err(format!(
- "{PACKAGE_TIERS_RELATIVE} schema_version must be {POLICY_SCHEMA_VERSION}"
- ));
- }
- if policy.spec_id != SPEC_ID {
- return Err(format!(
- "{PACKAGE_TIERS_RELATIVE} spec_id must be {SPEC_ID}"
- ));
- }
- if policy.direction != DIRECTION {
- return Err(format!(
- "{PACKAGE_TIERS_RELATIVE} direction must be {DIRECTION}"
- ));
- }
-
- let actual_kinds = unique_strings(
- "enforced_dependency_kinds",
- &policy.enforced_dependency_kinds,
- )?;
- let expected_kinds = EXPECTED_DEPENDENCY_KINDS
- .iter()
- .copied()
- .collect::<BTreeSet<_>>();
- if actual_kinds != expected_kinds {
- return Err(format!(
- "{PACKAGE_TIERS_RELATIVE} enforced_dependency_kinds must be build, dev, normal"
- ));
- }
-
- let actual_tiers = policy
- .tier
- .iter()
- .map(|tier| (tier.id.as_str(), tier.rank))
- .collect::<Vec<_>>();
- if actual_tiers != EXPECTED_TIERS {
- return Err(format!(
- "{PACKAGE_TIERS_RELATIVE} tiers must match the approved ordered architecture"
- ));
- }
-
- let mut placements = BTreeMap::new();
- for tier in &policy.tier {
- if tier.packages.is_empty() {
- return Err(format!(
- "{PACKAGE_TIERS_RELATIVE} tier {} must allocate at least one package",
- tier.id
- ));
- }
- for package in &tier.packages {
- if placements
- .insert(
- package.as_str(),
- PackagePlacement {
- tier: &tier.id,
- rank: tier.rank,
- },
- )
- .is_some()
- {
- return Err(format!(
- "{PACKAGE_TIERS_RELATIVE} package {package} is allocated more than once"
- ));
- }
- }
- }
-
- let actual_packages = placements
- .keys()
- .map(|package| (*package).to_owned())
- .collect::<BTreeSet<_>>();
- if &actual_packages != expected_packages {
- return Err(package_set_mismatch(expected_packages, &actual_packages));
- }
-
- let mut exception_keys = BTreeSet::new();
- for exception in &policy.temporary_exception {
- let owner = placements.get(exception.owner.as_str()).ok_or_else(|| {
- format!(
- "{PACKAGE_TIERS_RELATIVE} exception owner {} is not an approved package",
- exception.owner
- )
- })?;
- let dependency = placements
- .get(exception.dependency.as_str())
- .ok_or_else(|| {
- format!(
- "{PACKAGE_TIERS_RELATIVE} exception dependency {} is not an approved package",
- exception.dependency
- )
- })?;
- if dependency.rank <= owner.rank {
- return Err(format!(
- "{PACKAGE_TIERS_RELATIVE} exception {} -> {} does not describe an upward edge",
- exception.owner, exception.dependency
- ));
- }
- if !expected_kinds.contains(exception.kind.as_str()) {
- return Err(format!(
- "{PACKAGE_TIERS_RELATIVE} exception {} -> {} uses unknown dependency kind {}",
- exception.owner, exception.dependency, exception.kind
- ));
- }
- if exception.target.trim().is_empty() {
- return Err(format!(
- "{PACKAGE_TIERS_RELATIVE} exception {} -> {} must name its exact target",
- exception.owner, exception.dependency
- ));
- }
- let normalized_features = normalized_features(&exception.features);
- if normalized_features.len() != exception.features.len()
- || normalized_features.iter().ne(exception.features.iter())
- {
- return Err(format!(
- "{PACKAGE_TIERS_RELATIVE} exception {} -> {} features must be sorted and unique",
- exception.owner, exception.dependency
- ));
- }
- if exception.removal_step <= CURRENT_STEP {
- return Err(format!(
- "{PACKAGE_TIERS_RELATIVE} exception {} -> {} expired at Step {}",
- exception.owner, exception.dependency, exception.removal_step
- ));
- }
- if exception.rationale.trim().is_empty() {
- return Err(format!(
- "{PACKAGE_TIERS_RELATIVE} exception {} -> {} must include a rationale",
- exception.owner, exception.dependency
- ));
- }
- let key = (
- exception.owner.as_str(),
- exception.dependency.as_str(),
- exception.kind.as_str(),
- exception.target.as_str(),
- exception.features.clone(),
- exception.uses_default_features,
- );
- if !exception_keys.insert(key) {
- return Err(format!(
- "{PACKAGE_TIERS_RELATIVE} contains a duplicate exception for {} -> {}",
- exception.owner, exception.dependency
- ));
- }
- }
-
- Ok(placements)
-}
-
-fn unique_strings<'a>(label: &str, values: &'a [String]) -> Result<BTreeSet<&'a str>, String> {
- let unique = values.iter().map(String::as_str).collect::<BTreeSet<_>>();
- if unique.len() != values.len() {
- return Err(format!(
- "{PACKAGE_TIERS_RELATIVE} {label} must not contain duplicates"
- ));
- }
- Ok(unique)
-}
-
-fn package_set_mismatch(expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> String {
- let missing = expected
- .difference(actual)
- .cloned()
- .collect::<Vec<_>>()
- .join(", ");
- let extra = actual
- .difference(expected)
- .cloned()
- .collect::<Vec<_>>()
- .join(", ");
- format!("{PACKAGE_TIERS_RELATIVE} package catalog mismatch; missing: {missing}; extra: {extra}")
-}
-
-fn load_metadata(workspace_root: &Path) -> Result<CargoMetadata, String> {
- let output = Command::new("cargo")
- .args([
- "metadata",
- "--format-version",
- "1",
- "--locked",
- "--all-features",
- ])
- .current_dir(workspace_root)
- .output()
- .map_err(|error| format!("run cargo metadata: {error}"))?;
- if !output.status.success() {
- return Err(format!(
- "cargo metadata failed while checking dependency boundaries: {}",
- String::from_utf8_lossy(&output.stderr).trim()
- ));
- }
- serde_json::from_slice(&output.stdout)
- .map_err(|error| format!("parse cargo metadata for dependency boundaries: {error}"))
-}
-
-fn validate_metadata(
- policy: &DependencyBoundaryPolicy,
- placements: &BTreeMap<&str, PackagePlacement<'_>>,
- metadata: &CargoMetadata,
-) -> Result<(), String> {
- let resolve = metadata
- .resolve
- .as_ref()
- .ok_or_else(|| "cargo metadata did not include a resolved dependency graph".to_owned())?;
- let mut packages_by_id = BTreeMap::new();
- for package in &metadata.packages {
- if packages_by_id
- .insert(package.id.as_str(), package)
- .is_some()
- {
- return Err(format!(
- "cargo metadata contains duplicate package id {}",
- package.id
- ));
- }
- }
-
- let mut nodes_by_id = BTreeMap::new();
- for node in &resolve.nodes {
- if nodes_by_id.insert(node.id.as_str(), node).is_some() {
- return Err(format!(
- "cargo metadata contains duplicate resolve node {}",
- node.id
- ));
- }
- }
-
- let enforced_kinds = policy
- .enforced_dependency_kinds
- .iter()
- .map(String::as_str)
- .collect::<BTreeSet<_>>();
- let mut violations = Vec::new();
-
- for node in &resolve.nodes {
- let Some(owner_package) = packages_by_id.get(node.id.as_str()).copied() else {
- return Err(format!(
- "cargo metadata resolve node {} has no package record",
- node.id
- ));
- };
- let Some(owner_placement) = placements.get(owner_package.name.as_str()).copied() else {
- continue;
- };
-
- for resolved_dependency in &node.deps {
- let dependency_package = packages_by_id
- .get(resolved_dependency.pkg.as_str())
- .copied()
- .ok_or_else(|| {
- format!(
- "cargo metadata resolved dependency {} has no package record",
- resolved_dependency.pkg
- )
- })?;
- let Some(dependency_placement) =
- placements.get(dependency_package.name.as_str()).copied()
- else {
- continue;
- };
- if resolved_dependency.dep_kinds.is_empty() {
- return Err(format!(
- "cargo metadata edge {} -> {} has no dependency kind",
- owner_package.name, dependency_package.name
- ));
- }
-
- for dependency_kind in &resolved_dependency.dep_kinds {
- let kind = normalized_kind(dependency_kind.kind.as_deref());
- if !enforced_kinds.contains(kind) {
- continue;
- }
- let declaration =
- resolve_declaration(owner_package, dependency_package, dependency_kind)?;
- if dependency_placement.rank <= owner_placement.rank {
- continue;
- }
- if exception_matches(
- &policy.temporary_exception,
- owner_package,
- dependency_package,
- dependency_kind,
- declaration,
- ) {
- continue;
- }
- violations.push(format_violation(
- owner_package,
- owner_placement,
- dependency_package,
- dependency_placement,
- resolved_dependency,
- dependency_kind,
- declaration,
- ));
- }
- }
- }
-
- if violations.is_empty() {
- Ok(())
- } else {
- violations.sort();
- Err(format!(
- "forbidden package dependency direction(s):\n{}",
- violations.join("\n")
- ))
- }
-}
-
-fn resolve_declaration<'a>(
- owner: &'a CargoPackage,
- dependency: &CargoPackage,
- dependency_kind: &CargoDependencyKind,
-) -> Result<&'a CargoDependency, String> {
- let kind = normalized_kind(dependency_kind.kind.as_deref());
- let candidates = owner
- .dependencies
- .iter()
- .filter(|candidate| {
- candidate.name == dependency.name
- && normalized_kind(candidate.kind.as_deref()) == kind
- && candidate.target == dependency_kind.target
- })
- .collect::<Vec<_>>();
- match candidates.as_slice() {
- [declaration] => Ok(*declaration),
- [] => Err(format!(
- "cargo metadata edge {} -> {} kind={kind} target={} has no matching package dependency declaration",
- owner.name,
- dependency.name,
- target_label(dependency_kind.target.as_deref())
- )),
- _ => Err(format!(
- "cargo metadata edge {} -> {} kind={kind} target={} has ambiguous package dependency declarations",
- owner.name,
- dependency.name,
- target_label(dependency_kind.target.as_deref())
- )),
- }
-}
-
-fn exception_matches(
- exceptions: &[TemporaryException],
- owner: &CargoPackage,
- dependency: &CargoPackage,
- dependency_kind: &CargoDependencyKind,
- declaration: &CargoDependency,
-) -> bool {
- let kind = normalized_kind(dependency_kind.kind.as_deref());
- let target = target_label(dependency_kind.target.as_deref());
- let features = normalized_features(&declaration.features);
- exceptions.iter().any(|exception| {
- exception.owner == owner.name
- && exception.dependency == dependency.name
- && exception.kind == kind
- && exception.target == target
- && exception.features == features
- && exception.uses_default_features == declaration.uses_default_features
- })
-}
-
-fn format_violation(
- owner: &CargoPackage,
- owner_placement: PackagePlacement<'_>,
- dependency: &CargoPackage,
- dependency_placement: PackagePlacement<'_>,
- resolved_dependency: &CargoNodeDependency,
- dependency_kind: &CargoDependencyKind,
- declaration: &CargoDependency,
-) -> String {
- let features = normalized_features(&declaration.features);
- format!(
- "{} (tier={} rank={}) -> {} (tier={} rank={}); owner_id={}; dependency_id={}; alias={}; kind={}; target={}; features=[{}]; default_features={}",
- owner.name,
- owner_placement.tier,
- owner_placement.rank,
- dependency.name,
- dependency_placement.tier,
- dependency_placement.rank,
- owner.id,
- dependency.id,
- resolved_dependency.name,
- normalized_kind(dependency_kind.kind.as_deref()),
- target_label(dependency_kind.target.as_deref()),
- features.join(","),
- declaration.uses_default_features,
- )
-}
-
-fn normalized_kind(kind: Option<&str>) -> &str {
- kind.unwrap_or("normal")
-}
-
-fn target_label(target: Option<&str>) -> &str {
- target.unwrap_or("all")
-}
-
-fn normalized_features(features: &[String]) -> Vec<String> {
- features
- .iter()
- .cloned()
- .collect::<BTreeSet<_>>()
- .into_iter()
- .collect()
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- CargoMetadata, DependencyBoundaryPolicy, exception_matches, validate_metadata,
- validate_policy,
- };
- use serde::Deserialize;
- use std::collections::BTreeSet;
-
- const POLICY: &str = include_str!("../../../../contracts/releases/package_tiers.toml");
- const ARCHITECTURE: &str =
- include_str!("../../../../docs/specs/radroots_crates_release_v1.toml");
- const DOMAIN_TO_STORAGE: &str =
- include_str!("../../tests/fixtures/dependency-boundaries/domain-to-storage.json");
- const SPI_TO_ADAPTER: &str =
- include_str!("../../tests/fixtures/dependency-boundaries/spi-to-adapter.json");
- const SERVICE_TO_SDK: &str =
- include_str!("../../tests/fixtures/dependency-boundaries/service-to-sdk.json");
- const VALID_DOWNWARD: &str =
- include_str!("../../tests/fixtures/dependency-boundaries/valid-downward.json");
-
- #[derive(Deserialize)]
- struct ArchitectureCatalog {
- package: Vec<ArchitecturePackage>,
- }
-
- #[derive(Deserialize)]
- struct ArchitecturePackage {
- name: String,
- }
-
- fn policy() -> DependencyBoundaryPolicy {
- toml::from_str(POLICY).expect("package tier policy")
- }
-
- fn metadata(raw: &str) -> CargoMetadata {
- serde_json::from_str(raw).expect("Cargo metadata fixture")
- }
-
- fn placements<'a>(
- policy: &'a DependencyBoundaryPolicy,
- ) -> std::collections::BTreeMap<&'a str, super::PackagePlacement<'a>> {
- let architecture =
- toml::from_str::<ArchitectureCatalog>(ARCHITECTURE).expect("architecture catalog");
- let expected = architecture
- .package
- .into_iter()
- .map(|package| package.name)
- .collect::<BTreeSet<_>>();
- validate_policy(policy, &expected).expect("valid package tier policy")
- }
-
- #[test]
- fn policy_covers_the_exact_architecture_catalog() {
- let policy = policy();
- let placements = placements(&policy);
- assert_eq!(placements.len(), 19);
- }
-
- #[test]
- fn domain_to_storage_reports_resolved_alias_kind_target_and_features() {
- let policy = policy();
- let error = validate_metadata(&policy, &placements(&policy), &metadata(DOMAIN_TO_STORAGE))
- .expect_err("domain must not depend on storage");
- assert!(error.contains("radroots_event (tier=domain rank=1)"));
- assert!(error.contains("radroots_storage (tier=spi rank=2)"));
- assert!(error.contains("alias=storage_alias"));
- assert!(error.contains("kind=normal"));
- assert!(error.contains("target=cfg(unix)"));
- assert!(error.contains("features=[memory]"));
- }
-
- #[test]
- fn spi_to_adapter_reports_build_and_target_specific_edge() {
- let policy = policy();
- let error = validate_metadata(&policy, &placements(&policy), &metadata(SPI_TO_ADAPTER))
- .expect_err("SPI must not depend on adapter");
- assert!(error.contains("radroots_transport (tier=spi rank=2)"));
- assert!(error.contains("radroots_nostr (tier=adapter rank=3)"));
- assert!(error.contains("alias=protocol_adapter"));
- assert!(error.contains("kind=build"));
- assert!(error.contains("target=cfg(target_arch = \"wasm32\")"));
- assert!(error.contains("features=[events]"));
- }
-
- #[test]
- fn service_orchestration_to_sdk_is_forbidden() {
- let policy = policy();
- let error = validate_metadata(&policy, &placements(&policy), &metadata(SERVICE_TO_SDK))
- .expect_err("service orchestration must not depend on SDK");
- assert!(error.contains("radroots_sync (tier=orchestration rank=4)"));
- assert!(error.contains("radroots_sdk (tier=sdk rank=5)"));
- assert!(error.contains("alias=client_engine"));
- }
-
- #[test]
- fn valid_downward_dev_target_edge_passes() {
- let policy = policy();
- validate_metadata(&policy, &placements(&policy), &metadata(VALID_DOWNWARD))
- .expect("adapter dev dependency on SPI points downward");
- }
-
- #[test]
- fn migration_exception_is_exact_about_features() {
- let policy = policy();
- let owner = super::CargoPackage {
- id: "owner".to_owned(),
- name: "radroots_trade".to_owned(),
- dependencies: Vec::new(),
- };
- let dependency = super::CargoPackage {
- id: "dependency".to_owned(),
- name: "radroots_nostr".to_owned(),
- dependencies: Vec::new(),
- };
- let kind = super::CargoDependencyKind {
- kind: Some("dev".to_owned()),
- target: None,
- };
- let exact = super::CargoDependency {
- name: "radroots_nostr".to_owned(),
- kind: Some("dev".to_owned()),
- features: vec!["std".to_owned(), "events".to_owned()],
- target: None,
- uses_default_features: false,
- };
- assert!(exception_matches(
- &policy.temporary_exception,
- &owner,
- &dependency,
- &kind,
- &exact
- ));
-
- let broadened = super::CargoDependency {
- features: vec!["client".to_owned(), "events".to_owned(), "std".to_owned()],
- ..exact
- };
- assert!(!exception_matches(
- &policy.temporary_exception,
- &owner,
- &dependency,
- &kind,
- &broadened
- ));
- }
-
- #[test]
- fn metadata_without_resolve_graph_fails_closed() {
- let policy = policy();
- let mut fixture = metadata(VALID_DOWNWARD);
- fixture.resolve = None;
- let error = validate_metadata(&policy, &placements(&policy), &fixture)
- .expect_err("missing resolved graph must fail");
- assert!(error.contains("did not include a resolved dependency graph"));
- }
-}
diff --git a/tools/xtask/src/bindings.rs b/tools/xtask/src/bindings.rs
@@ -1,281 +0,0 @@
-use std::{
- collections::BTreeMap,
- env, fs,
- path::{Path, PathBuf},
- process::Command,
-};
-
-use serde::{Deserialize, Serialize};
-use sha2::{Digest, Sha256};
-
-use crate::fs::{workspace_root, write_if_changed};
-
-const FFI_PACKAGE: &str = "radroots_sdk_ffi";
-const FFI_VERSION: &str = "0.1.0-alpha";
-const BINDGEN_VERSION: &str = "0.29.5";
-
-#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
-struct SourceLock {
- schema_version: u16,
- source_package: String,
- source_version: String,
- source_sha256: String,
- generator: String,
- generator_version: String,
- language: String,
- outputs: BTreeMap<String, String>,
-}
-
-pub fn generate(args: &[String]) -> Result<(), String> {
- match args {
- [language] if language == "swift" => generate_language("swift"),
- [language] if language == "kotlin" => generate_language("kotlin"),
- _ => Err("usage: cargo xtask generate bindings <swift|kotlin>".to_owned()),
- }
-}
-
-fn generate_language(language: &'static str) -> Result<(), String> {
- let root = workspace_root()?;
- build_bindgen(&root)?;
- let target_dir = env::var_os("CARGO_TARGET_DIR")
- .map(PathBuf::from)
- .ok_or_else(|| "CARGO_TARGET_DIR must be set by extbuild for FFI generation".to_owned())?;
- let library = target_dir.join("debug").join(format!(
- "{}radroots_sdk_ffi{}",
- env::consts::DLL_PREFIX,
- env::consts::DLL_SUFFIX
- ));
- if !library.is_file() {
- return Err(format!("missing built FFI library: {}", library.display()));
- }
- let bindgen = target_dir
- .join("debug")
- .join(format!("radroots-sdk-bindgen{}", env::consts::EXE_SUFFIX));
- if !bindgen.is_file() {
- return Err(format!(
- "missing FFI bindgen executable: {}",
- bindgen.display()
- ));
- }
- let temporary = tempfile::tempdir()
- .map_err(|error| format!("failed to create binding output directory: {error}"))?;
- let status = Command::new(&bindgen)
- .args(["generate", "--library"])
- .arg(&library)
- .args(["--crate", FFI_PACKAGE, "--language", language, "--out-dir"])
- .arg(temporary.path())
- .current_dir(&root)
- .status()
- .map_err(|error| format!("failed to start {}: {error}", bindgen.display()))?;
- if !status.success() {
- return Err(format!(
- "{language} binding generation failed with {status}"
- ));
- }
- install_outputs(&root, language, temporary.path())?;
- println!("generated {language} bindings for {FFI_PACKAGE}");
- Ok(())
-}
-
-fn build_bindgen(root: &Path) -> Result<(), String> {
- let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
- let status = Command::new(cargo)
- .args([
- "build",
- "--locked",
- "-p",
- FFI_PACKAGE,
- "--lib",
- "--bin",
- "radroots-sdk-bindgen",
- ])
- .current_dir(root)
- .status()
- .map_err(|error| format!("failed to start Cargo for FFI generation: {error}"))?;
- if !status.success() {
- return Err(format!(
- "FFI library and bindgen build failed with {status}"
- ));
- }
- Ok(())
-}
-
-fn install_outputs(root: &Path, language: &'static str, temporary: &Path) -> Result<(), String> {
- let output_dir = root.join("generated").join(language);
- fs::create_dir_all(&output_dir)
- .map_err(|error| format!("failed to create {}: {error}", output_dir.display()))?;
- let mut generated = Vec::new();
- collect_files(temporary, &mut generated)?;
- generated.sort();
- let mut outputs = BTreeMap::new();
- for path in generated {
- let name = path
- .strip_prefix(temporary)
- .map_err(|error| format!("failed to relativize {}: {error}", path.display()))?
- .to_str()
- .ok_or_else(|| format!("non-UTF-8 generated binding path: {}", path.display()))?
- .replace('\\', "/");
- let contents = fs::read(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- let destination = output_dir.join(&name);
- let text = String::from_utf8(contents)
- .map_err(|error| format!("generated text binding is not UTF-8: {error}"))?;
- let text = normalize_generated_text(&text);
- outputs.insert(
- name.clone(),
- format!("{:x}", Sha256::digest(text.as_bytes())),
- );
- if let Some(parent) = destination.parent() {
- fs::create_dir_all(parent)
- .map_err(|error| format!("failed to create {}: {error}", parent.display()))?;
- }
- write_if_changed(&destination, &text)?;
- }
- if outputs.is_empty() {
- return Err(format!("{language} binding generator emitted no files"));
- }
- let lock = SourceLock {
- schema_version: 1,
- source_package: FFI_PACKAGE.to_owned(),
- source_version: FFI_VERSION.to_owned(),
- source_sha256: ffi_source_sha256(root)?,
- generator: "uniffi".to_owned(),
- generator_version: BINDGEN_VERSION.to_owned(),
- language: language.to_owned(),
- outputs,
- };
- let mut rendered = serde_json::to_string_pretty(&lock)
- .map_err(|error| format!("failed to render {language} source lock: {error}"))?;
- rendered.push('\n');
- write_if_changed(&output_dir.join("source.lock"), &rendered).map(|_| ())
-}
-
-fn normalize_generated_text(contents: &str) -> String {
- let mut normalized = contents
- .lines()
- .map(str::trim_end)
- .collect::<Vec<_>>()
- .join("\n");
- while normalized.ends_with('\n') {
- normalized.pop();
- }
- normalized.push('\n');
- normalized
-}
-
-pub fn check(root: &Path) -> Result<(), String> {
- check_language(
- root,
- "swift",
- &[
- "radroots_sdk.swift",
- "radroots_sdkFFI.h",
- "radroots_sdkFFI.modulemap",
- ],
- )?;
- check_language(root, "kotlin", &["uniffi/radroots_sdk/radroots_sdk.kt"])?;
- check_kotlin_schema_inventory(root)
-}
-
-fn check_kotlin_schema_inventory(root: &Path) -> Result<(), String> {
- let path = root.join("generated/kotlin/uniffi/radroots_sdk/radroots_sdk.kt");
- let source = fs::read_to_string(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- for required in [
- "class MobileClient",
- "data class CapabilityStatus",
- "enum class CapabilityAvailability",
- "enum class CapabilityMaturity",
- "sealed class Exception",
- ] {
- if !source.contains(required) {
- return Err(format!(
- "generated Kotlin schema inventory is missing {required}: {}",
- path.display()
- ));
- }
- }
- for forbidden in ["Keychain", "BackgroundTask", "Presentation"] {
- if source.contains(forbidden) {
- return Err(format!(
- "generated Kotlin bindings contain host-owned {forbidden} API: {}",
- path.display()
- ));
- }
- }
- Ok(())
-}
-
-fn check_language(root: &Path, language: &'static str, expected: &[&str]) -> Result<(), String> {
- let output_dir = root.join("generated").join(language);
- let lock_path = output_dir.join("source.lock");
- let raw = fs::read_to_string(&lock_path)
- .map_err(|error| format!("failed to read {}: {error}", lock_path.display()))?;
- let actual: SourceLock = serde_json::from_str(&raw)
- .map_err(|error| format!("failed to parse {}: {error}", lock_path.display()))?;
- let mut outputs = BTreeMap::new();
- for name in expected {
- let path = output_dir.join(name);
- let contents = fs::read(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- outputs.insert(
- (*name).to_owned(),
- format!("{:x}", Sha256::digest(contents)),
- );
- }
- let expected_lock = SourceLock {
- schema_version: 1,
- source_package: FFI_PACKAGE.to_owned(),
- source_version: FFI_VERSION.to_owned(),
- source_sha256: ffi_source_sha256(root)?,
- generator: "uniffi".to_owned(),
- generator_version: BINDGEN_VERSION.to_owned(),
- language: language.to_owned(),
- outputs,
- };
- if actual != expected_lock {
- return Err(format!(
- "stale generated {language} bindings: {}",
- lock_path.display()
- ));
- }
- Ok(())
-}
-
-fn ffi_source_sha256(root: &Path) -> Result<String, String> {
- let crate_dir = root.join("crates/ffi");
- let mut paths = vec![crate_dir.join("Cargo.toml"), crate_dir.join("README.md")];
- collect_files(&crate_dir.join("src"), &mut paths)?;
- paths.sort();
- let mut hasher = Sha256::new();
- for path in paths {
- let relative = path
- .strip_prefix(root)
- .map_err(|error| format!("failed to relativize {}: {error}", path.display()))?;
- hasher.update(relative.to_string_lossy().as_bytes());
- hasher.update([0]);
- hasher.update(
- fs::read(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?,
- );
- hasher.update([0]);
- }
- Ok(format!("{:x}", hasher.finalize()))
-}
-
-fn collect_files(dir: &Path, paths: &mut Vec<PathBuf>) -> Result<(), String> {
- let mut entries = fs::read_dir(dir)
- .map_err(|error| format!("failed to read {}: {error}", dir.display()))?
- .collect::<Result<Vec<_>, _>>()
- .map_err(|error| format!("failed to enumerate {}: {error}", dir.display()))?;
- entries.sort_by_key(std::fs::DirEntry::file_name);
- for entry in entries {
- let path = entry.path();
- if path.is_dir() {
- collect_files(&path, paths)?;
- } else if path.is_file() {
- paths.push(path);
- }
- }
- Ok(())
-}
diff --git a/tools/xtask/src/check.rs b/tools/xtask/src/check.rs
@@ -1,3710 +0,0 @@
-use std::{
- collections::{BTreeMap, BTreeSet},
- fs,
- io::Read,
- path::{Path, PathBuf},
- process::Command,
-};
-
-use flate2::read::GzDecoder;
-use serde::Deserialize;
-use tar::Archive;
-
-use crate::{
- contracts::validate_sdk_contracts,
- fs::workspace_root,
- output::{PackageOutput, package_outputs},
- package_matrix::{
- FORBIDDEN_PACKAGE_NAMES, WasmPackageSpec, package_specs, validate_package_matrix,
- wasm_package_specs,
- },
- package_metadata::{PACKAGE_FILES, check_package_distribution_metadata, package_description},
- ts::generated_header,
- wasm::validate_provenance_manifest,
- wasm_declarations::declaration_files,
-};
-
-const PACKAGE_VERSION: &str = "0.1.0";
-const PACKAGE_LICENSE: &str = "MIT OR Apache-2.0";
-const PACKAGE_HOMEPAGE: &str = "https://radroots.org";
-const PACKAGE_REPOSITORY_URL: &str = "git+https://github.com/radrootslabs/sdk.git";
-const PUBLISH_ACCESS: &str = "public";
-const ALLOWED_RETICULUM_PREVIEW_PACKAGE: &str = "radroots_transport_reticulum";
-const RADROOTS_FACADE_MODULES: [&str; 11] = [
- "client",
- "event",
- "farm",
- "identity",
- "knowledge",
- "listing",
- "signing",
- "storage",
- "sync",
- "trade",
- "transport",
-];
-const RADROOTS_FACADE_FEATURES: [&str; 9] = [
- "client",
- "default",
- "full",
- "geonames",
- "knowledge",
- "native",
- "nip46",
- "nostr",
- "radrootsd",
-];
-
-#[derive(Debug, Deserialize)]
-struct PnpmPackEntry {
- filename: String,
- files: Vec<PnpmPackFile>,
-}
-
-#[derive(Debug, Deserialize)]
-struct PnpmPackFile {
- path: String,
-}
-
-#[derive(Debug)]
-struct PackedPackage {
- package_name: String,
- tarball_path: PathBuf,
- files: Vec<NpmPackFile>,
-}
-
-#[derive(Debug)]
-struct NpmPackFile {
- path: String,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoMetadata {
- packages: Vec<CargoMetadataPackage>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoMetadataPackage {
- name: String,
-}
-
-#[derive(Debug, Deserialize)]
-struct PublicationPolicyFile {
- publication: PublicationPolicy,
- workspace_classification: WorkspacePublicationClassification,
- #[serde(default)]
- publish_order: PublishOrder,
-}
-
-#[derive(Debug, Default, Deserialize)]
-struct PublishOrder {
- crates: Vec<String>,
-}
-
-#[derive(Debug, Deserialize)]
-struct PublicationPolicy {
- frozen: bool,
- registry: String,
- final_enablement_step: u16,
- spec_id: String,
- approved_packages: Vec<String>,
- local_packages: Vec<String>,
- external_packages: Vec<String>,
-}
-
-#[derive(Debug, Deserialize)]
-struct WorkspacePublicationClassification {
- private: Vec<String>,
- build_codegen: Vec<String>,
- test_support: Vec<String>,
- preview: Vec<String>,
- retired: Vec<String>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CratesReleaseArchitecture {
- spec_id: String,
- package_count: usize,
- repositories: CratesReleaseRepositories,
- package: Vec<CratesReleasePackage>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CratesReleaseRepositories {
- lib: CratesReleaseRepository,
- sdk: CratesReleaseRepository,
-}
-
-#[derive(Debug, Deserialize)]
-struct CratesReleaseRepository {
- packages: Vec<String>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CratesReleasePackage {
- name: String,
-}
-
-pub fn check() -> Result<(), String> {
- validate_package_matrix()?;
- let root = workspace_root()?;
- crate::architecture::validate(&root)?;
- check_publication_policy(&root)?;
- check_public_rust_package_metadata(&root)?;
- check_radroots_facade_conformance(&root)?;
- validate_sdk_contracts(&root)?;
- check_sdk_feature_matrix(&root)?;
- check_forbidden_packages(&root)?;
- check_binding_crate_sources(&root)?;
- check_package_source_metadata(&root)?;
- check_generated_outputs(&root)?;
- check_package_build_artifacts(&root)?;
- check_npm_pack_payloads(&root)?;
- crate::bindings::check(&root)?;
- Ok(())
-}
-
-pub fn architecture_ci(root: &Path) -> Result<(), String> {
- validate_package_matrix()?;
- check_publication_policy(root)?;
- check_public_rust_package_metadata(root)?;
- check_radroots_facade_conformance(root)?;
- validate_sdk_contracts(root)?;
- check_sdk_feature_matrix(root)?;
- check_forbidden_packages(root)?;
- check_binding_crate_sources(root)?;
- check_package_source_metadata(root)?;
- check_generated_outputs(root)?;
- crate::bindings::check(root)
-}
-
-fn check_radroots_facade_conformance(root: &Path) -> Result<(), String> {
- let manifest_path = root.join("crates/radroots/Cargo.toml");
- let manifest_raw = fs::read_to_string(&manifest_path)
- .map_err(|error| format!("failed to read {}: {error}", manifest_path.display()))?;
- let manifest = manifest_raw
- .parse::<toml::Value>()
- .map_err(|error| format!("failed to parse {}: {error}", manifest_path.display()))?;
- let package = manifest
- .get("package")
- .and_then(toml::Value::as_table)
- .ok_or_else(|| "crates/radroots/Cargo.toml must define [package]".to_owned())?;
- for (field, expected) in [
- ("name", "radroots"),
- (
- "description",
- "Curated ordinary-user Rust facade for Radroots",
- ),
- ("readme", "README.md"),
- ] {
- if package.get(field).and_then(toml::Value::as_str) != Some(expected) {
- return Err(format!(
- "crates/radroots/Cargo.toml package.{field} must be {expected}"
- ));
- }
- }
- let publish = package
- .get("publish")
- .and_then(toml::Value::as_array)
- .ok_or_else(|| "radroots facade must set publish = [\"crates-io\"]".to_owned())?;
- if publish.len() != 1 || publish[0].as_str() != Some("crates-io") {
- return Err("radroots facade must set publish = [\"crates-io\"]".to_owned());
- }
- for field in [
- "version",
- "edition",
- "rust-version",
- "license",
- "repository",
- "homepage",
- ] {
- let inherited = package
- .get(field)
- .and_then(toml::Value::as_table)
- .and_then(|value| value.get("workspace"))
- .and_then(toml::Value::as_bool);
- if inherited != Some(true) {
- return Err(format!(
- "crates/radroots/Cargo.toml package.{field} must inherit workspace metadata"
- ));
- }
- }
- let library = manifest
- .get("lib")
- .and_then(toml::Value::as_table)
- .ok_or_else(|| "crates/radroots/Cargo.toml must define [lib]".to_owned())?;
- if library.get("name").and_then(toml::Value::as_str) != Some("radroots")
- || library.get("path").and_then(toml::Value::as_str) != Some("src/lib.rs")
- {
- return Err("radroots facade [lib] must use name radroots and path src/lib.rs".to_owned());
- }
- let dependencies = manifest
- .get("dependencies")
- .and_then(toml::Value::as_table)
- .ok_or_else(|| "radroots facade must define [dependencies]".to_owned())?;
- let actual_dependencies = dependencies
- .keys()
- .map(String::as_str)
- .collect::<BTreeSet<_>>();
- let expected_dependencies = [
- "radroots_core",
- "radroots_event",
- "radroots_identity",
- "radroots_sdk",
- "radroots_trade",
- "radroots_transport",
- ]
- .into_iter()
- .collect::<BTreeSet<_>>();
- if actual_dependencies != expected_dependencies {
- return Err(format!(
- "radroots facade dependencies must be exactly {}; found {}",
- expected_dependencies
- .into_iter()
- .collect::<Vec<_>>()
- .join(", "),
- actual_dependencies
- .into_iter()
- .collect::<Vec<_>>()
- .join(", ")
- ));
- }
- for (name, dependency) in dependencies {
- if dependency
- .as_table()
- .and_then(|value| value.get("workspace"))
- .and_then(toml::Value::as_bool)
- != Some(true)
- {
- return Err(format!(
- "radroots facade dependency {name} must inherit the governed workspace entry"
- ));
- }
- }
- for dependency_table in ["dev-dependencies", "build-dependencies"] {
- if manifest
- .get(dependency_table)
- .and_then(toml::Value::as_table)
- .is_some_and(|dependencies| !dependencies.is_empty())
- {
- return Err(format!(
- "radroots facade must not define [{dependency_table}]"
- ));
- }
- }
-
- let features = manifest
- .get("features")
- .and_then(toml::Value::as_table)
- .ok_or_else(|| "crates/radroots/Cargo.toml must define [features]".to_owned())?;
- let actual_features = features.keys().map(String::as_str).collect::<BTreeSet<_>>();
- let expected_features = RADROOTS_FACADE_FEATURES
- .into_iter()
- .collect::<BTreeSet<_>>();
- if actual_features != expected_features {
- return Err(format!(
- "radroots facade features must be exactly {}; found {}",
- expected_features.into_iter().collect::<Vec<_>>().join(", "),
- actual_features.into_iter().collect::<Vec<_>>().join(", ")
- ));
- }
- let defaults = features
- .get("default")
- .and_then(toml::Value::as_array)
- .ok_or_else(|| "radroots facade default feature must be an array".to_owned())?;
- if defaults.len() != 1 || defaults[0].as_str() != Some("client") {
- return Err("radroots facade default feature must be exactly client".to_owned());
- }
- for (name, expected) in [
- ("client", &["radroots_sdk/default"][..]),
- ("native", &["client", "radroots_sdk/native"]),
- ("nostr", &["client", "radroots_sdk/nostr"]),
- ("nip46", &["nostr", "radroots_sdk/nip46"]),
- ("radrootsd", &["client", "radroots_sdk/radrootsd"]),
- ("geonames", &["client", "radroots_sdk/geonames"]),
- ("knowledge", &["client", "radroots_sdk/knowledge"]),
- ("full", &["radroots_sdk/full"]),
- ] {
- let actual = features
- .get(name)
- .and_then(toml::Value::as_array)
- .ok_or_else(|| format!("radroots facade feature {name} must be an array"))?
- .iter()
- .filter_map(toml::Value::as_str)
- .collect::<Vec<_>>();
- if actual != expected {
- return Err(format!(
- "radroots facade feature {name} must forward exactly {}",
- expected.join(", ")
- ));
- }
- }
-
- if package.get("autoexamples").and_then(toml::Value::as_bool) != Some(false) {
- return Err("radroots facade must disable implicit example discovery".to_owned());
- }
- let examples = manifest
- .get("example")
- .and_then(toml::Value::as_array)
- .ok_or_else(|| "radroots facade must declare its ordinary example".to_owned())?;
- if examples.len() != 1
- || examples[0].get("name").and_then(toml::Value::as_str) != Some("ordinary_memory")
- || examples[0].get("path").and_then(toml::Value::as_str)
- != Some("examples/ordinary_memory.rs")
- {
- return Err("radroots facade must declare only the ordinary_memory example".to_owned());
- }
-
- let source_path = root.join("crates/radroots/src/lib.rs");
- let source = fs::read_to_string(&source_path)
- .map_err(|error| format!("failed to read {}: {error}", source_path.display()))?;
- let actual_modules = source
- .lines()
- .filter_map(|line| {
- line.trim()
- .strip_prefix("pub mod ")
- .and_then(|module| module.strip_suffix(';'))
- })
- .collect::<BTreeSet<_>>();
- let expected_modules = RADROOTS_FACADE_MODULES.into_iter().collect::<BTreeSet<_>>();
- if actual_modules != expected_modules {
- return Err(format!(
- "radroots facade modules must be exactly {}; found {}",
- expected_modules.into_iter().collect::<Vec<_>>().join(", "),
- actual_modules.into_iter().collect::<Vec<_>>().join(", ")
- ));
- }
- if source.contains("pub mod sdk") || source.contains("pub use radroots_sdk::*") {
- return Err(
- "radroots facade must not expose an sdk namespace or broad SDK re-export".to_owned(),
- );
- }
- if !source.contains("pub use radroots_sdk::{Client, ClientBuilder, Error, Result};") {
- return Err(
- "radroots facade must export exactly the four approved SDK root entry points"
- .to_owned(),
- );
- }
- for module in RADROOTS_FACADE_MODULES {
- let module_path = root
- .join("crates/radroots/src")
- .join(format!("{module}.rs"));
- if !module_path.is_file() {
- return Err(format!(
- "radroots facade module file {module}.rs is missing"
- ));
- }
- let module_source = fs::read_to_string(&module_path)
- .map_err(|error| format!("failed to read {}: {error}", module_path.display()))?;
- if module_source.contains("pub trait ") || module_source.contains("::*") {
- return Err(format!(
- "radroots facade module {module} must not define traits or wildcard reexports"
- ));
- }
- }
- let readme_path = root.join("crates/radroots/README.md");
- let readme = fs::read_to_string(&readme_path)
- .map_err(|error| format!("failed to read {}: {error}", readme_path.display()))?;
- if !readme.contains("```compile_fail\nuse radroots::sdk;\n```") {
- return Err("radroots facade README must compile-fail the SDK namespace".to_owned());
- }
- Ok(())
-}
-
-fn check_publication_policy(root: &Path) -> Result<(), String> {
- let policy_path = root.join("contracts/releases/publication.toml");
- let raw = fs::read_to_string(&policy_path)
- .map_err(|error| format!("failed to read {}: {error}", policy_path.display()))?;
- let policy_file = toml::from_str::<PublicationPolicyFile>(&raw)
- .map_err(|error| format!("failed to parse {}: {error}", policy_path.display()))?;
- let publish_order = policy_file.publish_order.crates.clone();
- let policy = policy_file.publication;
- if policy.registry != "crates-io" {
- return Err("publication.registry must be crates-io".to_owned());
- }
- if policy.final_enablement_step != 305 {
- return Err("publication.final_enablement_step must be 305".to_owned());
- }
- let architecture_path = root.join("docs/specs/radroots_crates_release_v1.toml");
- let architecture_raw = fs::read_to_string(&architecture_path)
- .map_err(|error| format!("failed to read {}: {error}", architecture_path.display()))?;
- let architecture = toml::from_str::<CratesReleaseArchitecture>(&architecture_raw)
- .map_err(|error| format!("failed to parse {}: {error}", architecture_path.display()))?;
- let expected_approved = policy_set(
- architecture
- .package
- .iter()
- .map(|package| package.name.clone()),
- "architecture.package.name",
- )?;
- if architecture.package_count != 19 || expected_approved.len() != architecture.package_count {
- return Err(format!(
- "release architecture must define exactly 19 unique packages, found package_count {} and {} unique package records",
- architecture.package_count,
- expected_approved.len()
- ));
- }
- if policy.spec_id != architecture.spec_id || policy.spec_id != "radroots.crates.release.v1" {
- return Err(format!(
- "publication.spec_id {} must match architecture id {}",
- policy.spec_id, architecture.spec_id
- ));
- }
- let approved = policy_set(policy.approved_packages, "publication.approved_packages")?;
- let local = policy_set(policy.local_packages, "publication.local_packages")?;
- let external = policy_set(policy.external_packages, "publication.external_packages")?;
- let expected_local = policy_set(
- architecture.repositories.sdk.packages,
- "architecture.repositories.sdk.packages",
- )?;
- let expected_external = policy_set(
- architecture.repositories.lib.packages,
- "architecture.repositories.lib.packages",
- )?;
- for (field, actual, expected) in [
- (
- "publication.approved_packages",
- &approved,
- &expected_approved,
- ),
- ("publication.local_packages", &local, &expected_local),
- (
- "publication.external_packages",
- &external,
- &expected_external,
- ),
- ] {
- if actual != expected {
- let missing = expected
- .difference(actual)
- .cloned()
- .collect::<Vec<_>>()
- .join(", ");
- let extra = actual
- .difference(expected)
- .cloned()
- .collect::<Vec<_>>()
- .join(", ");
- return Err(format!(
- "{field} is missing approved packages: {missing}; {field} has unapproved packages: {extra}"
- ));
- }
- }
- if !local.is_disjoint(&external) {
- return Err("local and external approved package ownership must not overlap".to_owned());
- }
- let mut owned = local.clone();
- owned.extend(external.iter().cloned());
- if owned != approved {
- return Err(
- "local and external package ownership must partition approved packages".to_owned(),
- );
- }
- if policy.frozen {
- if !publish_order.is_empty() {
- return Err(
- "publish_order.crates must remain empty while publication is frozen".to_owned(),
- );
- }
- } else {
- let publish_order_set = policy_set(publish_order.iter().cloned(), "publish_order.crates")?;
- if publish_order_set != local {
- return Err(
- "publish_order.crates must contain exactly the approved local packages when publication is enabled"
- .to_owned(),
- );
- }
- if publish_order != ["radroots_sdk", "radroots"] {
- return Err("publish_order.crates must place radroots_sdk before radroots".to_owned());
- }
- }
-
- let mut workspace_packages = BTreeSet::new();
- for path in sdk_manifest_paths(root)?.into_iter().skip(1) {
- let raw = fs::read_to_string(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- let manifest = raw
- .parse::<toml::Value>()
- .map_err(|error| format!("failed to parse {}: {error}", path.display()))?;
- let package = manifest
- .get("package")
- .and_then(toml::Value::as_table)
- .ok_or_else(|| format!("{} must define [package]", path.display()))?;
- let name = package
- .get("name")
- .and_then(toml::Value::as_str)
- .ok_or_else(|| format!("{} package.name must be a string", path.display()))?;
- if !workspace_packages.insert(name.to_owned()) {
- return Err(format!("duplicate workspace package name {name}"));
- }
- let publish = package.get("publish");
- if policy.frozen {
- if publish.and_then(toml::Value::as_bool) != Some(false) {
- return Err(format!(
- "publication freeze requires workspace package {name} to set publish = false"
- ));
- }
- } else if approved.contains(name) {
- let registries = publish.and_then(toml::Value::as_array).ok_or_else(|| {
- format!("approved package {name} must set publish = [\"crates-io\"]")
- })?;
- if registries.len() != 1 || registries[0].as_str() != Some("crates-io") {
- return Err(format!(
- "approved package {name} must set publish = [\"crates-io\"]"
- ));
- }
- } else if publish.and_then(toml::Value::as_bool) != Some(false) {
- return Err(format!(
- "non-approved workspace package {name} must set publish = false"
- ));
- }
- }
- let external_in_workspace = external
- .intersection(&workspace_packages)
- .cloned()
- .collect::<Vec<_>>();
- if !external_in_workspace.is_empty() {
- return Err(format!(
- "externally owned approved packages must not be workspace members: {}",
- external_in_workspace.join(", ")
- ));
- }
- let classification = policy_file.workspace_classification;
- let private = policy_set(classification.private, "workspace_classification.private")?;
- let build_codegen = policy_set(
- classification.build_codegen,
- "workspace_classification.build_codegen",
- )?;
- let test_support = policy_set(
- classification.test_support,
- "workspace_classification.test_support",
- )?;
- let preview = policy_set(classification.preview, "workspace_classification.preview")?;
- let retired = policy_set(classification.retired, "workspace_classification.retired")?;
- let classes = [
- ("private", &private),
- ("build-codegen", &build_codegen),
- ("test-support", &test_support),
- ("preview", &preview),
- ("retired", &retired),
- ];
- for index in 0..classes.len() {
- for other_index in (index + 1)..classes.len() {
- let overlap = classes[index]
- .1
- .intersection(classes[other_index].1)
- .cloned()
- .collect::<Vec<_>>();
- if !overlap.is_empty() {
- return Err(format!(
- "workspace classification overlap is not allowed between {} and {}: {}",
- classes[index].0,
- classes[other_index].0,
- overlap.join(", ")
- ));
- }
- }
- }
- let mut classified = BTreeSet::new();
- for (_, entries) in classes {
- classified.extend(entries.iter().cloned());
- }
- let local_workspace = local
- .intersection(&workspace_packages)
- .cloned()
- .collect::<BTreeSet<_>>();
- let public_classification_overlap = classified
- .intersection(&local_workspace)
- .cloned()
- .collect::<Vec<_>>();
- if !public_classification_overlap.is_empty() {
- return Err(format!(
- "approved local packages must not be classified as private workspace packages: {}",
- public_classification_overlap.join(", ")
- ));
- }
- let mut accounted = classified;
- accounted.extend(local_workspace.iter().cloned());
- if accounted != workspace_packages {
- let missing = workspace_packages
- .difference(&accounted)
- .cloned()
- .collect::<Vec<_>>()
- .join(", ");
- let extra = accounted
- .difference(&workspace_packages)
- .cloned()
- .collect::<Vec<_>>()
- .join(", ");
- return Err(format!(
- "workspace classification is missing packages: {missing}; workspace classification has unknown packages: {extra}"
- ));
- }
- if !policy.frozen && !local.is_subset(&workspace_packages) {
- let missing = local
- .difference(&workspace_packages)
- .cloned()
- .collect::<Vec<_>>()
- .join(", ");
- return Err(format!(
- "publication enablement is missing approved workspace packages: {missing}"
- ));
- }
- Ok(())
-}
-
-fn policy_set(
- values: impl IntoIterator<Item = String>,
- field: &str,
-) -> Result<BTreeSet<String>, String> {
- let mut result = BTreeSet::new();
- for value in values {
- if value.trim().is_empty() {
- return Err(format!("{field} must not contain empty package names"));
- }
- if !result.insert(value.clone()) {
- return Err(format!(
- "{field} must not contain duplicate package {value}"
- ));
- }
- }
- Ok(result)
-}
-
-fn package_field_configured(package: &toml::value::Table, field: &str) -> bool {
- match package.get(field) {
- Some(toml::Value::String(value)) => !value.trim().is_empty(),
- Some(toml::Value::Array(values)) => !values.is_empty(),
- Some(toml::Value::Table(value)) => value
- .get("workspace")
- .and_then(toml::Value::as_bool)
- .is_some_and(|configured| configured),
- _ => false,
- }
-}
-
-fn package_string_array<'a>(
- package: &'a toml::value::Table,
- package_name: &str,
- field: &str,
-) -> Result<Vec<&'a str>, String> {
- let values = package
- .get(field)
- .and_then(toml::Value::as_array)
- .ok_or_else(|| format!("public package {package_name} must define package.{field}"))?;
- if values.is_empty() {
- return Err(format!(
- "public package {package_name} package.{field} must not be empty"
- ));
- }
- let mut resolved = Vec::with_capacity(values.len());
- let mut unique = BTreeSet::new();
- for value in values {
- let value = value.as_str().ok_or_else(|| {
- format!("public package {package_name} package.{field} entries must be strings")
- })?;
- if value.trim().is_empty() || !unique.insert(value) {
- return Err(format!(
- "public package {package_name} package.{field} entries must be non-empty and unique"
- ));
- }
- resolved.push(value);
- }
- Ok(resolved)
-}
-
-fn check_public_rust_package_metadata(root: &Path) -> Result<(), String> {
- let policy_path = root.join("contracts/releases/publication.toml");
- let policy_raw = fs::read_to_string(&policy_path)
- .map_err(|error| format!("failed to read {}: {error}", policy_path.display()))?;
- let policy_file = toml::from_str::<PublicationPolicyFile>(&policy_raw)
- .map_err(|error| format!("failed to parse {}: {error}", policy_path.display()))?;
- let local = policy_set(
- policy_file.publication.local_packages,
- "publication.local_packages",
- )?;
-
- let mut manifests = BTreeMap::new();
- for manifest_path in sdk_manifest_paths(root)?.into_iter().skip(1) {
- let raw = fs::read_to_string(&manifest_path)
- .map_err(|error| format!("failed to read {}: {error}", manifest_path.display()))?;
- let manifest = raw
- .parse::<toml::Value>()
- .map_err(|error| format!("failed to parse {}: {error}", manifest_path.display()))?;
- let package_name = manifest
- .get("package")
- .and_then(toml::Value::as_table)
- .and_then(|package| package.get("name"))
- .and_then(toml::Value::as_str)
- .ok_or_else(|| format!("{} must define package.name", manifest_path.display()))?
- .to_owned();
- if manifests
- .insert(package_name.clone(), (manifest_path, manifest))
- .is_some()
- {
- return Err(format!("duplicate workspace package name {package_name}"));
- }
- }
-
- for package_name in local {
- let (manifest_path, manifest) = manifests
- .get(&package_name)
- .ok_or_else(|| format!("public package {package_name} has no workspace manifest"))?;
- let package = manifest
- .get("package")
- .and_then(toml::Value::as_table)
- .expect("workspace package manifest has a package table");
- for field in [
- "authors",
- "version",
- "edition",
- "rust-version",
- "license",
- "repository",
- "homepage",
- "documentation",
- "readme",
- "description",
- ] {
- if !package_field_configured(package, field) {
- return Err(format!(
- "public package {package_name} must configure package.{field}"
- ));
- }
- }
- let expected_documentation = format!("https://docs.rs/{package_name}");
- if package.get("documentation").and_then(toml::Value::as_str)
- != Some(expected_documentation.as_str())
- {
- return Err(format!(
- "public package {package_name} package.documentation must be {expected_documentation}"
- ));
- }
- if package.get("license-file").is_some() {
- return Err(format!(
- "public package {package_name} must use the workspace SPDX license expression"
- ));
- }
-
- let keywords = package_string_array(package, &package_name, "keywords")?;
- let categories = package_string_array(package, &package_name, "categories")?;
- if keywords.len() > 5 || categories.len() > 5 {
- return Err(format!(
- "public package {package_name} keywords and categories must respect crates.io limits"
- ));
- }
- let include = package_string_array(package, &package_name, "include")?;
- for required in [
- "src/**",
- "tests/**",
- "README.md",
- "LICENSE-APACHE",
- "LICENSE-MIT",
- ] {
- if !include.contains(&required) {
- return Err(format!(
- "public package {package_name} package.include must contain {required}"
- ));
- }
- }
-
- let package_root = manifest_path
- .parent()
- .expect("workspace member manifest has a parent");
- for relative in ["LICENSE-APACHE", "LICENSE-MIT"] {
- let package_bytes = fs::read(package_root.join(relative)).map_err(|error| {
- format!("public package {package_name} must include {relative}: {error}")
- })?;
- let root_bytes = fs::read(root.join(relative))
- .map_err(|error| format!("failed to read {relative}: {error}"))?;
- if package_bytes != root_bytes {
- return Err(format!(
- "public package {package_name} {relative} must match the workspace license"
- ));
- }
- }
- if !package_root.join("README.md").is_file() {
- return Err(format!(
- "public package {package_name} must include a package-local README.md"
- ));
- }
-
- let docs_rs = package
- .get("metadata")
- .and_then(toml::Value::as_table)
- .and_then(|metadata| metadata.get("docs"))
- .and_then(toml::Value::as_table)
- .and_then(|docs| docs.get("rs"))
- .and_then(toml::Value::as_table)
- .ok_or_else(|| {
- format!("public package {package_name} must define [package.metadata.docs.rs]")
- })?;
- if docs_rs
- .get("all-features")
- .and_then(toml::Value::as_bool)
- .unwrap_or(false)
- {
- return Err(format!(
- "public package {package_name} docs.rs must use an intentional feature set"
- ));
- }
- let docs_features = docs_rs
- .get("features")
- .and_then(toml::Value::as_array)
- .ok_or_else(|| format!("public package {package_name} docs.rs must define features"))?;
- let declared_features = manifest.get("features").and_then(toml::Value::as_table);
- for feature in docs_features {
- let feature = feature.as_str().ok_or_else(|| {
- format!("public package {package_name} docs.rs features must be strings")
- })?;
- if !declared_features.is_some_and(|features| features.contains_key(feature)) {
- return Err(format!(
- "public package {package_name} docs.rs selects unknown feature {feature}"
- ));
- }
- }
- }
- Ok(())
-}
-
-fn check_sdk_feature_matrix(root: &Path) -> Result<(), String> {
- let path = root.join("crates/sdk/Cargo.toml");
- let raw = fs::read_to_string(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- let manifest = raw
- .parse::<toml::Value>()
- .map_err(|error| format!("failed to parse {}: {error}", path.display()))?;
- let features = manifest
- .get("features")
- .and_then(toml::Value::as_table)
- .ok_or_else(|| format!("{} must define [features]", path.display()))?;
- let expected = BTreeSet::from([
- "default",
- "full",
- "geonames",
- "knowledge",
- "local-signing",
- "memory",
- "native",
- "nip46",
- "nostr",
- "radrootsd",
- "sqlite",
- "sync",
- ]);
- let actual = features.keys().map(String::as_str).collect::<BTreeSet<_>>();
- if actual != expected {
- return Err(format!(
- "crates/sdk/Cargo.toml feature vocabulary mismatch: expected {expected:?}, found {actual:?}"
- ));
- }
- for (feature, expected_entries) in [
- ("default", &["memory"][..]),
- ("memory", &["radroots_storage/memory"]),
- ("sqlite", &["dep:radroots_storage_sqlite"]),
- ("sync", &["dep:radroots_sync", "dep:uuid"]),
- (
- "nostr",
- &["sync", "dep:radroots_nostr", "dep:radroots_transport_nostr"],
- ),
- ("nip46", &["nostr", "dep:radroots_nostr_connect"]),
- (
- "local-signing",
- &[
- "dep:radroots_nostr",
- "dep:radroots_secrets",
- "radroots_nostr/signing",
- ],
- ),
- (
- "radrootsd",
- &["sync", "dep:reqwest", "dep:serde", "dep:serde_json"],
- ),
- ("geonames", &["dep:radroots_geonames"]),
- (
- "knowledge",
- &["radroots_event/knowledge", "radroots_event_codec/knowledge"],
- ),
- ("native", &["sqlite", "sync", "local-signing"]),
- (
- "full",
- &[
- "native",
- "nostr",
- "nip46",
- "radrootsd",
- "geonames",
- "knowledge",
- ],
- ),
- ] {
- let actual_entries = feature_entries(features, feature)?
- .into_iter()
- .collect::<BTreeSet<_>>();
- let expected_entries = expected_entries.iter().copied().collect::<BTreeSet<_>>();
- if actual_entries != expected_entries {
- return Err(format!(
- "crates/sdk/Cargo.toml feature `{feature}` mismatch: expected {expected_entries:?}, found {actual_entries:?}"
- ));
- }
- }
- check_sdk_workspace_reticulum_dependency_boundaries(root)?;
- Ok(())
-}
-
-fn check_sdk_workspace_reticulum_dependency_boundaries(root: &Path) -> Result<(), String> {
- for path in sdk_manifest_paths(root)? {
- let raw = fs::read_to_string(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- let manifest = raw
- .parse::<toml::Value>()
- .map_err(|error| format!("failed to parse {}: {error}", path.display()))?;
- let manifest_label = relative_path_string(root, &path)?;
- check_manifest_reticulum_dependency_boundaries(&manifest, &manifest_label)?;
- }
- check_cargo_lock_reticulum_package_names(root)?;
- check_cargo_metadata_reticulum_package_names(root)?;
- Ok(())
-}
-
-fn sdk_manifest_paths(root: &Path) -> Result<Vec<PathBuf>, String> {
- let root_manifest_path = root.join("Cargo.toml");
- let raw = fs::read_to_string(&root_manifest_path)
- .map_err(|error| format!("failed to read {}: {error}", root_manifest_path.display()))?;
- let manifest = raw
- .parse::<toml::Value>()
- .map_err(|error| format!("failed to parse {}: {error}", root_manifest_path.display()))?;
- let members = manifest
- .get("workspace")
- .and_then(|workspace| workspace.get("members"))
- .and_then(toml::Value::as_array)
- .ok_or_else(|| "Cargo.toml must define workspace.members".to_owned())?;
- let mut paths = Vec::with_capacity(members.len() + 1);
- paths.push(root_manifest_path);
- for member in members {
- let member = member
- .as_str()
- .ok_or_else(|| "Cargo.toml workspace.members entries must be strings".to_owned())?;
- if member.contains('*') {
- return Err(format!(
- "Cargo.toml workspace member globs are not supported by the SDK Reticulum gate: {member}"
- ));
- }
- paths.push(root.join(member).join("Cargo.toml"));
- }
- Ok(paths)
-}
-
-fn check_manifest_reticulum_dependency_boundaries(
- manifest: &toml::Value,
- manifest_label: &str,
-) -> Result<(), String> {
- let table = manifest
- .as_table()
- .ok_or_else(|| format!("{manifest_label} must parse as a TOML table"))?;
- inspect_manifest_table_for_reticulum_dependencies(table, manifest_label, &mut Vec::new())
-}
-
-fn inspect_manifest_table_for_reticulum_dependencies(
- table: &toml::map::Map<String, toml::Value>,
- manifest_label: &str,
- path: &mut Vec<String>,
-) -> Result<(), String> {
- for (key, value) in table {
- path.push(key.clone());
- if is_dependency_section_key(key) {
- let dependencies = value.as_table().ok_or_else(|| {
- format!(
- "{manifest_label} section `{}` must be a table",
- path.join(".")
- )
- })?;
- check_reticulum_dependency_table(dependencies, manifest_label, path)?;
- path.pop();
- continue;
- }
- if key == "features" && path.len() == 1 {
- let features = value.as_table().ok_or_else(|| {
- format!(
- "{manifest_label} section `{}` must be a table",
- path.join(".")
- )
- })?;
- check_reticulum_feature_table(features, manifest_label, path)?;
- }
- if let Some(nested) = value.as_table() {
- inspect_manifest_table_for_reticulum_dependencies(nested, manifest_label, path)?;
- }
- path.pop();
- }
- Ok(())
-}
-
-fn is_dependency_section_key(key: &str) -> bool {
- matches!(
- key,
- "dependencies" | "dev-dependencies" | "build-dependencies"
- )
-}
-
-fn check_reticulum_dependency_table(
- dependencies: &toml::map::Map<String, toml::Value>,
- manifest_label: &str,
- path: &[String],
-) -> Result<(), String> {
- let section = path.join(".");
- for (dependency_key, dependency_spec) in dependencies {
- reject_forbidden_reticulum_runtime_name(dependency_key, manifest_label, §ion)?;
- if let Some(package_name) = dependency_spec
- .as_table()
- .and_then(|table| table.get("package"))
- .and_then(toml::Value::as_str)
- {
- reject_forbidden_reticulum_runtime_name(package_name, manifest_label, §ion)?;
- }
- }
- Ok(())
-}
-
-fn check_reticulum_feature_table(
- features: &toml::map::Map<String, toml::Value>,
- manifest_label: &str,
- path: &[String],
-) -> Result<(), String> {
- let section = path.join(".");
- for (feature_name, entries) in features {
- if feature_name == "transport-reticulum-preview" || feature_name == "reticulum-runtime" {
- return Err(format!(
- "{manifest_label} feature `{feature_name}` must not introduce a Reticulum preview alias"
- ));
- }
- reject_forbidden_reticulum_runtime_name(feature_name, manifest_label, §ion)?;
- let entries = entries
- .as_array()
- .ok_or_else(|| format!("{manifest_label} feature `{feature_name}` must be an array"))?;
- for entry in entries {
- let entry = entry.as_str().ok_or_else(|| {
- format!("{manifest_label} feature `{feature_name}` entries must be strings")
- })?;
- check_reticulum_feature_entry(entry, manifest_label, feature_name)?;
- }
- }
- Ok(())
-}
-
-fn check_reticulum_feature_entry(
- entry: &str,
- manifest_label: &str,
- feature_name: &str,
-) -> Result<(), String> {
- let feature_dependency = entry.strip_prefix("dep:").unwrap_or(entry);
- let (dependency_name, dependency_feature) = feature_dependency
- .split_once('/')
- .map_or((feature_dependency, None), |(name, feature)| {
- (name, Some(feature))
- });
- if dependency_name == ALLOWED_RETICULUM_PREVIEW_PACKAGE && dependency_feature.is_some() {
- return Err(format!(
- "{manifest_label} feature `{feature_name}` must not enable Reticulum preview crate features through `{entry}`"
- ));
- }
- reject_forbidden_reticulum_runtime_name(dependency_name, manifest_label, "features")?;
- Ok(())
-}
-
-fn check_cargo_lock_reticulum_package_names(root: &Path) -> Result<(), String> {
- let path = root.join("Cargo.lock");
- let raw = fs::read_to_string(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- let lock = raw
- .parse::<toml::Value>()
- .map_err(|error| format!("failed to parse {}: {error}", path.display()))?;
- let packages = lock
- .get("package")
- .and_then(toml::Value::as_array)
- .ok_or_else(|| "Cargo.lock must define package entries".to_owned())?;
- for package in packages {
- let name = package
- .get("name")
- .and_then(toml::Value::as_str)
- .ok_or_else(|| "Cargo.lock package entry must define name".to_owned())?;
- reject_forbidden_reticulum_runtime_name(name, "Cargo.lock", "package")?;
- }
- Ok(())
-}
-
-fn check_cargo_metadata_reticulum_package_names(root: &Path) -> Result<(), String> {
- let output = Command::new("cargo")
- .args(["metadata", "--format-version", "1", "--all-features"])
- .current_dir(root)
- .output()
- .map_err(|error| format!("failed to run cargo metadata: {error}"))?;
- if !output.status.success() {
- return Err(format!(
- "cargo metadata failed: {}",
- String::from_utf8_lossy(&output.stderr).trim()
- ));
- }
- let metadata = serde_json::from_slice::<CargoMetadata>(&output.stdout)
- .map_err(|error| format!("failed to parse cargo metadata: {error}"))?;
- for package in metadata.packages {
- reject_forbidden_reticulum_runtime_name(&package.name, "cargo metadata", "packages")?;
- }
- Ok(())
-}
-
-fn reject_forbidden_reticulum_runtime_name(
- name: &str,
- manifest_label: &str,
- section: &str,
-) -> Result<(), String> {
- if is_forbidden_reticulum_runtime_name(name) {
- Err(format!(
- "{manifest_label} section `{section}` must not reference real Reticulum or Python runtime dependency `{name}`"
- ))
- } else {
- Ok(())
- }
-}
-
-fn is_forbidden_reticulum_runtime_name(name: &str) -> bool {
- if name == ALLOWED_RETICULUM_PREVIEW_PACKAGE {
- return false;
- }
- let normalized = normalize_package_name(name);
- matches!(
- normalized.as_str(),
- "rns" | "rnsd" | "reticulum" | "reticulum-rs" | "python" | "pyo3"
- ) || normalized.contains("reticulum")
- || normalized.starts_with("rns-")
- || normalized.contains("-rns-")
- || normalized.ends_with("-rns")
- || normalized.starts_with("python-")
- || normalized.starts_with("pyo3-")
-}
-
-fn normalize_package_name(name: &str) -> String {
- name.to_ascii_lowercase().replace('_', "-")
-}
-
-fn feature_entries<'features>(
- features: &'features toml::map::Map<String, toml::Value>,
- feature: &str,
-) -> Result<Vec<&'features str>, String> {
- features
- .get(feature)
- .and_then(toml::Value::as_array)
- .ok_or_else(|| format!("crates/sdk/Cargo.toml must define feature `{feature}`"))?
- .iter()
- .map(|entry| {
- entry
- .as_str()
- .ok_or_else(|| format!("feature `{feature}` must contain only string entries"))
- })
- .collect()
-}
-
-fn check_package_source_metadata(root: &Path) -> Result<(), String> {
- for spec in package_specs() {
- let package_dir = root.join(spec.package_dir);
- let package_json_path = package_dir.join("package.json");
- let index_path = package_dir.join("src/index.ts");
- let package_json =
- check_package_json(&package_json_path, spec.package_name, spec.package_dir)?;
- check_package_distribution_metadata(root, &package_dir, &package_json_path, &package_json)?;
- if !index_path.is_file() {
- return Err(format!("missing package index: {}", index_path.display()));
- }
- check_package_index(&index_path)?;
- }
- for spec in wasm_package_specs() {
- let package_dir = root.join(spec.package_dir);
- let package_json_path = package_dir.join("package.json");
- let package_json =
- check_package_json(&package_json_path, spec.package_name, spec.package_dir)?;
- check_package_distribution_metadata(root, &package_dir, &package_json_path, &package_json)?;
- }
- Ok(())
-}
-
-fn check_generated_outputs(root: &Path) -> Result<(), String> {
- let outputs = package_outputs()?;
- for output in &outputs {
- check_generated_package_artifact_inventory(root, output)?;
- }
- for output in outputs {
- for expected in output.files() {
- let path = root
- .join(output.spec.package_dir)
- .join(expected.relative_path);
- let actual = fs::read_to_string(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- if actual != expected.contents {
- return Err(format!("stale generated output: {}", path.display()));
- }
- }
- let expected = output.provenance_file();
- let path = root.join(&expected.relative_path);
- let actual = fs::read_to_string(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- if actual != expected.contents {
- return Err(format!("stale generated provenance: {}", path.display()));
- }
- }
- Ok(())
-}
-
-fn check_package_build_artifacts(root: &Path) -> Result<(), String> {
- for spec in package_specs() {
- let package_dir = root.join(spec.package_dir);
- let package_json_path = package_dir.join("package.json");
- let package_json = read_package_json_value(&package_json_path)?;
- let surface_paths = package_surface_paths(&package_json, &package_json_path)?;
- check_package_surface_artifacts(&package_dir, spec.package_name, &surface_paths)?;
- }
- for spec in wasm_package_specs() {
- check_wasm_package_surface(root, *spec)?;
- }
- Ok(())
-}
-
-fn check_generated_package_artifact_inventory(
- root: &Path,
- output: &PackageOutput,
-) -> Result<(), String> {
- let package_dir = root.join(output.spec.package_dir);
- let generated_dir = package_dir.join("src/generated");
- let expected = output
- .files()
- .into_iter()
- .map(|file| file.relative_path)
- .collect::<BTreeSet<_>>();
- let actual = generated_package_files(&package_dir, &generated_dir)?;
- if actual != expected {
- let missing = expected.difference(&actual).cloned().collect::<Vec<_>>();
- let extra = actual.difference(&expected).cloned().collect::<Vec<_>>();
- return Err(format!(
- "generated artifact inventory mismatch for {}: missing {:?}, extra {:?}",
- output.spec.package_name, missing, extra
- ));
- }
- Ok(())
-}
-
-fn generated_package_files(
- package_dir: &Path,
- generated_dir: &Path,
-) -> Result<BTreeSet<String>, String> {
- let mut files = BTreeSet::new();
- collect_package_files(package_dir, generated_dir, &mut files)?;
- Ok(files)
-}
-
-fn collect_package_files(
- package_dir: &Path,
- dir: &Path,
- files: &mut BTreeSet<String>,
-) -> Result<(), String> {
- for entry in
- fs::read_dir(dir).map_err(|error| format!("failed to read {}: {error}", dir.display()))?
- {
- let entry =
- entry.map_err(|error| format!("failed to read {} entry: {error}", dir.display()))?;
- let path = entry.path();
- let file_type = entry
- .file_type()
- .map_err(|error| format!("failed to inspect {}: {error}", path.display()))?;
- if file_type.is_dir() {
- collect_package_files(package_dir, &path, files)?;
- } else if file_type.is_file() {
- files.insert(relative_path_string(package_dir, &path)?);
- }
- }
- Ok(())
-}
-
-fn relative_path_string(base: &Path, path: &Path) -> Result<String, String> {
- let relative = path
- .strip_prefix(base)
- .map_err(|error| format!("failed to relativize {}: {error}", path.display()))?;
- Ok(relative
- .components()
- .map(|component| component.as_os_str().to_string_lossy())
- .collect::<Vec<_>>()
- .join("/"))
-}
-
-fn check_package_index(path: &Path) -> Result<(), String> {
- let raw = fs::read_to_string(path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- if raw.starts_with(generated_header()) {
- return Err(format!(
- "package index must be handwritten source: {}",
- path.display()
- ));
- }
- Ok(())
-}
-
-fn check_binding_crate_sources(root: &Path) -> Result<(), String> {
- for spec in package_specs() {
- let crate_src_dir = root.join(spec.crate_dir).join("src");
- let typescript_dir = crate_src_dir.join("typescript");
- if typescript_dir.exists() {
- return Err(format!(
- "forbidden crate TypeScript source directory exists: {}",
- typescript_dir.display()
- ));
- }
- check_no_typescript_files(&crate_src_dir)?;
- }
- for spec in wasm_package_specs() {
- check_no_typescript_files(&root.join(spec.crate_dir).join("src"))?;
- }
- Ok(())
-}
-
-fn check_no_typescript_files(dir: &Path) -> Result<(), String> {
- for entry in
- fs::read_dir(dir).map_err(|error| format!("failed to read {}: {error}", dir.display()))?
- {
- let entry =
- entry.map_err(|error| format!("failed to read {} entry: {error}", dir.display()))?;
- let path = entry.path();
- let file_type = entry
- .file_type()
- .map_err(|error| format!("failed to inspect {}: {error}", path.display()))?;
- if file_type.is_dir() {
- check_no_typescript_files(&path)?;
- } else if file_type.is_file()
- && path.extension().and_then(|extension| extension.to_str()) == Some("ts")
- {
- return Err(format!(
- "forbidden crate TypeScript source file exists: {}",
- path.display()
- ));
- }
- }
- Ok(())
-}
-
-fn check_forbidden_packages(root: &Path) -> Result<(), String> {
- for forbidden in FORBIDDEN_PACKAGE_NAMES {
- let package_leaf = forbidden.trim_start_matches("@radroots/").to_owned();
- let forbidden_dir = root.join("packages").join(package_leaf);
- if forbidden_dir.exists() {
- return Err(format!(
- "forbidden package directory exists: {}",
- forbidden_dir.display()
- ));
- }
- }
- Ok(())
-}
-
-fn check_package_json(
- path: &Path,
- expected_name: &str,
- expected_directory: &str,
-) -> Result<serde_json::Value, String> {
- let raw = fs::read_to_string(path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- let json = serde_json::from_str::<serde_json::Value>(&raw)
- .map_err(|error| format!("failed to parse {}: {error}", path.display()))?;
- let actual_name = json
- .get("name")
- .and_then(serde_json::Value::as_str)
- .ok_or_else(|| format!("package.json missing name: {}", path.display()))?;
- if actual_name != expected_name {
- return Err(format!(
- "package name mismatch in {}: expected {expected_name}, found {actual_name}",
- path.display()
- ));
- }
- if json.get("private").is_some() {
- return Err(format!(
- "public package must not set private: {}",
- path.display()
- ));
- }
- let _ = package_description(&json, path)?;
- require_string_field(&json, path, "version", PACKAGE_VERSION)?;
- require_string_field(&json, path, "license", PACKAGE_LICENSE)?;
- require_string_field(&json, path, "homepage", PACKAGE_HOMEPAGE)?;
- require_string_field(&json, path, "type", "module")?;
- require_bool_field(&json, path, "sideEffects", false)?;
- check_publish_config(&json, path)?;
- check_repository(&json, path, expected_directory)?;
- check_package_files(&json, path)?;
- check_no_pack_lifecycle_scripts(&json, path)?;
- check_workspace_dependencies(&json, path)?;
- Ok(json)
-}
-
-pub(crate) fn check_wasm_package_surface(root: &Path, spec: WasmPackageSpec) -> Result<(), String> {
- let package_dir = root.join(spec.package_dir);
- let package_json_path = package_dir.join("package.json");
- let json = check_package_json(&package_json_path, spec.package_name, spec.package_dir)?;
- check_package_distribution_metadata(root, &package_dir, &package_json_path, &json)?;
- let dist_manifest = package_dir.join("dist").join("package.json");
- if dist_manifest.exists() {
- return Err(format!(
- "generated package manifest is forbidden: {}",
- dist_manifest.display()
- ));
- }
- check_no_wasm_dist_ignore_files(&package_dir, spec)?;
- let surface_paths = package_surface_paths(&json, &package_json_path)?;
- check_public_wasm_declaration_inventory(&surface_paths, spec)?;
- check_package_surface_artifacts(&package_dir, spec.package_name, &surface_paths)?;
- check_wasm_runtime_files(&package_dir, spec)?;
- check_wasm_declaration_files(&package_dir, spec)?;
- validate_provenance_manifest(root, spec)?;
- Ok(())
-}
-
-fn require_string_field(
- json: &serde_json::Value,
- package_json_path: &Path,
- field: &'static str,
- expected: &str,
-) -> Result<(), String> {
- let actual = json
- .get(field)
- .and_then(serde_json::Value::as_str)
- .ok_or_else(|| {
- format!(
- "package.json missing {field}: {}",
- package_json_path.display()
- )
- })?;
- if actual != expected {
- return Err(format!(
- "package.json {field} mismatch in {}: expected {expected}, found {actual}",
- package_json_path.display()
- ));
- }
- Ok(())
-}
-
-fn require_bool_field(
- json: &serde_json::Value,
- package_json_path: &Path,
- field: &'static str,
- expected: bool,
-) -> Result<(), String> {
- let actual = json
- .get(field)
- .and_then(serde_json::Value::as_bool)
- .ok_or_else(|| {
- format!(
- "package.json missing {field}: {}",
- package_json_path.display()
- )
- })?;
- if actual != expected {
- return Err(format!(
- "package.json {field} mismatch in {}: expected {expected}, found {actual}",
- package_json_path.display()
- ));
- }
- Ok(())
-}
-
-fn check_publish_config(json: &serde_json::Value, package_json_path: &Path) -> Result<(), String> {
- let access = json
- .get("publishConfig")
- .and_then(|value| value.get("access"))
- .and_then(serde_json::Value::as_str)
- .ok_or_else(|| {
- format!(
- "package.json missing publishConfig.access: {}",
- package_json_path.display()
- )
- })?;
- if access != PUBLISH_ACCESS {
- return Err(format!(
- "package.json publishConfig.access mismatch in {}: expected {PUBLISH_ACCESS}, found {access}",
- package_json_path.display()
- ));
- }
- Ok(())
-}
-
-fn check_repository(
- json: &serde_json::Value,
- package_json_path: &Path,
- expected_directory: &str,
-) -> Result<(), String> {
- let repository = json.get("repository").ok_or_else(|| {
- format!(
- "package.json missing repository: {}",
- package_json_path.display()
- )
- })?;
- let repository_type = repository
- .get("type")
- .and_then(serde_json::Value::as_str)
- .ok_or_else(|| {
- format!(
- "package.json missing repository.type: {}",
- package_json_path.display()
- )
- })?;
- if repository_type != "git" {
- return Err(format!(
- "package.json repository.type mismatch in {}: expected git, found {repository_type}",
- package_json_path.display()
- ));
- }
- let repository_url = repository
- .get("url")
- .and_then(serde_json::Value::as_str)
- .ok_or_else(|| {
- format!(
- "package.json missing repository.url: {}",
- package_json_path.display()
- )
- })?;
- if repository_url != PACKAGE_REPOSITORY_URL {
- return Err(format!(
- "package.json repository.url mismatch in {}: expected {PACKAGE_REPOSITORY_URL}, found {repository_url}",
- package_json_path.display()
- ));
- }
- let repository_directory = repository
- .get("directory")
- .and_then(serde_json::Value::as_str)
- .ok_or_else(|| {
- format!(
- "package.json missing repository.directory: {}",
- package_json_path.display()
- )
- })?;
- if repository_directory != expected_directory {
- return Err(format!(
- "package.json repository.directory mismatch in {}: expected {expected_directory}, found {repository_directory}",
- package_json_path.display()
- ));
- }
- Ok(())
-}
-
-fn check_package_files(json: &serde_json::Value, package_json_path: &Path) -> Result<(), String> {
- let files = json
- .get("files")
- .and_then(serde_json::Value::as_array)
- .ok_or_else(|| {
- format!(
- "package.json missing files: {}",
- package_json_path.display()
- )
- })?;
- let actual = files
- .iter()
- .map(|value| {
- value.as_str().ok_or_else(|| {
- format!(
- "package.json files entries must be strings: {}",
- package_json_path.display()
- )
- })
- })
- .collect::<Result<Vec<_>, _>>()?;
- if actual != PACKAGE_FILES {
- return Err(format!(
- "package.json files must publish dist plus approved metadata only: {}",
- package_json_path.display()
- ));
- }
- Ok(())
-}
-
-fn check_workspace_dependencies(
- json: &serde_json::Value,
- package_json_path: &Path,
-) -> Result<(), String> {
- let Some(dependencies) = json.get("dependencies") else {
- return Ok(());
- };
- let dependencies = dependencies.as_object().ok_or_else(|| {
- format!(
- "package.json dependencies must be an object: {}",
- package_json_path.display()
- )
- })?;
- for (name, value) in dependencies {
- if name.starts_with("@radroots/") {
- let version = value.as_str().ok_or_else(|| {
- format!(
- "package.json dependency versions must be strings: {}",
- package_json_path.display()
- )
- })?;
- if version != "workspace:^" {
- return Err(format!(
- "package.json workspace dependency {name} must use workspace:^ in {}",
- package_json_path.display()
- ));
- }
- }
- }
- Ok(())
-}
-
-fn check_npm_pack_payloads(root: &Path) -> Result<(), String> {
- let pack_dir =
- tempfile::tempdir().map_err(|error| format!("failed to create pack temp dir: {error}"))?;
- let mut packed_packages = Vec::new();
- for spec in package_specs() {
- let package_dir = root.join(spec.package_dir);
- let package_json_path = package_dir.join("package.json");
- let json = read_package_json_value(&package_json_path)?;
- let expected_dist_files =
- expected_packed_dist_files(&package_dir, &json, &package_json_path, None)?;
- let required_files = required_npm_payload_files(&expected_dist_files);
- let packed = pnpm_pack_package(&package_dir, spec.package_name, pack_dir.path())?;
- let packed_json = read_packed_package_json(&packed, spec.package_name)?;
- check_packed_package_json(
- &json,
- &packed_json,
- &package_json_path,
- spec.package_name,
- spec.package_dir,
- )?;
- let payload_files = packed_payload_files(&packed)?;
- validate_npm_pack_payload(spec.package_name, &payload_files, &required_files, None)?;
- validate_packed_dist_inventory(spec.package_name, &payload_files, &expected_dist_files)?;
- packed_packages.push(packed);
- }
- for spec in wasm_package_specs() {
- let package_dir = root.join(spec.package_dir);
- let package_json_path = package_dir.join("package.json");
- let json = read_package_json_value(&package_json_path)?;
- let expected_dist_files =
- expected_packed_dist_files(&package_dir, &json, &package_json_path, Some(*spec))?;
- let required_files = required_npm_payload_files(&expected_dist_files);
- let packed = pnpm_pack_package(&package_dir, spec.package_name, pack_dir.path())?;
- let packed_json = read_packed_package_json(&packed, spec.package_name)?;
- check_packed_package_json(
- &json,
- &packed_json,
- &package_json_path,
- spec.package_name,
- spec.package_dir,
- )?;
- let payload_files = packed_payload_files(&packed)?;
- validate_npm_pack_payload(
- spec.package_name,
- &payload_files,
- &required_files,
- Some(&format!("dist/{}_bg.wasm", spec.out_name)),
- )?;
- validate_packed_dist_inventory(spec.package_name, &payload_files, &expected_dist_files)?;
- packed_packages.push(packed);
- }
- check_clean_consumer_smoke(&packed_packages)?;
- Ok(())
-}
-
-fn check_no_pack_lifecycle_scripts(
- json: &serde_json::Value,
- package_json_path: &Path,
-) -> Result<(), String> {
- let Some(scripts) = json.get("scripts") else {
- return Ok(());
- };
- let scripts = scripts.as_object().ok_or_else(|| {
- format!(
- "package.json scripts must be an object: {}",
- package_json_path.display()
- )
- })?;
- for forbidden in [
- "prepack",
- "postpack",
- "prepare",
- "prepublish",
- "prepublishOnly",
- ] {
- if scripts.contains_key(forbidden) {
- return Err(format!(
- "package.json script {forbidden} is forbidden because pnpm pack runs lifecycle scripts: {}",
- package_json_path.display()
- ));
- }
- }
- Ok(())
-}
-
-fn read_package_json_value(path: &Path) -> Result<serde_json::Value, String> {
- let raw = fs::read_to_string(path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- serde_json::from_str::<serde_json::Value>(&raw)
- .map_err(|error| format!("failed to parse {}: {error}", path.display()))
-}
-
-fn required_npm_payload_files(expected_dist_files: &BTreeSet<String>) -> BTreeSet<String> {
- let mut required = package_metadata_payload_files();
- required.extend(expected_dist_files.iter().cloned());
- required
-}
-
-fn package_metadata_payload_files() -> BTreeSet<String> {
- BTreeSet::from([
- "package.json".to_owned(),
- "README.md".to_owned(),
- "LICENSE-MIT".to_owned(),
- "LICENSE-APACHE".to_owned(),
- ])
-}
-
-fn expected_packed_dist_files(
- package_dir: &Path,
- json: &serde_json::Value,
- package_json_path: &Path,
- wasm_spec: Option<WasmPackageSpec>,
-) -> Result<BTreeSet<String>, String> {
- let mut expected = BTreeSet::new();
- for path in package_surface_paths(json, package_json_path)? {
- expected.insert(normalized_package_path(&path)?);
- }
- if let Some(spec) = wasm_spec {
- for path in wasm_runtime_files(spec) {
- expected.insert(path);
- }
- expected.insert(format!("dist/{}.provenance.json", spec.out_name));
- } else {
- expected.extend(expected_binding_generated_dist_files(package_dir)?);
- }
- Ok(expected)
-}
-
-fn expected_binding_generated_dist_files(package_dir: &Path) -> Result<BTreeSet<String>, String> {
- let generated_dir = package_dir.join("src/generated");
- let mut expected = BTreeSet::new();
- for source in generated_package_files(package_dir, &generated_dir)? {
- let stem = source
- .strip_prefix("src/generated/")
- .and_then(|path| path.strip_suffix(".ts"))
- .ok_or_else(|| {
- format!(
- "generated package source must be a TypeScript file under src/generated: {}",
- package_dir.join(&source).display()
- )
- })?;
- expected.insert(format!("dist/generated/{stem}.js"));
- expected.insert(format!("dist/generated/{stem}.d.ts"));
- }
- Ok(expected)
-}
-
-fn pnpm_pack_package(
- package_dir: &Path,
- package_name: &str,
- pack_destination: &Path,
-) -> Result<PackedPackage, String> {
- let output = Command::new("pnpm")
- .args(["pack", "--json", "--pack-destination"])
- .arg(pack_destination)
- .current_dir(package_dir)
- .output()
- .map_err(|error| {
- format!(
- "failed to run pnpm pack for {package_name} in {}: {error}",
- package_dir.display()
- )
- })?;
- if !output.status.success() {
- return Err(format!(
- "pnpm pack failed for {package_name} in {}: {}",
- package_dir.display(),
- String::from_utf8_lossy(&output.stderr)
- ));
- }
- let entry = parse_pnpm_pack_entry(package_name, &output.stdout, &output.stderr)?;
- packed_package_from_pnpm_entry(package_name, pack_destination, entry)
-}
-
-fn parse_pnpm_pack_entry(
- package_name: &str,
- stdout: &[u8],
- stderr: &[u8],
-) -> Result<PnpmPackEntry, String> {
- serde_json::from_slice::<PnpmPackEntry>(stdout).map_err(|error| {
- format!(
- "failed to parse pnpm pack output for {package_name}: {error}; stdout: {}; stderr: {}",
- String::from_utf8_lossy(stdout),
- String::from_utf8_lossy(stderr)
- )
- })
-}
-
-fn packed_package_from_pnpm_entry(
- package_name: &str,
- pack_destination: &Path,
- entry: PnpmPackEntry,
-) -> Result<PackedPackage, String> {
- if entry.filename.trim().is_empty() {
- return Err(format!(
- "pnpm pack output for {package_name} is missing tarball filename"
- ));
- }
- let tarball_path = PathBuf::from(&entry.filename);
- if !tarball_path.starts_with(pack_destination) {
- return Err(format!(
- "pnpm pack tarball for {package_name} must be written under {}: {}",
- pack_destination.display(),
- tarball_path.display()
- ));
- }
- if !tarball_path.is_file() {
- return Err(format!(
- "pnpm pack tarball for {package_name} does not exist: {}",
- tarball_path.display()
- ));
- }
- Ok(PackedPackage {
- package_name: package_name.to_owned(),
- tarball_path,
- files: entry
- .files
- .into_iter()
- .map(|file| NpmPackFile { path: file.path })
- .collect(),
- })
-}
-
-fn check_clean_consumer_smoke(packed_packages: &[PackedPackage]) -> Result<(), String> {
- let consumer_dir = tempfile::tempdir()
- .map_err(|error| format!("failed to create clean npm consumer temp dir: {error}"))?;
- fs::write(
- consumer_dir.path().join("package.json"),
- "{\n \"name\": \"radroots-sdk-package-smoke\",\n \"private\": true,\n \"type\": \"module\"\n}\n",
- )
- .map_err(|error| {
- format!(
- "failed to write clean npm consumer package.json in {}: {error}",
- consumer_dir.path().display()
- )
- })?;
- let npm_cache_dir = consumer_dir.path().join(".npm-cache");
- let mut install = Command::new("npm");
- install
- .args([
- "install",
- "--ignore-scripts",
- "--no-audit",
- "--no-fund",
- "--registry",
- "http://127.0.0.1:9",
- "--cache",
- ])
- .arg(&npm_cache_dir);
- for packed in packed_packages {
- install.arg(&packed.tarball_path);
- }
- let output = install
- .current_dir(consumer_dir.path())
- .output()
- .map_err(|error| {
- format!(
- "failed to run clean npm consumer install in {}: {error}",
- consumer_dir.path().display()
- )
- })?;
- if !output.status.success() {
- return Err(format!(
- "clean npm consumer install failed in {}: stdout: {}; stderr: {}",
- consumer_dir.path().display(),
- String::from_utf8_lossy(&output.stdout),
- String::from_utf8_lossy(&output.stderr)
- ));
- }
- let package_names = packed_packages
- .iter()
- .map(|package| package.package_name.clone())
- .collect::<Vec<_>>();
- let smoke_script = consumer_smoke_script(&package_names)?;
- fs::write(consumer_dir.path().join("smoke.mjs"), smoke_script).map_err(|error| {
- format!(
- "failed to write clean npm consumer smoke script in {}: {error}",
- consumer_dir.path().display()
- )
- })?;
- let output = Command::new("node")
- .arg("smoke.mjs")
- .current_dir(consumer_dir.path())
- .output()
- .map_err(|error| {
- format!(
- "failed to run clean npm consumer import smoke in {}: {error}",
- consumer_dir.path().display()
- )
- })?;
- if !output.status.success() {
- return Err(format!(
- "clean npm consumer import smoke failed in {}: stdout: {}; stderr: {}",
- consumer_dir.path().display(),
- String::from_utf8_lossy(&output.stdout),
- String::from_utf8_lossy(&output.stderr)
- ));
- }
- Ok(())
-}
-
-fn consumer_smoke_script(package_names: &[String]) -> Result<String, String> {
- let mut script = String::new();
- for package_name in package_names {
- let quoted = serde_json::to_string(package_name)
- .map_err(|error| format!("failed to quote package name {package_name}: {error}"))?;
- script.push_str("await import(");
- script.push_str("ed);
- script.push_str(");\n");
- }
- Ok(script)
-}
-
-fn packed_payload_files(packed: &PackedPackage) -> Result<BTreeSet<String>, String> {
- debug_assert!(packed.tarball_path.is_file());
- packed
- .files
- .iter()
- .map(|file| normalized_package_path(&file.path))
- .collect()
-}
-
-fn read_packed_package_json(
- packed: &PackedPackage,
- package_name: &str,
-) -> Result<serde_json::Value, String> {
- let file = fs::File::open(&packed.tarball_path).map_err(|error| {
- format!(
- "failed to open pnpm pack tarball for {package_name}: {}: {error}",
- packed.tarball_path.display()
- )
- })?;
- let decoder = GzDecoder::new(file);
- let mut archive = Archive::new(decoder);
- let entries = archive.entries().map_err(|error| {
- format!(
- "failed to read pnpm pack tarball entries for {package_name}: {}: {error}",
- packed.tarball_path.display()
- )
- })?;
- for entry in entries {
- let mut entry = entry.map_err(|error| {
- format!(
- "failed to read pnpm pack tarball entry for {package_name}: {}: {error}",
- packed.tarball_path.display()
- )
- })?;
- let path = entry.path().map_err(|error| {
- format!(
- "failed to read pnpm pack tarball entry path for {package_name}: {}: {error}",
- packed.tarball_path.display()
- )
- })?;
- if path.as_ref() != Path::new("package/package.json") {
- continue;
- }
- let mut raw = String::new();
- entry.read_to_string(&mut raw).map_err(|error| {
- format!(
- "failed to read packed package.json for {package_name}: {}: {error}",
- packed.tarball_path.display()
- )
- })?;
- return serde_json::from_str::<serde_json::Value>(&raw).map_err(|error| {
- format!(
- "failed to parse packed package.json for {package_name}: {}: {error}",
- packed.tarball_path.display()
- )
- });
- }
- Err(format!(
- "pnpm pack tarball for {package_name} is missing package/package.json: {}",
- packed.tarball_path.display()
- ))
-}
-
-fn check_packed_package_json(
- source_json: &serde_json::Value,
- packed_json: &serde_json::Value,
- package_json_path: &Path,
- expected_name: &str,
- expected_directory: &str,
-) -> Result<(), String> {
- let source_description = package_description(source_json, package_json_path)?;
- let packed_description = package_description(packed_json, package_json_path)?;
- if packed_description != source_description {
- return Err(format!(
- "packed package.json description mismatch in {}: expected {source_description}, found {packed_description}",
- package_json_path.display()
- ));
- }
- require_string_field(packed_json, package_json_path, "name", expected_name)?;
- require_string_field(packed_json, package_json_path, "version", PACKAGE_VERSION)?;
- require_string_field(packed_json, package_json_path, "license", PACKAGE_LICENSE)?;
- require_string_field(packed_json, package_json_path, "homepage", PACKAGE_HOMEPAGE)?;
- require_string_field(packed_json, package_json_path, "type", "module")?;
- require_bool_field(packed_json, package_json_path, "sideEffects", false)?;
- check_publish_config(packed_json, package_json_path)?;
- check_repository(packed_json, package_json_path, expected_directory)?;
- check_package_files(packed_json, package_json_path)?;
- check_no_pack_lifecycle_scripts(packed_json, package_json_path)?;
- check_same_packed_field(source_json, packed_json, package_json_path, "main")?;
- check_same_packed_field(source_json, packed_json, package_json_path, "types")?;
- check_same_packed_field(source_json, packed_json, package_json_path, "exports")?;
- check_same_packed_field(source_json, packed_json, package_json_path, "scripts")?;
- check_packed_dependency_maps(source_json, packed_json, package_json_path)?;
- let source_surface = package_surface_paths(source_json, package_json_path)?;
- let packed_surface = package_surface_paths(packed_json, package_json_path)?;
- if packed_surface != source_surface {
- return Err(format!(
- "packed package.json export surface mismatch in {}",
- package_json_path.display()
- ));
- }
- Ok(())
-}
-
-fn check_same_packed_field(
- source_json: &serde_json::Value,
- packed_json: &serde_json::Value,
- package_json_path: &Path,
- field: &'static str,
-) -> Result<(), String> {
- if source_json.get(field) != packed_json.get(field) {
- return Err(format!(
- "packed package.json {field} mismatch in {}",
- package_json_path.display()
- ));
- }
- Ok(())
-}
-
-fn check_packed_dependency_maps(
- source_json: &serde_json::Value,
- packed_json: &serde_json::Value,
- package_json_path: &Path,
-) -> Result<(), String> {
- for field in ["dependencies", "peerDependencies", "optionalDependencies"] {
- check_packed_dependency_map(source_json, packed_json, package_json_path, field)?;
- }
- Ok(())
-}
-
-fn check_packed_dependency_map(
- source_json: &serde_json::Value,
- packed_json: &serde_json::Value,
- package_json_path: &Path,
- field: &'static str,
-) -> Result<(), String> {
- let source_dependencies = optional_dependency_map(source_json, package_json_path, field)?;
- let packed_dependencies = optional_dependency_map(packed_json, package_json_path, field)?;
- let source_keys = source_dependencies
- .iter()
- .flat_map(|dependencies| dependencies.keys().cloned())
- .collect::<BTreeSet<_>>();
- let packed_keys = packed_dependencies
- .iter()
- .flat_map(|dependencies| dependencies.keys().cloned())
- .collect::<BTreeSet<_>>();
- if packed_keys != source_keys {
- return Err(format!(
- "packed package.json {field} keys mismatch in {}: expected {:?}, found {:?}",
- package_json_path.display(),
- source_keys,
- packed_keys
- ));
- }
- let Some(packed_dependencies) = packed_dependencies else {
- return Ok(());
- };
- let source_dependencies = source_dependencies.expect("matching dependency keys require source");
- for (name, packed_version) in packed_dependencies {
- let packed_version = packed_version.as_str().ok_or_else(|| {
- format!(
- "packed package.json {field} dependency versions must be strings in {}",
- package_json_path.display()
- )
- })?;
- if packed_version.starts_with("workspace:") {
- return Err(format!(
- "packed package.json {field} dependency {name} must not use workspace protocol in {}",
- package_json_path.display()
- ));
- }
- let source_version = source_dependencies
- .get(name)
- .and_then(serde_json::Value::as_str)
- .ok_or_else(|| {
- format!(
- "source package.json {field} dependency {name} must be a string in {}",
- package_json_path.display()
- )
- })?;
- let expected = expected_packed_dependency_version(name, source_version);
- if packed_version != expected {
- return Err(format!(
- "packed package.json {field} dependency {name} mismatch in {}: expected {expected}, found {packed_version}",
- package_json_path.display()
- ));
- }
- }
- Ok(())
-}
-
-fn optional_dependency_map<'a>(
- json: &'a serde_json::Value,
- package_json_path: &Path,
- field: &'static str,
-) -> Result<Option<&'a serde_json::Map<String, serde_json::Value>>, String> {
- json.get(field)
- .map(|value| {
- value.as_object().ok_or_else(|| {
- format!(
- "package.json {field} must be an object: {}",
- package_json_path.display()
- )
- })
- })
- .transpose()
-}
-
-fn expected_packed_dependency_version(name: &str, source_version: &str) -> String {
- if name.starts_with("@radroots/") && source_version == "workspace:^" {
- format!("^{PACKAGE_VERSION}")
- } else {
- source_version.to_owned()
- }
-}
-
-fn validate_npm_pack_payload(
- package_name: &str,
- payload_files: &BTreeSet<String>,
- required_files: &BTreeSet<String>,
- expected_wasm_file: Option<&str>,
-) -> Result<(), String> {
- let missing = required_files
- .difference(payload_files)
- .cloned()
- .collect::<Vec<_>>();
- if !missing.is_empty() {
- return Err(format!(
- "npm pack payload for {package_name} is missing required files: {missing:?}"
- ));
- }
- for path in payload_files {
- if let Some(reason) = forbidden_npm_payload_path(path) {
- return Err(format!(
- "npm pack payload for {package_name} includes forbidden {reason}: {path}"
- ));
- }
- }
- if let Some(expected_wasm_file) = expected_wasm_file {
- let wasm_files = payload_files
- .iter()
- .filter(|path| path.ends_with(".wasm"))
- .cloned()
- .collect::<Vec<_>>();
- if wasm_files != [expected_wasm_file.to_owned()] {
- return Err(format!(
- "npm pack payload for {package_name} must include exactly {expected_wasm_file}; found {wasm_files:?}"
- ));
- }
- }
- Ok(())
-}
-
-fn validate_packed_dist_inventory(
- package_name: &str,
- payload_files: &BTreeSet<String>,
- expected_dist_files: &BTreeSet<String>,
-) -> Result<(), String> {
- let actual_dist_files = payload_files
- .iter()
- .filter(|path| path.starts_with("dist/"))
- .cloned()
- .collect::<BTreeSet<_>>();
- if &actual_dist_files != expected_dist_files {
- let missing = expected_dist_files
- .difference(&actual_dist_files)
- .cloned()
- .collect::<Vec<_>>();
- let extra = actual_dist_files
- .difference(expected_dist_files)
- .cloned()
- .collect::<Vec<_>>();
- return Err(format!(
- "npm pack payload for {package_name} has dist inventory mismatch: missing {:?}, extra {:?}",
- missing, extra
- ));
- }
- Ok(())
-}
-
-fn normalized_package_path(path: &str) -> Result<String, String> {
- let normalized = path.trim_start_matches("./");
- if normalized.is_empty()
- || normalized.starts_with('/')
- || normalized.contains('\\')
- || normalized
- .split('/')
- .any(|segment| segment.is_empty() || segment == "." || segment == "..")
- {
- return Err(format!("invalid npm pack payload path: {path}"));
- }
- Ok(normalized.to_owned())
-}
-
-fn forbidden_npm_payload_path(path: &str) -> Option<&'static str> {
- if path.starts_with("src/") {
- return Some("source path");
- }
- if path.starts_with("contracts/") {
- return Some("contract path");
- }
- if path.contains("sdk-manifest") {
- return Some("manifest provenance path");
- }
- if path.ends_with(".tsbuildinfo") {
- return Some("TypeScript build info path");
- }
- if path
- .split('/')
- .any(|segment| matches!(segment, ".gitignore" | ".npmignore"))
- {
- return Some("ignore file");
- }
- None
-}
-
-fn check_public_wasm_declaration_inventory(
- surface_paths: &BTreeSet<String>,
- spec: WasmPackageSpec,
-) -> Result<(), String> {
- let actual = surface_paths
- .iter()
- .filter(|path| path.ends_with(".d.ts"))
- .cloned()
- .collect::<BTreeSet<_>>();
- let expected = BTreeSet::from([format!("./dist/{}.d.ts", spec.out_name)]);
- if actual != expected {
- return Err(format!(
- "public wasm declaration inventory mismatch for {}: expected {:?}, found {:?}",
- spec.package_name, expected, actual
- ));
- }
- Ok(())
-}
-
-fn check_no_wasm_dist_ignore_files(
- package_dir: &Path,
- spec: WasmPackageSpec,
-) -> Result<(), String> {
- for file_name in [".gitignore", ".npmignore"] {
- let path = package_dir.join("dist").join(file_name);
- if path.exists() {
- return Err(format!(
- "wasm dist ignore file would hide package payload from npm for {}: {}",
- spec.package_name,
- path.display()
- ));
- }
- }
- Ok(())
-}
-
-fn check_wasm_runtime_files(package_dir: &Path, spec: WasmPackageSpec) -> Result<(), String> {
- for relative in wasm_runtime_files(spec) {
- let path = package_dir.join(&relative);
- if !path.is_file() {
- return Err(format!(
- "missing wasm package runtime artifact for {}: {}",
- spec.package_name,
- path.display()
- ));
- }
- }
- Ok(())
-}
-
-fn wasm_runtime_files(spec: WasmPackageSpec) -> [String; 3] {
- [
- format!("dist/{}.js", spec.out_name),
- format!("dist/{}_bg.wasm", spec.out_name),
- format!("dist/{}_bg.wasm.d.ts", spec.out_name),
- ]
-}
-
-fn check_wasm_declaration_files(package_dir: &Path, spec: WasmPackageSpec) -> Result<(), String> {
- for expected in declaration_files(spec)? {
- let path = package_dir.join(&expected.relative_path);
- let actual = fs::read_to_string(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- if actual != expected.contents {
- return Err(format!(
- "stale dto_bindgen wasm declaration: {}",
- path.display()
- ));
- }
- }
- Ok(())
-}
-
-fn package_surface_paths(
- json: &serde_json::Value,
- package_json_path: &Path,
-) -> Result<BTreeSet<String>, String> {
- let mut paths = BTreeSet::new();
- collect_required_package_path(json, package_json_path, "main", &mut paths)?;
- collect_required_package_path(json, package_json_path, "types", &mut paths)?;
- let exports = json.get("exports").ok_or_else(|| {
- format!(
- "package.json missing exports: {}",
- package_json_path.display()
- )
- })?;
- match exports {
- serde_json::Value::String(path) => {
- validate_package_surface_path(path, package_json_path, "exports")?;
- paths.insert(path.clone());
- }
- serde_json::Value::Object(map) => {
- if map.keys().any(|key| key != ".") {
- return Err(format!(
- "package.json only supports root exports: {}",
- package_json_path.display()
- ));
- }
- let root_export = map.get(".").ok_or_else(|| {
- format!(
- "package.json missing root export: {}",
- package_json_path.display()
- )
- })?;
- collect_export_paths(root_export, package_json_path, "exports[\".\"]", &mut paths)?;
- }
- _ => {
- return Err(format!(
- "package.json exports must be a string or object: {}",
- package_json_path.display()
- ));
- }
- }
- Ok(paths)
-}
-
-fn collect_required_package_path(
- json: &serde_json::Value,
- package_json_path: &Path,
- field: &'static str,
- paths: &mut BTreeSet<String>,
-) -> Result<(), String> {
- let value = json
- .get(field)
- .and_then(serde_json::Value::as_str)
- .ok_or_else(|| {
- format!(
- "package.json missing {field}: {}",
- package_json_path.display()
- )
- })?;
- validate_package_surface_path(value, package_json_path, field)?;
- paths.insert(value.to_owned());
- Ok(())
-}
-
-fn collect_export_paths(
- value: &serde_json::Value,
- package_json_path: &Path,
- field: &str,
- paths: &mut BTreeSet<String>,
-) -> Result<(), String> {
- match value {
- serde_json::Value::String(path) => {
- validate_package_surface_path(path, package_json_path, field)?;
- paths.insert(path.clone());
- Ok(())
- }
- serde_json::Value::Object(map) => {
- for (key, value) in map {
- collect_export_paths(value, package_json_path, &format!("{field}.{key}"), paths)?;
- }
- Ok(())
- }
- _ => Err(format!(
- "package.json {field} must name file paths: {}",
- package_json_path.display()
- )),
- }
-}
-
-fn validate_package_surface_path(
- value: &str,
- package_json_path: &Path,
- field: &str,
-) -> Result<(), String> {
- if value.trim().is_empty()
- || value.trim() != value
- || !value.starts_with("./dist/")
- || value.contains('\\')
- || value.split('/').any(|segment| segment == "..")
- {
- return Err(format!(
- "package.json {field} must be a relative dist path: {}",
- package_json_path.display()
- ));
- }
- Ok(())
-}
-
-fn check_package_surface_artifacts(
- package_dir: &Path,
- package_name: &str,
- surface_paths: &BTreeSet<String>,
-) -> Result<(), String> {
- for relative in surface_paths {
- let normalized = relative.trim_start_matches("./");
- let path = package_dir.join(normalized);
- if !path.is_file() {
- return Err(format!(
- "missing package export artifact for {package_name}: {}",
- path.display()
- ));
- }
- }
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use std::{
- collections::BTreeSet,
- fs,
- path::{Path, PathBuf},
- time::{SystemTime, UNIX_EPOCH},
- };
-
- use flate2::{Compression, write::GzEncoder};
-
- use crate::{
- output::package_outputs,
- package_matrix::{WasmPackageSpec, validate_package_matrix},
- package_metadata::package_readme,
- };
-
- use super::{
- PackedPackage, check_binding_crate_sources, check_generated_package_artifact_inventory,
- check_manifest_reticulum_dependency_boundaries, check_no_typescript_files,
- check_package_distribution_metadata, check_package_index, check_package_json,
- check_package_surface_artifacts, check_packed_package_json, check_publication_policy,
- check_reticulum_feature_entry, check_wasm_package_surface, consumer_smoke_script,
- expected_packed_dist_files, is_forbidden_reticulum_runtime_name, normalized_package_path,
- parse_pnpm_pack_entry, read_packed_package_json, validate_npm_pack_payload,
- validate_packed_dist_inventory,
- };
-
- const APPROVED_CRATES: [&str; 19] = [
- "radroots_core",
- "radroots_identity",
- "radroots_blossom",
- "radroots_protocol",
- "radroots_event",
- "radroots_event_codec",
- "radroots_trade",
- "radroots_signing",
- "radroots_transport",
- "radroots_nostr",
- "radroots_nostr_connect",
- "radroots_secrets",
- "radroots_storage",
- "radroots_storage_sqlite",
- "radroots_transport_nostr",
- "radroots_sync",
- "radroots_geonames",
- "radroots_sdk",
- "radroots",
- ];
-
- fn toml_strings(values: &[&str]) -> String {
- values
- .iter()
- .map(|value| format!("\"{value}\""))
- .collect::<Vec<_>>()
- .join(", ")
- }
-
- fn write_publication_architecture(root: &Path) {
- fs::create_dir_all(root.join("docs/specs")).expect("create spec directory");
- let mut architecture = format!(
- "spec_id = \"radroots.crates.release.v1\"\npackage_count = 19\n\n[repositories.lib]\nversion = \"0.1.0-alpha\"\npackages = [{}]\n\n[repositories.sdk]\nversion = \"0.1.0\"\npackages = [{}]\n",
- toml_strings(&APPROVED_CRATES[..17]),
- toml_strings(&APPROVED_CRATES[17..]),
- );
- for name in APPROVED_CRATES {
- architecture.push_str(&format!("\n[[package]]\nname = \"{name}\"\n"));
- }
- fs::write(
- root.join("docs/specs/radroots_crates_release_v1.toml"),
- architecture,
- )
- .expect("write release architecture");
- }
-
- fn publication_policy(
- frozen: bool,
- approved: &[&str],
- private: &[&str],
- build_codegen: &[&str],
- ) -> String {
- let publish_order = if frozen {
- String::new()
- } else {
- "\n[publish_order]\ncrates = [\"radroots_sdk\", \"radroots\"]\n".to_owned()
- };
- format!(
- r#"[publication]
-frozen = {frozen}
-registry = "crates-io"
-final_enablement_step = 305
-spec_id = "radroots.crates.release.v1"
-approved_packages = [{}]
-local_packages = ["radroots_sdk", "radroots"]
-external_packages = [{}]
-
-[workspace_classification]
-private = [{}]
-build_codegen = [{}]
-test_support = []
-preview = []
-retired = []
-{publish_order}
-"#,
- toml_strings(approved),
- toml_strings(&APPROVED_CRATES[..17]),
- toml_strings(private),
- toml_strings(build_codegen),
- )
- }
-
- #[test]
- fn package_skeleton_is_valid() {
- validate_package_matrix().expect("package matrix validates");
- }
-
- #[test]
- fn publication_freeze_rejects_publishable_or_implicit_packages() {
- let root = test_root("publication_freeze");
- fs::create_dir_all(root.join("contracts/releases")).expect("create contracts");
- write_publication_architecture(&root);
- fs::create_dir_all(root.join("crates/a")).expect("create crate a");
- fs::create_dir_all(root.join("crates/b")).expect("create crate b");
- fs::write(
- root.join("Cargo.toml"),
- "[workspace]\nmembers = [\"crates/a\", \"crates/b\"]\n",
- )
- .expect("write workspace");
- fs::write(
- root.join("contracts/releases/publication.toml"),
- publication_policy(true, &APPROVED_CRATES, &["crate-a", "crate-b"], &[]),
- )
- .expect("write policy");
- fs::write(
- root.join("crates/a/Cargo.toml"),
- "[package]\nname = \"crate-a\"\nversion = \"0.1.0\"\npublish = false\n",
- )
- .expect("write crate a");
- fs::write(
- root.join("crates/b/Cargo.toml"),
- "[package]\nname = \"crate-b\"\nversion = \"0.1.0\"\npublish = false\n",
- )
- .expect("write crate b");
- check_publication_policy(&root).expect("private workspace satisfies freeze");
-
- fs::write(
- root.join("crates/a/Cargo.toml"),
- "[package]\nname = \"crate-a\"\nversion = \"0.1.0\"\npublish = [\"crates-io\"]\n",
- )
- .expect("make crate a publishable");
- let publishable =
- check_publication_policy(&root).expect_err("publishable crate must fail freeze");
- assert!(publishable.contains("publication freeze requires workspace package crate-a"));
-
- fs::write(
- root.join("crates/a/Cargo.toml"),
- "[package]\nname = \"crate-a\"\nversion = \"0.1.0\"\n",
- )
- .expect("remove publish control");
- let implicit =
- check_publication_policy(&root).expect_err("implicit publishability must fail freeze");
- assert!(implicit.contains("publication freeze requires workspace package crate-a"));
-
- fs::write(
- root.join("crates/a/Cargo.toml"),
- "[package]\nname = \"crate-a\"\nversion = \"0.1.0\"\npublish = false\n",
- )
- .expect("restore private crate a");
- let mut unapproved = APPROVED_CRATES.to_vec();
- unapproved.push("unapproved-public");
- fs::write(
- root.join("contracts/releases/publication.toml"),
- publication_policy(true, &unapproved, &["crate-a", "crate-b"], &[]),
- )
- .expect("write unapproved policy");
- let unapproved_error = check_publication_policy(&root)
- .expect_err("unapproved public package must fail exact catalog validation");
- assert!(unapproved_error.contains("unapproved packages: unapproved-public"));
-
- fs::write(
- root.join("contracts/releases/publication.toml"),
- publication_policy(true, &APPROVED_CRATES, &["crate-a"], &[]),
- )
- .expect("write unclassified policy");
- let unclassified =
- check_publication_policy(&root).expect_err("unclassified workspace package must fail");
- assert!(unclassified.contains("workspace classification is missing packages: crate-b"));
-
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn publication_enablement_allows_only_the_two_approved_packages() {
- let root = test_root("publication_enablement");
- fs::create_dir_all(root.join("contracts/releases")).expect("create contracts");
- write_publication_architecture(&root);
- for member in ["sdk", "facade", "tool"] {
- fs::create_dir_all(root.join("crates").join(member)).expect("create member");
- }
- fs::write(
- root.join("Cargo.toml"),
- "[workspace]\nmembers = [\"crates/sdk\", \"crates/facade\", \"crates/tool\"]\n",
- )
- .expect("write workspace");
- fs::write(
- root.join("contracts/releases/publication.toml"),
- publication_policy(false, &APPROVED_CRATES, &[], &["build-tool"]),
- )
- .expect("write policy");
- fs::write(
- root.join("crates/sdk/Cargo.toml"),
- "[package]\nname = \"radroots_sdk\"\nversion = \"0.1.0\"\npublish = [\"crates-io\"]\n",
- )
- .expect("write SDK manifest");
- fs::write(
- root.join("crates/facade/Cargo.toml"),
- "[package]\nname = \"radroots\"\nversion = \"0.1.0\"\npublish = [\"crates-io\"]\n",
- )
- .expect("write facade manifest");
- fs::write(
- root.join("crates/tool/Cargo.toml"),
- "[package]\nname = \"build-tool\"\nversion = \"0.1.0\"\npublish = false\n",
- )
- .expect("write tool manifest");
- check_publication_policy(&root).expect("approved package staging should pass");
-
- fs::write(
- root.join("crates/tool/Cargo.toml"),
- "[package]\nname = \"build-tool\"\nversion = \"0.1.0\"\npublish = [\"crates-io\"]\n",
- )
- .expect("make tool publishable");
- let extra = check_publication_policy(&root)
- .expect_err("non-approved publishable package must fail enablement");
- assert!(extra.contains("non-approved workspace package build-tool"));
-
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn reticulum_gate_allows_first_party_preview_dependency_only() {
- let manifest = manifest_value(
- r#"
-[package]
-name = "radroots_sdk"
-
-[features]
-runtime = ["dep:radroots_transport_reticulum"]
-
-[dependencies]
-radroots_transport_reticulum = { workspace = true, optional = true, default-features = false }
-"#,
- );
-
- check_manifest_reticulum_dependency_boundaries(&manifest, "crates/sdk/Cargo.toml")
- .expect("first-party preview dependency is allowed");
- }
-
- #[test]
- fn reticulum_gate_ignores_docs_rs_feature_selection() {
- let manifest = manifest_value(
- r#"
-[package]
-name = "radroots_sdk"
-
-[package.metadata.docs.rs]
-features = ["nostr", "nip46"]
-
-[features]
-nostr = []
-nip46 = ["nostr"]
-"#,
- );
-
- check_manifest_reticulum_dependency_boundaries(&manifest, "crates/sdk/Cargo.toml")
- .expect("docs.rs feature arrays are metadata, not Cargo feature tables");
- }
-
- #[test]
- fn reticulum_gate_rejects_workspace_package_alias_to_real_runtime() {
- let manifest = manifest_value(
- r#"
-[workspace.dependencies]
-rr_mesh = { package = "reticulum", version = "1" }
-"#,
- );
-
- let error = check_manifest_reticulum_dependency_boundaries(&manifest, "Cargo.toml")
- .expect_err("Reticulum package alias is rejected");
-
- assert!(error.contains("reticulum"));
- assert!(error.contains("workspace.dependencies"));
- }
-
- #[test]
- fn reticulum_gate_rejects_build_dependency_alias_to_python_binding() {
- let manifest = manifest_value(
- r#"
-[package]
-name = "radroots_sdk"
-
-[build-dependencies]
-ffi_bridge = { package = "pyo3", version = "0.24" }
-"#,
- );
-
- let error =
- check_manifest_reticulum_dependency_boundaries(&manifest, "crates/sdk/Cargo.toml")
- .expect_err("Python runtime binding package alias is rejected");
-
- assert!(error.contains("pyo3"));
- assert!(error.contains("build-dependencies"));
- }
-
- #[test]
- fn reticulum_gate_rejects_reticulum_feature_aliases_and_runtime_features() {
- let alias_manifest = manifest_value(
- r#"
-[package]
-name = "radroots_sdk"
-
-[features]
-transport-reticulum-preview = []
-"#,
- );
- let alias_error = check_manifest_reticulum_dependency_boundaries(
- &alias_manifest,
- "crates/sdk/Cargo.toml",
- )
- .expect_err("Reticulum feature alias is rejected");
-
- assert!(alias_error.contains("transport-reticulum-preview"));
-
- let entry_error = check_reticulum_feature_entry(
- "radroots_transport_reticulum/client",
- "crates/sdk/Cargo.toml",
- "runtime",
- )
- .expect_err("Reticulum preview crate runtime feature is rejected");
-
- assert!(entry_error.contains("radroots_transport_reticulum/client"));
- }
-
- #[test]
- fn reticulum_gate_package_name_classifier_is_runtime_scoped() {
- for name in [
- "reticulum",
- "reticulum-rs",
- "rns",
- "rns-client",
- "python",
- "python-runtime",
- "pyo3",
- "pyo3-ffi",
- "radroots-transport-reticulum",
- ] {
- assert!(
- is_forbidden_reticulum_runtime_name(name),
- "{name} must be forbidden"
- );
- }
- for name in ["radroots_transport_reticulum", "dto_bindgen_backend_python"] {
- assert!(
- !is_forbidden_reticulum_runtime_name(name),
- "{name} must be allowed"
- );
- }
- }
-
- #[test]
- fn rejects_crate_typescript_directories() {
- let root = test_root("typescript_dir");
- let typescript_dir = root
- .join("crates")
- .join("core_bindings")
- .join("src")
- .join("typescript");
- fs::create_dir_all(&typescript_dir).expect("create forbidden directory");
-
- let error = check_binding_crate_sources(&root).expect_err("forbidden directory rejected");
-
- assert!(error.contains("forbidden crate TypeScript source directory"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn rejects_crate_typescript_files() {
- let root = test_root("typescript_file");
- let src_dir = root.join("crates/core_bindings/src");
- fs::create_dir_all(&src_dir).expect("create crate source directory");
- fs::write(src_dir.join("types.ts"), "export type A = string;\n")
- .expect("write forbidden file");
-
- let error = check_no_typescript_files(&src_dir).expect_err("forbidden file rejected");
-
- assert!(error.contains("forbidden crate TypeScript source file"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn rejects_generated_package_index_source() {
- let root = test_root("generated_index");
- let path = root.join("src/index.ts");
- fs::create_dir_all(path.parent().expect("parent")).expect("create source directory");
- fs::write(
- &path,
- "// @generated by cargo xtask generate ts\n// Do not edit by hand.\nexport {};\n",
- )
- .expect("write generated index");
-
- let error = check_package_index(&path).expect_err("generated index rejected");
-
- assert!(error.contains("package index must be handwritten source"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn generated_package_artifact_inventory_rejects_extra_files() {
- let root = test_root("generated_inventory_extra");
- let output = package_outputs()
- .expect("package outputs")
- .into_iter()
- .find(|output| output.spec.key == "core")
- .expect("core output");
- let package_dir = root.join(output.spec.package_dir);
- fs::create_dir_all(package_dir.join("src/generated")).expect("create generated dir");
- for file in output.files() {
- let path = package_dir.join(file.relative_path);
- fs::write(path, file.contents).expect("write expected file");
- }
- fs::write(
- package_dir.join("src/generated").join("extra.ts"),
- "export type Extra = string;\n",
- )
- .expect("write extra file");
-
- let error = check_generated_package_artifact_inventory(&root, &output)
- .expect_err("extra generated file rejected");
-
- assert!(error.contains("generated artifact inventory mismatch"));
- assert!(error.contains("extra.ts"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn public_package_metadata_rejects_private_packages() {
- let root = test_root("private_package_json");
- let package_dir = root.join("packages").join("example");
- fs::create_dir_all(&package_dir).expect("create package");
- let package_json = package_json("example").replace(
- r#""sideEffects": false,"#,
- r#""private": true, "sideEffects": false,"#,
- );
- fs::write(package_dir.join("package.json"), package_json).expect("write package json");
-
- let error = check_package_json(
- &package_dir.join("package.json"),
- "@radroots/example",
- "packages/example",
- )
- .expect_err("private package rejected");
-
- assert!(error.contains("must not set private"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn public_package_metadata_rejects_src_generated_payloads() {
- let root = test_root("src_generated_package_payload");
- let package_dir = root.join("packages").join("example");
- fs::create_dir_all(&package_dir).expect("create package");
- let package_json = package_json("example").replace(
- r#""files": ["dist", "README.md", "LICENSE-MIT", "LICENSE-APACHE"]"#,
- r#""files": ["dist", "README.md", "LICENSE-MIT", "LICENSE-APACHE", "src/generated"]"#,
- );
- fs::write(package_dir.join("package.json"), package_json).expect("write package json");
-
- let error = check_package_json(
- &package_dir.join("package.json"),
- "@radroots/example",
- "packages/example",
- )
- .expect_err("src generated package payload rejected");
-
- assert!(error.contains("files must publish dist plus approved metadata only"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn public_package_metadata_rejects_pack_lifecycle_scripts() {
- let root = test_root("pack_lifecycle_scripts");
- let package_dir = root.join("packages").join("example");
- fs::create_dir_all(&package_dir).expect("create package");
- let package_json = package_json("example").replace(
- r#""type": "module","#,
- r#""scripts": {"prepack": "echo forbidden"}, "type": "module","#,
- );
- fs::write(package_dir.join("package.json"), package_json).expect("write package json");
-
- let error = check_package_json(
- &package_dir.join("package.json"),
- "@radroots/example",
- "packages/example",
- )
- .expect_err("pack lifecycle script rejected");
-
- assert!(error.contains("script prepack is forbidden"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn package_distribution_metadata_matches_root_license_files() {
- let root = test_root("package_distribution_metadata");
- let package_dir = root.join("packages").join("example");
- fs::create_dir_all(&package_dir).expect("create package");
- fs::write(root.join("LICENSE-MIT"), "MIT license\n").expect("write MIT license");
- fs::write(root.join("LICENSE-APACHE"), "Apache license\n").expect("write Apache license");
- fs::write(package_dir.join("package.json"), package_json("example"))
- .expect("write package json");
- fs::write(
- package_dir.join("README.md"),
- package_readme("@radroots/example", "Example package"),
- )
- .expect("write readme");
- fs::write(package_dir.join("LICENSE-MIT"), "MIT license\n")
- .expect("write package MIT license");
- fs::write(package_dir.join("LICENSE-APACHE"), "Apache license\n")
- .expect("write package Apache license");
- let json = check_package_json(
- &package_dir.join("package.json"),
- "@radroots/example",
- "packages/example",
- )
- .expect("valid package json");
-
- check_package_distribution_metadata(
- &root,
- &package_dir,
- &package_dir.join("package.json"),
- &json,
- )
- .expect("metadata matches");
-
- fs::write(package_dir.join("README.md"), "stale\n").expect("stale readme");
- let error = check_package_distribution_metadata(
- &root,
- &package_dir,
- &package_dir.join("package.json"),
- &json,
- )
- .expect_err("stale readme rejected");
- assert!(error.contains("stale package README"));
-
- fs::write(
- package_dir.join("README.md"),
- package_readme("@radroots/example", "Example package"),
- )
- .expect("restore readme");
- fs::write(package_dir.join("LICENSE-MIT"), "stale\n").expect("stale license");
- let error = check_package_distribution_metadata(
- &root,
- &package_dir,
- &package_dir.join("package.json"),
- &json,
- )
- .expect_err("stale license rejected");
- assert!(error.contains("stale package license metadata"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn package_surface_artifacts_require_dist_files() {
- let root = test_root("package_surface_artifacts");
- let package_dir = root.join("packages").join("example");
- fs::create_dir_all(package_dir.join("dist")).expect("create dist");
- fs::write(package_dir.join("dist").join("index.js"), "export {};\n").expect("write js");
- let surface_paths =
- BTreeSet::from(["./dist/index.js".to_owned(), "./dist/index.d.ts".to_owned()]);
-
- let error =
- check_package_surface_artifacts(&package_dir, "@radroots/example", &surface_paths)
- .expect_err("missing declaration should fail");
- assert!(error.contains("missing package export artifact"));
- assert!(error.contains("index.d.ts"));
-
- fs::write(package_dir.join("dist").join("index.d.ts"), "export {};\n").expect("write d.ts");
- check_package_surface_artifacts(&package_dir, "@radroots/example", &surface_paths)
- .expect("surface artifacts present");
-
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn binding_dist_inventory_includes_generated_js_and_declarations() {
- let root = test_root("binding_dist_inventory");
- let package_dir = root.join("packages").join("example");
- fs::create_dir_all(package_dir.join("src/generated")).expect("create generated dir");
- fs::write(
- package_dir.join("src/generated").join("types.ts"),
- "export type Example = string;\n",
- )
- .expect("write generated types");
- fs::write(
- package_dir.join("src/generated").join("constants.ts"),
- "export const EXAMPLE = \"example\";\n",
- )
- .expect("write generated constants");
- let json = package_json_value(&package_json("example"));
-
- let expected = expected_packed_dist_files(
- &package_dir,
- &json,
- Path::new("packages/example/package.json"),
- None,
- )
- .expect("expected dist files");
-
- assert_eq!(
- expected,
- BTreeSet::from([
- "dist/generated/constants.d.ts".to_owned(),
- "dist/generated/constants.js".to_owned(),
- "dist/generated/types.d.ts".to_owned(),
- "dist/generated/types.js".to_owned(),
- "dist/index.d.ts".to_owned(),
- "dist/index.js".to_owned(),
- ])
- );
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn npm_pack_payload_requires_metadata_and_export_files() {
- let payload_files = BTreeSet::from([
- "package.json".to_owned(),
- "README.md".to_owned(),
- "LICENSE-MIT".to_owned(),
- "dist/index.js".to_owned(),
- ]);
- let required_files = BTreeSet::from([
- "package.json".to_owned(),
- "README.md".to_owned(),
- "LICENSE-MIT".to_owned(),
- "LICENSE-APACHE".to_owned(),
- "dist/index.js".to_owned(),
- "dist/index.d.ts".to_owned(),
- ]);
-
- let error =
- validate_npm_pack_payload("@radroots/example", &payload_files, &required_files, None)
- .expect_err("missing files should fail");
- assert!(error.contains("missing required files"));
- assert!(error.contains("LICENSE-APACHE"));
- assert!(error.contains("dist/index.d.ts"));
- }
-
- #[test]
- fn packed_dist_inventory_rejects_missing_generated_files() {
- let expected_dist_files = BTreeSet::from([
- "dist/generated/types.d.ts".to_owned(),
- "dist/generated/types.js".to_owned(),
- "dist/index.d.ts".to_owned(),
- "dist/index.js".to_owned(),
- ]);
- let payload_files = BTreeSet::from([
- "package.json".to_owned(),
- "README.md".to_owned(),
- "LICENSE-MIT".to_owned(),
- "LICENSE-APACHE".to_owned(),
- "dist/generated/types.js".to_owned(),
- "dist/index.d.ts".to_owned(),
- "dist/index.js".to_owned(),
- ]);
-
- let error = validate_packed_dist_inventory(
- "@radroots/example",
- &payload_files,
- &expected_dist_files,
- )
- .expect_err("missing generated declaration rejected");
-
- assert!(error.contains("dist inventory mismatch"));
- assert!(error.contains("dist/generated/types.d.ts"));
- }
-
- #[test]
- fn packed_payload_path_normalization_rejects_invalid_paths() {
- assert_eq!(
- normalized_package_path("./dist/index.js").expect("valid path"),
- "dist/index.js"
- );
- for invalid in [
- "",
- "./",
- "/dist/index.js",
- "dist\\index.js",
- "dist/../index.js",
- "dist//index.js",
- "dist/./index.js",
- ] {
- let error = normalized_package_path(invalid).expect_err("invalid path rejected");
- assert!(error.contains("invalid npm pack payload path"));
- }
- }
-
- #[test]
- fn consumer_smoke_script_imports_package_roots() {
- let script = consumer_smoke_script(&[
- "@radroots/core-bindings".to_owned(),
- "@radroots/event-codec-wasm".to_owned(),
- ])
- .expect("smoke script renders");
-
- assert_eq!(
- script,
- "await import(\"@radroots/core-bindings\");\nawait import(\"@radroots/event-codec-wasm\");\n"
- );
- }
-
- #[test]
- fn pnpm_pack_json_parser_accepts_single_package_object() {
- let entry = parse_pnpm_pack_entry(
- "@radroots/example",
- br#"{
- "name": "@radroots/example",
- "version": "0.1.0",
- "filename": "/tmp/example.tgz",
- "files": [
- {"path": "dist/index.js"},
- {"path": "package.json"}
- ]
-}"#,
- b"",
- )
- .expect("pnpm pack output parses");
-
- assert_eq!(entry.filename, "/tmp/example.tgz");
- assert_eq!(
- entry
- .files
- .into_iter()
- .map(|file| file.path)
- .collect::<Vec<_>>(),
- ["dist/index.js", "package.json"]
- );
- }
-
- #[test]
- fn reads_packed_manifest_from_tgz() {
- let root = test_root("packed_manifest_tgz");
- fs::create_dir_all(&root).expect("create root");
- let tarball_path = root.join("example.tgz");
- let file = fs::File::create(&tarball_path).expect("create tarball");
- let encoder = GzEncoder::new(file, Compression::default());
- let mut builder = tar::Builder::new(encoder);
- let contents = br#"{"name":"@radroots/example","version":"0.1.0"}"#;
- let mut header = tar::Header::new_gnu();
- header
- .set_path("package/package.json")
- .expect("set manifest path");
- header.set_size(contents.len() as u64);
- header.set_mode(0o644);
- header.set_cksum();
- builder
- .append(&header, &contents[..])
- .expect("append manifest");
- let encoder = builder.into_inner().expect("finish tar");
- encoder.finish().expect("finish gzip");
- let packed = PackedPackage {
- package_name: "@radroots/example".to_owned(),
- tarball_path,
- files: Vec::new(),
- };
-
- let json =
- read_packed_package_json(&packed, "@radroots/example").expect("read packed manifest");
-
- assert_eq!(
- json.get("name").and_then(serde_json::Value::as_str),
- Some("@radroots/example")
- );
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn packed_manifest_accepts_pnpm_workspace_dependency_rewrite() {
- let source = package_json_value(&package_json_with_dependencies(
- "trade-bindings",
- r#""@radroots/core-bindings": "workspace:^",
- "@radroots/event-bindings": "workspace:^""#,
- ));
- let packed = package_json_value(&package_json_with_dependencies(
- "trade-bindings",
- r#""@radroots/core-bindings": "^0.1.0",
- "@radroots/event-bindings": "^0.1.0""#,
- ));
-
- check_packed_package_json(
- &source,
- &packed,
- Path::new("packages/trade-bindings/package.json"),
- "@radroots/trade-bindings",
- "packages/trade-bindings",
- )
- .expect("packed manifest accepted");
- }
-
- #[test]
- fn packed_manifest_rejects_workspace_dependency_ranges() {
- let source = package_json_value(&package_json_with_dependencies(
- "trade-bindings",
- r#""@radroots/core-bindings": "workspace:^""#,
- ));
- let packed = package_json_value(&package_json_with_dependencies(
- "trade-bindings",
- r#""@radroots/core-bindings": "workspace:^""#,
- ));
-
- let error = check_packed_package_json(
- &source,
- &packed,
- Path::new("packages/trade-bindings/package.json"),
- "@radroots/trade-bindings",
- "packages/trade-bindings",
- )
- .expect_err("workspace dependency rejected");
-
- assert!(error.contains("must not use workspace protocol"));
- }
-
- #[test]
- fn packed_manifest_rejects_internal_dependency_range_mismatch() {
- let source = package_json_value(&package_json_with_dependencies(
- "trade-bindings",
- r#""@radroots/core-bindings": "workspace:^""#,
- ));
- let packed = package_json_value(&package_json_with_dependencies(
- "trade-bindings",
- r#""@radroots/core-bindings": "^0.2.0""#,
- ));
-
- let error = check_packed_package_json(
- &source,
- &packed,
- Path::new("packages/trade-bindings/package.json"),
- "@radroots/trade-bindings",
- "packages/trade-bindings",
- )
- .expect_err("internal dependency mismatch rejected");
-
- assert!(error.contains("expected ^0.1.0"));
- }
-
- #[test]
- fn npm_pack_payload_rejects_source_and_provenance_internals() {
- let required_files = BTreeSet::from(["package.json".to_owned()]);
- for forbidden in [
- "src/generated/types.ts",
- "contracts/provenance/typescript/core.json",
- "dist/sdk-manifest.json",
- "dist/index.tsbuildinfo",
- "dist/.gitignore",
- ".npmignore",
- ] {
- let payload_files = BTreeSet::from(["package.json".to_owned(), forbidden.to_owned()]);
-
- let error = validate_npm_pack_payload(
- "@radroots/example",
- &payload_files,
- &required_files,
- None,
- )
- .expect_err("forbidden payload path should fail");
- assert!(error.contains("includes forbidden"));
- assert!(error.contains(forbidden));
- }
- }
-
- #[test]
- fn npm_pack_payload_requires_exact_wasm_file() {
- let required_files = BTreeSet::from([
- "package.json".to_owned(),
- "dist/example.js".to_owned(),
- "dist/example_bg.wasm".to_owned(),
- ]);
- let missing_wasm =
- BTreeSet::from(["package.json".to_owned(), "dist/example.js".to_owned()]);
- let error = validate_npm_pack_payload(
- "@radroots/example-wasm",
- &missing_wasm,
- &required_files,
- Some("dist/example_bg.wasm"),
- )
- .expect_err("missing wasm should fail");
- assert!(error.contains("missing required files"));
-
- let extra_wasm = BTreeSet::from([
- "package.json".to_owned(),
- "dist/example.js".to_owned(),
- "dist/example_bg.wasm".to_owned(),
- "dist/extra_bg.wasm".to_owned(),
- ]);
- let error = validate_npm_pack_payload(
- "@radroots/example-wasm",
- &extra_wasm,
- &required_files,
- Some("dist/example_bg.wasm"),
- )
- .expect_err("extra wasm should fail");
- assert!(error.contains("must include exactly dist/example_bg.wasm"));
- }
-
- #[test]
- fn wasm_package_surface_requires_exported_dist_files() {
- let root = test_root("wasm_surface");
- let package_dir = root.join("packages").join("example-wasm");
- fs::create_dir_all(package_dir.join("dist")).expect("create dist");
- fs::write(
- package_dir.join("package.json"),
- package_json("example-wasm").replace("./dist/index", "./dist/example"),
- )
- .expect("write package json");
- write_distribution_metadata(
- &root,
- &package_dir,
- "@radroots/example-wasm",
- "Example package",
- );
- fs::write(package_dir.join("dist").join("example.js"), "export {};\n").expect("write js");
- let spec = WasmPackageSpec {
- key: "example",
- crate_name: "radroots_example_wasm",
- crate_dir: "crates/example_wasm",
- package_name: "@radroots/example-wasm",
- package_dir: "packages/example-wasm",
- out_name: "example",
- out_dir: "../../packages/example-wasm/dist",
- };
-
- let missing =
- check_wasm_package_surface(&root, spec).expect_err("missing d.ts should fail");
- assert!(missing.contains("example.d.ts"));
- fs::write(
- package_dir.join("dist").join("example.d.ts"),
- dto_declaration("example.d.ts"),
- )
- .expect("write d.ts");
- fs::write(
- package_dir.join("dist").join("example_bg.wasm.d.ts"),
- dto_declaration("example_bg.wasm.d.ts"),
- )
- .expect("write d.ts");
- fs::write(package_dir.join("dist").join("example_bg.wasm"), b"\0asm").expect("write wasm");
- let error = check_wasm_package_surface(&root, spec)
- .expect_err("unknown declaration inventory rejected");
- assert!(error.contains("missing wasm declaration inventory"));
-
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn wasm_package_surface_rejects_generated_package_manifest() {
- let root = test_root("wasm_dist_package_manifest");
- let package_dir = root.join("packages").join("example-wasm");
- fs::create_dir_all(package_dir.join("dist")).expect("create dist");
- fs::write(
- package_dir.join("package.json"),
- package_json("example-wasm").replace("./dist/index", "./dist/example"),
- )
- .expect("write package json");
- write_distribution_metadata(
- &root,
- &package_dir,
- "@radroots/example-wasm",
- "Example package",
- );
- fs::write(package_dir.join("dist").join("example.js"), "export {};\n").expect("write js");
- fs::write(
- package_dir.join("dist").join("example.d.ts"),
- dto_declaration("example.d.ts"),
- )
- .expect("write d.ts");
- fs::write(
- package_dir.join("dist").join("example_bg.wasm.d.ts"),
- dto_declaration("example_bg.wasm.d.ts"),
- )
- .expect("write wasm d.ts");
- fs::write(package_dir.join("dist").join("package.json"), "{}\n")
- .expect("write forbidden manifest");
- let spec = WasmPackageSpec {
- key: "example",
- crate_name: "radroots_example_wasm",
- crate_dir: "crates/example_wasm",
- package_name: "@radroots/example-wasm",
- package_dir: "packages/example-wasm",
- out_name: "example",
- out_dir: "../../packages/example-wasm/dist",
- };
-
- let error =
- check_wasm_package_surface(&root, spec).expect_err("dist package manifest rejected");
- assert!(error.contains("generated package manifest is forbidden"));
-
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn wasm_package_surface_rejects_dist_ignore_files() {
- let root = test_root("wasm_dist_ignore");
- let package_dir = root.join("packages").join("example-wasm");
- fs::create_dir_all(package_dir.join("dist")).expect("create dist");
- fs::write(
- package_dir.join("package.json"),
- package_json("example-wasm").replace("./dist/index", "./dist/example"),
- )
- .expect("write package json");
- write_distribution_metadata(
- &root,
- &package_dir,
- "@radroots/example-wasm",
- "Example package",
- );
- fs::write(package_dir.join("dist").join(".gitignore"), "*\n").expect("write ignore");
- let spec = WasmPackageSpec {
- key: "example",
- crate_name: "radroots_example_wasm",
- crate_dir: "crates/example_wasm",
- package_name: "@radroots/example-wasm",
- package_dir: "packages/example-wasm",
- out_name: "example",
- out_dir: "../../packages/example-wasm/dist",
- };
-
- let error = check_wasm_package_surface(&root, spec).expect_err("dist ignore file rejected");
- assert!(error.contains("would hide package payload"));
-
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn wasm_package_surface_requires_runtime_wasm_artifact() {
- let root = test_root("wasm_runtime_artifact");
- let package_dir = root.join("packages").join("example-wasm");
- fs::create_dir_all(package_dir.join("dist")).expect("create dist");
- fs::write(
- package_dir.join("package.json"),
- package_json("example-wasm").replace("./dist/index", "./dist/example"),
- )
- .expect("write package json");
- write_distribution_metadata(
- &root,
- &package_dir,
- "@radroots/example-wasm",
- "Example package",
- );
- fs::write(package_dir.join("dist").join("example.js"), "export {};\n").expect("write js");
- fs::write(
- package_dir.join("dist").join("example.d.ts"),
- dto_declaration("example.d.ts"),
- )
- .expect("write d.ts");
- fs::write(
- package_dir.join("dist").join("example_bg.wasm.d.ts"),
- dto_declaration("example_bg.wasm.d.ts"),
- )
- .expect("write wasm d.ts");
- let spec = WasmPackageSpec {
- key: "example",
- crate_name: "radroots_example_wasm",
- crate_dir: "crates/example_wasm",
- package_name: "@radroots/example-wasm",
- package_dir: "packages/example-wasm",
- out_name: "example",
- out_dir: "../../packages/example-wasm/dist",
- };
-
- let error = check_wasm_package_surface(&root, spec).expect_err("missing wasm rejected");
- assert!(error.contains("missing wasm package runtime artifact"));
-
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn wasm_package_surface_rejects_subpath_exports() {
- let root = test_root("wasm_subpath_exports");
- let package_dir = root.join("packages").join("example-wasm");
- fs::create_dir_all(package_dir.join("dist")).expect("create dist");
- fs::write(
- package_dir.join("package.json"),
- package_json("example-wasm")
- .replace("./dist/index", "./dist/example")
- .replace(
- r#""exports": {
- ".": {
- "types": "./dist/example.d.ts",
- "import": "./dist/example.js",
- "default": "./dist/example.js"
- }
- }"#,
- r#""exports": {
- ".": "./dist/example.js",
- "./extra": "./dist/extra.js"
- }"#,
- ),
- )
- .expect("write package json");
- write_distribution_metadata(
- &root,
- &package_dir,
- "@radroots/example-wasm",
- "Example package",
- );
- fs::write(package_dir.join("dist").join("example.js"), "export {};\n").expect("write js");
- fs::write(
- package_dir.join("dist").join("example.d.ts"),
- dto_declaration("example.d.ts"),
- )
- .expect("write d.ts");
- fs::write(
- package_dir.join("dist").join("example_bg.wasm.d.ts"),
- dto_declaration("example_bg.wasm.d.ts"),
- )
- .expect("write wasm d.ts");
- let spec = WasmPackageSpec {
- key: "example",
- crate_name: "radroots_example_wasm",
- crate_dir: "crates/example_wasm",
- package_name: "@radroots/example-wasm",
- package_dir: "packages/example-wasm",
- out_name: "example",
- out_dir: "../../packages/example-wasm/dist",
- };
-
- let error = check_wasm_package_surface(&root, spec).expect_err("subpath export rejected");
- assert!(error.contains("only supports root exports"));
-
- let _ = fs::remove_dir_all(root);
- }
-
- fn test_root(name: &str) -> PathBuf {
- let stamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .expect("system time after epoch")
- .as_nanos();
- let root = std::env::temp_dir().join(format!(
- "radroots_sdk_xtask_check_{name}_{}_{}",
- std::process::id(),
- stamp
- ));
- let _ = fs::remove_dir_all(&root);
- root
- }
-
- fn dto_declaration(name: &str) -> String {
- format!(
- "// @generated by cargo xtask generate wasm via dto_bindgen\n// Do not edit by hand.\nexport type Generated = \"{name}\";\n"
- )
- }
-
- fn write_distribution_metadata(
- root: &Path,
- package_dir: &Path,
- package_name: &str,
- description: &str,
- ) {
- fs::write(root.join("LICENSE-MIT"), "MIT license\n").expect("write MIT license");
- fs::write(root.join("LICENSE-APACHE"), "Apache license\n").expect("write Apache license");
- fs::write(package_dir.join("LICENSE-MIT"), "MIT license\n")
- .expect("write package MIT license");
- fs::write(package_dir.join("LICENSE-APACHE"), "Apache license\n")
- .expect("write package Apache license");
- fs::write(
- package_dir.join("README.md"),
- package_readme(package_name, description),
- )
- .expect("write package README");
- }
-
- fn package_json(name: &str) -> String {
- format!(
- r#"{{
- "name": "@radroots/{name}",
- "version": "0.1.0",
- "description": "Example package",
- "license": "MIT OR Apache-2.0",
- "homepage": "https://radroots.org",
- "repository": {{
- "type": "git",
- "url": "git+https://github.com/radrootslabs/sdk.git",
- "directory": "packages/{name}"
- }},
- "publishConfig": {{
- "access": "public"
- }},
- "type": "module",
- "sideEffects": false,
- "files": ["dist", "README.md", "LICENSE-MIT", "LICENSE-APACHE"],
- "main": "./dist/index.js",
- "types": "./dist/index.d.ts",
- "exports": {{
- ".": {{
- "types": "./dist/index.d.ts",
- "import": "./dist/index.js",
- "default": "./dist/index.js"
- }}
- }}
-}}"#
- )
- }
-
- fn package_json_with_dependencies(name: &str, dependencies: &str) -> String {
- let raw = package_json(name);
- let body = raw.strip_suffix('}').expect("root package JSON object");
- format!(
- r#"{body},
- "dependencies": {{
- {dependencies}
- }}
-}}"#
- )
- }
-
- fn package_json_value(raw: &str) -> serde_json::Value {
- serde_json::from_str(raw).expect("package json parses")
- }
-
- fn manifest_value(raw: &str) -> toml::Value {
- raw.parse::<toml::Value>().expect("manifest parses")
- }
-}
diff --git a/tools/xtask/src/contracts.rs b/tools/xtask/src/contracts.rs
@@ -1,694 +0,0 @@
-use std::{
- collections::{BTreeMap, BTreeSet},
- fs,
- path::{Path, PathBuf},
-};
-
-use serde::Deserialize;
-
-use crate::package_matrix::{PackageSpec, WasmPackageSpec, package_specs, wasm_package_specs};
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct ExportContract {
- language: LanguageContract,
- packages: BTreeMap<String, String>,
- artifacts: Option<ExportArtifacts>,
- runtime: RuntimeContract,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct PackageContract {
- language: LanguageContract,
- sdk: SdkPackageContract,
- rollout: RolloutContract,
- operations: BTreeMap<String, String>,
- shared_types: BTreeMap<String, String>,
- artifacts: Option<SdkArtifacts>,
- npm_packages: Option<BTreeMap<String, NpmPackageContract>>,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct LanguageContract {
- id: String,
- repository: String,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct RuntimeContract {
- networking: String,
- signing: String,
- deterministic_codec: String,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct ExportArtifacts {
- models_dir: String,
- constants_dir: String,
- wasm_dist_dir: Option<String>,
- manifest_file: String,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct SdkPackageContract {
- package: Option<String>,
- package_family: Option<String>,
- module_format: Option<String>,
- deterministic_codec: String,
- signing: String,
- networking: String,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct NpmPackageContract {
- kind: String,
- crate_name: String,
- crate_dir: String,
- package: String,
- package_dir: String,
- out_name: Option<String>,
- out_dir: Option<String>,
-}
-
-#[derive(Debug)]
-struct ExpectedNpmPackage {
- kind: &'static str,
- crate_name: &'static str,
- crate_dir: &'static str,
- package: &'static str,
- package_dir: &'static str,
- out_name: Option<&'static str>,
- out_dir: Option<&'static str>,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct RolloutContract {
- stage: String,
- order: u32,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct SdkArtifacts {
- models_dir: String,
- runtime_dir: String,
- wasm_dist_dir: String,
- manifest_file: String,
-}
-
-pub fn validate_sdk_contracts(root: &Path) -> Result<(), String> {
- radroots_protocol::runtime::v1::validate_catalog(radroots_protocol::runtime::v1::CATALOG)
- .map_err(|error| error.to_string())?;
- let exports = load_contract_dir::<ExportContract>(&root.join("contracts").join("exports"))?;
- let packages = load_contract_dir::<PackageContract>(&root.join("contracts").join("packages"))?;
- if exports.is_empty() {
- return Err("contracts/exports must define at least one language".to_owned());
- }
- if packages.is_empty() {
- return Err("contracts/packages must define at least one language".to_owned());
- }
-
- let mut export_packages = BTreeMap::new();
- let mut export_languages = BTreeSet::new();
- for export in &exports {
- validate_language(&export.language, "exports")?;
- validate_non_empty_map(&export.packages, "exports packages")?;
- validate_runtime(
- &export.runtime.networking,
- &export.runtime.signing,
- &export.runtime.deterministic_codec,
- &format!("exports {}", export.language.id),
- )?;
- let artifacts = export
- .artifacts
- .as_ref()
- .ok_or_else(|| format!("exports {} artifacts are required", export.language.id))?;
- validate_non_empty(&artifacts.models_dir, "exports artifacts.models_dir")?;
- validate_non_empty(&artifacts.constants_dir, "exports artifacts.constants_dir")?;
- validate_non_empty(&artifacts.manifest_file, "exports artifacts.manifest_file")?;
- if export.language.id == "ts" {
- validate_non_empty(
- artifacts.wasm_dist_dir.as_deref().unwrap_or(""),
- "exports ts artifacts.wasm_dist_dir",
- )?;
- }
- if !export_languages.insert(export.language.id.clone()) {
- return Err(format!("duplicate exports language {}", export.language.id));
- }
- let packages = export
- .packages
- .values()
- .cloned()
- .collect::<BTreeSet<String>>();
- if export.language.id != "ts" && packages.len() != 1 {
- return Err(format!(
- "exports {} must resolve to one curated package",
- export.language.id
- ));
- }
- export_packages.insert(export.language.id.clone(), packages);
- }
-
- let mut package_languages = BTreeSet::new();
- let mut operation_keys: Option<BTreeSet<String>> = None;
- let mut shared_type_keys: Option<BTreeSet<String>> = None;
- let mut rollout_orders = BTreeMap::new();
- for package in &packages {
- validate_language(&package.language, "packages")?;
- if let Some(package_name) = package.sdk.package.as_deref() {
- validate_non_empty(package_name, "packages sdk.package")?;
- }
- if let Some(package_family) = package.sdk.package_family.as_deref() {
- validate_non_empty(package_family, "packages sdk.package_family")?;
- }
- validate_runtime(
- &package.sdk.networking,
- &package.sdk.signing,
- &package.sdk.deterministic_codec,
- &format!("packages {}", package.language.id),
- )?;
- if let Some(module_format) = package.sdk.module_format.as_deref() {
- validate_non_empty(module_format, "packages sdk.module_format")?;
- }
- if package.sdk.package.is_none() && package.sdk.package_family.is_none() {
- return Err(format!(
- "packages {} sdk.package or sdk.package_family is required",
- package.language.id
- ));
- }
- validate_rollout(&package.language.id, &package.rollout)?;
- validate_non_empty_map(&package.operations, "packages operations")?;
- validate_non_empty_map(&package.shared_types, "packages shared_types")?;
- if package.language.id == "ts" {
- let artifacts = package
- .artifacts
- .as_ref()
- .ok_or_else(|| "packages ts artifacts are required".to_owned())?;
- validate_non_empty(&artifacts.models_dir, "packages ts artifacts.models_dir")?;
- validate_non_empty(&artifacts.runtime_dir, "packages ts artifacts.runtime_dir")?;
- validate_non_empty(
- &artifacts.wasm_dist_dir,
- "packages ts artifacts.wasm_dist_dir",
- )?;
- validate_non_empty(
- &artifacts.manifest_file,
- "packages ts artifacts.manifest_file",
- )?;
- }
- if !package_languages.insert(package.language.id.clone()) {
- return Err(format!(
- "duplicate packages language {}",
- package.language.id
- ));
- }
- let Some(packages_for_language) = export_packages.get(&package.language.id) else {
- return Err(format!(
- "packages {} is missing a matching export contract",
- package.language.id
- ));
- };
- if package.language.id == "ts" {
- let npm_packages = package
- .npm_packages
- .as_ref()
- .ok_or_else(|| "packages ts npm_packages are required".to_owned())?;
- validate_ts_npm_packages(package, packages_for_language, npm_packages)?;
- } else {
- if package.npm_packages.is_some() {
- return Err(format!(
- "packages {} npm_packages is only supported for ts",
- package.language.id
- ));
- }
- let sdk_package = package.sdk.package.as_ref().ok_or_else(|| {
- format!("packages {} sdk.package is required", package.language.id)
- })?;
- let expected = [sdk_package.clone()].into_iter().collect::<BTreeSet<_>>();
- if packages_for_language != &expected {
- return Err(format!(
- "exports {} must resolve to package {}",
- package.language.id, sdk_package
- ));
- }
- }
- let current_operations = package.operations.keys().cloned().collect::<BTreeSet<_>>();
- match &operation_keys {
- Some(expected) if expected != ¤t_operations => {
- return Err(format!(
- "packages {} operations must match the shared operation set",
- package.language.id
- ));
- }
- None => operation_keys = Some(current_operations),
- _ => {}
- }
- let current_shared_types = package
- .shared_types
- .keys()
- .cloned()
- .collect::<BTreeSet<_>>();
- match &shared_type_keys {
- Some(expected) if expected != ¤t_shared_types => {
- return Err(format!(
- "packages {} shared_types must match the shared type set",
- package.language.id
- ));
- }
- None => shared_type_keys = Some(current_shared_types),
- _ => {}
- }
- rollout_orders.insert(package.language.id.clone(), package.rollout.order);
- }
-
- if export_languages != package_languages {
- return Err("contracts/exports and contracts/packages languages must match".to_owned());
- }
- if rollout_orders.get("ts") != Some(&1) {
- return Err("packages ts rollout.order must be 1".to_owned());
- }
- Ok(())
-}
-
-fn validate_ts_npm_packages(
- package: &PackageContract,
- export_packages: &BTreeSet<String>,
- npm_packages: &BTreeMap<String, NpmPackageContract>,
-) -> Result<(), String> {
- if package.sdk.package.is_some() {
- return Err(
- "packages ts sdk.package must not be set for multi-package npm output".to_owned(),
- );
- }
- let package_family = package
- .sdk
- .package_family
- .as_deref()
- .ok_or_else(|| "packages ts sdk.package_family is required".to_owned())?;
- if package_family != "@radroots" {
- return Err("packages ts sdk.package_family must be @radroots".to_owned());
- }
- let expected = expected_ts_npm_packages();
- let expected_keys = expected.keys().cloned().collect::<BTreeSet<_>>();
- let actual_keys = npm_packages.keys().cloned().collect::<BTreeSet<_>>();
- if actual_keys != expected_keys {
- let missing = expected_keys
- .difference(&actual_keys)
- .cloned()
- .collect::<Vec<_>>();
- let extra = actual_keys
- .difference(&expected_keys)
- .cloned()
- .collect::<Vec<_>>();
- return Err(format!(
- "packages ts npm_packages must match package matrix: missing {:?}, extra {:?}",
- missing, extra
- ));
- }
- for (key, expected_package) in expected {
- let actual = npm_packages
- .get(&key)
- .ok_or_else(|| format!("packages ts npm package {key} is missing"))?;
- validate_npm_package(&key, actual, &expected_package)?;
- }
- let expected_names = npm_packages
- .values()
- .map(|package| package.package.clone())
- .collect::<BTreeSet<_>>();
- if export_packages != &expected_names {
- return Err("exports ts package set must match TypeScript npm package matrix".to_owned());
- }
- Ok(())
-}
-
-fn expected_ts_npm_packages() -> BTreeMap<String, ExpectedNpmPackage> {
- let mut expected = BTreeMap::new();
- for spec in package_specs() {
- expected.insert(spec.key.to_owned(), expected_from_package_spec(*spec));
- }
- for spec in wasm_package_specs() {
- expected.insert(spec.key.to_owned(), expected_from_wasm_package_spec(*spec));
- }
- expected
-}
-
-fn expected_from_package_spec(spec: PackageSpec) -> ExpectedNpmPackage {
- ExpectedNpmPackage {
- kind: "bindings",
- crate_name: spec.crate_name,
- crate_dir: spec.crate_dir,
- package: spec.package_name,
- package_dir: spec.package_dir,
- out_name: None,
- out_dir: None,
- }
-}
-
-fn expected_from_wasm_package_spec(spec: WasmPackageSpec) -> ExpectedNpmPackage {
- ExpectedNpmPackage {
- kind: "wasm",
- crate_name: spec.crate_name,
- crate_dir: spec.crate_dir,
- package: spec.package_name,
- package_dir: spec.package_dir,
- out_name: Some(spec.out_name),
- out_dir: Some(spec.out_dir),
- }
-}
-
-fn validate_npm_package(
- key: &str,
- actual: &NpmPackageContract,
- expected: &ExpectedNpmPackage,
-) -> Result<(), String> {
- validate_npm_field(key, "kind", &actual.kind, expected.kind)?;
- validate_npm_field(key, "crate_name", &actual.crate_name, expected.crate_name)?;
- validate_npm_field(key, "crate_dir", &actual.crate_dir, expected.crate_dir)?;
- validate_npm_field(key, "package", &actual.package, expected.package)?;
- validate_npm_field(
- key,
- "package_dir",
- &actual.package_dir,
- expected.package_dir,
- )?;
- validate_optional_npm_field(
- key,
- "out_name",
- actual.out_name.as_deref(),
- expected.out_name,
- )?;
- validate_optional_npm_field(key, "out_dir", actual.out_dir.as_deref(), expected.out_dir)?;
- Ok(())
-}
-
-fn validate_npm_field(key: &str, field: &str, actual: &str, expected: &str) -> Result<(), String> {
- validate_non_empty(actual, &format!("packages ts npm package {key} {field}"))?;
- if actual != expected {
- return Err(format!(
- "packages ts npm package {key} {field} must be {expected}"
- ));
- }
- Ok(())
-}
-
-fn validate_optional_npm_field(
- key: &str,
- field: &str,
- actual: Option<&str>,
- expected: Option<&str>,
-) -> Result<(), String> {
- match (actual, expected) {
- (Some(actual), Some(expected)) => validate_npm_field(key, field, actual, expected),
- (None, None) => Ok(()),
- (Some(_), None) => Err(format!(
- "packages ts npm package {key} {field} must not be set"
- )),
- (None, Some(expected)) => Err(format!(
- "packages ts npm package {key} {field} must be {expected}"
- )),
- }
-}
-
-fn load_contract_dir<T>(dir: &Path) -> Result<Vec<T>, String>
-where
- T: for<'de> Deserialize<'de>,
-{
- let read_dir =
- fs::read_dir(dir).map_err(|error| format!("failed to read {}: {error}", dir.display()))?;
- let mut entries = read_dir
- .collect::<Result<Vec<_>, _>>()
- .map_err(|error| format!("failed to read {} entry: {error}", dir.display()))?;
- entries.sort_by_key(|entry| entry.file_name());
- let mut contracts = Vec::new();
- for entry in entries {
- let path = entry.path();
- if path.extension().and_then(|extension| extension.to_str()) != Some("toml") {
- continue;
- }
- contracts.push(parse_toml(&path)?);
- }
- Ok(contracts)
-}
-
-fn parse_toml<T>(path: &PathBuf) -> Result<T, String>
-where
- T: for<'de> Deserialize<'de>,
-{
- let raw = fs::read_to_string(path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- toml::from_str(&raw).map_err(|error| format!("failed to parse {}: {error}", path.display()))
-}
-
-fn validate_language(language: &LanguageContract, family: &str) -> Result<(), String> {
- validate_non_empty(&language.id, &format!("{family} language.id"))?;
- validate_non_empty(
- &language.repository,
- &format!("{family} language.repository"),
- )
-}
-
-fn validate_runtime(
- networking: &str,
- signing: &str,
- deterministic_codec: &str,
- family: &str,
-) -> Result<(), String> {
- validate_non_empty(networking, &format!("{family} networking"))?;
- validate_non_empty(signing, &format!("{family} signing"))?;
- validate_non_empty(
- deterministic_codec,
- &format!("{family} deterministic_codec"),
- )
-}
-
-fn validate_rollout(language: &str, rollout: &RolloutContract) -> Result<(), String> {
- validate_non_empty(&rollout.stage, "packages rollout.stage")?;
- if !matches!(rollout.stage.as_str(), "active" | "next" | "deferred") {
- return Err(format!("packages {language} rollout.stage is invalid"));
- }
- if rollout.order == 0 {
- return Err(format!(
- "packages {language} rollout.order must be greater than zero"
- ));
- }
- Ok(())
-}
-
-fn validate_non_empty(value: &str, field: &str) -> Result<(), String> {
- if value.trim().is_empty() || value.trim() != value {
- return Err(format!("{field} must be non-empty"));
- }
- Ok(())
-}
-
-fn validate_non_empty_map(map: &BTreeMap<String, String>, field: &str) -> Result<(), String> {
- if map.is_empty() {
- return Err(format!("{field} must not be empty"));
- }
- for (key, value) in map {
- validate_non_empty(key, field)?;
- validate_non_empty(value, field)?;
- }
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use std::{
- fs,
- time::{SystemTime, UNIX_EPOCH},
- };
-
- use super::validate_sdk_contracts;
-
- #[test]
- fn current_sdk_contracts_validate() {
- let root = crate::fs::workspace_root().expect("workspace root");
- validate_sdk_contracts(&root).expect("sdk contracts validate");
- }
-
- #[test]
- fn rejects_mismatched_language_sets() {
- let root = test_root("language_mismatch");
- write_contract(&root, "contracts/exports/ts.toml", EXPORT_TS);
- let error = validate_sdk_contracts(&root).expect_err("missing packages should fail");
- assert!(error.contains("contracts/packages"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn rejects_package_export_mismatch() {
- let root = test_root("package_mismatch");
- write_contract(
- &root,
- "contracts/exports/ts.toml",
- EXPORT_TS
- .replace("@radroots/core-bindings", "@radroots/other")
- .as_str(),
- );
- write_contract(&root, "contracts/packages/ts.toml", PACKAGE_TS);
- let error = validate_sdk_contracts(&root).expect_err("mismatch should fail");
- assert!(error.contains("exports ts package set"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn rejects_ts_package_matrix_drift() {
- let root = test_root("ts_matrix_drift");
- write_contract(&root, "contracts/exports/ts.toml", EXPORT_TS);
- write_contract(
- &root,
- "contracts/packages/ts.toml",
- PACKAGE_TS
- .replace(
- "package = \"@radroots/core-bindings\"",
- "package = \"@radroots/other\"",
- )
- .as_str(),
- );
- let error = validate_sdk_contracts(&root).expect_err("mismatch should fail");
- assert!(error.contains("packages ts npm package core package"));
- let _ = fs::remove_dir_all(root);
- }
-
- fn test_root(name: &str) -> std::path::PathBuf {
- let stamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .expect("time")
- .as_nanos();
- std::env::temp_dir().join(format!("radroots_sdk_contracts_{name}_{stamp}"))
- }
-
- fn write_contract(root: &std::path::Path, relative: &str, contents: &str) {
- let path = root.join(relative);
- fs::create_dir_all(path.parent().expect("parent")).expect("create parent");
- fs::write(path, contents).expect("write contract");
- }
-
- const EXPORT_TS: &str = r#"[language]
-id = "ts"
-repository = "sdk-typescript"
-
-[packages]
-"radroots_core" = "@radroots/core-bindings"
-"radroots_event" = "@radroots/event-bindings"
-"radroots_identity" = "@radroots/identity-bindings"
-"radroots_replica_schema" = "@radroots/replica-schema-bindings"
-"radroots_trade" = "@radroots/trade-bindings"
-"radroots_event_codec_wasm" = "@radroots/event-codec-wasm"
-"radroots_replica_store_wasm" = "@radroots/replica-store-wasm"
-"radroots_replica_sync_wasm" = "@radroots/replica-sync-wasm"
-
-[artifacts]
-models_dir = "src/generated"
-constants_dir = "src/generated"
-wasm_dist_dir = "dist"
-manifest_file = "export-manifest.json"
-
-[runtime]
-networking = "native"
-signing = "native"
-deterministic_codec = "wasm"
-"#;
-
- const PACKAGE_TS: &str = r#"[language]
-id = "ts"
-repository = "sdk-typescript"
-
-[sdk]
-package_family = "@radroots"
-module_format = "esm"
-deterministic_codec = "wasm"
-signing = "native"
-networking = "native"
-
-[npm_packages.core]
-kind = "bindings"
-crate_name = "radroots_core_bindings"
-crate_dir = "crates/core_bindings"
-package = "@radroots/core-bindings"
-package_dir = "packages/core-bindings"
-
-[npm_packages.event]
-kind = "bindings"
-crate_name = "radroots_event_bindings"
-crate_dir = "crates/event_bindings"
-package = "@radroots/event-bindings"
-package_dir = "packages/event-bindings"
-
-[npm_packages.identity]
-kind = "bindings"
-crate_name = "radroots_identity_bindings"
-crate_dir = "crates/identity_bindings"
-package = "@radroots/identity-bindings"
-package_dir = "packages/identity-bindings"
-
-[npm_packages.replica_schema]
-kind = "bindings"
-crate_name = "radroots_replica_schema_bindings"
-crate_dir = "crates/replica_schema_bindings"
-package = "@radroots/replica-schema-bindings"
-package_dir = "packages/replica-schema-bindings"
-
-[npm_packages.trade]
-kind = "bindings"
-crate_name = "radroots_trade_bindings"
-crate_dir = "crates/trade_bindings"
-package = "@radroots/trade-bindings"
-package_dir = "packages/trade-bindings"
-
-[npm_packages.event_codec]
-kind = "wasm"
-crate_name = "radroots_event_codec_wasm"
-crate_dir = "crates/event_codec_wasm"
-package = "@radroots/event-codec-wasm"
-package_dir = "packages/event-codec-wasm"
-out_name = "radroots_event_codec_wasm"
-out_dir = "../../packages/event-codec-wasm/dist"
-
-[npm_packages.replica_store]
-kind = "wasm"
-crate_name = "radroots_replica_store_wasm"
-crate_dir = "crates/replica_store_wasm"
-package = "@radroots/replica-store-wasm"
-package_dir = "packages/replica-store-wasm"
-out_name = "radroots_replica_store_wasm"
-out_dir = "../../packages/replica-store-wasm/dist"
-
-[npm_packages.replica_sync]
-kind = "wasm"
-crate_name = "radroots_replica_sync_wasm"
-crate_dir = "crates/replica_sync_wasm"
-package = "@radroots/replica-sync-wasm"
-package_dir = "packages/replica-sync-wasm"
-out_name = "radroots_replica_sync_wasm"
-out_dir = "../../packages/replica-sync-wasm/dist"
-
-[rollout]
-stage = "active"
-order = 1
-
-[operations]
-"farm.build_draft" = "farm.buildDraft"
-
-[shared_types]
-"RadrootsNip01EventWireParts" = "RadrootsNip01EventWireParts"
-"RadrootsNip01EventWireDto" = "RadrootsNip01EventWireDto"
-"RadrootsEventDraft" = "RadrootsEventDraft"
-"RadrootsSignedEventDto" = "RadrootsSignedEventDto"
-"RadrootsVerifiedSignedEventDto" = "RadrootsVerifiedSignedEventDto"
-"RadrootsEventEnvelopeDto" = "RadrootsEventEnvelopeDto"
-
-[artifacts]
-models_dir = "src/generated"
-runtime_dir = "src/runtime"
-wasm_dist_dir = "dist"
-manifest_file = "export-manifest.json"
-"#;
-}
diff --git a/tools/xtask/src/coverage.rs b/tools/xtask/src/coverage.rs
@@ -1,156 +0,0 @@
-use std::{fs, path::Path, process::Command};
-
-use crate::{
- check,
- coverage_policy::{CoverageContract, evaluate_report, validate_contract},
- fs::workspace_root,
- generate, wasm,
-};
-
-pub fn run(args: &[String]) -> Result<(), String> {
- match args {
- [command] if command == "run" => run_coverage(),
- [command] if command == "check" => check_coverage(),
- [] => Err(usage()),
- _ => Err(usage()),
- }
-}
-
-fn usage() -> String {
- "usage: cargo xtask coverage run | cargo xtask coverage check".to_owned()
-}
-
-fn run_coverage() -> Result<(), String> {
- let root = workspace_root()?;
- let contract = load_contract(&root)?;
- validate_contract(&contract)?;
- preflight(&contract)?;
- generate::generate_ts()?;
- wasm::generate(&[])?;
- check::check()?;
- clean_report_output(&root, &contract)?;
- run_llvm_cov(&root, &contract)?;
- evaluate_report(&root, &root.join(&contract.report.output), &contract)
-}
-
-fn check_coverage() -> Result<(), String> {
- let root = workspace_root()?;
- let contract = load_contract(&root)?;
- validate_contract(&contract)?;
- evaluate_report(&root, &root.join(&contract.report.output), &contract)
-}
-
-fn load_contract(root: &Path) -> Result<CoverageContract, String> {
- let path = root.join("contracts").join("coverage.toml");
- let raw = fs::read_to_string(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- toml::from_str(&raw).map_err(|error| format!("failed to parse {}: {error}", path.display()))
-}
-
-fn preflight(contract: &CoverageContract) -> Result<(), String> {
- require_command("rustup", &["--version"], "install rustup")?;
- require_command(
- "rustup",
- &[
- "run",
- &contract.toolchain.coverage_rust,
- "cargo",
- "llvm-cov",
- "--version",
- ],
- "install cargo-llvm-cov for the SDK Rust toolchain",
- )?;
- let component_output = output(
- "rustup",
- &[
- "component",
- "list",
- "--toolchain",
- &contract.toolchain.coverage_rust,
- ],
- )
- .map_err(|error| format!("failed to inspect Rust components: {error}"))?;
- if !component_output
- .lines()
- .any(|line| line.starts_with("llvm-tools") && line.contains("(installed)"))
- {
- return Err(format!(
- "missing llvm-tools-preview for Rust toolchain {}; run `rustup component add llvm-tools-preview --toolchain {}`",
- contract.toolchain.coverage_rust, contract.toolchain.coverage_rust
- ));
- }
- let target_output = output(
- "rustup",
- &["target", "list", "--toolchain", &contract.toolchain.rust],
- )
- .map_err(|error| format!("failed to inspect Rust targets: {error}"))?;
- let expected_target = format!("{} (installed)", contract.toolchain.wasm_target);
- if !target_output.lines().any(|line| line == expected_target) {
- return Err(format!(
- "missing Rust target {}; run `rustup target add {} --toolchain {}`",
- contract.toolchain.wasm_target, contract.toolchain.wasm_target, contract.toolchain.rust
- ));
- }
- require_command("wasm-pack", &["--version"], "install wasm-pack")?;
- require_command("pnpm", &["--version"], "install pnpm")?;
- Ok(())
-}
-
-fn require_command(command: &str, args: &[&str], install_hint: &str) -> Result<(), String> {
- output(command, args)
- .map(|_| ())
- .map_err(|error| format!("missing {command}: {error}; {install_hint}"))
-}
-
-fn output(command: &str, args: &[&str]) -> Result<String, String> {
- let output = Command::new(command)
- .args(args)
- .output()
- .map_err(|error| error.to_string())?;
- if !output.status.success() {
- return Err(String::from_utf8_lossy(&output.stderr).trim().to_string());
- }
- Ok(String::from_utf8_lossy(&output.stdout).to_string())
-}
-
-fn clean_report_output(root: &Path, contract: &CoverageContract) -> Result<(), String> {
- let output_path = root.join(&contract.report.output);
- if let Some(parent) = output_path.parent()
- && parent.exists()
- {
- fs::remove_dir_all(parent)
- .map_err(|error| format!("failed to remove {}: {error}", parent.display()))?;
- }
- Ok(())
-}
-
-fn run_llvm_cov(root: &Path, contract: &CoverageContract) -> Result<(), String> {
- let output_path = root.join(&contract.report.output);
- if let Some(parent) = output_path.parent() {
- fs::create_dir_all(parent)
- .map_err(|error| format!("failed to create {}: {error}", parent.display()))?;
- }
- let status = Command::new("rustup")
- .current_dir(root)
- .args([
- "run",
- &contract.toolchain.coverage_rust,
- "cargo",
- "llvm-cov",
- "--workspace",
- "--all-features",
- "--json",
- "--branch",
- "--output-path",
- &contract.report.output,
- "--ignore-filename-regex",
- &contract.report.ignore_filename_regex,
- "--no-fail-fast",
- ])
- .status()
- .map_err(|error| format!("failed to run cargo llvm-cov: {error}"))?;
- if !status.success() {
- return Err(format!("cargo llvm-cov failed with status {status}"));
- }
- Ok(())
-}
diff --git a/tools/xtask/src/coverage_policy.rs b/tools/xtask/src/coverage_policy.rs
@@ -1,803 +0,0 @@
-use std::{
- collections::BTreeMap,
- fs,
- path::{Component, Path, PathBuf},
-};
-
-use serde::Deserialize;
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct CoverageContract {
- policy: CoveragePolicy,
- pub(crate) toolchain: CoverageToolchain,
- pub(crate) report: CoverageReport,
- generated: GeneratedCoveragePolicy,
- scopes: BTreeMap<String, CoverageScope>,
- exclusions: BTreeMap<String, CoverageExclusion>,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct CoveragePolicy {
- enforce: bool,
- require_regions: bool,
- require_functions: bool,
- require_executable_lines: bool,
- require_branches: bool,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct CoverageToolchain {
- pub(crate) rust: String,
- pub(crate) coverage_rust: String,
- pub(crate) wasm_target: String,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct CoverageReport {
- pub(crate) output: String,
- pub(crate) ignore_filename_regex: String,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct GeneratedCoveragePolicy {
- typescript: String,
- binding_crates: String,
- wasm_glue: String,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct CoverageScope {
- paths: Vec<String>,
- threshold: f64,
- #[serde(default = "default_true")]
- branches_applicable: bool,
- branch_reason: Option<String>,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct CoverageExclusion {
- paths: Vec<String>,
- reason: String,
-}
-
-const fn default_true() -> bool {
- true
-}
-
-#[derive(Debug, Deserialize)]
-struct LlvmCovReport {
- data: Vec<LlvmCovData>,
-}
-
-#[derive(Debug, Deserialize)]
-struct LlvmCovData {
- files: Vec<LlvmCovFile>,
- #[serde(default)]
- functions: Vec<LlvmCovFunction>,
- totals: LlvmCovSummary,
-}
-
-#[derive(Debug, Deserialize)]
-struct LlvmCovFunction {
- count: u64,
- filenames: Vec<String>,
- regions: Vec<Vec<u64>>,
- #[serde(default)]
- branches: Vec<Vec<u64>>,
-}
-
-#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
-struct FunctionKey {
- filenames: Vec<String>,
- definition: RegionKey,
-}
-
-#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
-struct RegionKey {
- line_start: u64,
- column_start: u64,
- line_end: u64,
- column_end: u64,
- kind: u64,
-}
-
-#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
-struct BranchKey {
- line_start: u64,
- column_start: u64,
- line_end: u64,
- column_end: u64,
- kind: u64,
-}
-
-#[derive(Debug)]
-struct CoverageSource {
- raw: String,
- cfg_test_lines: Vec<bool>,
- coverage_off_lines: Vec<bool>,
-}
-
-#[derive(Debug, Deserialize)]
-struct LlvmCovFile {
- filename: String,
- summary: LlvmCovSummary,
-}
-
-#[derive(Debug, Deserialize)]
-struct LlvmCovSummary {
- lines: LlvmCovMetric,
- functions: LlvmCovMetric,
- regions: LlvmCovMetric,
- branches: LlvmCovMetric,
-}
-
-#[derive(Debug, Deserialize)]
-struct LlvmCovMetric {
- count: u64,
- covered: u64,
- percent: f64,
-}
-
-#[derive(Debug, Default)]
-struct MetricAccumulator {
- count: u64,
- covered: u64,
-}
-
-impl MetricAccumulator {
- fn add(&mut self, metric: &LlvmCovMetric) {
- self.count += metric.count;
- self.covered += metric.covered;
- }
-
- fn metric(&self) -> LlvmCovMetric {
- LlvmCovMetric {
- count: self.count,
- covered: self.covered,
- percent: metric_percent(self.count, self.covered),
- }
- }
-}
-
-#[derive(Debug, Default)]
-struct SummaryAccumulator {
- lines: MetricAccumulator,
- functions: MetricAccumulator,
- regions: MetricAccumulator,
- branches: MetricAccumulator,
- matched_files: usize,
-}
-
-impl SummaryAccumulator {
- fn add(&mut self, summary: &LlvmCovSummary) {
- self.lines.add(&summary.lines);
- self.functions.add(&summary.functions);
- self.regions.add(&summary.regions);
- self.branches.add(&summary.branches);
- self.matched_files += 1;
- }
-
- fn summary(&self) -> LlvmCovSummary {
- LlvmCovSummary {
- lines: self.lines.metric(),
- functions: self.functions.metric(),
- regions: self.regions.metric(),
- branches: self.branches.metric(),
- }
- }
-}
-
-pub(crate) fn validate_contract(contract: &CoverageContract) -> Result<(), String> {
- validate_non_empty(&contract.toolchain.rust, "toolchain.rust")?;
- validate_non_empty(&contract.toolchain.coverage_rust, "toolchain.coverage_rust")?;
- validate_non_empty(&contract.toolchain.wasm_target, "toolchain.wasm_target")?;
- validate_non_empty(&contract.report.output, "report.output")?;
- validate_non_empty(
- &contract.report.ignore_filename_regex,
- "report.ignore_filename_regex",
- )?;
- validate_non_empty(&contract.generated.typescript, "generated.typescript")?;
- validate_non_empty(
- &contract.generated.binding_crates,
- "generated.binding_crates",
- )?;
- validate_non_empty(&contract.generated.wasm_glue, "generated.wasm_glue")?;
- if contract.scopes.is_empty() {
- return Err("contracts/coverage.toml scopes must not be empty".to_owned());
- }
- for (name, scope) in &contract.scopes {
- validate_non_empty(name, "scope name")?;
- validate_threshold(scope.threshold, &format!("scopes.{name}.threshold"))?;
- if scope.paths.is_empty() {
- return Err(format!("scopes.{name}.paths must not be empty"));
- }
- for path in &scope.paths {
- validate_non_empty(path, &format!("scopes.{name}.paths entry"))?;
- }
- if scope.branches_applicable {
- if scope.branch_reason.is_some() {
- return Err(format!(
- "scopes.{name}.branch_reason requires branches_applicable = false"
- ));
- }
- } else {
- validate_non_empty(
- scope.branch_reason.as_deref().unwrap_or_default(),
- &format!("scopes.{name}.branch_reason"),
- )?;
- }
- }
- if contract.exclusions.is_empty() {
- return Err("contracts/coverage.toml exclusions must not be empty".to_owned());
- }
- for (name, exclusion) in &contract.exclusions {
- validate_non_empty(name, "exclusion name")?;
- validate_non_empty(&exclusion.reason, &format!("exclusions.{name}.reason"))?;
- if exclusion.paths.is_empty() {
- return Err(format!("exclusions.{name}.paths must not be empty"));
- }
- for path in &exclusion.paths {
- validate_non_empty(path, &format!("exclusions.{name}.paths entry"))?;
- }
- }
- Ok(())
-}
-
-fn validate_threshold(threshold: f64, field: &str) -> Result<(), String> {
- if (0.0..=100.0).contains(&threshold) {
- Ok(())
- } else {
- Err(format!(
- "contracts/coverage.toml {field} must be between 0 and 100"
- ))
- }
-}
-
-fn validate_non_empty(value: &str, field: &str) -> Result<(), String> {
- if value.trim().is_empty() {
- Err(format!("contracts/coverage.toml {field} must not be empty"))
- } else {
- Ok(())
- }
-}
-
-pub(crate) fn evaluate_report(
- root: &Path,
- report_path: &Path,
- contract: &CoverageContract,
-) -> Result<(), String> {
- let raw = fs::read_to_string(report_path)
- .map_err(|error| format!("failed to read {}: {error}", report_path.display()))?;
- let report = serde_json::from_str::<LlvmCovReport>(&raw)
- .map_err(|error| format!("failed to parse {}: {error}", report_path.display()))?;
- let data = report
- .data
- .first()
- .ok_or_else(|| format!("{} did not include coverage data", report_path.display()))?;
- validate_metric(
- "total executable lines",
- &data.totals.lines,
- contract.policy.require_executable_lines,
- )?;
- validate_metric(
- "total functions",
- &data.totals.functions,
- contract.policy.require_functions,
- )?;
- validate_metric(
- "total regions",
- &data.totals.regions,
- contract.policy.require_regions,
- )?;
- validate_metric(
- "total branches",
- &data.totals.branches,
- contract.policy.require_branches,
- )?;
- let mut failures = Vec::new();
- for (scope_name, scope) in &contract.scopes {
- let scope_summary = match scope_summary(root, data, scope) {
- Ok(summary) => summary,
- Err(error) => {
- failures.push(format!("coverage scope {scope_name}: {error}"));
- continue;
- }
- };
- collect_scope_metric_failure(
- &mut failures,
- scope_name,
- "executable lines",
- &scope_summary.lines,
- scope.threshold,
- contract.policy.require_executable_lines,
- );
- collect_scope_metric_failure(
- &mut failures,
- scope_name,
- "functions",
- &scope_summary.functions,
- scope.threshold,
- contract.policy.require_functions,
- );
- collect_scope_metric_failure(
- &mut failures,
- scope_name,
- "regions",
- &scope_summary.regions,
- scope.threshold,
- contract.policy.require_regions,
- );
- if scope.branches_applicable {
- collect_scope_metric_failure(
- &mut failures,
- scope_name,
- "branches",
- &scope_summary.branches,
- scope.threshold,
- contract.policy.require_branches,
- );
- } else if scope_summary.branches.count != 0 {
- failures.push(format!(
- "coverage scope {scope_name}: declared branches inapplicable but measured {} branch records",
- scope_summary.branches.count
- ));
- }
- }
- if !contract.policy.enforce {
- println!(
- "coverage policy parsed and measured; enforcement disabled in {}",
- report_path.display()
- );
- return Ok(());
- }
- if !failures.is_empty() {
- return Err(failures.join("\n"));
- }
- println!("coverage policy passed using {}", report_path.display());
- Ok(())
-}
-
-fn scope_summary(
- root: &Path,
- data: &LlvmCovData,
- scope: &CoverageScope,
-) -> Result<LlvmCovSummary, String> {
- let mut accumulator = SummaryAccumulator::default();
- for file in &data.files {
- let filename = report_filename(root, &file.filename);
- if scope
- .paths
- .iter()
- .any(|pattern| path_matches(pattern, &filename))
- {
- accumulator.add(&file.summary);
- }
- }
- if accumulator.matched_files == 0 {
- return Err(format!(
- "matched no report files for {}",
- scope.paths.join(", ")
- ));
- }
- let mut summary = accumulator.summary();
- if !data.functions.is_empty() {
- let (lines, functions, regions, branches) = normalized_source_metrics(root, data, scope)?;
- summary.lines = lines;
- summary.functions = functions;
- summary.regions = regions;
- if branches.count > 0 {
- summary.branches = branches;
- }
- }
- Ok(summary)
-}
-
-fn normalized_source_metrics(
- root: &Path,
- data: &LlvmCovData,
- scope: &CoverageScope,
-) -> Result<(LlvmCovMetric, LlvmCovMetric, LlvmCovMetric, LlvmCovMetric), String> {
- let mut groups = BTreeMap::<FunctionKey, Vec<&LlvmCovFunction>>::new();
- for function in &data.functions {
- if function.filenames.is_empty() || function.regions.is_empty() {
- continue;
- }
- let Some(definition) = function
- .regions
- .first()
- .and_then(|region| region_key(region))
- else {
- continue;
- };
- groups
- .entry(FunctionKey {
- filenames: function.filenames.clone(),
- definition,
- })
- .or_default()
- .push(function);
- }
- let mut function_count = 0_u64;
- let mut function_covered = 0_u64;
- let mut all_regions = BTreeMap::<(String, RegionKey), bool>::new();
- let mut lines = BTreeMap::<(String, u64), bool>::new();
- let mut branches = BTreeMap::<(String, BranchKey), (bool, bool)>::new();
- let mut source_cache = BTreeMap::<PathBuf, Option<CoverageSource>>::new();
- for variants in groups.values() {
- let primary_definition = variants.iter().find_map(|function| {
- let region = function.regions.first()?;
- let filename = region_filename(function, region)?;
- let relative = report_filename(root, filename);
- if scope.paths.iter().any(|path| path_matches(path, &relative)) {
- Some((filename, region[0]))
- } else {
- None
- }
- });
- let Some((primary, definition_line)) = primary_definition else {
- continue;
- };
- if variants.iter().all(|function| {
- function.regions.first().is_some_and(|region| {
- region
- .first()
- .is_some_and(|line| is_ignorable_source_line(primary, *line, &mut source_cache))
- })
- }) {
- continue;
- }
- if is_authored_function_line(primary, definition_line, &mut source_cache) {
- function_count += 1;
- if variants.iter().any(|function| function.count > 0) {
- function_covered += 1;
- }
- }
- let mut regions = BTreeMap::<(String, RegionKey), bool>::new();
- for function in variants {
- for region in &function.regions {
- let filename = region_filename(function, region)
- .expect("grouped coverage functions always retain a source filename");
- let relative = report_filename(root, filename);
- if !scope.paths.iter().any(|path| path_matches(path, &relative)) {
- continue;
- }
- let Some(key) = region_key(region) else {
- continue;
- };
- let covered = region.get(4).is_some_and(|count| *count > 0);
- regions
- .entry((filename.to_owned(), key))
- .and_modify(|existing| *existing |= covered)
- .or_insert(covered);
- }
- }
- for ((filename, region), covered) in regions {
- if is_ignorable_source_line(&filename, region.line_start, &mut source_cache) {
- continue;
- }
- let filename = normalized_path_string(Path::new(&filename));
- all_regions
- .entry((filename.clone(), region.clone()))
- .and_modify(|existing| *existing |= covered)
- .or_insert(covered);
- if region.kind == 0 {
- for line in region.line_start..=region.line_end {
- if !is_ignorable_source_line(&filename, line, &mut source_cache) {
- lines
- .entry((filename.clone(), line))
- .and_modify(|existing| *existing |= covered)
- .or_insert(covered);
- }
- }
- }
- }
- for function in variants {
- for branch in &function.branches {
- let filename = branch_filename(function, branch)
- .expect("grouped coverage functions always retain a source filename");
- let relative = report_filename(root, filename);
- if !scope.paths.iter().any(|path| path_matches(path, &relative)) {
- continue;
- }
- let Some(key) = branch_key(branch) else {
- continue;
- };
- if is_ignorable_branch(filename, &key, &mut source_cache) {
- continue;
- }
- let true_covered = branch.get(4).is_some_and(|count| *count > 0);
- let false_covered = branch.get(5).is_some_and(|count| *count > 0);
- let filename = normalized_path_string(Path::new(filename));
- branches
- .entry((filename, key))
- .and_modify(|covered| {
- covered.0 |= true_covered;
- covered.1 |= false_covered;
- })
- .or_insert((true_covered, false_covered));
- }
- }
- }
- let line_count = lines.len() as u64;
- let line_covered = lines.values().filter(|covered| **covered).count() as u64;
- let branch_count = (branches.len() * 2) as u64;
- let branch_covered = branches
- .values()
- .map(|covered| u64::from(covered.0) + u64::from(covered.1))
- .sum::<u64>();
- let region_count = all_regions.len() as u64;
- let region_covered = all_regions.values().filter(|covered| **covered).count() as u64;
- Ok((
- LlvmCovMetric {
- count: line_count,
- covered: line_covered,
- percent: metric_percent(line_count, line_covered),
- },
- LlvmCovMetric {
- count: function_count,
- covered: function_covered,
- percent: metric_percent(function_count, function_covered),
- },
- LlvmCovMetric {
- count: region_count,
- covered: region_covered,
- percent: metric_percent(region_count, region_covered),
- },
- LlvmCovMetric {
- count: branch_count,
- covered: branch_covered,
- percent: metric_percent(branch_count, branch_covered),
- },
- ))
-}
-
-fn region_filename<'a>(function: &'a LlvmCovFunction, region: &[u64]) -> Option<&'a str> {
- region
- .get(5)
- .and_then(|index| function.filenames.get(*index as usize))
- .or_else(|| function.filenames.first())
- .map(String::as_str)
-}
-
-fn branch_filename<'a>(function: &'a LlvmCovFunction, branch: &[u64]) -> Option<&'a str> {
- branch
- .get(6)
- .and_then(|index| function.filenames.get(*index as usize))
- .or_else(|| function.filenames.first())
- .map(String::as_str)
-}
-
-fn is_authored_function_line(
- filename: &str,
- line: u64,
- cache: &mut BTreeMap<PathBuf, Option<CoverageSource>>,
-) -> bool {
- let path = PathBuf::from(filename);
- if !cache.contains_key(&path) {
- let _ = is_ignorable_source_line(filename, line, cache);
- }
- cache
- .get(&path)
- .and_then(Option::as_ref)
- .and_then(|source| source.raw.lines().nth(line.saturating_sub(1) as usize))
- .is_some_and(|source_line| source_line.contains("fn "))
-}
-
-fn region_key(region: &[u64]) -> Option<RegionKey> {
- Some(RegionKey {
- line_start: *region.first()?,
- column_start: *region.get(1)?,
- line_end: *region.get(2)?,
- column_end: *region.get(3)?,
- kind: *region.get(7)?,
- })
-}
-
-fn branch_key(branch: &[u64]) -> Option<BranchKey> {
- Some(BranchKey {
- line_start: *branch.first()?,
- column_start: *branch.get(1)?,
- line_end: *branch.get(2)?,
- column_end: *branch.get(3)?,
- kind: *branch.get(8)?,
- })
-}
-
-fn is_ignorable_source_line(
- filename: &str,
- line: u64,
- cache: &mut BTreeMap<PathBuf, Option<CoverageSource>>,
-) -> bool {
- let path = PathBuf::from(filename);
- let source = cache.entry(path.clone()).or_insert_with(|| {
- fs::read_to_string(path).ok().map(|raw| CoverageSource {
- raw: raw.clone(),
- cfg_test_lines: annotated_source_lines(&raw, "cfg(test)"),
- coverage_off_lines: annotated_source_lines(
- &raw,
- "cfg_attr(coverage_nightly, coverage(off))",
- ),
- })
- });
- let Some(source) = source else {
- return false;
- };
- line.checked_sub(1)
- .map(|index| {
- let index = index as usize;
- source.cfg_test_lines.get(index).copied().unwrap_or(false)
- || source
- .coverage_off_lines
- .get(index)
- .copied()
- .unwrap_or(false)
- })
- .unwrap_or(false)
-}
-
-fn is_ignorable_branch(
- filename: &str,
- branch: &BranchKey,
- cache: &mut BTreeMap<PathBuf, Option<CoverageSource>>,
-) -> bool {
- if is_ignorable_source_line(filename, branch.line_start, cache) {
- return true;
- }
- if branch.line_start != branch.line_end {
- return false;
- }
- let path = PathBuf::from(filename);
- let Some(Some(source)) = cache.get(&path) else {
- return false;
- };
- let Some(line) = source
- .raw
- .lines()
- .nth(branch.line_start.saturating_sub(1) as usize)
- else {
- return false;
- };
- let start = branch.column_start.saturating_sub(1) as usize;
- let end = branch.column_end.saturating_sub(1) as usize;
- let slice = line.get(start..end);
- (branch.column_end == branch.column_start + 1 && slice == Some("?"))
- || line.contains("unreachable!()")
- || (line.contains("assert!(matches!(") && slice == Some("matches!"))
-}
-
-fn annotated_source_lines(source: &str, marker: &str) -> Vec<bool> {
- let mut pending = false;
- let mut depth = None;
- let mut lines = Vec::with_capacity(source.lines().count());
- for line in source.lines() {
- let trimmed = line.trim();
- let mut annotated = depth.is_some();
- let marker_matches = if marker == "cfg(test)" {
- trimmed.starts_with("#[cfg(test)]") || trimmed.starts_with("#[cfg(all(test,")
- } else {
- trimmed.contains(marker)
- };
- if depth.is_none() && marker_matches {
- pending = true;
- annotated = true;
- } else if depth.is_none() && pending {
- annotated = true;
- let content =
- !trimmed.is_empty() && !trimmed.starts_with("//") && !trimmed.starts_with("#[");
- if content {
- let delta = brace_delta(trimmed);
- if delta > 0 {
- depth = Some(0_i64);
- pending = false;
- } else if trimmed.contains('{') || trimmed.ends_with(';') {
- pending = false;
- }
- }
- }
- lines.push(annotated);
- if let Some(current) = depth.as_mut() {
- *current += brace_delta(trimmed);
- if *current <= 0 {
- depth = None;
- }
- }
- }
- lines
-}
-
-fn brace_delta(line: &str) -> i64 {
- line.bytes().filter(|byte| *byte == b'{').count() as i64
- - line.bytes().filter(|byte| *byte == b'}').count() as i64
-}
-
-fn report_filename(root: &Path, filename: &str) -> String {
- let path = normalize_path(Path::new(filename));
- let root = normalize_path(root);
- let relative = path.strip_prefix(&root).unwrap_or(&path);
- normalized_path_string(relative)
-}
-
-fn normalize_path(path: &Path) -> PathBuf {
- let mut normalized = PathBuf::new();
- for component in path.components() {
- match component {
- Component::CurDir => {}
- Component::ParentDir => {
- let _ = normalized.pop();
- }
- Component::Prefix(_) | Component::RootDir | Component::Normal(_) => {
- normalized.push(component.as_os_str());
- }
- }
- }
- normalized
-}
-
-fn normalized_path_string(path: &Path) -> String {
- normalize_path(path).to_string_lossy().replace('\\', "/")
-}
-
-fn path_matches(pattern: &str, path: &str) -> bool {
- if let Some(prefix) = pattern.strip_suffix("/**") {
- path == prefix || path.starts_with(&format!("{prefix}/"))
- } else {
- path == pattern
- }
-}
-
-fn collect_scope_metric_failure(
- failures: &mut Vec<String>,
- scope_name: &str,
- metric_name: &str,
- metric: &LlvmCovMetric,
- threshold: f64,
- required: bool,
-) {
- if let Err(error) = validate_metric(metric_name, metric, required) {
- failures.push(format!("coverage scope {scope_name}: {error}"));
- }
- if let Err(error) = enforce_metric(metric_name, metric, threshold) {
- failures.push(format!("coverage scope {scope_name}: {error}"));
- }
-}
-
-fn validate_metric(name: &str, metric: &LlvmCovMetric, required: bool) -> Result<(), String> {
- if required && metric.count == 0 {
- return Err(format!(
- "coverage report did not include required {name} records"
- ));
- }
- if metric.covered > metric.count {
- return Err(format!("coverage report has invalid {name} counts"));
- }
- Ok(())
-}
-
-fn enforce_metric(name: &str, metric: &LlvmCovMetric, threshold: f64) -> Result<(), String> {
- if metric.percent < threshold {
- return Err(format!(
- "coverage {name} {:.3}% ({}/{}) is below required {:.1}%",
- metric.percent, metric.covered, metric.count, threshold
- ));
- }
- Ok(())
-}
-
-fn metric_percent(count: u64, covered: u64) -> f64 {
- if count == 0 {
- 0.0
- } else {
- covered as f64 * 100.0 / count as f64
- }
-}
-
-#[cfg(test)]
-#[path = "coverage_policy_tests.rs"]
-mod tests;
diff --git a/tools/xtask/src/coverage_policy_tests.rs b/tools/xtask/src/coverage_policy_tests.rs
@@ -1,661 +0,0 @@
-use std::{
- fs,
- path::{Path, PathBuf},
- time::{SystemTime, UNIX_EPOCH},
-};
-
-use super::{
- BranchKey, CoverageContract, LlvmCovFunction, LlvmCovMetric, LlvmCovReport,
- annotated_source_lines, brace_delta, branch_filename, branch_key, enforce_metric,
- evaluate_report, is_authored_function_line, is_ignorable_branch, is_ignorable_source_line,
- metric_percent, normalized_source_metrics, path_matches, region_filename, region_key,
- report_filename, validate_contract, validate_metric,
-};
-
-const CONTRACT: &str = r#"
-[policy]
-enforce = true
-require_regions = true
-require_functions = true
-require_executable_lines = true
-require_branches = true
-
-[toolchain]
-rust = "1.97.1"
-coverage_rust = "nightly"
-wasm_target = "wasm32-unknown-unknown"
-
-[report]
-output = "target/sdk-coverage/summary.json"
-ignore_filename_regex = "generated"
-
-[generated]
-typescript = "generated TypeScript is checked elsewhere"
-binding_crates = "generated binding crates are checked elsewhere"
-wasm_glue = "wasm glue is checked through package validation"
-
-[scopes.xtask_policy]
-paths = ["tools/xtask/src/coverage_policy.rs"]
-threshold = 90.0
-
-[exclusions.generated]
-paths = ["packages/*/src/generated/**"]
-reason = "generated output is checked through reproducibility"
-"#;
-
-#[derive(Clone, Copy)]
-struct Metrics {
- lines: (u64, u64, f64),
- functions: (u64, u64, f64),
- regions: (u64, u64, f64),
- branches: (u64, u64, f64),
-}
-
-fn covered() -> Metrics {
- Metrics {
- lines: (100, 100, 100.0),
- functions: (50, 50, 100.0),
- regions: (200, 200, 100.0),
- branches: (80, 80, 100.0),
- }
-}
-
-fn contract(raw: &str) -> CoverageContract {
- toml::from_str::<CoverageContract>(raw).expect("contract parses")
-}
-
-fn test_root(name: &str) -> PathBuf {
- let stamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .expect("time")
- .as_nanos();
- let root = std::env::temp_dir().join(format!(
- "radroots_sdk_coverage_policy_{name}_{}_{}",
- std::process::id(),
- stamp
- ));
- let _ = fs::remove_dir_all(&root);
- fs::create_dir_all(&root).expect("create root");
- root
-}
-
-fn metric_json(metric: (u64, u64, f64)) -> String {
- format!(
- r#"{{"count":{},"covered":{},"percent":{}}}"#,
- metric.0, metric.1, metric.2
- )
-}
-
-fn summary_json(metrics: Metrics) -> String {
- format!(
- r#"{{"lines":{},"functions":{},"regions":{},"branches":{}}}"#,
- metric_json(metrics.lines),
- metric_json(metrics.functions),
- metric_json(metrics.regions),
- metric_json(metrics.branches)
- )
-}
-
-fn report_json(filename: &str, file_metrics: Metrics, totals: Metrics) -> String {
- format!(
- r#"{{"data":[{{"files":[{{"filename":"{}","summary":{}}}],"totals":{}}}]}}"#,
- filename,
- summary_json(file_metrics),
- summary_json(totals)
- )
-}
-
-fn write_report(root: &Path, raw: &str) -> PathBuf {
- let report_path = root.join("summary.json");
- fs::write(&report_path, raw).expect("write report");
- report_path
-}
-
-fn scope_file(root: &Path) -> String {
- root.join("tools/xtask/src/coverage_policy.rs")
- .display()
- .to_string()
-}
-
-#[test]
-fn validates_contract_shape() {
- validate_contract(&contract(CONTRACT)).expect("contract validates");
-}
-
-#[test]
-fn rejects_blank_contract_fields() {
- let cases = [
- ("rust = \"1.97.1\"", "rust = \" \"", "toolchain.rust"),
- (
- "coverage_rust = \"nightly\"",
- "coverage_rust = \" \"",
- "toolchain.coverage_rust",
- ),
- (
- "wasm_target = \"wasm32-unknown-unknown\"",
- "wasm_target = \" \"",
- "toolchain.wasm_target",
- ),
- (
- "output = \"target/sdk-coverage/summary.json\"",
- "output = \" \"",
- "report.output",
- ),
- (
- "ignore_filename_regex = \"generated\"",
- "ignore_filename_regex = \" \"",
- "report.ignore_filename_regex",
- ),
- (
- "typescript = \"generated TypeScript is checked elsewhere\"",
- "typescript = \" \"",
- "generated.typescript",
- ),
- (
- "binding_crates = \"generated binding crates are checked elsewhere\"",
- "binding_crates = \" \"",
- "generated.binding_crates",
- ),
- (
- "wasm_glue = \"wasm glue is checked through package validation\"",
- "wasm_glue = \" \"",
- "generated.wasm_glue",
- ),
- (
- "paths = [\"tools/xtask/src/coverage_policy.rs\"]",
- "paths = [\" \"]",
- "scopes.xtask_policy.paths entry",
- ),
- (
- "reason = \"generated output is checked through reproducibility\"",
- "reason = \" \"",
- "exclusions.generated.reason",
- ),
- (
- "paths = [\"packages/*/src/generated/**\"]",
- "paths = [\" \"]",
- "exclusions.generated.paths entry",
- ),
- ];
-
- for (from, to, expected) in cases {
- let raw = CONTRACT.replace(from, to);
- let error = validate_contract(&contract(&raw)).expect_err("invalid contract");
- assert!(error.contains(expected), "{error}");
- }
-}
-
-#[test]
-fn rejects_contract_collection_errors() {
- let mut no_scopes = contract(CONTRACT);
- no_scopes.scopes.clear();
- assert_eq!(
- validate_contract(&no_scopes).unwrap_err(),
- "contracts/coverage.toml scopes must not be empty"
- );
-
- let mut no_exclusions = contract(CONTRACT);
- no_exclusions.exclusions.clear();
- assert_eq!(
- validate_contract(&no_exclusions).unwrap_err(),
- "contracts/coverage.toml exclusions must not be empty"
- );
-
- let cases = [
- (
- CONTRACT.replace("[scopes.xtask_policy]", "[scopes.\"\"]"),
- "scope name",
- ),
- (
- CONTRACT.replace("[exclusions.generated]", "[exclusions.\"\"]"),
- "exclusion name",
- ),
- (
- CONTRACT.replace("threshold = 90.0", "threshold = 101.0"),
- "scopes.xtask_policy.threshold",
- ),
- (
- CONTRACT.replace(
- "threshold = 90.0",
- "threshold = 90.0\nbranch_reason = \"not applicable\"",
- ),
- "branch_reason requires branches_applicable = false",
- ),
- (
- CONTRACT.replace(
- "threshold = 90.0",
- "threshold = 90.0\nbranches_applicable = false",
- ),
- "scopes.xtask_policy.branch_reason",
- ),
- (
- CONTRACT.replace(
- "paths = [\"tools/xtask/src/coverage_policy.rs\"]",
- "paths = []",
- ),
- "scopes.xtask_policy.paths must not be empty",
- ),
- (
- CONTRACT.replace("paths = [\"packages/*/src/generated/**\"]", "paths = []"),
- "exclusions.generated.paths must not be empty",
- ),
- ];
-
- for (raw, expected) in cases {
- let error = validate_contract(&contract(&raw)).expect_err("invalid contract");
- assert!(error.contains(expected), "{error}");
- }
-}
-
-#[test]
-fn matches_recursive_scope_paths() {
- assert!(path_matches(
- "crates/sdk/src/**",
- "crates/sdk/src/adapters/radrootsd.rs"
- ));
- assert!(path_matches("crates/sdk/src/**", "crates/sdk/src"));
- assert!(path_matches(
- "tools/xtask/src/coverage_policy.rs",
- "tools/xtask/src/coverage_policy.rs"
- ));
- assert!(!path_matches(
- "crates/sdk/src/**",
- "crates/sql_wasm_runtime/src/lib.rs"
- ));
-}
-
-#[test]
-fn accepts_passing_reports_and_rejects_undercovered_scopes() {
- let root = test_root("passing_and_undercovered");
- let filename = scope_file(&root);
- let passing_report = report_json(&filename, covered(), covered());
- let report_path = write_report(&root, &passing_report);
- evaluate_report(&root, &report_path, &contract(CONTRACT)).expect("passing report");
-
- let mut undercovered = covered();
- undercovered.lines = (100, 89, 89.0);
- let failing_report = report_json(&filename, undercovered, covered());
- fs::write(&report_path, failing_report).expect("write failing report");
- let error = evaluate_report(&root, &report_path, &contract(CONTRACT))
- .expect_err("undercovered report rejected");
- assert!(error.contains("coverage scope xtask_policy"), "{error}");
- fs::remove_dir_all(root).expect("cleanup");
-}
-
-#[test]
-fn disabled_enforcement_accepts_measured_undercoverage() {
- let root = test_root("disabled");
- let filename = scope_file(&root);
- let mut undercovered = covered();
- undercovered.lines = (100, 0, 0.0);
- undercovered.functions = (50, 0, 0.0);
- undercovered.regions = (200, 0, 0.0);
- undercovered.branches = (80, 0, 0.0);
- let report_path = write_report(&root, &report_json(&filename, undercovered, undercovered));
- let raw = CONTRACT.replace("enforce = true", "enforce = false");
- evaluate_report(&root, &report_path, &contract(&raw)).expect("disabled policy passes");
- fs::remove_dir_all(root).expect("cleanup");
-}
-
-#[test]
-fn branch_inapplicability_is_explicit_and_rejects_measured_branches() {
- let root = test_root("branch_inapplicability");
- let filename = scope_file(&root);
- let raw = CONTRACT.replace(
- "threshold = 90.0",
- "threshold = 90.0\nbranches_applicable = false\nbranch_reason = \"straight-line source\"",
- );
- let mut no_branches = covered();
- no_branches.branches = (0, 0, 0.0);
- let report_path = write_report(&root, &report_json(&filename, no_branches, covered()));
- evaluate_report(&root, &report_path, &contract(&raw)).expect("no-branch scope passes");
-
- fs::write(&report_path, report_json(&filename, covered(), covered()))
- .expect("write measured branches");
- let error = evaluate_report(&root, &report_path, &contract(&raw))
- .expect_err("measured branch drift rejected");
- assert!(error.contains("declared branches inapplicable"), "{error}");
- fs::remove_dir_all(root).expect("cleanup");
-}
-
-#[test]
-fn duplicate_harness_variants_are_merged_and_test_modules_are_excluded() {
- let root = test_root("duplicate_harness_variants");
- let filename = scope_file(&root);
- let source_path = Path::new(&filename);
- fs::create_dir_all(source_path.parent().expect("source parent")).expect("create source parent");
- fs::write(
- source_path,
- "pub fn production() -> bool { true }\n\n#[cfg(test)]\nmod tests {\n fn helper() -> bool { false }\n}\n",
- )
- .expect("write source");
- let report = format!(
- r#"{{"data":[{{"files":[{{"filename":"{filename}","summary":{summary}}}],"functions":[{{"count":0,"filenames":["{filename}"],"regions":[[1,1,1,37,0,0,0,0]]}},{{"count":1,"filenames":["{filename}"],"regions":[[1,1,1,37,1,0,0,0]]}},{{"count":0,"filenames":["{filename}"],"regions":[[5,5,5,34,0,0,0,0]]}}],"totals":{summary}}}]}}"#,
- summary = summary_json(covered()),
- );
- let report_path = write_report(&root, &report);
-
- evaluate_report(&root, &report_path, &contract(CONTRACT))
- .expect("normalized production coverage passes");
- fs::remove_dir_all(root).expect("cleanup");
-}
-
-#[test]
-fn normalized_details_fail_closed_and_merge_every_supported_record_shape() {
- let root = test_root("normalized_record_shapes");
- let filename = scope_file(&root);
- let source_path = Path::new(&filename);
- fs::create_dir_all(source_path.parent().expect("source parent")).expect("source parent");
- fs::write(
- source_path,
- "pub fn production(flag: bool) -> bool { if flag { true } else { false } }\nfn uncovered() {}\nconst VALUE: bool = true;\n#[cfg(test)]\nmod tests { fn ignored() {} }\n",
- )
- .expect("source");
- let outside = root.join("outside.rs").display().to_string();
- fs::write(&outside, "fn outside() {}\n").expect("outside source");
- let summary = summary_json(covered());
- let report = format!(
- r#"{{"data":[{{"files":[],"functions":[
- {{"count":0,"filenames":[],"regions":[],"branches":[]}},
- {{"count":0,"filenames":["{filename}"],"regions":[[1,1,1]],"branches":[]}},
- {{"count":0,"filenames":["{outside}"],"regions":[[1,1,1,16,0,0,0,0]],"branches":[]}},
- {{"count":0,"filenames":["{filename}"],"regions":[[2,1,2,18,0,0,0,0]],"branches":[]}},
- {{"count":1,"filenames":["{filename}"],"regions":[[3,1,3,25,1,0,0,0]],"branches":[]}},
- {{"count":1,"filenames":["{filename}","{outside}"],"regions":[[1,1,1,76,1,0,0,0],[1,1,5,36,1,0,0,0],[2,1,2,18,0,0,0,1],[4,1,4,13,0,0,0,0],[1,1,1,2,0,1,0,0],[1]],"branches":[[1,40,1,47,1,0,0,0,4],[2,1,2,3,0,1,0,0,4],[4,1,4,3,0,0,0,0,4],[99,1,99,3,0,0,0,0,4],[1,1,1,2,0,0,1,0,4],[1]]}},
- {{"count":0,"filenames":["{filename}","{outside}"],"regions":[[1,1,1,76,0,0,0,0],[2,1,2,18,1,0,0,0]],"branches":[[1,40,1,47,0,1,0,0,4],[2,1,2,3,1,0,0,0,4]]}}
- ],"totals":{summary}}}]}}"#,
- );
- let parsed = serde_json::from_str::<LlvmCovReport>(&report).expect("detail report");
- let policy = contract(CONTRACT);
- let scope = policy.scopes.get("xtask_policy").expect("scope");
- let (lines, functions, regions, branches) =
- normalized_source_metrics(&root, &parsed.data[0], scope).expect("normalized details");
- assert!(lines.count >= 2);
- assert_eq!(functions.count, 2);
- assert_eq!(functions.covered, 1);
- assert!(regions.count >= 2);
- assert_eq!(branches.count, 6);
- assert_eq!(branches.covered, 4);
- fs::remove_dir_all(root).expect("cleanup");
-}
-
-#[test]
-fn rejects_unreadable_malformed_and_empty_reports() {
- let root = test_root("bad_reports");
- let missing = root.join("missing.json");
- assert!(evaluate_report(&root, &missing, &contract(CONTRACT)).is_err());
-
- let malformed = write_report(&root, "{");
- assert!(evaluate_report(&root, &malformed, &contract(CONTRACT)).is_err());
-
- fs::write(&malformed, r#"{"data":[]}"#).expect("write empty report");
- assert!(evaluate_report(&root, &malformed, &contract(CONTRACT)).is_err());
- fs::remove_dir_all(root).expect("cleanup");
-}
-
-#[test]
-fn rejects_required_total_metric_failures() {
- let root = test_root("total_metrics");
- let filename = scope_file(&root);
- let mut totals = covered();
- let cases = [
- Metrics {
- lines: (0, 0, 0.0),
- ..totals
- },
- {
- totals = covered();
- totals.functions = (0, 0, 0.0);
- totals
- },
- {
- totals = covered();
- totals.regions = (0, 0, 0.0);
- totals
- },
- {
- totals = covered();
- totals.branches = (0, 0, 0.0);
- totals
- },
- {
- totals = covered();
- totals.lines = (1, 2, 200.0);
- totals
- },
- ];
-
- let report_path = root.join("summary.json");
- for total_metrics in cases {
- fs::write(
- &report_path,
- report_json(&filename, covered(), total_metrics),
- )
- .expect("write report");
- assert!(evaluate_report(&root, &report_path, &contract(CONTRACT)).is_err());
- }
- fs::remove_dir_all(root).expect("cleanup");
-}
-
-#[test]
-fn rejects_scope_metric_validation_and_missing_scope_files() {
- let root = test_root("scope_metrics");
- let filename = scope_file(&root);
- let other_filename = root
- .join("tools/xtask/src/coverage.rs")
- .display()
- .to_string();
- let report_path = root.join("summary.json");
-
- fs::write(
- &report_path,
- report_json(&other_filename, covered(), covered()),
- )
- .expect("write unmatched report");
- let error = evaluate_report(&root, &report_path, &contract(CONTRACT))
- .expect_err("unmatched scope rejected");
- assert!(error.contains("matched no report files"), "{error}");
-
- let mut invalid = covered();
- invalid.lines = (0, 0, 0.0);
- fs::write(&report_path, report_json(&filename, invalid, covered())).expect("write report");
- assert!(evaluate_report(&root, &report_path, &contract(CONTRACT)).is_err());
-
- invalid = covered();
- invalid.functions = (1, 2, 200.0);
- fs::write(&report_path, report_json(&filename, invalid, covered())).expect("write report");
- assert!(evaluate_report(&root, &report_path, &contract(CONTRACT)).is_err());
- fs::remove_dir_all(root).expect("cleanup");
-}
-
-#[test]
-fn metric_helpers_cover_edges() {
- let valid = LlvmCovMetric {
- count: 10,
- covered: 10,
- percent: 100.0,
- };
- validate_metric("lines", &valid, true).expect("metric validates");
- enforce_metric("lines", &valid, 100.0).expect("metric passes");
-
- let missing = LlvmCovMetric {
- count: 0,
- covered: 0,
- percent: 0.0,
- };
- assert!(validate_metric("lines", &missing, true).is_err());
- assert!(enforce_metric("lines", &missing, 100.0).is_err());
-
- let invalid = LlvmCovMetric {
- count: 1,
- covered: 2,
- percent: 200.0,
- };
- assert!(validate_metric("lines", &invalid, true).is_err());
- assert_eq!(metric_percent(0, 0), 0.0);
- assert_eq!(metric_percent(4, 2), 50.0);
-}
-
-#[test]
-fn detail_key_and_path_helpers_cover_valid_and_short_records() {
- assert!(region_key(&[1, 2, 3, 4, 5, 6, 7, 8]).is_some());
- assert!(region_key(&[1, 2, 3]).is_none());
- assert!(branch_key(&[1, 2, 3, 4, 5, 6, 7, 8, 9]).is_some());
- assert!(branch_key(&[1, 2, 3]).is_none());
- assert_eq!(brace_delta("fn value() { if true { 1 } else { 0 } }"), 0);
- assert_eq!(brace_delta("{{"), 2);
- assert_eq!(brace_delta("}"), -1);
-
- let root = Path::new("/workspace/sdk");
- assert_eq!(
- report_filename(root, "/workspace/sdk/crates/sdk/src/lib.rs"),
- "crates/sdk/src/lib.rs"
- );
- assert_eq!(
- report_filename(root, "/elsewhere/lib.rs"),
- "/elsewhere/lib.rs"
- );
- assert_eq!(
- report_filename(
- root,
- "/workspace/sdk/crates/sdk/src/adapters/../../tests/unit.rs"
- ),
- "crates/sdk/tests/unit.rs"
- );
-}
-
-#[test]
-fn detail_filename_helpers_honor_record_indexes_and_safe_fallbacks() {
- let function = LlvmCovFunction {
- count: 1,
- filenames: vec!["primary.rs".to_owned(), "expanded.rs".to_owned()],
- regions: Vec::new(),
- branches: Vec::new(),
- };
- assert_eq!(
- region_filename(&function, &[1, 1, 1, 2, 1, 1, 0, 0]),
- Some("expanded.rs")
- );
- assert_eq!(
- region_filename(&function, &[1, 1, 1, 2, 1, 99, 0, 0]),
- Some("primary.rs")
- );
- assert_eq!(region_filename(&function, &[1]), Some("primary.rs"));
- assert_eq!(
- branch_filename(&function, &[1, 1, 1, 2, 1, 0, 1, 0, 4]),
- Some("expanded.rs")
- );
- assert_eq!(
- branch_filename(&function, &[1, 1, 1, 2, 1, 0, 99, 0, 4]),
- Some("primary.rs")
- );
- assert_eq!(branch_filename(&function, &[1]), Some("primary.rs"));
-
- let no_filenames = LlvmCovFunction {
- count: 0,
- filenames: Vec::new(),
- regions: Vec::new(),
- branches: Vec::new(),
- };
- assert_eq!(region_filename(&no_filenames, &[1]), None);
- assert_eq!(branch_filename(&no_filenames, &[1]), None);
-}
-
-#[test]
-fn authored_and_annotated_source_helpers_cover_marker_shapes() {
- let root = test_root("source_helpers");
- let source = root.join("source.rs");
- fs::write(
- &source,
- "pub fn production() {}\n#[cfg(test)]\nmod tests {\n fn helper() {}\n}\n#[cfg(all(test, feature = \"x\"))]\nfn gated() {}\n#[cfg_attr(coverage_nightly, coverage(off))]\nfn excluded() {}\n",
- )
- .expect("source");
- let filename = source.display().to_string();
- let mut cache = std::collections::BTreeMap::new();
-
- assert!(is_authored_function_line(&filename, 1, &mut cache));
- assert!(!is_authored_function_line(&filename, 2, &mut cache));
- assert!(!is_ignorable_source_line(&filename, 1, &mut cache));
- assert!(is_ignorable_source_line(&filename, 2, &mut cache));
- assert!(is_ignorable_source_line(&filename, 4, &mut cache));
- assert!(is_ignorable_source_line(&filename, 7, &mut cache));
- assert!(is_ignorable_source_line(&filename, 9, &mut cache));
- assert!(!is_ignorable_source_line(&filename, 0, &mut cache));
- assert!(!is_ignorable_source_line(
- root.join("missing.rs").to_str().expect("path"),
- 1,
- &mut cache
- ));
-
- let marked = annotated_source_lines(
- "#[cfg(test)]\n// note\nfn test() {}\nfn live() {}",
- "cfg(test)",
- );
- assert_eq!(marked, vec![true, true, true, false]);
- let coverage_off = annotated_source_lines(
- "#[cfg_attr(coverage_nightly, coverage(off))]\n#[inline]\nfn excluded() {\n if true {\n work();\n }\n}\nfn live() {}",
- "cfg_attr(coverage_nightly, coverage(off))",
- );
- assert_eq!(
- coverage_off,
- vec![true, true, true, true, true, true, true, false]
- );
- assert_eq!(
- annotated_source_lines("#[cfg(test)]\nfn declaration();\nfn live() {}", "cfg(test)"),
- vec![true, true, false]
- );
- fs::remove_dir_all(root).expect("cleanup");
-}
-
-#[test]
-fn branch_filter_only_excludes_explicit_synthetic_constructs() {
- let root = test_root("branch_helpers");
- let source = root.join("source.rs");
- fs::write(
- &source,
- "fn value() {\n let _ = call()?;\n unreachable!();\n assert!(matches!(value, Some(_)));\n if live { work(); }\n}\n",
- )
- .expect("source");
- let filename = source.display().to_string();
- let mut cache = std::collections::BTreeMap::new();
- assert!(!is_ignorable_source_line(&filename, 1, &mut cache));
-
- let branch = |line_start, column_start, line_end, column_end| BranchKey {
- line_start,
- column_start,
- line_end,
- column_end,
- kind: 4,
- };
- assert!(is_ignorable_branch(
- &filename,
- &branch(2, 16, 2, 17),
- &mut cache
- ));
- assert!(is_ignorable_branch(
- &filename,
- &branch(3, 2, 3, 10),
- &mut cache
- ));
- assert!(is_ignorable_branch(
- &filename,
- &branch(4, 10, 4, 18),
- &mut cache
- ));
- assert!(!is_ignorable_branch(
- &filename,
- &branch(5, 2, 5, 9),
- &mut cache
- ));
- assert!(!is_ignorable_branch(
- &filename,
- &branch(5, 2, 6, 1),
- &mut cache
- ));
- assert!(!is_ignorable_branch(
- root.join("missing.rs").to_str().expect("path"),
- &branch(1, 1, 1, 2),
- &mut cache
- ));
- fs::remove_dir_all(root).expect("cleanup");
-}
diff --git a/tools/xtask/src/dto_roots.rs b/tools/xtask/src/dto_roots.rs
@@ -1,969 +0,0 @@
-use std::collections::{BTreeMap, BTreeSet};
-
-use dto_bindgen_backend_ts::{
- DtoRegistryRenderOptions, DtoTypesModule, TypeScriptDeclaration, TypeScriptModule,
- TypeScriptType, render_registry_types,
-};
-use dto_bindgen_core::{Registry, RootDescriptor, build_registry};
-
-#[derive(Clone, Copy, Debug)]
-pub struct DtoPackageRootSet {
- pub package_key: &'static str,
- roots: fn() -> Vec<RootDescriptor>,
-}
-
-impl DtoPackageRootSet {
- pub fn roots(&self) -> Vec<RootDescriptor> {
- (self.roots)()
- }
-
- pub fn registry(&self) -> Registry {
- build_registry(self.roots())
- }
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct ManualDescriptorFamily {
- pub package_key: &'static str,
- pub source_family: &'static str,
- pub reason: &'static str,
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct SdkLocalWrapperAllowance {
- pub package_key: &'static str,
- pub shape_family: &'static str,
- pub reason: &'static str,
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-struct DtoExternalOverride {
- target_type: &'static str,
- import_name: &'static str,
- from_package_key: &'static str,
- from: &'static str,
-}
-
-const CORE_BINDINGS_PACKAGE_KEY: &str = "core";
-const CORE_BINDINGS_PACKAGE_NAME: &str = "@radroots/core-bindings";
-const EVENT_BINDINGS_PACKAGE_KEY: &str = "event";
-const EVENT_BINDINGS_PACKAGE_NAME: &str = "@radroots/event-bindings";
-
-pub const DTO_PACKAGE_ROOTS: &[DtoPackageRootSet] = &[DtoPackageRootSet {
- package_key: "core",
- roots: core_roots,
-}];
-
-pub const MANUAL_DESCRIPTOR_FAMILIES: &[ManualDescriptorFamily] = &[
- ManualDescriptorFamily {
- package_key: "core",
- source_family: "decimal, currency, money, quantity, percent, quantity price, unit, and discount value families",
- reason: "custom serde, string-backed newtypes, aliases, and tagged enum wire forms require source-owned manual descriptors",
- },
- ManualDescriptorFamily {
- package_key: "event",
- source_family: "event timestamps, counters, and optional metadata fields",
- reason: "large integers and source-specific optional/null policy must be explicit",
- },
- ManualDescriptorFamily {
- package_key: "event",
- source_family: "GeoJSON coordinate arrays",
- reason: "fixed-size Rust arrays must preserve tuple semantics in TypeScript",
- },
- ManualDescriptorFamily {
- package_key: "trade",
- source_family: "trade listing roots and package projection count fields",
- reason: "core aliases, source-owned event imports, and count-family numeric policy require explicit descriptors",
- },
- ManualDescriptorFamily {
- package_key: "replica_schema",
- source_family: "untagged query wrappers and serde_json value fields",
- reason: "schema query shapes are generated and not all source fields map to derive-supported DTOs",
- },
-];
-
-pub const SDK_LOCAL_WRAPPER_ALLOWANCES: &[SdkLocalWrapperAllowance] = &[
- SdkLocalWrapperAllowance {
- package_key: "core",
- shape_family: "Currency and Decimal package aliases",
- reason: "string-backed source descriptors require canonical named TypeScript package aliases",
- },
- SdkLocalWrapperAllowance {
- package_key: "replica_schema",
- shape_family: "generated query argument wrappers",
- reason: "schema operation inputs are generated package shapes rather than source-owned public DTO structs",
- },
- SdkLocalWrapperAllowance {
- package_key: "trade",
- shape_family: "marketplace, query, projection, sort, review, and backoffice DTO shapes",
- reason: "these are SDK package contract shapes layered over source-owned trade, events, and core DTOs",
- },
-];
-
-const EVENT_CORE_EXTERNAL_OVERRIDES: &[DtoExternalOverride] = &[
- core_override("Currency"),
- core_override("Decimal"),
- core_override("Discount"),
- core_override("DiscountScope"),
- core_override("DiscountThreshold"),
- core_override("DiscountValue"),
- core_override("Money"),
- core_override("Percent"),
- core_override("Quantity"),
- core_override("QuantityPrice"),
- core_override("Unit"),
- core_override("UnitDimension"),
-];
-
-const TRADE_EXTERNAL_OVERRIDES: &[DtoExternalOverride] = &[
- core_override("Currency"),
- core_override("Decimal"),
- core_override("Discount"),
- core_override("DiscountValue"),
- core_override("Money"),
- core_override("Quantity"),
- core_override("QuantityPrice"),
- core_override("Unit"),
- event_override("RadrootsFarmRef"),
- event_override("RadrootsOperationalListing"),
- event_override("RadrootsOperationalListingAvailability"),
- event_override("RadrootsOperationalListingBin"),
- event_override("RadrootsOperationalListingDeliveryMethod"),
- event_override("RadrootsOperationalListingImage"),
- event_override("RadrootsOperationalListingProduct"),
- event_override("RadrootsOperationalListingPublicLocation"),
- event_override("RadrootsOperationalListingStatus"),
- event_override("RadrootsEventPtr"),
- event_override("RadrootsPlotRef"),
- event_override("RadrootsResourceAreaRef"),
-];
-
-const TRADE_REQUIRED_EXTERNAL_PACKAGE_IMPORTS: &[&str] =
- &[CORE_BINDINGS_PACKAGE_NAME, EVENT_BINDINGS_PACKAGE_NAME];
-const EVENT_BINDINGS_TYPES_TS: &str =
- include_str!("../../../packages/event-bindings/src/generated/types.ts");
-
-pub fn package_root_set(package_key: &str) -> Option<&'static DtoPackageRootSet> {
- DTO_PACKAGE_ROOTS
- .iter()
- .find(|root_set| root_set.package_key == package_key)
-}
-
-pub fn core_types_module() -> Result<DtoTypesModule, String> {
- let root_set = package_root_set("core").ok_or_else(|| "missing core DTO roots".to_owned())?;
- let rendered =
- render_registry_types(&root_set.registry(), &DtoRegistryRenderOptions::default())?;
- Ok(with_type_aliases_sorted(
- rendered,
- [
- type_alias("Currency", TypeScriptType::String),
- type_alias("Decimal", TypeScriptType::String),
- ],
- ))
-}
-
-pub fn replica_schema_types_module() -> Result<DtoTypesModule, String> {
- render_registry_types(
- &radroots_replica_schema_bindings::dto_registry(),
- &DtoRegistryRenderOptions::default(),
- )
-}
-
-fn core_roots() -> Vec<RootDescriptor> {
- radroots_core_bindings::dto_roots()
-}
-
-fn core_import_options(
- mut options: DtoRegistryRenderOptions,
-) -> Result<DtoRegistryRenderOptions, String> {
- let package_exports = BTreeMap::from([(
- CORE_BINDINGS_PACKAGE_KEY,
- type_exports(core_types_module()?.body_ts()),
- )]);
- for override_target in EVENT_CORE_EXTERNAL_OVERRIDES {
- validate_external_override_target(*override_target, &package_exports)?;
- options = options.with_external_override(
- override_target.target_type,
- override_target.import_name,
- override_target.from,
- );
- }
- Ok(options)
-}
-
-fn trade_import_options(
- registry: &Registry,
- mut options: DtoRegistryRenderOptions,
-) -> Result<DtoRegistryRenderOptions, String> {
- let package_exports = generated_external_package_exports()?;
- for override_target in TRADE_EXTERNAL_OVERRIDES {
- validate_external_override_target(*override_target, &package_exports)?;
- if let Ok(type_id) = registry.type_id_by_export_name(override_target.target_type) {
- options = options.with_external_type(
- type_id,
- override_target.import_name,
- override_target.from,
- );
- }
- options = options.with_external_override(
- override_target.target_type,
- override_target.import_name,
- override_target.from,
- );
- }
-
- Ok(options)
-}
-
-fn generated_external_package_exports() -> Result<BTreeMap<&'static str, BTreeSet<String>>, String>
-{
- Ok(BTreeMap::from([
- (
- CORE_BINDINGS_PACKAGE_KEY,
- type_exports(core_types_module()?.body_ts()),
- ),
- (
- EVENT_BINDINGS_PACKAGE_KEY,
- type_exports(EVENT_BINDINGS_TYPES_TS),
- ),
- ]))
-}
-
-fn validate_external_override_target(
- override_target: DtoExternalOverride,
- package_exports: &BTreeMap<&'static str, BTreeSet<String>>,
-) -> Result<(), String> {
- let exports = package_exports
- .get(override_target.from_package_key)
- .ok_or_else(|| {
- format!(
- "external DTO override `{}` references unknown package key `{}`",
- override_target.target_type, override_target.from_package_key
- )
- })?;
- if exports.contains(override_target.import_name) {
- return Ok(());
- }
- Err(format!(
- "external DTO override `{}` imports `{}` from `{}`, but package `{}` does not export it",
- override_target.target_type,
- override_target.import_name,
- override_target.from,
- override_target.from_package_key
- ))
-}
-
-fn validate_external_override_usage(
- module: &DtoTypesModule,
- overrides: &[DtoExternalOverride],
-) -> Result<(), String> {
- let imports = imported_type_inventory(module.imports_ts().unwrap_or_default());
- let package_exports = generated_external_package_exports()?;
- for package_name in TRADE_REQUIRED_EXTERNAL_PACKAGE_IMPORTS {
- if !imports.contains_key(*package_name) {
- return Err(format!(
- "expected generated DTO imports from `{package_name}` but none were emitted"
- ));
- }
- }
-
- for override_target in overrides {
- if type_exports(module.body_ts()).contains(override_target.target_type) {
- return Err(format!(
- "external DTO override `{}` from `{}` was emitted locally instead of imported",
- override_target.target_type, override_target.from
- ));
- }
- if imports
- .get(override_target.from)
- .is_some_and(|names| names.contains(override_target.import_name))
- {
- validate_external_override_target(*override_target, &package_exports)?;
- }
- }
- Ok(())
-}
-
-fn imported_type_inventory(imports_ts: &str) -> BTreeMap<String, BTreeSet<String>> {
- let mut imports = BTreeMap::new();
- let mut pending_names: Option<Vec<String>> = None;
-
- for line in imports_ts.lines() {
- let line = line.trim();
- if let Some(single) = line.strip_prefix("import type { ") {
- if let Some((name, from)) = single.split_once(" } from ") {
- insert_import_names(&mut imports, from, name);
- }
- } else if line == "import type {" {
- pending_names = Some(Vec::new());
- } else if let Some(from) = line.strip_prefix("} from ") {
- if let Some(names) = pending_names.take() {
- for name in names {
- insert_import(&mut imports, from, &name);
- }
- }
- } else if let Some(names) = pending_names.as_mut()
- && let Some(name) = line.strip_suffix(',')
- {
- names.push(name.to_owned());
- }
- }
-
- imports
-}
-
-fn with_additional_type_imports(
- module: DtoTypesModule,
- from: &str,
- names: impl IntoIterator<Item = &'static str>,
-) -> DtoTypesModule {
- let mut imports = imported_type_inventory(module.imports_ts().unwrap_or_default());
- imports
- .entry(from.to_owned())
- .or_default()
- .extend(names.into_iter().map(str::to_owned));
- let imports_ts = imports
- .into_iter()
- .map(|(from, names)| {
- format!(
- "import type {{ {} }} from \"{from}\";",
- names.into_iter().collect::<Vec<_>>().join(", ")
- )
- })
- .collect::<Vec<_>>()
- .join("\n");
- let imports_ts = if imports_ts.is_empty() {
- imports_ts
- } else {
- format!("{imports_ts}\n\n")
- };
- DtoTypesModule::new(imports_ts, module.body_ts())
-}
-
-fn insert_import_names(imports: &mut BTreeMap<String, BTreeSet<String>>, from: &str, names: &str) {
- for name in names.split(',') {
- insert_import(imports, from, name);
- }
-}
-
-fn insert_import(imports: &mut BTreeMap<String, BTreeSet<String>>, from: &str, name: &str) {
- let from = from.trim_end_matches(';').trim_matches('"');
- imports
- .entry(from.to_owned())
- .or_default()
- .insert(name.trim().to_owned());
-}
-
-fn type_exports(types_ts: &str) -> BTreeSet<String> {
- types_ts
- .lines()
- .filter_map(|line| line.strip_prefix("export type "))
- .filter_map(|rest| rest.split([' ', '<']).next())
- .map(str::to_owned)
- .collect()
-}
-
-const fn core_override(export_name: &'static str) -> DtoExternalOverride {
- DtoExternalOverride {
- target_type: export_name,
- import_name: export_name,
- from_package_key: CORE_BINDINGS_PACKAGE_KEY,
- from: CORE_BINDINGS_PACKAGE_NAME,
- }
-}
-
-const fn event_override(export_name: &'static str) -> DtoExternalOverride {
- DtoExternalOverride {
- target_type: export_name,
- import_name: export_name,
- from_package_key: EVENT_BINDINGS_PACKAGE_KEY,
- from: EVENT_BINDINGS_PACKAGE_NAME,
- }
-}
-
-fn with_event_sdk_wrappers(module: DtoTypesModule) -> DtoTypesModule {
- let mut declarations = module
- .body_ts()
- .split("\n\n")
- .filter(|declaration| !declaration.trim().is_empty())
- .map(str::to_owned)
- .collect::<Vec<_>>();
- declarations.push(type_alias(
- "RadrootsOperationalListingProductTagKeys",
- TypeScriptType::readonly_tuple(
- [
- "key", "title", "category", "summary", "process", "lot", "location", "profile",
- "year",
- ]
- .into_iter()
- .map(TypeScriptType::literal_string)
- .collect::<Vec<_>>(),
- ),
- ));
- declarations.sort_by(|left, right| declaration_name(left).cmp(declaration_name(right)));
- DtoTypesModule::new(
- module.imports_ts().unwrap_or_default(),
- declarations.join("\n\n"),
- )
-}
-
-fn declaration_name(declaration: &str) -> &str {
- declaration
- .strip_prefix("export type ")
- .and_then(|rest| rest.split([' ', '<']).next())
- .unwrap_or(declaration)
-}
-
-fn with_type_aliases_sorted(
- module: DtoTypesModule,
- aliases: impl IntoIterator<Item = String>,
-) -> DtoTypesModule {
- let mut declarations = module
- .body_ts()
- .split("\n\n")
- .filter(|declaration| !declaration.trim().is_empty())
- .map(str::to_owned)
- .collect::<Vec<_>>();
- declarations.extend(aliases);
- declarations.sort_by(|left, right| declaration_name(left).cmp(declaration_name(right)));
- DtoTypesModule::new(
- module.imports_ts().unwrap_or_default(),
- declarations.join("\n\n"),
- )
-}
-
-fn type_alias(name: impl Into<String>, type_expr: TypeScriptType) -> String {
- TypeScriptModule::new("types.ts")
- .with_declaration(TypeScriptDeclaration::type_alias(name, type_expr))
- .render_source()
- .trim()
- .to_owned()
-}
-
-#[cfg(test)]
-mod tests {
- use std::collections::{BTreeMap, BTreeSet};
-
- use dto_bindgen_core::SourceSpan;
-
- use super::{
- CORE_BINDINGS_PACKAGE_KEY, CORE_BINDINGS_PACKAGE_NAME, DTO_PACKAGE_ROOTS,
- DtoExternalOverride, MANUAL_DESCRIPTOR_FAMILIES, SDK_LOCAL_WRAPPER_ALLOWANCES,
- event_override, imported_type_inventory, package_root_set, type_exports,
- validate_external_override_target, validate_external_override_usage,
- with_additional_type_imports,
- };
- use dto_bindgen_backend_ts::DtoTypesModule;
-
- const CORE_BINDINGS_TYPES_TS: &str =
- include_str!("../../../packages/core-bindings/src/generated/types.ts");
- const EVENT_BINDINGS_TYPES_TS: &str =
- include_str!("../../../packages/event-bindings/src/generated/types.ts");
- const REPLICA_SCHEMA_BINDINGS_TYPES_TS: &str =
- include_str!("../../../packages/replica-schema-bindings/src/generated/types.ts");
- const TRADE_BINDINGS_TYPES_TS: &str =
- include_str!("../../../packages/trade-bindings/src/generated/types.ts");
- const REPLICA_SCHEMA_MODEL_SOURCES: &[&str] = &[
- include_str!("../../../../lib/crates/replica_schema/src/models/farm.rs"),
- include_str!("../../../../lib/crates/replica_schema/src/models/farm_gcs_location.rs"),
- include_str!("../../../../lib/crates/replica_schema/src/models/farm_member.rs"),
- include_str!("../../../../lib/crates/replica_schema/src/models/farm_member_claim.rs"),
- include_str!("../../../../lib/crates/replica_schema/src/models/farm_tag.rs"),
- include_str!("../../../../lib/crates/replica_schema/src/models/gcs_location.rs"),
- include_str!("../../../../lib/crates/replica_schema/src/models/log_error.rs"),
- include_str!("../../../../lib/crates/replica_schema/src/models/media_image.rs"),
- include_str!("../../../../lib/crates/replica_schema/src/models/nostr_event_head.rs"),
- include_str!("../../../../lib/crates/replica_schema/src/models/nostr_profile.rs"),
- include_str!("../../../../lib/crates/replica_schema/src/models/nostr_profile_relay.rs"),
- include_str!("../../../../lib/crates/replica_schema/src/models/nostr_relay.rs"),
- include_str!("../../../../lib/crates/replica_schema/src/models/plot.rs"),
- include_str!("../../../../lib/crates/replica_schema/src/models/plot_gcs_location.rs"),
- include_str!("../../../../lib/crates/replica_schema/src/models/plot_tag.rs"),
- include_str!("../../../../lib/crates/replica_schema/src/models/trade_product.rs"),
- include_str!("../../../../lib/crates/replica_schema/src/models/trade_product_location.rs"),
- include_str!("../../../../lib/crates/replica_schema/src/models/trade_product_media.rs"),
- ];
- const CORE_TYPE_INVENTORY: &[&str] = &[
- "Currency",
- "Decimal",
- "Discount",
- "DiscountScope",
- "DiscountThreshold",
- "DiscountValue",
- "Money",
- "Percent",
- "Quantity",
- "QuantityPrice",
- "Unit",
- "UnitDimension",
- ];
- const EVENT_TYPE_INVENTORY: &[&str] = &[
- "JobFeedbackStatus",
- "JobInputType",
- "JobPaymentRequest",
- "RadrootsAccountClaim",
- "RadrootsAddressableRef",
- "RadrootsAppData",
- "RadrootsCommercialDomain",
- "RadrootsContributionAttestation",
- "RadrootsCoop",
- "RadrootsCoopLocation",
- "RadrootsCoopRef",
- "RadrootsDocument",
- "RadrootsDocumentSubject",
- "RadrootsEventEnvelopeDto",
- "RadrootsEventPtr",
- "RadrootsEventRef",
- "RadrootsEvidenceBounty",
- "RadrootsFarm",
- "RadrootsFarmPublicLocation",
- "RadrootsFarmRef",
- "RadrootsFollow",
- "RadrootsFollowProfile",
- "RadrootsGcsLocation",
- "RadrootsGeoChat",
- "RadrootsGeoJsonPoint",
- "RadrootsGeoJsonPolygon",
- "RadrootsGiftWrap",
- "RadrootsGiftWrapRecipient",
- "RadrootsJobFeedback",
- "RadrootsJobInput",
- "RadrootsJobParam",
- "RadrootsJobRequest",
- "RadrootsJobResult",
- "RadrootsKnowledgeChangeProposal",
- "RadrootsKnowledgeCitationSpan",
- "RadrootsKnowledgeClaim",
- "RadrootsKnowledgeFieldContext",
- "RadrootsKnowledgeFieldReport",
- "RadrootsKnowledgeLocation",
- "RadrootsKnowledgeLocationPrecision",
- "RadrootsKnowledgeNodeRef",
- "RadrootsKnowledgeObservation",
- "RadrootsKnowledgeObservationValue",
- "RadrootsKnowledgeRelation",
- "RadrootsKnowledgeReview",
- "RadrootsKnowledgeReviewScope",
- "RadrootsKnowledgeReviewScore",
- "RadrootsKnowledgeReviewTarget",
- "RadrootsKnowledgeSource",
- "RadrootsList",
- "RadrootsListEntry",
- "RadrootsListSet",
- "RadrootsMessage",
- "RadrootsMessageFile",
- "RadrootsMessageFileDimensions",
- "RadrootsMessageRecipient",
- "RadrootsNip01EventWireDto",
- "RadrootsOperationalListing",
- "RadrootsOperationalListingAvailability",
- "RadrootsOperationalListingBin",
- "RadrootsOperationalListingDeliveryMethod",
- "RadrootsOperationalListingImage",
- "RadrootsOperationalListingImageSize",
- "RadrootsOperationalListingParseError",
- "RadrootsOperationalListingProduct",
- "RadrootsOperationalListingProductTagKeys",
- "RadrootsOperationalListingPublicLocation",
- "RadrootsOperationalListingStatus",
- "RadrootsOperationalListingValidationError",
- "RadrootsPlot",
- "RadrootsPlotLocation",
- "RadrootsPlotRef",
- "RadrootsPost",
- "RadrootsProfileType",
- "RadrootsReaction",
- "RadrootsRelayDocument",
- "RadrootsResourceArea",
- "RadrootsResourceAreaLocation",
- "RadrootsResourceAreaRef",
- "RadrootsResourceHarvestCap",
- "RadrootsResourceHarvestProduct",
- "RadrootsRightsAssertion",
- "RadrootsSeal",
- "RadrootsSignedEventDto",
- "RadrootsSignedEventVerificationStateDto",
- "RadrootsSocialFarmAnchor",
- "RadrootsSocialLocation",
- "RadrootsSocialMediaDimensions",
- "RadrootsSocialMediaMetadata",
- "RadrootsSocialMediaThumbnail",
- "RadrootsSocialTarget",
- "RadrootsVerifiedSignedEventDto",
- "RadrootsVerifiedSignedEventVerificationStateDto",
- "RadrootsWikiArticle",
- "RadrootsWikiArticleVersionRef",
- "RadrootsWikiMergeRequest",
- "RadrootsWikiRedirect",
- ];
- const TRADE_TYPE_INVENTORY: &[&str] = &[
- "RadrootsOperationalListingSubtotal",
- "RadrootsOperationalListingTotal",
- "RadrootsOperationalListingTradeProjection",
- "RadrootsTradeAgreementClaimV1",
- "RadrootsTradeAgreementStateV1",
- "RadrootsTradeAttestationRecordV1",
- "RadrootsTradeAttestationResultV1",
- "RadrootsTradeAttestationStateV1",
- "RadrootsTradeConflictStateV1",
- "RadrootsTradeEvidenceStateV1",
- "RadrootsTradeFacetCount",
- "RadrootsTradeFulfillmentStateV1",
- "RadrootsTradeListingBackofficeOverlay",
- "RadrootsTradeListingBackofficeQuery",
- "RadrootsTradeListingBackofficeView",
- "RadrootsTradeListingBinProjection",
- "RadrootsTradeListingFacets",
- "RadrootsTradeListingMarketStatus",
- "RadrootsTradeListingProjection",
- "RadrootsTradeListingQuery",
- "RadrootsTradeListingSort",
- "RadrootsTradeListingSortField",
- "RadrootsTradeMarketplaceListingSummary",
- "RadrootsTradeModerationFlag",
- "RadrootsTradeModerationSeverity",
- "RadrootsTradeModerationStatus",
- "RadrootsTradeNegotiationStateV1",
- "RadrootsTradePaymentStateV1",
- "RadrootsTradePrivateTermsStateV1",
- "RadrootsTradeProjectionV1",
- "RadrootsTradeReducerIssueV1",
- "RadrootsTradeReviewPriority",
- "RadrootsTradeReviewQueueEntry",
- "RadrootsTradeReviewStatus",
- "RadrootsTradeSortDirection",
- ];
-
- #[test]
- fn approved_source_roots_build_registries() {
- for root_set in DTO_PACKAGE_ROOTS {
- let registry = root_set.registry();
- assert!(
- !registry.has_errors(),
- "registry for {} has diagnostics: {:?}",
- root_set.package_key,
- registry.diagnostics
- );
- assert!(!registry.roots.is_empty());
- }
- }
-
- #[test]
- fn package_roots_are_explicit_not_discovered() {
- assert!(package_root_set("core").is_some());
- assert!(package_root_set("event").is_none());
- assert!(package_root_set("trade").is_none());
- }
-
- #[test]
- fn manual_descriptor_catalog_covers_known_review_families() {
- assert!(
- MANUAL_DESCRIPTOR_FAMILIES
- .iter()
- .any(|family| family.source_family.contains("GeoJSON"))
- );
- assert!(SDK_LOCAL_WRAPPER_ALLOWANCES.iter().any(|allowance| {
- allowance
- .shape_family
- .contains("marketplace, query, projection")
- }));
- }
-
- #[test]
- fn external_override_target_validation_rejects_missing_generated_export() {
- let package_exports = BTreeMap::from([(
- CORE_BINDINGS_PACKAGE_KEY,
- BTreeSet::from(["ExistingType".to_owned()]),
- )]);
- let error = validate_external_override_target(
- DtoExternalOverride {
- target_type: "MissingType",
- import_name: "MissingType",
- from_package_key: CORE_BINDINGS_PACKAGE_KEY,
- from: CORE_BINDINGS_PACKAGE_NAME,
- },
- &package_exports,
- )
- .expect_err("missing target package export must fail");
-
- assert_eq!(
- error,
- "external DTO override `MissingType` imports `MissingType` from `@radroots/core-bindings`, but package `core` does not export it"
- );
- }
-
- #[test]
- fn external_override_usage_rejects_absent_required_package_import() {
- let module = DtoTypesModule::new("", "export type RadrootsTradeListing = string;");
- let error = validate_external_override_usage(&module, &[])
- .expect_err("missing required package import must fail");
-
- assert_eq!(
- error,
- "expected generated DTO imports from `@radroots/core-bindings` but none were emitted"
- );
- }
-
- #[test]
- fn external_override_usage_rejects_local_emission_for_imported_type() {
- let module = DtoTypesModule::new(
- "import type { Decimal } from \"@radroots/core-bindings\";\n\nimport type { RadrootsFarmRef } from \"@radroots/event-bindings\";\n\n",
- "export type RadrootsFarmRef = { pubkey: string, d_tag: string, };",
- );
- let error = validate_external_override_usage(&module, &[event_override("RadrootsFarmRef")])
- .expect_err("local emission of imported type must fail");
-
- assert_eq!(
- error,
- "external DTO override `RadrootsFarmRef` from `@radroots/event-bindings` was emitted locally instead of imported"
- );
- }
-
- #[test]
- fn generated_import_inventory_parses_single_and_multiline_imports() {
- let imports = imported_type_inventory(
- "import type { One, Four } from \"@radroots/one\";\nimport type {\n Two,\n Three,\n} from \"@radroots/many\";\n\n",
- );
-
- assert_eq!(
- imports.get("@radroots/one").expect("single import package"),
- &BTreeSet::from(["Four".to_owned(), "One".to_owned()])
- );
- assert_eq!(
- imports.get("@radroots/many").expect("multi import package"),
- &BTreeSet::from(["Three".to_owned(), "Two".to_owned()])
- );
- }
-
- #[test]
- fn additional_type_imports_merge_with_generated_package_imports() {
- let module = DtoTypesModule::new(
- "import type { Decimal, Money } from \"@radroots/core-bindings\";\n\n",
- "export type Listing = { discounts: Array<Discount>, };",
- );
- let merged = with_additional_type_imports(module, CORE_BINDINGS_PACKAGE_NAME, ["Discount"]);
-
- assert_eq!(
- merged.imports_ts(),
- Some("import type { Decimal, Discount, Money } from \"@radroots/core-bindings\";\n\n")
- );
- }
-
- #[test]
- fn generated_type_export_inventory_parses_type_aliases() {
- assert_eq!(
- type_exports("export type Alpha = string;\nexport type Beta<T> = T;\n"),
- BTreeSet::from(["Alpha".to_owned(), "Beta".to_owned()])
- );
- }
-
- #[test]
- fn core_type_inventory_matches_current_package_surface() {
- let actual = type_inventory(CORE_BINDINGS_TYPES_TS);
-
- assert_eq!(actual, CORE_TYPE_INVENTORY);
- assert!(!CORE_BINDINGS_TYPES_TS.contains("RadrootsCore"));
- }
-
- #[test]
- fn event_type_inventory_matches_current_package_surface() {
- let actual = type_inventory(EVENT_BINDINGS_TYPES_TS);
-
- assert_eq!(actual, EVENT_TYPE_INVENTORY);
- assert!(!EVENT_BINDINGS_TYPES_TS.contains("RadrootsOrder"));
- }
-
- #[test]
- fn event_generated_types_expose_current_signed_event_dto_shapes() {
- let wire = type_declaration(EVENT_BINDINGS_TYPES_TS, "RadrootsNip01EventWireDto");
- let envelope = type_declaration(EVENT_BINDINGS_TYPES_TS, "RadrootsEventEnvelopeDto");
- let signed = type_declaration(EVENT_BINDINGS_TYPES_TS, "RadrootsSignedEventDto");
- let verified = type_declaration(EVENT_BINDINGS_TYPES_TS, "RadrootsVerifiedSignedEventDto");
-
- assert!(wire.contains("pubkey: string"));
- assert!(wire.contains("extra: { [key: string]: unknown }"));
- assert!(envelope.contains("author: string"));
- assert!(!envelope.contains("pubkey"));
- assert!(!envelope.contains("extra"));
- assert!(signed.contains("state: RadrootsSignedEventVerificationStateDto"));
- assert!(signed.contains("envelope: RadrootsEventEnvelopeDto"));
- assert!(signed.contains("wire: RadrootsNip01EventWireDto"));
- assert!(signed.contains("raw_json: string"));
- assert!(verified.contains("state: RadrootsVerifiedSignedEventVerificationStateDto"));
- assert!(verified.contains("signed_event: RadrootsSignedEventDto"));
- assert!(
- !EVENT_BINDINGS_TYPES_TS.contains("export type RadrootsEventEnvelope ="),
- "event package must not export the retired raw envelope type name"
- );
- }
-
- #[test]
- fn trade_type_inventory_matches_current_package_surface() {
- let actual = type_inventory(TRADE_BINDINGS_TYPES_TS);
-
- assert_eq!(actual, TRADE_TYPE_INVENTORY);
- }
-
- #[test]
- fn replica_schema_generated_types_preserve_source_schema_contracts() {
- let actual = type_inventory(REPLICA_SCHEMA_BINDINGS_TYPES_TS);
- let trade_product_filter = type_declaration(
- REPLICA_SCHEMA_BINDINGS_TYPES_TS,
- "ITradeProductFieldsFilter",
- );
- let trade_product_partial = type_declaration(
- REPLICA_SCHEMA_BINDINGS_TYPES_TS,
- "ITradeProductFieldsPartial",
- );
-
- assert!(actual.contains(&"Farm"));
- assert!(actual.contains(&"GcsLocation"));
- assert!(actual.contains(&"NostrEventHead"));
- assert!(actual.contains(&"ReplicaStoreJsonValue"));
- assert!(actual.contains(&"ITradeProductFieldsPartial"));
- assert!(!actual.contains(&"NostrEventState"));
- assert!(REPLICA_SCHEMA_BINDINGS_TYPES_TS.contains(
- "export type ReplicaStoreJsonValue = null | boolean | number | string | Array<ReplicaStoreJsonValue> | { [key: string]: ReplicaStoreJsonValue };"
- ));
- assert!(
- REPLICA_SCHEMA_BINDINGS_TYPES_TS
- .contains("export type IFarmFindOneResolve = ReplicaSchemaResult<Farm | null>;")
- );
- assert!(actual.contains(&"ReplicaSchemaResult"));
- assert!(actual.contains(&"ReplicaSchemaResultList"));
- assert!(actual.contains(&"ReplicaSchemaResultPass"));
- assert!(actual.contains(&"ReplicaSchemaError"));
- assert!(trade_product_filter.contains("year?: bigint"));
- assert!(trade_product_filter.contains("qty_avail?: bigint"));
- assert!(trade_product_partial.contains("year?: ReplicaStoreJsonValue | null"));
- assert!(trade_product_partial.contains("qty_avail?: ReplicaStoreJsonValue | null"));
- }
-
- #[test]
- fn replica_schema_generated_types_match_source_public_inventory() {
- let actual = type_inventory(REPLICA_SCHEMA_BINDINGS_TYPES_TS)
- .into_iter()
- .collect::<BTreeSet<_>>();
- let missing = source_public_schema_type_inventory()
- .into_iter()
- .filter(|name| !actual.contains(name))
- .collect::<Vec<_>>();
-
- assert!(
- missing.is_empty(),
- "missing generated replica schema exports: {missing:?}"
- );
- }
-
- #[test]
- fn trade_package_imports_source_owned_support_types() {
- assert!(TRADE_BINDINGS_TYPES_TS.contains("from \"@radroots/core-bindings\""));
- assert!(TRADE_BINDINGS_TYPES_TS.contains("from \"@radroots/event-bindings\""));
-
- let imports = imported_type_inventory(TRADE_BINDINGS_TYPES_TS);
- assert!(
- imports
- .get("@radroots/event-bindings")
- .is_some_and(|names| names.contains("RadrootsFarmRef"))
- );
- assert!(
- imports
- .get("@radroots/event-bindings")
- .is_some_and(|names| names.contains("RadrootsOperationalListing"))
- );
- assert!(
- imports
- .get("@radroots/event-bindings")
- .is_some_and(|names| names.contains("RadrootsOperationalListingBin"))
- );
-
- for duplicate in [
- "export type RadrootsOperationalListing = ",
- "export type RadrootsFarmRef = ",
- "export type RadrootsOrderEconomics = ",
- "export type RadrootsOrderInventoryCommitment = ",
- "export type RadrootsCommercialMessagePayload = ",
- "export type RadrootsCommercialMessageType = ",
- "export type RadrootsOrderStatus = ",
- "export type RadrootsTradeOrderWorkflowMessage = ",
- "export type RadrootsOrderWorkflowProjection = ",
- "export type RadrootsTradeMarketplaceOrderSummary = ",
- "export type RadrootsTradeOrderQuery = ",
- ] {
- assert!(!TRADE_BINDINGS_TYPES_TS.contains(duplicate));
- }
-
- let projection = type_declaration(TRADE_BINDINGS_TYPES_TS, "RadrootsTradeProjectionV1");
- assert!(projection.contains("agreement_state: RadrootsTradeAgreementStateV1"));
- assert!(projection.contains("negotiation_state: RadrootsTradeNegotiationStateV1"));
- assert!(projection.contains("agreement_claims: Array<RadrootsTradeAgreementClaimV1>"));
- assert!(projection.contains("issues: Array<RadrootsTradeReducerIssueV1>"));
- assert!(projection.contains("projection_digest: string"));
- for stale_counter in [
- "root_event_id",
- "last_message_type",
- "last_discount_request",
- "last_discount_offer",
- "accepted_discount",
- "last_discount_decline_reason",
- "has_requested_discounts",
- "question_count",
- "answer_count",
- "discount_request_count",
- "discount_offer_count",
- "discount_accept_count",
- "discount_decline_count",
- ] {
- assert!(!projection.contains(stale_counter));
- }
- }
-
- fn type_inventory(types_ts: &str) -> Vec<&str> {
- types_ts
- .lines()
- .filter_map(|line| line.strip_prefix("export type "))
- .map(|rest| rest.split([' ', '<']).next().expect("type name"))
- .collect()
- }
-
- fn source_public_schema_type_inventory() -> Vec<&'static str> {
- let mut names = BTreeSet::new();
-
- for source in REPLICA_SCHEMA_MODEL_SOURCES {
- for line in source.lines() {
- if let Some(name) = public_rust_type_name(line)
- && !name.ends_with("Ts")
- {
- names.insert(name);
- }
- }
- }
-
- names.into_iter().collect()
- }
-
- fn public_rust_type_name(line: &'static str) -> Option<&'static str> {
- let line = line.trim_start();
-
- ["pub struct ", "pub enum ", "pub type "]
- .into_iter()
- .find_map(|prefix| {
- line.strip_prefix(prefix).map(|rest| {
- rest.split(|char: char| !(char == '_' || char.is_ascii_alphanumeric()))
- .next()
- .expect("type name")
- })
- })
- }
-
- fn type_declaration<'a>(types_ts: &'a str, name: &str) -> &'a str {
- types_ts
- .lines()
- .find(|line| line.starts_with(&format!("export type {name} = ")))
- .unwrap_or_else(|| panic!("missing type declaration for {name}"))
- }
-
- fn span() -> SourceSpan {
- SourceSpan::new("tools/xtask/src/dto_roots.rs", 1, 1)
- }
-}
diff --git a/tools/xtask/src/fs.rs b/tools/xtask/src/fs.rs
@@ -1,61 +0,0 @@
-use std::{
- fs,
- path::{Path, PathBuf},
-};
-
-pub fn workspace_root() -> Result<PathBuf, String> {
- let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
- manifest_dir
- .parent()
- .and_then(Path::parent)
- .map(Path::to_path_buf)
- .ok_or_else(|| {
- format!(
- "cannot resolve workspace root from {}",
- manifest_dir.display()
- )
- })
-}
-
-#[allow(dead_code)]
-pub fn write_if_changed(path: &Path, contents: &str) -> Result<bool, String> {
- if let Ok(existing) = fs::read_to_string(path)
- && existing == contents
- {
- return Ok(false);
- }
- if let Some(parent) = path.parent() {
- fs::create_dir_all(parent)
- .map_err(|error| format!("failed to create {}: {error}", parent.display()))?;
- }
- fs::write(path, contents)
- .map_err(|error| format!("failed to write {}: {error}", path.display()))?;
- Ok(true)
-}
-
-pub fn write_bytes_if_changed(path: &Path, contents: &[u8]) -> Result<bool, String> {
- if let Ok(existing) = fs::read(path)
- && existing == contents
- {
- return Ok(false);
- }
- if let Some(parent) = path.parent() {
- fs::create_dir_all(parent)
- .map_err(|error| format!("failed to create {}: {error}", parent.display()))?;
- }
- fs::write(path, contents)
- .map_err(|error| format!("failed to write {}: {error}", path.display()))?;
- Ok(true)
-}
-
-#[cfg(test)]
-mod tests {
- use super::workspace_root;
-
- #[test]
- fn resolves_workspace_root() {
- let root = workspace_root().expect("workspace root resolves");
- assert!(root.join("Cargo.toml").is_file());
- assert!(root.join("packages").is_dir());
- }
-}
diff --git a/tools/xtask/src/generate.rs b/tools/xtask/src/generate.rs
@@ -1,35 +0,0 @@
-use crate::{
- fs::workspace_root, output::package_outputs, package_matrix::validate_package_matrix,
- package_metadata,
-};
-
-pub fn generate_all() -> Result<(), String> {
- generate_ts()?;
- generate_package_metadata()
-}
-
-pub fn generate_ts() -> Result<(), String> {
- validate_package_matrix()?;
- let root = workspace_root()?;
- for output in package_outputs()? {
- for generated_file in output.files() {
- let path = root
- .join(output.spec.package_dir)
- .join(generated_file.relative_path);
- crate::fs::write_if_changed(&path, &generated_file.contents)?;
- }
- let provenance_file = output.provenance_file();
- crate::fs::write_if_changed(
- &root.join(&provenance_file.relative_path),
- &provenance_file.contents,
- )?;
- println!("generated TypeScript package {}", output.spec.package_name);
- }
- Ok(())
-}
-
-pub fn generate_package_metadata() -> Result<(), String> {
- validate_package_matrix()?;
- let root = workspace_root()?;
- package_metadata::generate_package_metadata(&root)
-}
diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs
@@ -1,281 +0,0 @@
-mod api_qualification;
-mod architecture;
-mod bindings;
-mod check;
-mod contracts;
-mod coverage;
-mod coverage_policy;
-#[allow(dead_code)]
-mod dto_roots;
-mod fs;
-mod generate;
-mod manifest;
-mod output;
-mod package_matrix;
-mod package_metadata;
-mod portable_qualification;
-mod release_graph;
-mod release_qualification;
-mod smoke;
-mod supply_chain_qualification;
-mod target_qualification;
-mod ts;
-mod wasm;
-mod wasm_declarations;
-
-enum CommandAction<'a> {
- Architecture,
- ArchitectureCi,
- CheckApiBoundaries,
- CheckDependencyBoundaries,
- GenerateAll,
- GenerateBindings(&'a [String]),
- GenerateTs,
- GenerateWasm(&'a [String]),
- GeneratePackageMetadata,
- Coverage(&'a [String]),
- QualifyFeatures,
- QualifyGraph,
- QualifyApi,
- QualifyPortable,
- QualifySupplyChain,
- QualifyTargets,
- Check,
- Smoke(&'a [String]),
-}
-
-fn main() {
- if let Err(error) = run(std::env::args().skip(1)) {
- eprintln!("{error}");
- std::process::exit(1);
- }
-}
-
-fn run(args: impl IntoIterator<Item = String>) -> Result<(), String> {
- let args = args.into_iter().collect::<Vec<_>>();
- match command_action(&args)? {
- CommandAction::Architecture => architecture::validate(&fs::workspace_root()?),
- CommandAction::ArchitectureCi => {
- let root = fs::workspace_root()?;
- architecture::validate_ci(&root)?;
- check::architecture_ci(&root)
- }
- CommandAction::CheckApiBoundaries => {
- architecture::validate_api_boundaries(&fs::workspace_root()?)
- }
- CommandAction::CheckDependencyBoundaries => {
- architecture::validate_dependency_boundaries(&fs::workspace_root()?)
- }
- CommandAction::GenerateAll => generate::generate_all(),
- CommandAction::GenerateBindings(rest) => bindings::generate(rest),
- CommandAction::GenerateTs => generate::generate_ts(),
- CommandAction::GenerateWasm(rest) => wasm::generate(rest),
- CommandAction::GeneratePackageMetadata => generate::generate_package_metadata(),
- CommandAction::Coverage(rest) => coverage::run(rest),
- CommandAction::QualifyFeatures => {
- release_qualification::run_feature_matrix(&fs::workspace_root()?)
- }
- CommandAction::QualifyGraph => release_graph::run(&fs::workspace_root()?),
- CommandAction::QualifyApi => api_qualification::run(&fs::workspace_root()?),
- CommandAction::QualifyPortable => portable_qualification::run(&fs::workspace_root()?),
- CommandAction::QualifySupplyChain => {
- supply_chain_qualification::run(&fs::workspace_root()?)
- }
- CommandAction::QualifyTargets => target_qualification::run(&fs::workspace_root()?),
- CommandAction::Check => check::check(),
- CommandAction::Smoke(rest) => smoke::run(rest),
- }
-}
-
-fn command_action(args: &[String]) -> Result<CommandAction<'_>, String> {
- match args {
- [command] if command == "architecture" => Ok(CommandAction::Architecture),
- [command] if command == "architecture-ci" => Ok(CommandAction::ArchitectureCi),
- [command] if command == "check-api-boundaries" => Ok(CommandAction::CheckApiBoundaries),
- [command] if command == "check-dependency-boundaries" => {
- Ok(CommandAction::CheckDependencyBoundaries)
- }
- [command] if command == "generate" => Ok(CommandAction::GenerateAll),
- [command, target] if command == "generate" && target == "ts" => {
- Ok(CommandAction::GenerateTs)
- }
- [command, target, rest @ ..] if command == "generate" && target == "wasm" => {
- Ok(CommandAction::GenerateWasm(rest))
- }
- [command, target, rest @ ..] if command == "generate" && target == "bindings" => {
- Ok(CommandAction::GenerateBindings(rest))
- }
- [command, target] if command == "generate" && target == "package-metadata" => {
- Ok(CommandAction::GeneratePackageMetadata)
- }
- [command, rest @ ..] if command == "coverage" => Ok(CommandAction::Coverage(rest)),
- [command, target] if command == "release" && target == "qualify-features" => {
- Ok(CommandAction::QualifyFeatures)
- }
- [command, target] if command == "release" && target == "qualify-graph" => {
- Ok(CommandAction::QualifyGraph)
- }
- [command, target] if command == "release" && target == "qualify-api" => {
- Ok(CommandAction::QualifyApi)
- }
- [command, target] if command == "release" && target == "qualify-portable" => {
- Ok(CommandAction::QualifyPortable)
- }
- [command, target] if command == "release" && target == "qualify-supply-chain" => {
- Ok(CommandAction::QualifySupplyChain)
- }
- [command, target] if command == "release" && target == "qualify-targets" => {
- Ok(CommandAction::QualifyTargets)
- }
- [command] if command == "check" => Ok(CommandAction::Check),
- [command, rest @ ..] if command == "smoke" => Ok(CommandAction::Smoke(rest)),
- [] => Err(usage()),
- _ => Err(usage()),
- }
-}
-
-fn usage() -> String {
- "usage: cargo xtask architecture | cargo xtask architecture-ci | cargo xtask check-api-boundaries | cargo xtask check-dependency-boundaries | cargo xtask generate | cargo xtask generate ts | cargo xtask generate wasm [--package <key>] | cargo xtask generate bindings <swift|kotlin> | cargo xtask generate package-metadata | cargo xtask check | cargo xtask smoke facade-rust-local | sdk-rust-local | front-doors-rust-local | cargo xtask coverage run | cargo xtask coverage check | cargo xtask release qualify-features | cargo xtask release qualify-graph | cargo xtask release qualify-api | cargo xtask release qualify-portable | cargo xtask release qualify-supply-chain | cargo xtask release qualify-targets"
- .to_owned()
-}
-
-#[cfg(test)]
-mod tests {
- use super::{CommandAction, command_action};
-
- #[test]
- fn accepts_architecture() {
- let args = ["architecture".to_owned()];
- assert!(matches!(
- command_action(&args).expect("action"),
- CommandAction::Architecture
- ));
- }
-
- #[test]
- fn accepts_architecture_ci() {
- let args = ["architecture-ci".to_owned()];
- assert!(matches!(
- command_action(&args).expect("action"),
- CommandAction::ArchitectureCi
- ));
- }
-
- #[test]
- fn accepts_api_boundary_check() {
- let args = ["check-api-boundaries".to_owned()];
- assert!(matches!(
- command_action(&args).expect("action"),
- CommandAction::CheckApiBoundaries
- ));
- }
-
- #[test]
- fn accepts_dependency_boundary_check() {
- let args = ["check-dependency-boundaries".to_owned()];
- assert!(matches!(
- command_action(&args).expect("action"),
- CommandAction::CheckDependencyBoundaries
- ));
- }
-
- #[test]
- fn accepts_generate_ts() {
- let args = ["generate".to_owned(), "ts".to_owned()];
- assert!(matches!(
- command_action(&args).expect("action"),
- CommandAction::GenerateTs
- ));
- }
-
- #[test]
- fn accepts_generate_all() {
- let args = ["generate".to_owned()];
- assert!(matches!(
- command_action(&args).expect("action"),
- CommandAction::GenerateAll
- ));
- }
-
- #[test]
- fn accepts_generate_wasm() {
- let args = ["generate".to_owned(), "wasm".to_owned()];
- assert!(matches!(
- command_action(&args).expect("action"),
- CommandAction::GenerateWasm(rest) if rest.is_empty()
- ));
- }
-
- #[test]
- fn accepts_generate_bindings() {
- let args = [
- "generate".to_owned(),
- "bindings".to_owned(),
- "swift".to_owned(),
- ];
- assert!(matches!(
- command_action(&args).expect("action"),
- CommandAction::GenerateBindings(rest) if rest == ["swift"]
- ));
- }
-
- #[test]
- fn accepts_generate_package_metadata() {
- let args = ["generate".to_owned(), "package-metadata".to_owned()];
- assert!(matches!(
- command_action(&args).expect("action"),
- CommandAction::GeneratePackageMetadata
- ));
- }
-
- #[test]
- fn accepts_check() {
- let args = ["check".to_owned()];
- assert!(matches!(
- command_action(&args).expect("action"),
- CommandAction::Check
- ));
- }
-
- #[test]
- fn accepts_smoke() {
- let args = ["smoke".to_owned(), "sdk-rust-local".to_owned()];
- assert!(matches!(
- command_action(&args).expect("action"),
- CommandAction::Smoke(rest) if rest == ["sdk-rust-local"]
- ));
- }
-
- #[test]
- fn accepts_coverage_run() {
- let args = ["coverage".to_owned(), "run".to_owned()];
- assert!(matches!(
- command_action(&args).expect("action"),
- CommandAction::Coverage(rest) if rest == ["run"]
- ));
- }
-
- #[test]
- fn accepts_supply_chain_qualification() {
- let args = ["release".to_owned(), "qualify-supply-chain".to_owned()];
- assert!(matches!(
- command_action(&args).expect("action"),
- CommandAction::QualifySupplyChain
- ));
- }
-
- #[test]
- fn accepts_release_graph_qualification() {
- let args = ["release".to_owned(), "qualify-graph".to_owned()];
- assert!(matches!(
- command_action(&args).expect("action"),
- CommandAction::QualifyGraph
- ));
- }
-
- #[test]
- fn rejects_unknown_command() {
- let args = ["generate".to_owned(), "swift".to_owned()];
- assert!(command_action(&args).is_err());
- }
-}
diff --git a/tools/xtask/src/manifest.rs b/tools/xtask/src/manifest.rs
@@ -1,53 +0,0 @@
-use serde_json::json;
-
-use crate::package_matrix::PackageSpec;
-
-pub fn manifest_relative_path(spec: PackageSpec) -> String {
- format!("contracts/provenance/typescript/{}.json", spec.key)
-}
-
-pub fn package_manifest(spec: PackageSpec) -> serde_json::Value {
- json!({
- "package": spec.package_name,
- "crate": spec.crate_name,
- "version": "0.1.0",
- "license": "MIT OR Apache-2.0",
- "repository": "https://github.com/radrootslabs/sdk",
- "repository_directory": spec.package_dir,
- "generator": "radroots_sdk_xtask",
- "generated": false
- })
-}
-
-#[cfg(test)]
-mod tests {
- use crate::{
- manifest::{manifest_relative_path, package_manifest},
- package_matrix::package_specs,
- };
-
- #[test]
- fn manifest_path_is_outside_package_source() {
- assert_eq!(
- manifest_relative_path(package_specs()[0]),
- "contracts/provenance/typescript/core.json"
- );
- }
-
- #[test]
- fn manifest_records_package_and_crate() {
- let manifest = package_manifest(package_specs()[0]);
- assert_eq!(manifest["package"], package_specs()[0].package_name);
- assert_eq!(manifest["crate"], package_specs()[0].crate_name);
- assert_eq!(manifest["version"], "0.1.0");
- assert_eq!(manifest["license"], "MIT OR Apache-2.0");
- assert_eq!(
- manifest["repository"],
- "https://github.com/radrootslabs/sdk"
- );
- assert_eq!(
- manifest["repository_directory"],
- package_specs()[0].package_dir
- );
- }
-}
diff --git a/tools/xtask/src/output.rs b/tools/xtask/src/output.rs
@@ -1,493 +0,0 @@
-use dto_bindgen_backend_ts::{DtoTypesModule, TypeScriptImport, TypeScriptModule};
-use sha2::{Digest, Sha256};
-
-use crate::{
- dto_roots,
- manifest::manifest_relative_path,
- manifest::package_manifest,
- package_matrix::{PackageSpec, package_specs},
- ts::{generated_constants_file, generated_header, generated_kinds_file, generated_types_file},
-};
-
-pub struct PackageOutput {
- pub spec: PackageSpec,
- pub types_ts: Option<TsSource>,
- pub types_imports: Vec<TypeScriptImport>,
- pub constants_ts: Option<TsSource>,
- pub kinds_ts: Option<TsSource>,
-}
-
-pub struct GeneratedFile {
- pub relative_path: String,
- pub contents: String,
-}
-
-pub enum TsSource {
- DtoRegistry(DtoTypesModule),
- Module(TypeScriptModule),
- CanonicalNativeSnapshot { contents: &'static str },
-}
-
-impl TsSource {
- fn canonical_native_snapshot(
- contents: &'static str,
- sha256: &'static str,
- source_packages: &'static [&'static str],
- ) -> Result<Self, String> {
- let actual = format!("{:x}", Sha256::digest(contents.as_bytes()));
- if actual != sha256 {
- return Err(format!(
- "canonical native type snapshot drifted: expected {sha256}, found {actual}; regenerate from and review against {}",
- source_packages.join(", ")
- ));
- }
- Ok(Self::CanonicalNativeSnapshot { contents })
- }
-
- fn render(&self) -> String {
- match self {
- Self::DtoRegistry(module) => module.body_ts().to_owned(),
- Self::Module(module) => module.render_source(),
- Self::CanonicalNativeSnapshot { contents } => (*contents).to_owned(),
- }
- }
-
- fn imports(&self) -> Option<&str> {
- match self {
- Self::DtoRegistry(module) => module.imports_ts(),
- Self::Module(_) | Self::CanonicalNativeSnapshot { .. } => None,
- }
- }
-}
-
-const EVENT_BINDINGS_TYPES_TS: &str =
- include_str!("../../../packages/event-bindings/src/generated/types.ts");
-const EVENT_BINDINGS_TYPES_SHA256: &str =
- "fe4496950852715c573ff2abc7df8edede7161de75d9c68c3f4d6836e67f829a";
-const TRADE_BINDINGS_TYPES_TS: &str =
- include_str!("../../../packages/trade-bindings/src/generated/types.ts");
-const TRADE_BINDINGS_TYPES_SHA256: &str =
- "49560c572206095bd7de5cfd377c234a7f136e4affc13d53516ae2c4b8998e87";
-const EVENT_NATIVE_SOURCES: &[&str] = &["radroots_event", "radroots_core"];
-const TRADE_NATIVE_SOURCES: &[&str] = &["radroots_trade", "radroots_event", "radroots_core"];
-
-impl PackageOutput {
- pub fn files(&self) -> Vec<GeneratedFile> {
- let mut files = Vec::new();
- if let Some(types_ts) = &self.types_ts {
- let imports = combined_imports(
- structured_imports_ts(&self.types_imports).as_deref(),
- types_ts.imports(),
- );
- files.push(GeneratedFile {
- relative_path: format!("src/generated/{}", generated_types_file()),
- contents: render_ts(types_ts, imports.as_deref()),
- });
- }
- if let Some(constants_ts) = &self.constants_ts {
- files.push(GeneratedFile {
- relative_path: format!("src/generated/{}", generated_constants_file()),
- contents: render_ts(constants_ts, None),
- });
- }
- if let Some(kinds_ts) = &self.kinds_ts {
- files.push(GeneratedFile {
- relative_path: format!("src/generated/{}", generated_kinds_file()),
- contents: render_ts(kinds_ts, None),
- });
- }
- files
- }
-
- pub fn provenance_file(&self) -> GeneratedFile {
- GeneratedFile {
- relative_path: manifest_relative_path(self.spec),
- contents: render_manifest(self),
- }
- }
-}
-
-pub fn package_outputs() -> Result<Vec<PackageOutput>, String> {
- Ok(vec![
- PackageOutput {
- spec: spec_by_key("core"),
- types_ts: Some(TsSource::DtoRegistry(dto_roots::core_types_module()?)),
- types_imports: Vec::new(),
- constants_ts: None,
- kinds_ts: None,
- },
- PackageOutput {
- spec: spec_by_key("event"),
- types_ts: Some(TsSource::canonical_native_snapshot(
- EVENT_BINDINGS_TYPES_TS,
- EVENT_BINDINGS_TYPES_SHA256,
- EVENT_NATIVE_SOURCES,
- )?),
- types_imports: Vec::new(),
- constants_ts: Some(TsSource::Module(radroots_event_bindings::constants_module())),
- kinds_ts: Some(TsSource::Module(radroots_event_bindings::kinds_module())),
- },
- PackageOutput {
- spec: spec_by_key("identity"),
- types_ts: None,
- types_imports: Vec::new(),
- constants_ts: Some(TsSource::Module(
- radroots_identity_bindings::constants_module(),
- )),
- kinds_ts: None,
- },
- PackageOutput {
- spec: spec_by_key("replica_schema"),
- types_ts: Some(TsSource::DtoRegistry(
- dto_roots::replica_schema_types_module()?,
- )),
- types_imports: Vec::new(),
- constants_ts: None,
- kinds_ts: None,
- },
- PackageOutput {
- spec: spec_by_key("trade"),
- types_ts: Some(TsSource::canonical_native_snapshot(
- TRADE_BINDINGS_TYPES_TS,
- TRADE_BINDINGS_TYPES_SHA256,
- TRADE_NATIVE_SOURCES,
- )?),
- types_imports: Vec::new(),
- constants_ts: None,
- kinds_ts: None,
- },
- ])
-}
-
-fn spec_by_key(key: &str) -> PackageSpec {
- package_specs()
- .iter()
- .copied()
- .find(|spec| spec.key == key)
- .unwrap_or_else(|| panic!("missing package spec for {key}"))
-}
-
-fn render_ts(source: &TsSource, imports: Option<&str>) -> String {
- if matches!(source, TsSource::CanonicalNativeSnapshot { .. }) {
- return source.render();
- }
- let body = source.render();
- let imports = imports.unwrap_or("");
- let mut rendered = format!("{}{}{}", generated_header(), imports, body.trim_start());
- if !rendered.ends_with('\n') {
- rendered.push('\n');
- }
- rendered
-}
-
-fn combined_imports(first: Option<&str>, second: Option<&str>) -> Option<String> {
- match (first, second) {
- (Some(first), Some(second)) => Some(format!("{first}{second}")),
- (Some(first), None) => Some(first.to_owned()),
- (None, Some(second)) => Some(second.to_owned()),
- (None, None) => None,
- }
-}
-
-fn structured_imports_ts(imports: &[TypeScriptImport]) -> Option<String> {
- if imports.is_empty() {
- return None;
- }
- Some(
- imports
- .iter()
- .cloned()
- .fold(TypeScriptModule::new("types.ts"), |module, import| {
- module.with_import(import)
- })
- .render_source(),
- )
-}
-
-fn render_manifest(output: &PackageOutput) -> String {
- let spec = output.spec;
- let mut value = package_manifest(spec);
- value["generated"] = serde_json::Value::Bool(true);
- value["outputs"] = serde_json::Value::Object(
- output
- .files()
- .into_iter()
- .map(|file| {
- (
- file.relative_path,
- serde_json::Value::String(format!(
- "{:x}",
- Sha256::digest(file.contents.as_bytes())
- )),
- )
- })
- .collect(),
- );
- if matches!(spec.key, "event" | "trade") {
- let (sha256, source_packages) = match spec.key {
- "event" => (EVENT_BINDINGS_TYPES_SHA256, EVENT_NATIVE_SOURCES),
- "trade" => (TRADE_BINDINGS_TYPES_SHA256, TRADE_NATIVE_SOURCES),
- _ => unreachable!(),
- };
- value["types_source"] =
- serde_json::Value::String("canonical_native_type_snapshot".to_owned());
- value["types_sha256"] = serde_json::Value::String(sha256.to_owned());
- value["types_source_packages"] = serde_json::Value::Array(
- source_packages
- .iter()
- .map(|package| serde_json::Value::String((*package).to_owned()))
- .collect(),
- );
- }
- format!(
- "{}\n",
- serde_json::to_string_pretty(&value).expect("manifest json serializes")
- )
-}
-
-#[cfg(test)]
-mod tests {
- use super::{PackageOutput, TsSource, package_outputs, render_ts};
- use crate::package_matrix::package_specs;
- use dto_bindgen_backend_ts::{
- DtoTypesModule, TypeScriptDeclaration, TypeScriptImport, TypeScriptModule, TypeScriptType,
- };
-
- const TRADE_BINDINGS_TYPES_TS: &str =
- include_str!("../../../packages/trade-bindings/src/generated/types.ts");
- const REPLICA_SCHEMA_BINDINGS_TYPES_TS: &str =
- include_str!("../../../packages/replica-schema-bindings/src/generated/types.ts");
- const EVENT_BINDINGS_CONSTANTS_TS: &str =
- include_str!("../../../packages/event-bindings/src/generated/constants.ts");
- const EVENT_BINDINGS_KINDS_TS: &str =
- include_str!("../../../packages/event-bindings/src/generated/kinds.ts");
- const IDENTITY_BINDINGS_CONSTANTS_TS: &str =
- include_str!("../../../packages/identity-bindings/src/generated/constants.ts");
-
- #[test]
- fn renders_sdk_header() {
- let output = render_ts(&test_module(), None);
- assert!(output.starts_with("// @generated by cargo xtask generate ts"));
- assert!(output.contains("export type A = string;"));
- }
-
- #[test]
- fn renders_import_prelude_after_header() {
- let output = render_ts(&test_module(), Some("import type { B } from \"b\";\n\n"));
- assert!(output.starts_with(
- "// @generated by cargo xtask generate ts\n// Do not edit by hand.\nimport type"
- ));
- assert!(output.contains("export type A = string;"));
- }
-
- #[test]
- fn renders_module_sources() {
- let output = render_ts(&test_module(), None);
- assert_eq!(
- output,
- "// @generated by cargo xtask generate ts\n// Do not edit by hand.\nexport type A = string;\n"
- );
- }
-
- #[test]
- fn canonical_native_snapshot_rejects_digest_drift() {
- let error = match TsSource::canonical_native_snapshot(
- "drifted snapshot\n",
- super::EVENT_BINDINGS_TYPES_SHA256,
- super::EVENT_NATIVE_SOURCES,
- ) {
- Ok(_) => panic!("digest drift must fail closed"),
- Err(error) => error,
- };
-
- assert!(error.contains("canonical native type snapshot drifted"));
- assert!(error.contains(super::EVENT_BINDINGS_TYPES_SHA256));
- assert!(error.contains("radroots_event"));
- }
-
- #[test]
- fn includes_core_and_schema_outputs() {
- let package_names = package_outputs()
- .expect("package outputs")
- .into_iter()
- .map(|output| output.spec.package_name)
- .collect::<Vec<_>>();
- assert!(package_names.contains(&"@radroots/core-bindings"));
- assert!(package_names.contains(&"@radroots/event-bindings"));
- assert!(package_names.contains(&"@radroots/identity-bindings"));
- assert!(package_names.contains(&"@radroots/replica-schema-bindings"));
- assert!(package_names.contains(&"@radroots/trade-bindings"));
- }
-
- #[test]
- fn dto_registry_source_uses_generated_package_files() {
- let output = PackageOutput {
- spec: package_specs()[0],
- types_ts: Some(TsSource::DtoRegistry(DtoTypesModule::new(
- "import type { ExternalThing } from \"@radroots/external-bindings\";\n\n",
- "export type SyntheticThing = { external: ExternalThing, };",
- ))),
- types_imports: vec![TypeScriptImport::type_only(
- ["LocalPrelude"],
- "@radroots/local",
- )],
- constants_ts: None,
- kinds_ts: None,
- };
- let files = output.files();
- let types = files
- .iter()
- .find(|file| file.relative_path == "src/generated/types.ts")
- .expect("types file");
- let manifest = output.provenance_file();
-
- assert_eq!(
- types.contents,
- "// @generated by cargo xtask generate ts\n// Do not edit by hand.\nimport type { LocalPrelude } from \"@radroots/local\";\nimport type { ExternalThing } from \"@radroots/external-bindings\";\n\nexport type SyntheticThing = { external: ExternalThing, };\n"
- );
- assert_eq!(
- manifest.relative_path,
- "contracts/provenance/typescript/core.json"
- );
- assert!(manifest.contents.contains("\"generated\": true"));
- assert!(
- !files
- .iter()
- .any(|file| file.relative_path == "src/index.ts")
- );
- }
-
- #[test]
- fn package_outputs_do_not_generate_package_indices() {
- for output in package_outputs().expect("package outputs") {
- assert!(
- !output
- .files()
- .iter()
- .any(|file| file.relative_path == "src/index.ts"),
- "{} index must remain handwritten source",
- output.spec.package_name
- );
- }
- }
-
- #[test]
- fn trade_output_uses_canonical_native_snapshot_and_matches_checked_in_types() {
- let output = package_outputs()
- .expect("package outputs")
- .into_iter()
- .find(|output| output.spec.key == "trade")
- .expect("trade output");
-
- assert!(matches!(
- output.types_ts,
- Some(TsSource::CanonicalNativeSnapshot { .. })
- ));
- assert!(output.types_imports.is_empty());
-
- let types = output
- .files()
- .into_iter()
- .find(|file| file.relative_path == "src/generated/types.ts")
- .expect("types file");
-
- assert_eq!(types.contents, TRADE_BINDINGS_TYPES_TS);
- }
-
- #[test]
- fn event_output_uses_canonical_native_snapshot_and_matches_checked_in_types() {
- let output = package_outputs()
- .expect("package outputs")
- .into_iter()
- .find(|output| output.spec.key == "event")
- .expect("event output");
-
- assert!(matches!(
- output.types_ts,
- Some(TsSource::CanonicalNativeSnapshot { .. })
- ));
- let types = output
- .files()
- .into_iter()
- .find(|file| file.relative_path == "src/generated/types.ts")
- .expect("types file");
- assert_eq!(types.contents, super::EVENT_BINDINGS_TYPES_TS);
-
- let manifest = output.provenance_file();
- assert!(manifest.contents.contains("canonical_native_type_snapshot"));
- assert!(manifest.contents.contains("radroots_event"));
- assert!(manifest.contents.contains("radroots_core"));
- }
-
- #[test]
- fn replica_schema_output_uses_dto_registry_and_matches_checked_in_types() {
- let output = package_outputs()
- .expect("package outputs")
- .into_iter()
- .find(|output| output.spec.key == "replica_schema")
- .expect("replica_schema output");
-
- assert!(matches!(output.types_ts, Some(TsSource::DtoRegistry(_))));
- assert!(output.types_imports.is_empty());
-
- let types = output
- .files()
- .into_iter()
- .find(|file| file.relative_path == "src/generated/types.ts")
- .expect("types file");
-
- assert_eq!(types.contents, REPLICA_SCHEMA_BINDINGS_TYPES_TS);
- }
-
- #[test]
- fn events_constants_and_kinds_use_modules_and_match_checked_in_files() {
- let output = package_outputs()
- .expect("package outputs")
- .into_iter()
- .find(|output| output.spec.key == "event")
- .expect("event output");
-
- assert!(matches!(output.constants_ts, Some(TsSource::Module(_))));
- assert!(matches!(output.kinds_ts, Some(TsSource::Module(_))));
-
- let files = output.files();
- let constants = files
- .iter()
- .find(|file| file.relative_path == "src/generated/constants.ts")
- .expect("constants file");
- let kinds = files
- .iter()
- .find(|file| file.relative_path == "src/generated/kinds.ts")
- .expect("kinds file");
-
- assert_eq!(constants.contents, EVENT_BINDINGS_CONSTANTS_TS);
- assert_eq!(kinds.contents, EVENT_BINDINGS_KINDS_TS);
- }
-
- #[test]
- fn identity_constants_use_module_and_match_checked_in_file() {
- let output = package_outputs()
- .expect("package outputs")
- .into_iter()
- .find(|output| output.spec.key == "identity")
- .expect("identity output");
-
- assert!(matches!(output.constants_ts, Some(TsSource::Module(_))));
-
- let constants = output
- .files()
- .into_iter()
- .find(|file| file.relative_path == "src/generated/constants.ts")
- .expect("constants file");
-
- assert_eq!(constants.contents, IDENTITY_BINDINGS_CONSTANTS_TS);
- }
-
- fn test_module() -> TsSource {
- TsSource::Module(
- TypeScriptModule::new("src/generated/test.ts").with_declaration(
- TypeScriptDeclaration::type_alias("A", TypeScriptType::String),
- ),
- )
- }
-}
diff --git a/tools/xtask/src/package_matrix.rs b/tools/xtask/src/package_matrix.rs
@@ -1,199 +0,0 @@
-use std::collections::BTreeSet;
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct PackageSpec {
- pub key: &'static str,
- pub crate_name: &'static str,
- pub crate_dir: &'static str,
- pub package_name: &'static str,
- pub package_dir: &'static str,
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct WasmPackageSpec {
- pub key: &'static str,
- pub crate_name: &'static str,
- pub crate_dir: &'static str,
- pub package_name: &'static str,
- pub package_dir: &'static str,
- pub out_name: &'static str,
- pub out_dir: &'static str,
-}
-
-pub const PACKAGE_SPECS: [PackageSpec; 5] = [
- PackageSpec {
- key: "core",
- crate_name: "radroots_core_bindings",
- crate_dir: "crates/core_bindings",
- package_name: "@radroots/core-bindings",
- package_dir: "packages/core-bindings",
- },
- PackageSpec {
- key: "event",
- crate_name: "radroots_event_bindings",
- crate_dir: "crates/event_bindings",
- package_name: "@radroots/event-bindings",
- package_dir: "packages/event-bindings",
- },
- PackageSpec {
- key: "identity",
- crate_name: "radroots_identity_bindings",
- crate_dir: "crates/identity_bindings",
- package_name: "@radroots/identity-bindings",
- package_dir: "packages/identity-bindings",
- },
- PackageSpec {
- key: "replica_schema",
- crate_name: "radroots_replica_schema_bindings",
- crate_dir: "crates/replica_schema_bindings",
- package_name: "@radroots/replica-schema-bindings",
- package_dir: "packages/replica-schema-bindings",
- },
- PackageSpec {
- key: "trade",
- crate_name: "radroots_trade_bindings",
- crate_dir: "crates/trade_bindings",
- package_name: "@radroots/trade-bindings",
- package_dir: "packages/trade-bindings",
- },
-];
-
-pub const WASM_PACKAGE_SPECS: [WasmPackageSpec; 3] = [
- WasmPackageSpec {
- key: "event_codec",
- crate_name: "radroots_event_codec_wasm",
- crate_dir: "crates/event_codec_wasm",
- package_name: "@radroots/event-codec-wasm",
- package_dir: "packages/event-codec-wasm",
- out_name: "radroots_event_codec_wasm",
- out_dir: "../../packages/event-codec-wasm/dist",
- },
- WasmPackageSpec {
- key: "replica_store",
- crate_name: "radroots_replica_store_wasm",
- crate_dir: "crates/replica_store_wasm",
- package_name: "@radroots/replica-store-wasm",
- package_dir: "packages/replica-store-wasm",
- out_name: "radroots_replica_store_wasm",
- out_dir: "../../packages/replica-store-wasm/dist",
- },
- WasmPackageSpec {
- key: "replica_sync",
- crate_name: "radroots_replica_sync_wasm",
- crate_dir: "crates/replica_sync_wasm",
- package_name: "@radroots/replica-sync-wasm",
- package_dir: "packages/replica-sync-wasm",
- out_name: "radroots_replica_sync_wasm",
- out_dir: "../../packages/replica-sync-wasm/dist",
- },
-];
-
-pub const FORBIDDEN_PACKAGE_NAMES: [&str; 2] =
- ["@radroots/tangle-db-schema-bindings", "@radroots/contracts"];
-
-pub fn package_specs() -> &'static [PackageSpec] {
- &PACKAGE_SPECS
-}
-
-pub fn wasm_package_specs() -> &'static [WasmPackageSpec] {
- &WASM_PACKAGE_SPECS
-}
-
-pub fn validate_package_matrix() -> Result<(), String> {
- let mut crate_names = BTreeSet::new();
- let mut package_names = BTreeSet::new();
- let mut package_dirs = BTreeSet::new();
- for spec in package_specs() {
- if FORBIDDEN_PACKAGE_NAMES.contains(&spec.package_name) {
- return Err(format!(
- "forbidden package in matrix: {}",
- spec.package_name
- ));
- }
- if !crate_names.insert(spec.crate_name) {
- return Err(format!("duplicate crate in matrix: {}", spec.crate_name));
- }
- if !package_names.insert(spec.package_name) {
- return Err(format!(
- "duplicate package in matrix: {}",
- spec.package_name
- ));
- }
- if !package_dirs.insert(spec.package_dir) {
- return Err(format!(
- "duplicate package directory in matrix: {}",
- spec.package_dir
- ));
- }
- }
- for spec in wasm_package_specs() {
- if FORBIDDEN_PACKAGE_NAMES.contains(&spec.package_name) {
- return Err(format!(
- "forbidden package in matrix: {}",
- spec.package_name
- ));
- }
- if !crate_names.insert(spec.crate_name) {
- return Err(format!("duplicate crate in matrix: {}", spec.crate_name));
- }
- if !package_names.insert(spec.package_name) {
- return Err(format!(
- "duplicate package in matrix: {}",
- spec.package_name
- ));
- }
- if !package_dirs.insert(spec.package_dir) {
- return Err(format!(
- "duplicate package directory in matrix: {}",
- spec.package_dir
- ));
- }
- }
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- FORBIDDEN_PACKAGE_NAMES, package_specs, validate_package_matrix, wasm_package_specs,
- };
-
- #[test]
- fn package_matrix_is_valid() {
- validate_package_matrix().expect("package matrix is valid");
- }
-
- #[test]
- fn approved_package_count_is_stable() {
- assert_eq!(package_specs().len(), 5);
- assert_eq!(wasm_package_specs().len(), 3);
- }
-
- #[test]
- fn forbidden_names_are_absent() {
- for spec in package_specs() {
- assert!(!FORBIDDEN_PACKAGE_NAMES.contains(&spec.package_name));
- }
- for spec in wasm_package_specs() {
- assert!(!FORBIDDEN_PACKAGE_NAMES.contains(&spec.package_name));
- }
- }
-
- #[test]
- fn replica_schema_package_uses_current_name() {
- assert!(
- package_specs()
- .iter()
- .any(|spec| spec.package_name == "@radroots/replica-schema-bindings")
- );
- }
-
- #[test]
- fn wasm_packages_use_sdk_package_names() {
- assert!(
- wasm_package_specs()
- .iter()
- .any(|spec| spec.package_name == "@radroots/replica-store-wasm")
- );
- }
-}
diff --git a/tools/xtask/src/package_metadata.rs b/tools/xtask/src/package_metadata.rs
@@ -1,181 +0,0 @@
-use std::{fs, path::Path};
-
-use crate::{
- fs::{write_bytes_if_changed, write_if_changed},
- package_matrix::{package_specs, wasm_package_specs},
-};
-
-pub(crate) const PACKAGE_README_FILE: &str = "README.md";
-pub(crate) const PACKAGE_LICENSE_FILES: [&str; 2] = ["LICENSE-MIT", "LICENSE-APACHE"];
-pub(crate) const PACKAGE_FILES: [&str; 4] = [
- "dist",
- PACKAGE_README_FILE,
- PACKAGE_LICENSE_FILES[0],
- PACKAGE_LICENSE_FILES[1],
-];
-
-pub(crate) fn generate_package_metadata(root: &Path) -> Result<(), String> {
- for spec in package_specs() {
- write_package_metadata(root, &root.join(spec.package_dir))?;
- println!("generated package metadata {}", spec.package_name);
- }
- for spec in wasm_package_specs() {
- write_package_metadata(root, &root.join(spec.package_dir))?;
- println!("generated package metadata {}", spec.package_name);
- }
- Ok(())
-}
-
-pub(crate) fn check_package_distribution_metadata(
- root: &Path,
- package_dir: &Path,
- package_json_path: &Path,
- json: &serde_json::Value,
-) -> Result<(), String> {
- let package_name = package_name(json, package_json_path)?;
- let description = package_description(json, package_json_path)?;
- let readme_path = package_dir.join(PACKAGE_README_FILE);
- let expected_readme = package_readme(package_name, description);
- check_text_file(&readme_path, &expected_readme, "stale package README")?;
- for file_name in PACKAGE_LICENSE_FILES {
- let source_path = root.join(file_name);
- let package_path = package_dir.join(file_name);
- let expected = fs::read(&source_path)
- .map_err(|error| format!("failed to read {}: {error}", source_path.display()))?;
- let actual = fs::read(&package_path)
- .map_err(|error| format!("failed to read {}: {error}", package_path.display()))?;
- if actual != expected {
- return Err(format!(
- "stale package license metadata: {}",
- package_path.display()
- ));
- }
- }
- Ok(())
-}
-
-pub(crate) fn package_description<'a>(
- json: &'a serde_json::Value,
- package_json_path: &Path,
-) -> Result<&'a str, String> {
- let description = json
- .get("description")
- .and_then(serde_json::Value::as_str)
- .ok_or_else(|| {
- format!(
- "package.json missing description: {}",
- package_json_path.display()
- )
- })?;
- if description.trim().is_empty() || description.trim() != description {
- return Err(format!(
- "package.json description must be non-empty and trimmed: {}",
- package_json_path.display()
- ));
- }
- Ok(description)
-}
-
-pub(crate) fn package_readme(package_name: &str, description: &str) -> String {
- format!(
- "# {package_name}\n\n{description}\n\nThis package publishes generated ESM JavaScript, TypeScript declarations, and any runtime artifacts from the Radroots SDK build pipeline. Runtime files are distributed from `dist/`; source and provenance metadata are kept outside the npm package payload.\n\n## License\n\nLicensed under either MIT or Apache-2.0, at your option. See `LICENSE-MIT` and `LICENSE-APACHE`.\n"
- )
-}
-
-fn write_package_metadata(root: &Path, package_dir: &Path) -> Result<bool, String> {
- let package_json_path = package_dir.join("package.json");
- let json = read_package_json(&package_json_path)?;
- let package_name = package_name(&json, &package_json_path)?;
- let description = package_description(&json, &package_json_path)?;
- let mut changed = write_if_changed(
- &package_dir.join(PACKAGE_README_FILE),
- &package_readme(package_name, description),
- )?;
- for file_name in PACKAGE_LICENSE_FILES {
- let source_path = root.join(file_name);
- let contents = fs::read(&source_path)
- .map_err(|error| format!("failed to read {}: {error}", source_path.display()))?;
- changed |= write_bytes_if_changed(&package_dir.join(file_name), &contents)?;
- }
- Ok(changed)
-}
-
-fn read_package_json(path: &Path) -> Result<serde_json::Value, String> {
- let raw = fs::read_to_string(path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- serde_json::from_str::<serde_json::Value>(&raw)
- .map_err(|error| format!("failed to parse {}: {error}", path.display()))
-}
-
-fn package_name<'a>(
- json: &'a serde_json::Value,
- package_json_path: &Path,
-) -> Result<&'a str, String> {
- json.get("name")
- .and_then(serde_json::Value::as_str)
- .ok_or_else(|| format!("package.json missing name: {}", package_json_path.display()))
-}
-
-fn check_text_file(path: &Path, expected: &str, label: &str) -> Result<(), String> {
- let actual = fs::read_to_string(path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- if actual != expected {
- return Err(format!("{label}: {}", path.display()));
- }
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use std::{
- fs,
- path::PathBuf,
- time::{SystemTime, UNIX_EPOCH},
- };
-
- use super::{package_readme, write_package_metadata};
-
- #[test]
- fn package_metadata_generation_is_deterministic() {
- let root = test_root("metadata_generation");
- let package_dir = root.join("packages/example");
- fs::create_dir_all(&package_dir).expect("create package");
- fs::write(root.join("LICENSE-MIT"), "MIT license\n").expect("write MIT license");
- fs::write(root.join("LICENSE-APACHE"), "Apache license\n").expect("write Apache license");
- fs::write(
- package_dir.join("package.json"),
- r#"{
- "name": "@radroots/example",
- "description": "Example package"
-}"#,
- )
- .expect("write package json");
-
- assert!(write_package_metadata(&root, &package_dir).expect("first generation"));
- assert_eq!(
- fs::read_to_string(package_dir.join("README.md")).expect("read README"),
- package_readme("@radroots/example", "Example package")
- );
- assert_eq!(
- fs::read_to_string(package_dir.join("LICENSE-MIT")).expect("read MIT"),
- "MIT license\n"
- );
- assert!(!write_package_metadata(&root, &package_dir).expect("second generation"));
-
- let _ = fs::remove_dir_all(root);
- }
-
- fn test_root(name: &str) -> PathBuf {
- let stamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .expect("system time after epoch")
- .as_nanos();
- let root = std::env::temp_dir().join(format!(
- "radroots_sdk_xtask_package_metadata_{name}_{}_{}",
- std::process::id(),
- stamp
- ));
- let _ = fs::remove_dir_all(&root);
- root
- }
-}
diff --git a/tools/xtask/src/portable_qualification.rs b/tools/xtask/src/portable_qualification.rs
@@ -1,58 +0,0 @@
-use std::{fs, path::Path};
-
-use serde::Deserialize;
-
-#[derive(Debug, Deserialize)]
-struct PortableMatrix {
- schema_version: u32,
- no_std_target: String,
- no_std_cross_compiler: String,
- no_std_cross_cflags: String,
- wasm_target: String,
- portable_public_packages: Vec<String>,
- std_only_public_packages: Vec<String>,
-}
-
-pub fn run(workspace_root: &Path) -> Result<(), String> {
- load(workspace_root).map(|_| ())
-}
-
-fn load(workspace_root: &Path) -> Result<PortableMatrix, String> {
- let path = workspace_root.join("contracts/releases/portable_matrix.toml");
- let raw = fs::read_to_string(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- let matrix = toml::from_str::<PortableMatrix>(&raw)
- .map_err(|error| format!("failed to parse {}: {error}", path.display()))?;
- if matrix.schema_version != 1
- || matrix.no_std_target != "thumbv7em-none-eabihf"
- || matrix.no_std_cross_compiler != "zig cc"
- || matrix.no_std_cross_cflags != "-target thumb-freestanding-eabihf -mcpu=cortex_m4"
- || matrix.wasm_target != "wasm32-unknown-unknown"
- || !matrix.portable_public_packages.is_empty()
- || matrix.std_only_public_packages != ["radroots", "radroots_sdk"]
- {
- return Err(
- "SDK portability contract must classify both public front doors as std-only".to_owned(),
- );
- }
- Ok(matrix)
-}
-
-#[cfg(test)]
-mod tests {
- use super::load;
-
- #[test]
- fn current_contract_rejects_nominal_front_door_portability() {
- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
- .parent()
- .and_then(std::path::Path::parent)
- .expect("workspace root");
- let matrix = load(root).expect("portable matrix");
- assert!(matrix.portable_public_packages.is_empty());
- assert_eq!(
- matrix.std_only_public_packages,
- ["radroots", "radroots_sdk"]
- );
- }
-}
diff --git a/tools/xtask/src/release_graph.rs b/tools/xtask/src/release_graph.rs
@@ -1,687 +0,0 @@
-use std::{
- collections::{BTreeMap, BTreeSet},
- fs,
- path::Path,
- process::Command,
-};
-
-use serde::Deserialize;
-
-const ARCHITECTURE_RELATIVE: &str = "docs/specs/radroots_crates_release_v1.toml";
-const SPEC_ID: &str = "radroots.crates.release.v1";
-const RELEASE_VERSION: &str = "0.1.0-alpha";
-const REGISTRY_KINDS: &[&str] = &["build", "normal"];
-const ALL_KINDS: &[&str] = &["build", "dev", "normal"];
-
-#[derive(Debug, Deserialize)]
-struct Architecture {
- spec_id: String,
- package_count: usize,
- repositories: ArchitectureRepositories,
- package: Vec<ArchitecturePackage>,
-}
-
-#[derive(Debug, Deserialize)]
-struct ArchitectureRepositories {
- lib: ArchitectureRepository,
- sdk: ArchitectureRepository,
-}
-
-#[derive(Debug, Deserialize)]
-struct ArchitectureRepository {
- url: String,
- packages: Vec<String>,
-}
-
-#[derive(Debug, Deserialize)]
-struct ArchitecturePackage {
- name: String,
- publish_order_hint: usize,
- #[serde(default)]
- required_radroots_dependencies: Vec<String>,
- #[serde(default)]
- optional_radroots_dependencies: Vec<String>,
- #[serde(flatten)]
- _other: BTreeMap<String, toml::Value>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoMetadata {
- packages: Vec<CargoPackage>,
- workspace_members: Vec<String>,
- resolve: Option<CargoResolve>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoPackage {
- id: String,
- name: String,
- version: String,
- source: Option<String>,
- manifest_path: String,
- #[serde(default)]
- dependencies: Vec<CargoDependency>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoDependency {
- name: String,
- rename: Option<String>,
- #[serde(default)]
- kind: Option<String>,
- #[serde(default)]
- features: Vec<String>,
- #[serde(default)]
- target: Option<String>,
- #[serde(default = "default_true")]
- uses_default_features: bool,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoResolve {
- nodes: Vec<CargoNode>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoNode {
- id: String,
- #[serde(default)]
- deps: Vec<CargoNodeDependency>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoNodeDependency {
- name: String,
- pkg: String,
- #[serde(default)]
- dep_kinds: Vec<CargoDependencyKind>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CargoDependencyKind {
- #[serde(default)]
- kind: Option<String>,
- #[serde(default)]
- target: Option<String>,
-}
-
-fn default_true() -> bool {
- true
-}
-
-pub fn run(workspace_root: &Path) -> Result<(), String> {
- let architecture = load_architecture(workspace_root)?;
- let metadata = load_metadata(workspace_root)?;
- let order = validate(&architecture, &metadata)?;
- println!("resolved publication order:");
- for (index, package) in order.iter().enumerate() {
- println!("{:02} {package}", index + 1);
- }
- Ok(())
-}
-
-fn load_architecture(workspace_root: &Path) -> Result<Architecture, String> {
- let path = workspace_root.join(ARCHITECTURE_RELATIVE);
- let raw =
- fs::read_to_string(&path).map_err(|error| format!("read {}: {error}", path.display()))?;
- toml::from_str(&raw).map_err(|error| format!("parse {}: {error}", path.display()))
-}
-
-fn load_metadata(workspace_root: &Path) -> Result<CargoMetadata, String> {
- let output = Command::new("cargo")
- .args([
- "metadata",
- "--format-version",
- "1",
- "--locked",
- "--all-features",
- ])
- .current_dir(workspace_root)
- .output()
- .map_err(|error| format!("run cargo metadata: {error}"))?;
- if !output.status.success() {
- return Err(format!(
- "cargo metadata failed while resolving the release graph: {}",
- String::from_utf8_lossy(&output.stderr).trim()
- ));
- }
- serde_json::from_slice(&output.stdout)
- .map_err(|error| format!("parse cargo metadata release graph: {error}"))
-}
-
-fn validate(architecture: &Architecture, metadata: &CargoMetadata) -> Result<Vec<String>, String> {
- let hints = validate_architecture(architecture)?;
- let approved = hints.keys().copied().collect::<BTreeSet<_>>();
- let packages = unique_packages(metadata)?;
- let nodes = unique_nodes(metadata)?;
- let workspace_members = metadata
- .workspace_members
- .iter()
- .map(String::as_str)
- .collect::<BTreeSet<_>>();
- let facade_is_local = metadata.workspace_members.iter().any(|id| {
- packages
- .get(id.as_str())
- .is_some_and(|package| package.name == "radroots")
- });
-
- let mut selected = BTreeSet::new();
- for name in &approved {
- let matching = packages
- .values()
- .filter(|package| package.name == **name)
- .copied()
- .collect::<Vec<_>>();
- if matching.len() > 1 {
- return Err(format!(
- "release package {name} resolves to multiple package identities"
- ));
- }
- let Some(package) = matching.first().copied() else {
- if facade_is_local {
- return Err(format!(
- "complete SDK release graph is missing approved package {name}"
- ));
- }
- continue;
- };
- if facade_is_local || workspace_members.contains(package.id.as_str()) {
- selected.insert(*name);
- }
- if package.version != RELEASE_VERSION {
- return Err(format!(
- "release package {name} resolved version {} instead of {RELEASE_VERSION}",
- package.version
- ));
- }
- validate_package_source(architecture, package, &workspace_members)?;
- if !nodes.contains_key(package.id.as_str()) {
- return Err(format!(
- "release package {name} has no Cargo resolve node; manifest={}",
- package.manifest_path
- ));
- }
- }
- if selected.is_empty() {
- return Err("release graph selected no approved workspace packages".to_owned());
- }
- if facade_is_local && selected.len() != architecture.package_count {
- return Err(format!(
- "complete SDK release graph selected {} packages instead of {}",
- selected.len(),
- architecture.package_count
- ));
- }
-
- let mut dependency_sets = selected
- .iter()
- .map(|name| (*name, BTreeSet::new()))
- .collect::<BTreeMap<_, _>>();
- let mut violations = Vec::new();
-
- for owner_name in &selected {
- let owner = packages
- .values()
- .find(|package| package.name == **owner_name)
- .copied()
- .ok_or_else(|| format!("selected release package {owner_name} disappeared"))?;
- let node = nodes
- .get(owner.id.as_str())
- .copied()
- .ok_or_else(|| format!("release package {owner_name} has no resolve node"))?;
- for edge in &node.deps {
- let dependency = packages
- .get(edge.pkg.as_str())
- .copied()
- .ok_or_else(|| format!("resolved dependency {} has no package record", edge.pkg))?;
- if edge.dep_kinds.is_empty() {
- return Err(format!(
- "resolved edge {} -> {} has no dependency kind",
- owner.name, dependency.name
- ));
- }
- for edge_kind in &edge.dep_kinds {
- let kind = normalized_kind(edge_kind.kind.as_deref());
- if !ALL_KINDS.contains(&kind) {
- return Err(format!(
- "resolved edge {} -> {} uses unsupported dependency kind {kind}",
- owner.name, dependency.name
- ));
- }
- let declaration = resolve_declaration(owner, dependency, edge_kind)?;
- if is_radroots_family(&dependency.name)
- && !approved.contains(dependency.name.as_str())
- {
- violations.push(format_edge(
- owner,
- dependency,
- edge,
- edge_kind,
- declaration,
- "public-to-private edge",
- ));
- }
- if REGISTRY_KINDS.contains(&kind) && selected.contains(dependency.name.as_str()) {
- dependency_sets
- .get_mut(owner_name)
- .expect("selected owner dependency set")
- .insert(dependency.name.as_str());
- }
- }
- }
- }
- if !violations.is_empty() {
- violations.sort();
- return Err(format!(
- "release graph contains forbidden edge(s):\n{}",
- violations.join("\n")
- ));
- }
-
- validate_declared_architecture_edges(architecture, &selected, &dependency_sets)?;
- let order = dependency_first_order(&selected, &dependency_sets, &hints)?;
- let expected = selected.iter().copied().collect::<Vec<_>>();
- let mut expected = expected;
- expected.sort_by_key(|name| hints[name]);
- if order != expected {
- return Err(format!(
- "Cargo-derived publication order [{}] differs from architecture order [{}]",
- order.join(", "),
- expected.join(", ")
- ));
- }
- Ok(order.into_iter().map(str::to_owned).collect())
-}
-
-fn validate_package_source(
- architecture: &Architecture,
- package: &CargoPackage,
- workspace_members: &BTreeSet<&str>,
-) -> Result<(), String> {
- if workspace_members.contains(package.id.as_str()) {
- return if package.source.is_none() {
- Ok(())
- } else {
- Err(format!(
- "workspace release package {} must resolve locally; source={:?}",
- package.name, package.source
- ))
- };
- }
-
- let repository = [
- &architecture.repositories.lib,
- &architecture.repositories.sdk,
- ]
- .into_iter()
- .find(|repository| repository.packages.iter().any(|name| name == &package.name))
- .ok_or_else(|| {
- format!(
- "release package {} has no repository allocation",
- package.name
- )
- })?;
- let source = package.source.as_deref().ok_or_else(|| {
- format!(
- "external release package {} must resolve from an exact Git revision",
- package.name
- )
- })?;
- validate_exact_git_source(source, &repository.url).map_err(|reason| {
- format!(
- "external release package {} has invalid source {source}: {reason}",
- package.name
- )
- })
-}
-
-fn validate_exact_git_source(source: &str, repository_url: &str) -> Result<(), &'static str> {
- let canonical_url = repository_url
- .trim_end_matches('/')
- .trim_end_matches(".git");
- let prefix = format!("git+{canonical_url}.git?rev=");
- let revision_and_commit = source
- .strip_prefix(&prefix)
- .ok_or("repository URL or exact-revision query does not match its allocation")?;
- let (revision, commit) = revision_and_commit
- .split_once('#')
- .ok_or("resolved commit fragment is absent")?;
- if revision.len() != 40
- || commit.len() != 40
- || !revision.bytes().all(|byte| byte.is_ascii_hexdigit())
- || !commit.bytes().all(|byte| byte.is_ascii_hexdigit())
- {
- return Err("revision and resolved commit must be full hexadecimal object IDs");
- }
- if revision != commit {
- return Err("resolved commit does not equal the requested exact revision");
- }
- Ok(())
-}
-
-fn validate_architecture(architecture: &Architecture) -> Result<BTreeMap<&str, usize>, String> {
- if architecture.spec_id != SPEC_ID {
- return Err(format!("architecture spec_id must be {SPEC_ID}"));
- }
- if architecture.package_count != 19 || architecture.package.len() != 19 {
- return Err("architecture must define exactly 19 release packages".to_owned());
- }
- let mut hints = BTreeMap::new();
- let mut seen_hints = BTreeSet::new();
- for package in &architecture.package {
- if hints
- .insert(package.name.as_str(), package.publish_order_hint)
- .is_some()
- {
- return Err(format!("duplicate architecture package {}", package.name));
- }
- if !seen_hints.insert(package.publish_order_hint) {
- return Err(format!(
- "duplicate publish_order_hint {}",
- package.publish_order_hint
- ));
- }
- }
- if seen_hints != (1..=19).collect::<BTreeSet<_>>() {
- return Err("publish_order_hint values must be the exact range 1..=19".to_owned());
- }
- Ok(hints)
-}
-
-fn unique_packages(metadata: &CargoMetadata) -> Result<BTreeMap<&str, &CargoPackage>, String> {
- let mut packages = BTreeMap::new();
- for package in &metadata.packages {
- if packages.insert(package.id.as_str(), package).is_some() {
- return Err(format!("duplicate Cargo package id {}", package.id));
- }
- }
- Ok(packages)
-}
-
-fn unique_nodes(metadata: &CargoMetadata) -> Result<BTreeMap<&str, &CargoNode>, String> {
- let resolve = metadata
- .resolve
- .as_ref()
- .ok_or_else(|| "cargo metadata did not include a resolved release graph".to_owned())?;
- let mut nodes = BTreeMap::new();
- for node in &resolve.nodes {
- if nodes.insert(node.id.as_str(), node).is_some() {
- return Err(format!("duplicate Cargo resolve node {}", node.id));
- }
- }
- Ok(nodes)
-}
-
-fn resolve_declaration<'a>(
- owner: &'a CargoPackage,
- dependency: &CargoPackage,
- edge_kind: &CargoDependencyKind,
-) -> Result<&'a CargoDependency, String> {
- let kind = normalized_kind(edge_kind.kind.as_deref());
- let candidates = owner
- .dependencies
- .iter()
- .filter(|candidate| {
- candidate.name == dependency.name
- && normalized_kind(candidate.kind.as_deref()) == kind
- && candidate.target == edge_kind.target
- })
- .collect::<Vec<_>>();
- match candidates.as_slice() {
- [declaration] => Ok(*declaration),
- [] => Err(format!(
- "resolved edge {} -> {} kind={kind} target={} has no matching declaration",
- owner.name,
- dependency.name,
- target_label(edge_kind.target.as_deref())
- )),
- _ => Err(format!(
- "resolved edge {} -> {} kind={kind} target={} has ambiguous declarations",
- owner.name,
- dependency.name,
- target_label(edge_kind.target.as_deref())
- )),
- }
-}
-
-fn format_edge(
- owner: &CargoPackage,
- dependency: &CargoPackage,
- edge: &CargoNodeDependency,
- edge_kind: &CargoDependencyKind,
- declaration: &CargoDependency,
- reason: &str,
-) -> String {
- let features = declaration
- .features
- .iter()
- .cloned()
- .collect::<BTreeSet<_>>()
- .into_iter()
- .collect::<Vec<_>>()
- .join(",");
- format!(
- "{reason}: {} -> {}; alias={}; declaration_alias={}; kind={}; target={}; features=[{}]; default_features={}; owner_manifest={}; dependency_manifest={}",
- owner.name,
- dependency.name,
- edge.name,
- declaration.rename.as_deref().unwrap_or(&declaration.name),
- normalized_kind(edge_kind.kind.as_deref()),
- target_label(edge_kind.target.as_deref()),
- features,
- declaration.uses_default_features,
- owner.manifest_path,
- dependency.manifest_path,
- )
-}
-
-fn validate_declared_architecture_edges<'a>(
- architecture: &'a Architecture,
- selected: &BTreeSet<&'a str>,
- resolved: &BTreeMap<&'a str, BTreeSet<&'a str>>,
-) -> Result<(), String> {
- for package in &architecture.package {
- if !selected.contains(package.name.as_str()) {
- continue;
- }
- for optional in &package.optional_radroots_dependencies {
- if !selected.contains(optional.as_str()) {
- return Err(format!(
- "{} optional architecture dependency {optional} is outside the selected public graph",
- package.name
- ));
- }
- }
- let declared = package
- .required_radroots_dependencies
- .iter()
- .map(String::as_str)
- .filter(|dependency| selected.contains(dependency))
- .collect::<BTreeSet<_>>();
- let actual = resolved
- .get(package.name.as_str())
- .ok_or_else(|| format!("missing resolved dependency set for {}", package.name))?;
- if !declared.is_subset(actual) {
- let missing = declared.difference(actual).copied().collect::<Vec<_>>();
- return Err(format!(
- "{} architecture dependency declarations are absent from the all-feature Cargo graph: {}",
- package.name,
- missing.join(", ")
- ));
- }
- }
- Ok(())
-}
-
-fn dependency_first_order<'a>(
- selected: &BTreeSet<&'a str>,
- dependency_sets: &BTreeMap<&'a str, BTreeSet<&'a str>>,
- hints: &BTreeMap<&'a str, usize>,
-) -> Result<Vec<&'a str>, String> {
- let mut remaining = dependency_sets.clone();
- let mut order = Vec::with_capacity(selected.len());
- while !remaining.is_empty() {
- let next = remaining
- .iter()
- .filter(|(_, dependencies)| dependencies.iter().all(|name| order.contains(name)))
- .map(|(name, _)| *name)
- .min_by_key(|name| hints[name]);
- let Some(next) = next else {
- let blocked = remaining
- .iter()
- .map(|(name, dependencies)| {
- format!(
- "{name}->[{}]",
- dependencies.iter().copied().collect::<Vec<_>>().join(",")
- )
- })
- .collect::<Vec<_>>()
- .join("; ");
- return Err(format!(
- "release graph contains a publication cycle: {blocked}"
- ));
- };
- remaining.remove(next);
- order.push(next);
- }
- Ok(order)
-}
-
-fn is_radroots_family(name: &str) -> bool {
- name == "radroots" || name.starts_with("radroots_")
-}
-
-fn normalized_kind(kind: Option<&str>) -> &str {
- kind.unwrap_or("normal")
-}
-
-fn target_label(target: Option<&str>) -> &str {
- target.unwrap_or("all")
-}
-
-#[cfg(test)]
-mod tests {
- use super::{Architecture, CargoMetadata, validate, validate_exact_git_source};
-
- const ARCHITECTURE: &str = include_str!("../../../docs/specs/radroots_crates_release_v1.toml");
-
- #[test]
- fn current_architecture_contract_has_a_complete_order() {
- let architecture = toml::from_str::<Architecture>(ARCHITECTURE).expect("architecture");
- let hints = super::validate_architecture(&architecture).expect("valid order");
- assert_eq!(hints.len(), 19);
- assert_eq!(hints["radroots_core"], 1);
- assert_eq!(hints["radroots"], 19);
- }
-
- #[test]
- fn exact_cross_repository_git_sources_require_the_requested_commit() {
- const REVISION: &str = "f7f456d906d91aadbb9fac4850afaa8c5723c3e9";
- let source =
- format!("git+https://github.com/radrootslabs/lib.git?rev={REVISION}#{REVISION}");
- validate_exact_git_source(&source, "https://github.com/radrootslabs/lib")
- .expect("exact source");
-
- let drifted = format!(
- "git+https://github.com/radrootslabs/lib.git?rev={REVISION}#{}",
- "1".repeat(40)
- );
- assert!(
- validate_exact_git_source(&drifted, "https://github.com/radrootslabs/lib").is_err()
- );
- assert!(
- validate_exact_git_source(
- "registry+https://github.com/rust-lang/crates.io-index",
- "https://github.com/radrootslabs/lib"
- )
- .is_err()
- );
- }
-
- #[test]
- fn metadata_without_resolve_graph_fails_closed() {
- let architecture = toml::from_str::<Architecture>(ARCHITECTURE).expect("architecture");
- let metadata: CargoMetadata =
- serde_json::from_str(r#"{"packages":[],"workspace_members":[],"resolve":null}"#)
- .expect("metadata");
- let error = validate(&architecture, &metadata).expect_err("missing graph");
- assert!(
- error.contains("selected no approved workspace packages")
- || error.contains("resolved release graph")
- );
- }
-
- #[test]
- fn public_to_private_target_edge_reports_complete_provenance() {
- let architecture = toml::from_str::<Architecture>(ARCHITECTURE).expect("architecture");
- let metadata: CargoMetadata = serde_json::from_str(
- r#"{
- "packages": [
- {
- "id": "core",
- "name": "radroots_core",
- "version": "0.1.0-alpha",
- "source": null,
- "manifest_path": "/capsule/crates/core/Cargo.toml",
- "dependencies": [{
- "name": "radroots_private_runtime",
- "rename": "private_alias",
- "kind": "build",
- "features": ["danger"],
- "target": "cfg(unix)",
- "uses_default_features": false
- }]
- },
- {
- "id": "private",
- "name": "radroots_private_runtime",
- "version": "0.1.0-alpha",
- "source": null,
- "manifest_path": "/capsule/crates/private/Cargo.toml",
- "dependencies": []
- }
- ],
- "workspace_members": ["core"],
- "resolve": {"nodes": [
- {"id": "core", "deps": [{
- "name": "private_alias",
- "pkg": "private",
- "dep_kinds": [{"kind": "build", "target": "cfg(unix)"}]
- }]},
- {"id": "private", "deps": []}
- ]}
- }"#,
- )
- .expect("metadata");
- let error = validate(&architecture, &metadata).expect_err("private edge");
- assert!(error.contains("public-to-private edge"));
- assert!(error.contains("alias=private_alias"));
- assert!(error.contains("kind=build"));
- assert!(error.contains("target=cfg(unix)"));
- assert!(error.contains("features=[danger]"));
- assert!(error.contains("default_features=false"));
- }
-
- #[test]
- fn registry_graph_cycle_fails_closed() {
- let architecture = toml::from_str::<Architecture>(ARCHITECTURE).expect("architecture");
- let metadata: CargoMetadata = serde_json::from_str(
- r#"{
- "packages": [
- {"id":"core","name":"radroots_core","version":"0.1.0-alpha","source":null,"manifest_path":"/core/Cargo.toml","dependencies":[{"name":"radroots_identity","rename":null,"kind":null,"features":[],"target":null,"uses_default_features":true}]},
- {"id":"identity","name":"radroots_identity","version":"0.1.0-alpha","source":null,"manifest_path":"/identity/Cargo.toml","dependencies":[{"name":"radroots_core","rename":null,"kind":null,"features":[],"target":null,"uses_default_features":true}]}
- ],
- "workspace_members":["core","identity"],
- "resolve":{"nodes":[
- {"id":"core","deps":[{"name":"radroots_identity","pkg":"identity","dep_kinds":[{"kind":null,"target":null}]}]},
- {"id":"identity","deps":[{"name":"radroots_core","pkg":"core","dep_kinds":[{"kind":null,"target":null}]}]}
- ]}
- }"#,
- )
- .expect("metadata");
- let error = validate(&architecture, &metadata).expect_err("cycle");
- assert!(error.contains("publication cycle"));
- assert!(error.contains("radroots_core"));
- assert!(error.contains("radroots_identity"));
- }
-}
diff --git a/tools/xtask/src/release_qualification.rs b/tools/xtask/src/release_qualification.rs
@@ -1,135 +0,0 @@
-use std::{fs, path::Path, process::Command};
-
-use serde::Deserialize;
-
-#[derive(Debug, Deserialize)]
-struct Architecture {
- repositories: Repositories,
- package: Vec<Package>,
-}
-
-#[derive(Debug, Deserialize)]
-struct Repositories {
- sdk: Repository,
-}
-
-#[derive(Debug, Deserialize)]
-struct Repository {
- packages: Vec<String>,
-}
-
-#[derive(Debug, Deserialize)]
-struct Package {
- name: String,
- features: Vec<String>,
-}
-
-#[derive(Debug, PartialEq, Eq)]
-struct CargoInvocation {
- args: Vec<String>,
-}
-
-pub fn run_feature_matrix(workspace_root: &Path) -> Result<(), String> {
- for invocation in feature_matrix(workspace_root)? {
- eprintln!("cargo {}", invocation.args.join(" "));
- let status = Command::new("cargo")
- .args(&invocation.args)
- .current_dir(workspace_root)
- .status()
- .map_err(|error| format!("failed to start cargo: {error}"))?;
- if !status.success() {
- return Err(format!(
- "public feature qualification failed: cargo {}",
- invocation.args.join(" ")
- ));
- }
- }
- Ok(())
-}
-
-fn feature_matrix(workspace_root: &Path) -> Result<Vec<CargoInvocation>, String> {
- let path = workspace_root.join("docs/specs/radroots_crates_release_v1.toml");
- let raw = fs::read_to_string(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- let architecture = toml::from_str::<Architecture>(&raw)
- .map_err(|error| format!("failed to parse {}: {error}", path.display()))?;
- let mut packages = architecture
- .package
- .into_iter()
- .filter(|package| {
- architecture
- .repositories
- .sdk
- .packages
- .contains(&package.name)
- })
- .collect::<Vec<_>>();
- packages.sort_by(|left, right| left.name.cmp(&right.name));
- if packages.len() != 2 {
- return Err(format!(
- "SDK feature qualification requires exactly 2 public packages, found {}",
- packages.len()
- ));
- }
-
- let mut invocations = Vec::new();
- for package in packages {
- invocations.push(check_invocation(&package.name, None, true));
- invocations.push(check_invocation(&package.name, None, false));
- for feature in package.features {
- invocations.push(check_invocation(&package.name, Some(&feature), true));
- }
- invocations.push(CargoInvocation {
- args: vec![
- "check".to_owned(),
- "-p".to_owned(),
- package.name,
- "--all-targets".to_owned(),
- "--all-features".to_owned(),
- "--locked".to_owned(),
- ],
- });
- }
- Ok(invocations)
-}
-
-fn check_invocation(
- package: &str,
- feature: Option<&str>,
- no_default_features: bool,
-) -> CargoInvocation {
- let mut args = vec!["check".to_owned(), "-p".to_owned(), package.to_owned()];
- if no_default_features {
- args.push("--lib".to_owned());
- args.push("--no-default-features".to_owned());
- } else {
- args.push("--all-targets".to_owned());
- }
- if let Some(feature) = feature {
- args.push("--features".to_owned());
- args.push(feature.to_owned());
- }
- args.push("--locked".to_owned());
- CargoInvocation { args }
-}
-
-#[cfg(test)]
-mod tests {
- use super::feature_matrix;
-
- #[test]
- fn current_catalog_generates_both_front_door_feature_matrices() {
- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
- .parent()
- .and_then(std::path::Path::parent)
- .expect("workspace root");
- let matrix = feature_matrix(root).expect("feature matrix");
- assert!(matrix.len() > 20);
- for package in ["radroots", "radroots_sdk"] {
- assert!(matrix.iter().any(|entry| {
- entry.args.windows(2).any(|pair| pair == ["-p", package])
- && entry.args.iter().any(|arg| arg == "--all-features")
- }));
- }
- }
-}
diff --git a/tools/xtask/src/smoke.rs b/tools/xtask/src/smoke.rs
@@ -1,236 +0,0 @@
-use std::{fs, path::Path, process::Command};
-
-use crate::{
- fs::workspace_root,
- wasm::{ResolvedRustToolchain, resolve_rust_toolchain},
-};
-
-pub fn run(args: &[String]) -> Result<(), String> {
- match args {
- [target] if target == "sdk-rust-local" => sdk_rust_local(),
- [target] if target == "facade-rust-local" => facade_rust_local(),
- [target] if target == "front-doors-rust-local" => {
- facade_rust_local()?;
- sdk_rust_local()
- }
- _ => Err(
- "usage: cargo xtask smoke facade-rust-local | sdk-rust-local | front-doors-rust-local"
- .to_owned(),
- ),
- }
-}
-
-fn facade_rust_local() -> Result<(), String> {
- let root = workspace_root()?;
- let facade_path = root.join("crates/radroots");
- let lib_root = sibling_lib_root(&root)?;
- let toolchain = resolve_rust_toolchain(&root)?;
- run_clean_consumer(
- "facade",
- &render_facade_consumer_manifest(&facade_path, &lib_root)?,
- FACADE_CONSUMER_MAIN,
- &toolchain,
- )
-}
-
-fn sdk_rust_local() -> Result<(), String> {
- let root = workspace_root()?;
- let sdk_path = root.join("crates/sdk");
- let lib_root = sibling_lib_root(&root)?;
- let toolchain = resolve_rust_toolchain(&root)?;
- run_clean_consumer(
- "SDK",
- &render_sdk_consumer_manifest(&sdk_path, &lib_root)?,
- SDK_CONSUMER_MAIN,
- &toolchain,
- )
-}
-
-fn sibling_lib_root(root: &Path) -> Result<std::path::PathBuf, String> {
- Ok(root
- .parent()
- .ok_or_else(|| format!("{} has no repository parent", root.display()))?
- .join("lib"))
-}
-
-fn run_clean_consumer(
- label: &str,
- manifest: &str,
- main: &str,
- toolchain: &ResolvedRustToolchain,
-) -> Result<(), String> {
- let tempdir =
- tempfile::tempdir().map_err(|error| format!("failed to create smoke tempdir: {error}"))?;
- fs::write(tempdir.path().join("Cargo.toml"), manifest)
- .map_err(|error| format!("failed to write {label} smoke manifest: {error}"))?;
- let src_dir = tempdir.path().join("src");
- fs::create_dir_all(&src_dir)
- .map_err(|error| format!("failed to create {}: {error}", src_dir.display()))?;
- fs::write(src_dir.join("main.rs"), main)
- .map_err(|error| format!("failed to write {label} smoke consumer: {error}"))?;
- let status = smoke_cargo_run_command(tempdir.path(), toolchain)
- .status()
- .map_err(|error| format!("failed to run {label} smoke consumer: {error}"))?;
- if status.success() {
- Ok(())
- } else {
- Err(format!(
- "{label} Rust local smoke failed with status {status}"
- ))
- }
-}
-
-fn render_facade_consumer_manifest(facade_path: &Path, lib_root: &Path) -> Result<String, String> {
- let facade_path = serde_json::to_string(&facade_path.to_string_lossy())
- .map_err(|error| format!("failed to render facade path: {error}"))?;
- let mut manifest = format!(
- r#"[package]
-name = "radroots_facade_host_smoke"
-version = "0.1.0"
-edition = "2024"
-publish = false
-
-[dependencies]
-radroots = {{ path = {facade_path} }}
-
-[patch.crates-io]
-"#,
- );
- append_lib_patches(&mut manifest, lib_root)?;
- Ok(manifest)
-}
-
-fn render_sdk_consumer_manifest(sdk_path: &Path, lib_root: &Path) -> Result<String, String> {
- let sdk_path = serde_json::to_string(&sdk_path.to_string_lossy())
- .map_err(|error| format!("failed to render SDK path: {error}"))?;
- let mut manifest = format!(
- r#"[package]
-name = "radroots_sdk_host_smoke"
-version = "0.1.0"
-edition = "2024"
-publish = false
-
-[dependencies]
-radroots_sdk = {{ path = {sdk_path}, default-features = true }}
-
-[patch.crates-io]
-"#,
- );
- append_lib_patches(&mut manifest, lib_root)?;
- Ok(manifest)
-}
-
-fn append_lib_patches(manifest: &mut String, lib_root: &Path) -> Result<(), String> {
- for (package, relative_path) in [
- ("radroots_blossom", "crates/blossom"),
- ("radroots_core", "crates/core"),
- ("radroots_event", "crates/event"),
- ("radroots_event_codec", "crates/event_codec"),
- ("radroots_geonames", "crates/geonames"),
- ("radroots_identity", "crates/identity"),
- ("radroots_nostr", "crates/nostr"),
- ("radroots_nostr_connect", "crates/nostr_connect"),
- ("radroots_protocol", "crates/protocol"),
- ("radroots_secrets", "crates/secrets"),
- ("radroots_signing", "crates/signing"),
- ("radroots_storage", "crates/storage"),
- ("radroots_storage_sqlite", "crates/storage_sqlite"),
- ("radroots_sync", "crates/sync"),
- ("radroots_trade", "crates/trade"),
- ("radroots_transport", "crates/transport"),
- ("radroots_transport_nostr", "crates/transport_nostr"),
- ] {
- let path = serde_json::to_string(&lib_root.join(relative_path).to_string_lossy())
- .map_err(|error| format!("failed to render {package} patch path: {error}"))?;
- manifest.push_str(&format!("{package} = {{ path = {path} }}\n"));
- }
- Ok(())
-}
-
-fn smoke_cargo_run_command(path: &Path, toolchain: &ResolvedRustToolchain) -> Command {
- let mut command = Command::new(&toolchain.cargo);
- command.arg("run").arg("--quiet").current_dir(path);
- toolchain.apply_to_command(&mut command);
- command
-}
-
-const SDK_CONSUMER_MAIN: &str = r#"use radroots_sdk::{ClientBuilder, error::ErrorKind};
-
-fn main() {
- let error = match ClientBuilder::new().build() {
- Ok(_) => panic!("empty SDK builder must fail closed"),
- Err(error) => error,
- };
- assert_eq!(error.kind(), ErrorKind::MissingStorage);
-}
-"#;
-
-const FACADE_CONSUMER_MAIN: &str = r#"fn main() -> radroots::Result<()> {
- let client = radroots::client::memory().build()?;
- assert!(!client.is_closed());
- assert!(radroots::client::local_only().is_local_only());
- Ok(())
-}
-"#;
-
-#[cfg(test)]
-mod tests {
- use std::path::{Path, PathBuf};
-
- use crate::wasm::ResolvedRustToolchain;
-
- use super::{
- render_facade_consumer_manifest, render_sdk_consumer_manifest, run, smoke_cargo_run_command,
- };
-
- #[test]
- fn retired_sdk_knowledge_smoke_is_not_a_command_surface() {
- assert!(run(&["knowledge-rust-local".to_owned()]).is_err());
- }
-
- #[test]
- fn sdk_smoke_consumer_uses_final_api_and_local_lower_package_patches() {
- let manifest = render_sdk_consumer_manifest(
- Path::new("/tmp/radroots_sdk"),
- Path::new("/tmp/radroots_lib"),
- )
- .expect("manifest");
-
- assert!(manifest.contains("name = \"radroots_sdk_host_smoke\""));
- assert!(manifest.contains("radroots_sdk = { path = \"/tmp/radroots_sdk\""));
- assert!(
- manifest.contains("radroots_storage = { path = \"/tmp/radroots_lib/crates/storage\" }")
- );
- assert!(!manifest.contains("runtime ="));
- assert!(!manifest.contains("signer-adapters"));
- }
-
- #[test]
- fn facade_smoke_consumer_uses_curated_front_door() {
- let manifest = render_facade_consumer_manifest(
- Path::new("/tmp/radroots_facade"),
- Path::new("/tmp/radroots_lib"),
- )
- .expect("manifest");
-
- assert!(manifest.contains("name = \"radroots_facade_host_smoke\""));
- assert!(manifest.contains("radroots = { path = \"/tmp/radroots_facade\" }"));
- assert!(!manifest.contains("radroots_sdk = { path"));
- }
-
- #[test]
- fn smoke_command_uses_resolved_cargo_path() {
- let cargo = PathBuf::from("/tmp/rust-toolchain/bin/cargo");
- let toolchain = ResolvedRustToolchain {
- channel: "1.97.0".to_owned(),
- rustc: PathBuf::from("/tmp/rust-toolchain/bin/rustc"),
- cargo: cargo.clone(),
- bin_dir: PathBuf::from("/tmp/rust-toolchain/bin"),
- };
-
- let command = smoke_cargo_run_command(Path::new("/tmp/smoke"), &toolchain);
-
- assert_eq!(command.get_program(), cargo.as_os_str());
- assert_eq!(command.get_args().next(), Some(std::ffi::OsStr::new("run")));
- }
-}
diff --git a/tools/xtask/src/supply_chain_qualification.rs b/tools/xtask/src/supply_chain_qualification.rs
@@ -1,729 +0,0 @@
-use std::collections::{BTreeMap, BTreeSet};
-use std::fs;
-use std::path::{Path, PathBuf};
-use std::process::Command;
-
-use serde::{Deserialize, Serialize};
-use serde_json::{Value, json};
-use sha2::{Digest, Sha256};
-
-const CONTRACT_PATH: &str = "contracts/releases/supply_chain.toml";
-const DENY_PATH: &str = "deny.toml";
-const SBOM_FILENAME: &str = "radroots-release-v1-sbom.json";
-
-#[derive(Debug, Deserialize)]
-struct Contract {
- schema_version: u16,
- spec_id: String,
- package_version: String,
- tools: Tools,
- sbom: Sbom,
- advisory_exception: Vec<AdvisoryException>,
- package: Vec<Package>,
-}
-
-#[derive(Debug, Deserialize)]
-struct Tools {
- cargo_deny: String,
- cargo_cyclonedx: String,
-}
-
-#[derive(Debug, Deserialize)]
-struct Sbom {
- format: String,
- spec_version: String,
- target: String,
- all_features: bool,
- source_date_epoch: u64,
-}
-
-#[derive(Clone, Debug, Deserialize, Serialize)]
-struct AdvisoryException {
- id: String,
- package: String,
- affected_version: String,
- introduced_by: String,
- classification: String,
- mitigation: String,
- remove_when: String,
-}
-
-#[derive(Debug, Deserialize)]
-struct Package {
- name: String,
- manifest_path: String,
-}
-
-#[derive(Debug, Deserialize)]
-struct Metadata {
- packages: Vec<MetadataPackage>,
-}
-
-#[derive(Debug, Deserialize)]
-struct MetadataPackage {
- name: String,
- version: String,
- manifest_path: PathBuf,
-}
-
-struct GeneratedSboms(Vec<PathBuf>);
-
-impl Drop for GeneratedSboms {
- fn drop(&mut self) {
- for path in &self.0 {
- let _ = fs::remove_file(path);
- }
- }
-}
-
-pub fn run(root: &Path) -> Result<(), String> {
- let contract = load(root)?;
- validate(root, &contract, 2)?;
- verify_tools(&contract)?;
- let metadata = load_metadata(root)?;
- qualify_dependencies(root, &contract)?;
- let sbom_hashes = qualify_sboms(root, &contract, &metadata)?;
- let provenance = build_provenance(root, &contract, &sbom_hashes)?;
- validate_provenance(&provenance, &contract)?;
- eprintln!(
- "qualified {} package supply chains; provenance sha256={}",
- contract.package.len(),
- sha256(&serde_json::to_vec(&provenance).map_err(|error| error.to_string())?)
- );
- Ok(())
-}
-
-fn load(root: &Path) -> Result<Contract, String> {
- let path = root.join(CONTRACT_PATH);
- let raw = fs::read_to_string(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- toml::from_str(&raw).map_err(|error| format!("failed to parse {}: {error}", path.display()))
-}
-
-fn validate(root: &Path, contract: &Contract, expected_packages: usize) -> Result<(), String> {
- if contract.schema_version != 1
- || contract.spec_id != "radroots.crates.release.v1"
- || contract.package_version != "0.1.0-alpha"
- || contract.tools.cargo_deny != "0.19.8"
- || contract.tools.cargo_cyclonedx != "0.5.9"
- || contract.sbom.format != "json"
- || contract.sbom.spec_version != "1.5"
- || contract.sbom.target != "all"
- || !contract.sbom.all_features
- || contract.sbom.source_date_epoch != 0
- {
- return Err("invalid supply-chain qualification contract".to_owned());
- }
-
- let names = contract
- .package
- .iter()
- .map(|package| package.name.as_str())
- .collect::<BTreeSet<_>>();
- let manifests = contract
- .package
- .iter()
- .map(|package| package.manifest_path.as_str())
- .collect::<BTreeSet<_>>();
- if names.len() != expected_packages
- || names.len() != contract.package.len()
- || manifests.len() != contract.package.len()
- {
- return Err(format!(
- "supply-chain contract requires exactly {expected_packages} unique packages and manifests"
- ));
- }
- for package in &contract.package {
- let path = root.join(&package.manifest_path);
- let raw = fs::read_to_string(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- let manifest: toml::Value = toml::from_str(&raw)
- .map_err(|error| format!("failed to parse {}: {error}", path.display()))?;
- let package_table = manifest
- .get("package")
- .and_then(toml::Value::as_table)
- .ok_or_else(|| format!("{} has no package table", path.display()))?;
- let name = package_table.get("name").and_then(toml::Value::as_str);
- let version = package_table.get("version").and_then(toml::Value::as_str);
- let inherits_workspace_version = package_table
- .get("version")
- .and_then(toml::Value::as_table)
- .and_then(|version| version.get("workspace"))
- .and_then(toml::Value::as_bool)
- == Some(true);
- if name != Some(package.name.as_str())
- || version != Some(contract.package_version.as_str()) && !inherits_workspace_version
- {
- return Err(format!(
- "{} does not declare {} {}",
- path.display(),
- package.name,
- contract.package_version
- ));
- }
- }
-
- validate_exceptions(root, contract)
-}
-
-fn validate_exceptions(root: &Path, contract: &Contract) -> Result<(), String> {
- let expected = BTreeSet::from([
- "CARGO-YANKED-SPIN-0.9.8".to_owned(),
- "RUSTSEC-2024-0384".to_owned(),
- "RUSTSEC-2024-0421".to_owned(),
- ]);
- let actual = contract
- .advisory_exception
- .iter()
- .map(|exception| exception.id.clone())
- .collect::<BTreeSet<_>>();
- if actual != expected || actual.len() != contract.advisory_exception.len() {
- return Err("supply-chain advisory exceptions are not the exact approved set".to_owned());
- }
- for exception in &contract.advisory_exception {
- if exception.package.is_empty()
- || exception.affected_version.is_empty()
- || exception.introduced_by.is_empty()
- || exception.classification.is_empty()
- || exception.mitigation.is_empty()
- || exception.remove_when.is_empty()
- {
- return Err(format!("advisory exception {} is incomplete", exception.id));
- }
- let exact = match exception.id.as_str() {
- "RUSTSEC-2024-0384" => {
- exception.package == "instant"
- && exception.affected_version == "0.1.13"
- && exception.classification == "unmaintained"
- && exception.remove_when == "nostr >=0.45.0 stable"
- }
- "RUSTSEC-2024-0421" => {
- exception.package == "idna"
- && exception.affected_version == "0.5.0"
- && exception.classification == "vulnerability"
- && exception.remove_when == "nostr >=0.45.0 stable"
- }
- "CARGO-YANKED-SPIN-0.9.8" => {
- exception.package == "spin"
- && exception.affected_version == "0.9.8"
- && exception.classification == "yanked"
- && exception.remove_when
- == "sqlx no longer resolves flume 0.12.0 with spin 0.9.8"
- }
- _ => false,
- };
- if !exact {
- return Err(format!(
- "advisory exception {} differs from its exact approved policy",
- exception.id
- ));
- }
- }
-
- let lock_raw = fs::read_to_string(root.join("Cargo.lock"))
- .map_err(|error| format!("failed to read Cargo.lock: {error}"))?;
- let lock: toml::Value = toml::from_str(&lock_raw)
- .map_err(|error| format!("failed to parse Cargo.lock: {error}"))?;
- let locked_packages = lock
- .get("package")
- .and_then(toml::Value::as_array)
- .ok_or_else(|| "Cargo.lock contains no packages".to_owned())?;
- for exception in &contract.advisory_exception {
- let present = locked_packages.iter().any(|package| {
- package.get("name").and_then(toml::Value::as_str) == Some(exception.package.as_str())
- && package.get("version").and_then(toml::Value::as_str)
- == Some(exception.affected_version.as_str())
- });
- if !present {
- return Err(format!(
- "advisory exception {} is stale because {} {} is absent from Cargo.lock",
- exception.id, exception.package, exception.affected_version
- ));
- }
- }
- let patched_url_present = locked_packages.iter().any(|package| {
- package.get("name").and_then(toml::Value::as_str) == Some("url")
- && package
- .get("version")
- .and_then(toml::Value::as_str)
- .and_then(|version| semver::Version::parse(version).ok())
- .is_some_and(|version| version >= semver::Version::new(2, 5, 4))
- });
- if !patched_url_present {
- return Err("the IDNA exception requires url 2.5.4 or newer in Cargo.lock".to_owned());
- }
-
- let deny_raw = fs::read_to_string(root.join(DENY_PATH))
- .map_err(|error| format!("failed to read {DENY_PATH}: {error}"))?;
- let deny: toml::Value = toml::from_str(&deny_raw)
- .map_err(|error| format!("failed to parse {DENY_PATH}: {error}"))?;
- let ignored = deny
- .get("advisories")
- .and_then(|value| value.get("ignore"))
- .and_then(toml::Value::as_array)
- .ok_or_else(|| "deny.toml advisories.ignore is missing".to_owned())?
- .iter()
- .filter_map(toml::Value::as_str)
- .map(str::to_owned)
- .collect::<BTreeSet<_>>();
- let expected_ignored = expected
- .iter()
- .filter(|id| id.starts_with("RUSTSEC-"))
- .cloned()
- .collect::<BTreeSet<_>>();
- if ignored != expected_ignored {
- return Err("deny.toml advisory ignores differ from the governed exceptions".to_owned());
- }
- let allowed_git = deny
- .get("sources")
- .and_then(|value| value.get("allow-git"))
- .and_then(toml::Value::as_array)
- .ok_or_else(|| "deny.toml sources.allow-git is missing".to_owned())?
- .iter()
- .filter_map(toml::Value::as_str)
- .collect::<BTreeSet<_>>();
- if allowed_git != BTreeSet::from(["https://github.com/radrootslabs/lib.git"]) {
- return Err(
- "deny.toml Git sources must allow only the allocated Radroots lib repository"
- .to_owned(),
- );
- }
-
- let relay_source = root.join("crates/transport_nostr/src/relay.rs");
- if relay_source.exists() {
- let source = fs::read_to_string(&relay_source)
- .map_err(|error| format!("failed to read {}: {error}", relay_source.display()))?;
- if !source.contains("Url::parse(canonical)") {
- return Err(
- "the IDNA exception requires patched relay URL canonicalization".to_owned(),
- );
- }
- }
- Ok(())
-}
-
-fn verify_tools(contract: &Contract) -> Result<(), String> {
- verify_tool(
- &["deny", "--version"],
- "cargo-deny",
- &contract.tools.cargo_deny,
- )?;
- verify_tool(
- &["cyclonedx", "--version"],
- "cargo-cyclonedx",
- &contract.tools.cargo_cyclonedx,
- )
-}
-
-fn verify_tool(args: &[&str], name: &str, expected: &str) -> Result<(), String> {
- let output = Command::new("cargo")
- .args(args)
- .output()
- .map_err(|error| format!("failed to start {name}: {error}"))?;
- if !output.status.success() {
- return Err(format!("{name} {expected} is required"));
- }
- let stdout = String::from_utf8_lossy(&output.stdout);
- let installed = stdout
- .split_whitespace()
- .find_map(|value| semver::Version::parse(value).ok())
- .ok_or_else(|| format!("could not parse {name} version: {stdout}"))?;
- let expected = semver::Version::parse(expected)
- .map_err(|error| format!("invalid governed {name} version: {error}"))?;
- if installed != expected {
- return Err(format!("{name} {expected} is required, found {installed}"));
- }
- Ok(())
-}
-
-fn load_metadata(root: &Path) -> Result<Metadata, String> {
- let output = Command::new("cargo")
- .args(["metadata", "--format-version", "1", "--locked", "--no-deps"])
- .current_dir(root)
- .output()
- .map_err(|error| format!("failed to start cargo metadata: {error}"))?;
- if !output.status.success() {
- return Err("locked cargo metadata failed".to_owned());
- }
- serde_json::from_slice(&output.stdout)
- .map_err(|error| format!("failed to parse cargo metadata: {error}"))
-}
-
-fn qualify_dependencies(root: &Path, contract: &Contract) -> Result<(), String> {
- let mut saw_governed_yank = false;
- for package in &contract.package {
- let manifest = root.join(&package.manifest_path);
- let common = [
- "deny",
- "-L",
- "error",
- "--manifest-path",
- manifest
- .to_str()
- .ok_or_else(|| "non-UTF-8 package manifest path".to_owned())?,
- "--all-features",
- "--locked",
- "check",
- ];
- saw_governed_yank |= qualify_advisories(root, &manifest, &package.name)?;
- run_command(
- root,
- "cargo",
- common
- .iter()
- .copied()
- .chain(["bans", "licenses", "sources"])
- .collect::<Vec<_>>(),
- &format!("dependency policy failed for {}", package.name),
- )?;
- }
- if !saw_governed_yank {
- return Err("the governed spin 0.9.8 yank is stale and must be removed".to_owned());
- }
- Ok(())
-}
-
-fn qualify_advisories(root: &Path, manifest: &Path, package: &str) -> Result<bool, String> {
- let output = Command::new("cargo")
- .args([
- "deny",
- "--format",
- "json",
- "--log-level",
- "warn",
- "--manifest-path",
- manifest
- .to_str()
- .ok_or_else(|| "non-UTF-8 package manifest path".to_owned())?,
- "--all-features",
- "--locked",
- "check",
- "--allow",
- "advisory-not-detected",
- "advisories",
- ])
- .current_dir(root)
- .output()
- .map_err(|error| format!("failed to start cargo-deny: {error}"))?;
- let stdout = String::from_utf8_lossy(&output.stdout);
- let stderr = String::from_utf8_lossy(&output.stderr);
- if !output.status.success() {
- return Err(format!(
- "advisory qualification failed for {package}: {stdout}{stderr}"
- ));
- }
-
- let mut saw_governed_yank = false;
- for line in stdout.lines().chain(stderr.lines()) {
- let Ok(message) = serde_json::from_str::<Value>(line) else {
- continue;
- };
- if message.get("type").and_then(Value::as_str) != Some("diagnostic") {
- continue;
- }
- let fields = message
- .get("fields")
- .and_then(Value::as_object)
- .ok_or_else(|| "cargo-deny emitted a malformed diagnostic".to_owned())?;
- let code = fields
- .get("code")
- .and_then(Value::as_str)
- .unwrap_or_default();
- let krate = fields
- .get("graphs")
- .and_then(Value::as_array)
- .and_then(|graphs| graphs.first())
- .and_then(|graph| graph.get("Krate"));
- let exact_governed_yank = code == "yanked"
- && krate
- .and_then(|krate| krate.get("name"))
- .and_then(Value::as_str)
- == Some("spin")
- && krate
- .and_then(|krate| krate.get("version"))
- .and_then(Value::as_str)
- == Some("0.9.8");
- if exact_governed_yank {
- saw_governed_yank = true;
- } else {
- return Err(format!(
- "unapproved cargo-deny diagnostic for {package}: {line}"
- ));
- }
- }
- Ok(saw_governed_yank)
-}
-
-fn qualify_sboms(
- root: &Path,
- contract: &Contract,
- metadata: &Metadata,
-) -> Result<BTreeMap<String, String>, String> {
- let mut paths = metadata
- .packages
- .iter()
- .map(|package| {
- package
- .manifest_path
- .parent()
- .expect("manifest has parent")
- .join(SBOM_FILENAME)
- })
- .collect::<Vec<_>>();
- paths.sort();
- paths.dedup();
- if let Some(path) = paths.iter().find(|path| path.exists()) {
- return Err(format!(
- "refusing to overwrite pre-existing SBOM {}",
- path.display()
- ));
- }
- let _generated = GeneratedSboms(paths);
-
- let status = Command::new("cargo")
- .args([
- "cyclonedx",
- "--quiet",
- "--manifest-path",
- "Cargo.toml",
- "--format",
- &contract.sbom.format,
- "--all-features",
- "--target",
- &contract.sbom.target,
- "--spec-version",
- &contract.sbom.spec_version,
- "--license-strict",
- "--license-accept-named",
- "MIT/Apache-2.0",
- "--license-accept-named",
- "Apache-2.0/MIT",
- "--license-accept-named",
- "Apache-2.0 / MIT",
- "--override-filename",
- "radroots-release-v1-sbom",
- ])
- .env(
- "SOURCE_DATE_EPOCH",
- contract.sbom.source_date_epoch.to_string(),
- )
- .current_dir(root)
- .status()
- .map_err(|error| format!("failed to start cargo-cyclonedx: {error}"))?;
- if !status.success() {
- return Err("CycloneDX SBOM generation failed".to_owned());
- }
-
- let by_name = metadata
- .packages
- .iter()
- .map(|package| (package.name.as_str(), package))
- .collect::<BTreeMap<_, _>>();
- let mut hashes = BTreeMap::new();
- for package in &contract.package {
- let metadata_package = by_name
- .get(package.name.as_str())
- .ok_or_else(|| format!("cargo metadata omitted {}", package.name))?;
- if metadata_package.version != contract.package_version {
- return Err(format!("{} metadata version drifted", package.name));
- }
- let path = metadata_package
- .manifest_path
- .parent()
- .expect("manifest has parent")
- .join(SBOM_FILENAME);
- let raw = fs::read(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- let mut sbom: Value = serde_json::from_slice(&raw)
- .map_err(|error| format!("invalid SBOM {}: {error}", path.display()))?;
- validate_sbom(&sbom, &package.name, &contract.package_version)?;
- normalize_sbom(&mut sbom, root);
- hashes.insert(
- package.name.clone(),
- sha256(&serde_json::to_vec(&sbom).map_err(|error| error.to_string())?),
- );
- }
- Ok(hashes)
-}
-
-fn validate_sbom(sbom: &Value, name: &str, version: &str) -> Result<(), String> {
- let component = sbom
- .pointer("/metadata/component")
- .and_then(Value::as_object)
- .ok_or_else(|| format!("{name} SBOM has no root component"))?;
- if sbom.get("bomFormat").and_then(Value::as_str) != Some("CycloneDX")
- || sbom.get("specVersion").and_then(Value::as_str) != Some("1.5")
- || component.get("name").and_then(Value::as_str) != Some(name)
- || component.get("version").and_then(Value::as_str) != Some(version)
- || sbom
- .get("components")
- .and_then(Value::as_array)
- .is_none_or(Vec::is_empty)
- || sbom
- .get("dependencies")
- .and_then(Value::as_array)
- .is_none_or(Vec::is_empty)
- {
- return Err(format!(
- "{name} SBOM is incomplete or identifies the wrong package"
- ));
- }
- Ok(())
-}
-
-fn normalize_sbom(value: &mut Value, root: &Path) {
- normalize_sbom_value(value, root.to_string_lossy().as_ref());
-}
-
-fn normalize_sbom_value(value: &mut Value, root: &str) {
- match value {
- Value::Object(object) => {
- object.remove("serialNumber");
- for value in object.values_mut() {
- normalize_sbom_value(value, root);
- }
- }
- Value::Array(values) => {
- for value in values {
- normalize_sbom_value(value, root);
- }
- }
- Value::String(string) if string.contains(root) => {
- *string = string.replace(root, "$REPOSITORY");
- }
- _ => {}
- }
-}
-
-fn build_provenance(
- root: &Path,
- contract: &Contract,
- sbom_hashes: &BTreeMap<String, String>,
-) -> Result<Value, String> {
- let revision = command_stdout(root, "git", &["rev-parse", "HEAD"])?;
- if revision.len() != 40 || !revision.bytes().all(|byte| byte.is_ascii_hexdigit()) {
- return Err("Git provenance revision is not a full commit ID".to_owned());
- }
- let lock = fs::read(root.join("Cargo.lock"))
- .map_err(|error| format!("failed to read Cargo.lock: {error}"))?;
- let packages = contract
- .package
- .iter()
- .map(|package| {
- json!({
- "name": package.name,
- "version": contract.package_version,
- "manifestPath": package.manifest_path,
- "sbomSha256": sbom_hashes.get(&package.name),
- })
- })
- .collect::<Vec<_>>();
- Ok(json!({
- "schemaVersion": 1,
- "specId": contract.spec_id,
- "source": {
- "gitCommit": revision,
- "cargoLockSha256": sha256(&lock),
- },
- "tools": {
- "cargoDeny": contract.tools.cargo_deny,
- "cargoCyclonedx": contract.tools.cargo_cyclonedx,
- },
- "packages": packages,
- "advisoryExceptions": contract.advisory_exception,
- }))
-}
-
-fn validate_provenance(provenance: &Value, contract: &Contract) -> Result<(), String> {
- if provenance.get("schemaVersion").and_then(Value::as_u64) != Some(1)
- || provenance.get("specId").and_then(Value::as_str) != Some(contract.spec_id.as_str())
- || provenance
- .get("packages")
- .and_then(Value::as_array)
- .is_none_or(|packages| packages.len() != contract.package.len())
- || provenance
- .pointer("/source/cargoLockSha256")
- .and_then(Value::as_str)
- .is_none_or(|hash| hash.len() != 64)
- {
- return Err("generated supply-chain provenance is incomplete".to_owned());
- }
- Ok(())
-}
-
-fn run_command(root: &Path, program: &str, args: Vec<&str>, failure: &str) -> Result<(), String> {
- let status = Command::new(program)
- .args(args)
- .current_dir(root)
- .status()
- .map_err(|error| format!("failed to start {program}: {error}"))?;
- if status.success() {
- Ok(())
- } else {
- Err(failure.to_owned())
- }
-}
-
-fn command_stdout(root: &Path, program: &str, args: &[&str]) -> Result<String, String> {
- let output = Command::new(program)
- .args(args)
- .current_dir(root)
- .output()
- .map_err(|error| format!("failed to start {program}: {error}"))?;
- if !output.status.success() {
- return Err(format!("{program} {} failed", args.join(" ")));
- }
- String::from_utf8(output.stdout)
- .map(|value| value.trim().to_owned())
- .map_err(|error| format!("{program} emitted non-UTF-8 output: {error}"))
-}
-
-fn sha256(bytes: &[u8]) -> String {
- format!("{:x}", Sha256::digest(bytes))
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- fn root() -> PathBuf {
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .parent()
- .and_then(Path::parent)
- .expect("workspace root")
- .to_path_buf()
- }
-
- #[test]
- fn current_contract_is_exact_and_exception_bound() {
- let root = root();
- let contract = load(&root).expect("contract");
- validate(&root, &contract, 2).expect("valid contract");
- }
-
- #[test]
- fn sbom_validation_rejects_wrong_identity() {
- let sbom = json!({
- "bomFormat": "CycloneDX",
- "specVersion": "1.5",
- "metadata": {"component": {"name": "wrong", "version": "0.1.0-alpha"}},
- "components": [{}],
- "dependencies": [{}],
- });
- assert!(validate_sbom(&sbom, "radroots_core", "0.1.0-alpha").is_err());
- }
-
- #[test]
- fn normalization_removes_random_and_absolute_identity() {
- let mut sbom = json!({
- "serialNumber": "urn:uuid:random",
- "path": "/workspace/crate",
- });
- normalize_sbom(&mut sbom, Path::new("/workspace"));
- assert!(sbom.get("serialNumber").is_none());
- assert_eq!(
- sbom.get("path").and_then(Value::as_str),
- Some("$REPOSITORY/crate")
- );
- }
-}
diff --git a/tools/xtask/src/target_qualification.rs b/tools/xtask/src/target_qualification.rs
@@ -1,199 +0,0 @@
-use std::{fs, path::Path, process::Command};
-
-use serde::Deserialize;
-
-#[derive(Debug, Deserialize)]
-struct TargetMatrix {
- schema_version: u32,
- msrv_toolchain: String,
- current_toolchain: String,
- operating_system: Vec<OperatingSystem>,
-}
-
-#[derive(Debug, Deserialize)]
-struct OperatingSystem {
- name: String,
- target: String,
- cross_compiler: Option<String>,
- cross_cflags: Option<String>,
-}
-
-#[derive(Debug, Deserialize)]
-struct Architecture {
- repositories: Repositories,
-}
-
-#[derive(Debug, Deserialize)]
-struct Repositories {
- sdk: Repository,
-}
-
-#[derive(Debug, Deserialize)]
-struct Repository {
- packages: Vec<String>,
-}
-
-pub fn run(workspace_root: &Path) -> Result<(), String> {
- let (matrix, packages) = load(workspace_root)?;
- for toolchain in [&matrix.msrv_toolchain, &matrix.current_toolchain] {
- run_command(
- workspace_root,
- "rustup",
- &[
- "run",
- toolchain,
- "cargo",
- "check",
- "--workspace",
- "--all-targets",
- "--locked",
- ],
- )?;
- }
- for operating_system in matrix.operating_system {
- for package in &packages {
- run_target_command(
- workspace_root,
- &operating_system,
- &[
- "check",
- "-p",
- package,
- "--lib",
- "--target",
- &operating_system.target,
- "--locked",
- ],
- )?;
- }
- }
- Ok(())
-}
-
-fn load(workspace_root: &Path) -> Result<(TargetMatrix, Vec<String>), String> {
- let matrix_path = workspace_root.join("contracts/releases/target_matrix.toml");
- let matrix = toml::from_str::<TargetMatrix>(&read(&matrix_path)?)
- .map_err(|error| format!("failed to parse {}: {error}", matrix_path.display()))?;
- validate(&matrix)?;
-
- let architecture_path = workspace_root.join("docs/specs/radroots_crates_release_v1.toml");
- let architecture = toml::from_str::<Architecture>(&read(&architecture_path)?)
- .map_err(|error| format!("failed to parse {}: {error}", architecture_path.display()))?;
- let mut packages = architecture.repositories.sdk.packages;
- packages.sort();
- if packages != ["radroots", "radroots_sdk"] {
- return Err(format!(
- "target qualification requires exactly the two SDK front doors, found {packages:?}"
- ));
- }
- Ok((matrix, packages))
-}
-
-fn read(path: &Path) -> Result<String, String> {
- fs::read_to_string(path).map_err(|error| format!("failed to read {}: {error}", path.display()))
-}
-
-fn validate(matrix: &TargetMatrix) -> Result<(), String> {
- if matrix.schema_version != 1 {
- return Err(format!(
- "target matrix schema_version must be 1, found {}",
- matrix.schema_version
- ));
- }
- if matrix.msrv_toolchain != "1.97.1" || matrix.current_toolchain != "stable" {
- return Err("target matrix must retain MSRV 1.97.1 and current stable".to_owned());
- }
- let expected = [
- (
- "linux",
- "x86_64-unknown-linux-gnu",
- Some("zig cc"),
- Some("-target x86_64-linux-gnu"),
- ),
- ("macos", "aarch64-apple-darwin", None, None),
- (
- "windows",
- "x86_64-pc-windows-gnu",
- Some("x86_64-w64-mingw32-gcc"),
- None,
- ),
- ];
- if matrix.operating_system.len() != expected.len()
- || expected.iter().any(|(name, target, compiler, cflags)| {
- !matrix.operating_system.iter().any(|entry| {
- entry.name == *name
- && entry.target == *target
- && entry.cross_compiler.as_deref() == *compiler
- && entry.cross_cflags.as_deref() == *cflags
- })
- })
- {
- return Err(
- "target matrix must contain the exact Linux, macOS, and Windows triples".to_owned(),
- );
- }
- Ok(())
-}
-
-fn run_target_command(
- workspace_root: &Path,
- operating_system: &OperatingSystem,
- args: &[&str],
-) -> Result<(), String> {
- eprintln!("cargo {}", args.join(" "));
- let mut command = Command::new("cargo");
- command.args(args).current_dir(workspace_root);
- let target_key = operating_system.target.replace('-', "_");
- if let Some(compiler) = operating_system.cross_compiler.as_deref() {
- command.env(format!("CC_{target_key}"), compiler);
- }
- if let Some(cflags) = operating_system.cross_cflags.as_deref() {
- command.env(format!("CFLAGS_{target_key}"), cflags);
- }
- let status = command
- .status()
- .map_err(|error| format!("failed to start cargo: {error}"))?;
- if status.success() {
- Ok(())
- } else {
- Err(format!(
- "target qualification failed: cargo {}",
- args.join(" ")
- ))
- }
-}
-
-fn run_command(workspace_root: &Path, program: &str, args: &[&str]) -> Result<(), String> {
- eprintln!("{program} {}", args.join(" "));
- let status = Command::new(program)
- .args(args)
- .current_dir(workspace_root)
- .status()
- .map_err(|error| format!("failed to start {program}: {error}"))?;
- if status.success() {
- Ok(())
- } else {
- Err(format!(
- "target qualification failed: {program} {}",
- args.join(" ")
- ))
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::load;
-
- #[test]
- fn current_contract_selects_exact_toolchains_targets_and_packages() {
- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
- .parent()
- .and_then(std::path::Path::parent)
- .expect("workspace root");
- let (matrix, packages) = load(root).expect("target matrix");
- assert_eq!(matrix.msrv_toolchain, "1.97.1");
- assert_eq!(matrix.current_toolchain, "stable");
- assert_eq!(matrix.operating_system.len(), 3);
- assert_eq!(packages, ["radroots", "radroots_sdk"]);
- }
-}
diff --git a/tools/xtask/src/ts.rs b/tools/xtask/src/ts.rs
@@ -1,37 +0,0 @@
-pub fn generated_header() -> &'static str {
- "// @generated by cargo xtask generate ts\n// Do not edit by hand.\n"
-}
-
-pub fn generated_types_file() -> &'static str {
- "types.ts"
-}
-
-pub fn generated_constants_file() -> &'static str {
- "constants.ts"
-}
-
-pub fn generated_kinds_file() -> &'static str {
- "kinds.ts"
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- generated_constants_file, generated_header, generated_kinds_file, generated_types_file,
- };
-
- #[test]
- fn generated_header_matches_contract() {
- assert_eq!(
- generated_header(),
- "// @generated by cargo xtask generate ts\n// Do not edit by hand.\n"
- );
- }
-
- #[test]
- fn generated_file_names_are_stable() {
- assert_eq!(generated_types_file(), "types.ts");
- assert_eq!(generated_constants_file(), "constants.ts");
- assert_eq!(generated_kinds_file(), "kinds.ts");
- }
-}
diff --git a/tools/xtask/src/wasm.rs b/tools/xtask/src/wasm.rs
@@ -1,1147 +0,0 @@
-use std::{
- collections::{BTreeMap, BTreeSet},
- env,
- ffi::OsStr,
- fs, io,
- path::{Path, PathBuf},
- process::{Command, Stdio},
-};
-
-use serde::{Deserialize, Serialize};
-use sha2::{Digest, Sha256};
-
-use crate::{
- check::check_wasm_package_surface,
- fs::workspace_root,
- package_matrix::{WasmPackageSpec, validate_package_matrix, wasm_package_specs},
- wasm_declarations::write_declaration_files,
-};
-
-pub(crate) const WASM_TARGET: &str = "wasm32-unknown-unknown";
-const WASM_C_COMPILER_ENV: &str = "CC_wasm32_unknown_unknown";
-const WASM_CFLAGS_ENV: &str = "CFLAGS_wasm32_unknown_unknown";
-const WASM_C_TARGET_ARG: &str = "--target=wasm32-unknown-unknown";
-
-pub fn generate(args: &[String]) -> Result<(), String> {
- validate_package_matrix()?;
- let specs = selected_specs(args)?;
- let root = workspace_root()?;
- let toolchain = resolve_wasm_toolchain(&root)?;
- println!(
- "using Rust toolchain {} for {}: rustc={}, cargo={}",
- toolchain.channel(),
- WASM_TARGET,
- toolchain.rustc().display(),
- toolchain.cargo().display()
- );
- let wasm_c_compiler = if specs
- .iter()
- .any(|spec| wasm_package_requires_c_compiler(*spec))
- {
- let compiler = resolve_wasm_c_compiler()?;
- println!(
- "using WASM C compiler for {}: {}",
- WASM_TARGET,
- compiler.path.display()
- );
- Some(compiler)
- } else {
- None
- };
- for spec in specs {
- let dist_dir = root.join(spec.package_dir).join("dist");
- if dist_dir.exists() {
- fs::remove_dir_all(&dist_dir)
- .map_err(|error| format!("failed to remove {}: {error}", dist_dir.display()))?;
- }
- let mut command = Command::new(&toolchain.wasm_pack);
- command.current_dir(&root);
- for arg in wasm_pack_args(spec) {
- command.arg(arg);
- }
- toolchain.apply_to_command(&mut command);
- if let Some(compiler) = wasm_c_compiler.as_ref()
- && wasm_package_requires_c_compiler(spec)
- {
- compiler.apply_to_command(&mut command);
- }
- let status = command.status().map_err(|error| {
- format!(
- "failed to start wasm-pack for {} while generating {}: {error}",
- spec.key, spec.package_name
- )
- })?;
- if !status.success() {
- return Err(format!(
- "wasm-pack failed for {} while generating {} with status {status}; rerun `cargo xtask generate wasm --package {}` after fixing the wasm toolchain",
- spec.key, spec.package_name, spec.key
- ));
- }
- remove_wasm_pack_gitignore(&dist_dir, spec)?;
- write_declaration_files(&root, spec)?;
- write_provenance_manifest(&root, spec)?;
- check_wasm_package_surface(&root, spec)?;
- println!("generated wasm package {}", spec.package_name);
- }
- Ok(())
-}
-
-#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
-struct WasmProvenanceManifest {
- schema_version: u16,
- package: String,
- package_version: String,
- rust_crate: String,
- rust_crate_version: String,
- target: String,
- deterministic_codec: bool,
- networking: bool,
- signing: bool,
- source_sha256: String,
- outputs: BTreeMap<String, String>,
-}
-
-fn write_provenance_manifest(root: &Path, spec: WasmPackageSpec) -> Result<(), String> {
- let manifest = provenance_manifest(root, spec)?;
- let dist_dir = root.join(spec.package_dir).join("dist");
- let path = provenance_manifest_path(&dist_dir, spec);
- let mut rendered = serde_json::to_string_pretty(&manifest)
- .map_err(|error| format!("failed to render {}: {error}", path.display()))?;
- rendered.push('\n');
- fs::write(&path, rendered)
- .map_err(|error| format!("failed to write {}: {error}", path.display()))
-}
-
-fn provenance_manifest(
- root: &Path,
- spec: WasmPackageSpec,
-) -> Result<WasmProvenanceManifest, String> {
- let package_version = manifest_version(&root.join(spec.package_dir).join("package.json"))?;
- let rust_crate_version = manifest_version(&root.join(spec.crate_dir).join("Cargo.toml"))?;
- let source_sha256 = source_tree_sha256(root, spec)?;
- let dist_dir = root.join(spec.package_dir).join("dist");
- let mut outputs = BTreeMap::new();
- for relative in wasm_output_files(spec) {
- let path = dist_dir.join(&relative);
- let bytes = fs::read(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- outputs.insert(relative, format!("{:x}", Sha256::digest(bytes)));
- }
- Ok(WasmProvenanceManifest {
- schema_version: 1,
- package: spec.package_name.to_owned(),
- package_version,
- rust_crate: spec.crate_name.to_owned(),
- rust_crate_version,
- target: WASM_TARGET.to_owned(),
- deterministic_codec: spec.key == "event_codec",
- networking: false,
- signing: false,
- source_sha256,
- outputs,
- })
-}
-
-pub(crate) fn validate_provenance_manifest(
- root: &Path,
- spec: WasmPackageSpec,
-) -> Result<(), String> {
- let path = provenance_manifest_path(&root.join(spec.package_dir).join("dist"), spec);
- let raw = fs::read_to_string(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- let actual: WasmProvenanceManifest = serde_json::from_str(&raw)
- .map_err(|error| format!("failed to parse {}: {error}", path.display()))?;
- let expected = provenance_manifest(root, spec)?;
- if actual != expected {
- return Err(format!(
- "stale or invalid WASM provenance manifest: {}",
- path.display()
- ));
- }
- Ok(())
-}
-
-fn manifest_version(path: &Path) -> Result<String, String> {
- let raw = fs::read_to_string(path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- if path.extension() == Some(OsStr::new("json")) {
- let value: serde_json::Value = serde_json::from_str(&raw)
- .map_err(|error| format!("failed to parse {}: {error}", path.display()))?;
- return value["version"]
- .as_str()
- .map(str::to_owned)
- .ok_or_else(|| format!("{} must define string version", path.display()));
- }
- let value: toml::Value = raw
- .parse()
- .map_err(|error| format!("failed to parse {}: {error}", path.display()))?;
- value["package"]["version"]
- .as_str()
- .map(str::to_owned)
- .ok_or_else(|| format!("{} must define explicit package.version", path.display()))
-}
-
-fn source_tree_sha256(root: &Path, spec: WasmPackageSpec) -> Result<String, String> {
- let crate_dir = root.join(spec.crate_dir);
- let mut files = vec![crate_dir.join("Cargo.toml")];
- collect_source_files(&crate_dir.join("src"), &mut files)?;
- files.sort();
- let mut hasher = Sha256::new();
- for path in files {
- let relative = path
- .strip_prefix(root)
- .map_err(|error| format!("failed to relativize {}: {error}", path.display()))?;
- hasher.update(relative.to_string_lossy().as_bytes());
- hasher.update([0]);
- hasher.update(
- fs::read(&path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?,
- );
- hasher.update([0]);
- }
- Ok(format!("{:x}", hasher.finalize()))
-}
-
-fn collect_source_files(dir: &Path, files: &mut Vec<PathBuf>) -> Result<(), String> {
- let mut entries = fs::read_dir(dir)
- .map_err(|error| format!("failed to read {}: {error}", dir.display()))?
- .collect::<Result<Vec<_>, _>>()
- .map_err(|error| format!("failed to enumerate {}: {error}", dir.display()))?;
- entries.sort_by_key(std::fs::DirEntry::file_name);
- for entry in entries {
- let path = entry.path();
- if path.is_dir() {
- collect_source_files(&path, files)?;
- } else if path.is_file() {
- files.push(path);
- }
- }
- Ok(())
-}
-
-fn wasm_output_files(spec: WasmPackageSpec) -> [String; 4] {
- [
- format!("{}.js", spec.out_name),
- format!("{}.d.ts", spec.out_name),
- format!("{}_bg.wasm", spec.out_name),
- format!("{}_bg.wasm.d.ts", spec.out_name),
- ]
-}
-
-fn provenance_manifest_path(dist_dir: &Path, spec: WasmPackageSpec) -> PathBuf {
- dist_dir.join(format!("{}.provenance.json", spec.out_name))
-}
-
-struct WasmToolchain {
- wasm_pack: PathBuf,
- rust: ResolvedRustToolchain,
-}
-
-impl WasmToolchain {
- fn apply_to_command(&self, command: &mut Command) {
- self.rust.apply_to_command(command);
- }
-
- fn rustc(&self) -> &Path {
- &self.rust.rustc
- }
-
- fn cargo(&self) -> &Path {
- &self.rust.cargo
- }
-
- fn channel(&self) -> &str {
- &self.rust.channel
- }
-}
-
-pub(crate) struct ResolvedRustToolchain {
- pub(crate) channel: String,
- pub(crate) rustc: PathBuf,
- pub(crate) cargo: PathBuf,
- pub(crate) bin_dir: PathBuf,
-}
-
-impl ResolvedRustToolchain {
- pub(crate) fn apply_to_command(&self, command: &mut Command) {
- prepend_path(command, &self.bin_dir);
- command.env("RUSTC", &self.rustc);
- command.env("CARGO", &self.cargo);
- command.env("RUSTUP_TOOLCHAIN", &self.channel);
- }
-}
-
-fn resolve_wasm_toolchain(root: &Path) -> Result<WasmToolchain, String> {
- let wasm_pack = resolve_required_path_tool("wasm-pack")?;
- let rust = resolve_rust_toolchain(root)?;
- Ok(WasmToolchain { wasm_pack, rust })
-}
-
-pub(crate) fn resolve_rust_toolchain(root: &Path) -> Result<ResolvedRustToolchain, String> {
- let toolchain_path = root.join("rust-toolchain.toml");
- let channel = read_rust_toolchain_channel(&toolchain_path)?;
- let rustc = rustup_tool_for_channel("rustc", &channel)?;
- let cargo = rustup_tool_for_channel("cargo", &channel)?;
- let rustc_bin = rustc.parent().ok_or_else(|| {
- format!(
- "rustup resolved rustc for toolchain {channel} without a parent path: {}",
- rustc.display()
- )
- })?;
- let cargo_bin = cargo.parent().ok_or_else(|| {
- format!(
- "rustup resolved cargo for toolchain {channel} without a parent path: {}",
- cargo.display()
- )
- })?;
- if rustc_bin != cargo_bin {
- return Err(format!(
- "rustup resolved mismatched Rust tool paths for toolchain {channel}: rustc={}, cargo={}",
- rustc.display(),
- cargo.display()
- ));
- }
- let bin_dir = rustc_bin.to_path_buf();
- ensure_wasm_target_installed(&channel)?;
- Ok(ResolvedRustToolchain {
- channel,
- rustc,
- cargo,
- bin_dir,
- })
-}
-
-fn wasm_pack_args(spec: WasmPackageSpec) -> Vec<&'static str> {
- vec![
- "build",
- spec.crate_dir,
- "--release",
- "--target",
- "web",
- "--out-dir",
- spec.out_dir,
- "--out-name",
- spec.out_name,
- "--no-pack",
- ]
-}
-
-fn remove_wasm_pack_gitignore(dist_dir: &Path, spec: WasmPackageSpec) -> Result<(), String> {
- let ignore_path = dist_dir.join(".gitignore");
- let contents = match fs::read_to_string(&ignore_path) {
- Ok(contents) => contents,
- Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
- Err(error) => {
- return Err(format!(
- "failed to read wasm-pack ignore file for {}: {}: {error}",
- spec.package_name,
- ignore_path.display()
- ));
- }
- };
- if contents.trim() != "*" {
- return Err(format!(
- "unexpected wasm-pack ignore file for {}: {}; refusing to remove it",
- spec.package_name,
- ignore_path.display()
- ));
- }
- fs::remove_file(&ignore_path).map_err(|error| {
- format!(
- "failed to remove wasm-pack ignore file for {}: {}: {error}",
- spec.package_name,
- ignore_path.display()
- )
- })
-}
-
-fn selected_specs(args: &[String]) -> Result<Vec<WasmPackageSpec>, String> {
- match args {
- [] => Ok(wasm_package_specs().to_vec()),
- [flag, key] if flag == "--package" => wasm_package_specs()
- .iter()
- .copied()
- .find(|spec| spec.key == key)
- .map(|spec| vec![spec])
- .ok_or_else(|| format!("unknown wasm package: {key}")),
- _ => Err("usage: cargo xtask generate wasm [--package <key>]".to_owned()),
- }
-}
-
-fn wasm_package_requires_c_compiler(spec: WasmPackageSpec) -> bool {
- spec.key == "event_codec"
-}
-
-fn resolve_required_path_tool(name: &str) -> Result<PathBuf, String> {
- let path = env::var_os("PATH").ok_or_else(|| {
- format!("missing {name}: PATH is not set; install {name} and expose it on PATH")
- })?;
- resolve_path_tool_from_path(name, &path)
-}
-
-fn resolve_path_tool_from_path(name: &str, path: &std::ffi::OsStr) -> Result<PathBuf, String> {
- let matches = executable_matches(name, path);
- match matches.as_slice() {
- [] => Err(format!(
- "missing {name}: install {name} and rerun `cargo xtask generate wasm`"
- )),
- [tool] => Ok(tool.clone()),
- _ => Err(format!(
- "ambiguous {name}: found {}; remove duplicate {name} entries from PATH before running `cargo xtask generate wasm`",
- matches
- .iter()
- .map(|path| path.display().to_string())
- .collect::<Vec<_>>()
- .join(", ")
- )),
- }
-}
-
-fn executable_matches(name: &str, path: &std::ffi::OsStr) -> Vec<PathBuf> {
- let mut seen = BTreeSet::new();
- let mut matches = Vec::new();
- for dir in env::split_paths(path) {
- let candidate = dir.join(name);
- if !is_executable_file(&candidate) {
- continue;
- }
- let key = fs::canonicalize(&candidate).unwrap_or_else(|_| candidate.clone());
- if seen.insert(key) {
- matches.push(candidate);
- }
- }
- matches
-}
-
-fn is_executable_file(path: &Path) -> bool {
- if !path.is_file() {
- return false;
- }
- #[cfg(unix)]
- {
- use std::os::unix::fs::PermissionsExt;
- path.metadata()
- .map(|metadata| metadata.permissions().mode() & 0o111 != 0)
- .unwrap_or(false)
- }
- #[cfg(not(unix))]
- {
- true
- }
-}
-
-fn read_rust_toolchain_channel(path: &Path) -> Result<String, String> {
- let contents = fs::read_to_string(path)
- .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
- rust_toolchain_channel_from(&contents)
-}
-
-fn rust_toolchain_channel_from(contents: &str) -> Result<String, String> {
- let value = contents
- .parse::<toml::Value>()
- .map_err(|error| format!("failed to parse rust-toolchain.toml: {error}"))?;
- let channel = value
- .get("toolchain")
- .and_then(|toolchain| toolchain.get("channel"))
- .and_then(toml::Value::as_str)
- .ok_or_else(|| "rust-toolchain.toml must define toolchain.channel".to_owned())?
- .trim();
- if channel.is_empty() {
- return Err("rust-toolchain.toml toolchain.channel must not be empty".to_owned());
- }
- Ok(channel.to_owned())
-}
-
-fn rustup_tool_for_channel(name: &str, channel: &str) -> Result<PathBuf, String> {
- let output = Command::new("rustup")
- .arg("which")
- .arg("--toolchain")
- .arg(channel)
- .arg(name)
- .output()
- .map_err(|error| {
- format!(
- "failed to resolve {name} for Rust toolchain {channel} with rustup: {error}; install rustup toolchain {channel}"
- )
- })?;
- if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr);
- return Err(format!(
- "failed to resolve {name} for Rust toolchain {channel}: {}; run `rustup toolchain install {channel}`",
- stderr.trim()
- ));
- }
- let path = String::from_utf8(output.stdout)
- .map_err(|error| format!("rustup emitted non-UTF-8 {name} path: {error}"))?;
- let trimmed = path.trim();
- if trimmed.is_empty() {
- return Err(format!(
- "rustup returned an empty {name} path for Rust toolchain {channel}"
- ));
- }
- Ok(PathBuf::from(trimmed))
-}
-
-fn ensure_wasm_target_installed(channel: &str) -> Result<(), String> {
- let output = Command::new("rustup")
- .args(rustup_target_list_args(channel))
- .output()
- .map_err(|error| {
- format!(
- "failed to verify {WASM_TARGET} target for Rust toolchain {channel} with rustup: {error}; install rustup toolchain {channel}"
- )
- })?;
- if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr);
- return Err(format!(
- "failed to verify {WASM_TARGET} target for Rust toolchain {channel}: {}; run `rustup target add {WASM_TARGET} --toolchain {channel}`",
- stderr.trim()
- ));
- }
- let stdout = String::from_utf8_lossy(&output.stdout);
- validate_target_list(&stdout, WASM_TARGET, channel)
-}
-
-fn rustup_target_list_args(channel: &str) -> [&str; 5] {
- ["target", "list", "--installed", "--toolchain", channel]
-}
-
-fn validate_target_list(output: &str, target: &str, channel: &str) -> Result<(), String> {
- if target_list_contains(output, target) {
- Ok(())
- } else {
- Err(format!(
- "missing Rust target {target} for Rust toolchain {channel}: run `rustup target add {target} --toolchain {channel}`"
- ))
- }
-}
-
-fn target_list_contains(output: &str, target: &str) -> bool {
- output.lines().any(|line| line.trim() == target)
-}
-
-#[derive(Debug)]
-struct WasmCCompiler {
- path: PathBuf,
- cflags: ResolvedWasmCFlags,
-}
-
-impl WasmCCompiler {
- fn apply_to_command(&self, command: &mut Command) {
- command.env(WASM_C_COMPILER_ENV, &self.path);
- command.env(WASM_CFLAGS_ENV, &self.cflags.value);
- }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct ResolvedWasmCFlags {
- value: String,
- args: Vec<String>,
-}
-
-fn resolve_wasm_c_compiler() -> Result<WasmCCompiler, String> {
- let path = env::var_os("PATH").unwrap_or_default();
- let explicit = env::var_os(WASM_C_COMPILER_ENV);
- let cflags = resolve_wasm_cflags(env::var_os(WASM_CFLAGS_ENV).as_deref())?;
- let candidates = wasm_c_compiler_candidates(&path);
- resolve_wasm_c_compiler_with(
- explicit.as_deref(),
- &path,
- candidates,
- cflags,
- verify_wasm_c_compiler,
- )
-}
-
-fn resolve_wasm_c_compiler_with<F>(
- explicit: Option<&OsStr>,
- path: &OsStr,
- candidates: Vec<PathBuf>,
- cflags: ResolvedWasmCFlags,
- verify: F,
-) -> Result<WasmCCompiler, String>
-where
- F: Fn(&Path, &ResolvedWasmCFlags) -> Result<(), String>,
-{
- if let Some(explicit) = explicit {
- let compiler = resolve_explicit_wasm_c_compiler(explicit, path)?;
- verify(&compiler, &cflags).map_err(|error| {
- format!(
- "{WASM_C_COMPILER_ENV} does not name a WASM-capable C compiler: {}; {error}",
- compiler.display()
- )
- })?;
- return Ok(WasmCCompiler {
- path: compiler,
- cflags,
- });
- }
-
- let mut checked = Vec::new();
- for candidate in candidates {
- if !is_executable_file(&candidate) {
- continue;
- }
- checked.push(candidate.display().to_string());
- if verify(&candidate, &cflags).is_ok() {
- return Ok(WasmCCompiler {
- path: candidate,
- cflags,
- });
- }
- }
- let checked = if checked.is_empty() {
- "none".to_owned()
- } else {
- checked.join(", ")
- };
- Err(format!(
- "missing suitable WASM C compiler for {WASM_TARGET}: set {WASM_C_COMPILER_ENV} to a clang-style compiler that accepts `{WASM_C_TARGET_ARG}`; checked candidates: {checked}"
- ))
-}
-
-fn resolve_wasm_cflags(value: Option<&OsStr>) -> Result<ResolvedWasmCFlags, String> {
- let raw = match value {
- Some(value) => Some(
- value
- .to_str()
- .ok_or_else(|| format!("{WASM_CFLAGS_ENV} must be valid UTF-8 compiler flags"))?,
- ),
- None => None,
- };
- resolve_wasm_cflags_from_str(raw)
-}
-
-fn resolve_wasm_cflags_from_str(value: Option<&str>) -> Result<ResolvedWasmCFlags, String> {
- let raw = value.unwrap_or("");
- let words = parse_cflags_words(raw)?;
- let mut has_required_target = false;
- for index in 0..words.len() {
- let word = &words[index];
- if word == WASM_C_TARGET_ARG {
- has_required_target = true;
- } else if wasm_cflags_target_conflict(index, &words) {
- return Err(format!(
- "{WASM_CFLAGS_ENV} contains unsupported target flag `{}`; use `{WASM_C_TARGET_ARG}` or omit the target so xtask can prepend it",
- wasm_cflags_target_display(index, &words)
- ));
- }
- }
- let value = if has_required_target {
- raw.to_owned()
- } else {
- let trimmed = raw.trim();
- if trimmed.is_empty() {
- WASM_C_TARGET_ARG.to_owned()
- } else {
- format!("{WASM_C_TARGET_ARG} {trimmed}")
- }
- };
- let args = parse_cflags_words(&value)?;
- Ok(ResolvedWasmCFlags { value, args })
-}
-
-fn wasm_cflags_target_conflict(index: usize, words: &[String]) -> bool {
- let word = &words[index];
- word == "--target"
- || word == "-target"
- || word.starts_with("--target=")
- || word.starts_with("-target=")
-}
-
-fn wasm_cflags_target_display(index: usize, words: &[String]) -> String {
- let word = &words[index];
- if matches!(word.as_str(), "--target" | "-target")
- && let Some(target) = words.get(index + 1)
- {
- return format!("{word} {target}");
- }
- word.clone()
-}
-
-fn parse_cflags_words(value: &str) -> Result<Vec<String>, String> {
- let mut words = Vec::new();
- let mut current = String::new();
- let mut quote = CFlagsQuote::None;
- let mut in_word = false;
- let mut chars = value.chars();
- while let Some(character) = chars.next() {
- match quote {
- CFlagsQuote::None => match character {
- character if character.is_whitespace() => {
- if in_word {
- words.push(std::mem::take(&mut current));
- in_word = false;
- }
- }
- '\'' => {
- in_word = true;
- quote = CFlagsQuote::Single;
- }
- '"' => {
- in_word = true;
- quote = CFlagsQuote::Double;
- }
- '\\' => {
- in_word = true;
- let Some(escaped) = chars.next() else {
- return Err(format!("{WASM_CFLAGS_ENV} ends with an unfinished escape"));
- };
- current.push(escaped);
- }
- character => {
- in_word = true;
- current.push(character);
- }
- },
- CFlagsQuote::Single => {
- if character == '\'' {
- quote = CFlagsQuote::None;
- } else {
- current.push(character);
- }
- }
- CFlagsQuote::Double => {
- if character == '"' {
- quote = CFlagsQuote::None;
- } else if character == '\\' {
- let Some(escaped) = chars.next() else {
- return Err(format!("{WASM_CFLAGS_ENV} ends with an unfinished escape"));
- };
- current.push(escaped);
- } else {
- current.push(character);
- }
- }
- }
- }
- match quote {
- CFlagsQuote::None => {
- if in_word {
- words.push(current);
- }
- Ok(words)
- }
- CFlagsQuote::Single => Err(format!(
- "{WASM_CFLAGS_ENV} contains an unterminated ' quote"
- )),
- CFlagsQuote::Double => Err(format!(
- "{WASM_CFLAGS_ENV} contains an unterminated \" quote"
- )),
- }
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum CFlagsQuote {
- None,
- Single,
- Double,
-}
-
-fn resolve_explicit_wasm_c_compiler(value: &OsStr, path: &OsStr) -> Result<PathBuf, String> {
- if value.is_empty() {
- return Err(format!(
- "{WASM_C_COMPILER_ENV} is set but empty; set it to a compiler path that accepts `{WASM_C_TARGET_ARG}`"
- ));
- }
- let candidate = PathBuf::from(value);
- if candidate.components().count() > 1 {
- if is_executable_file(&candidate) {
- Ok(candidate)
- } else {
- Err(format!(
- "{WASM_C_COMPILER_ENV} is not executable: {}",
- candidate.display()
- ))
- }
- } else {
- first_executable_match(value, path).ok_or_else(|| {
- format!(
- "{WASM_C_COMPILER_ENV} command was not found on PATH: {}",
- value.to_string_lossy()
- )
- })
- }
-}
-
-fn first_executable_match(name: &OsStr, path: &OsStr) -> Option<PathBuf> {
- for dir in env::split_paths(path) {
- let candidate = dir.join(name);
- if is_executable_file(&candidate) {
- return Some(candidate);
- }
- }
- None
-}
-
-fn wasm_c_compiler_candidates(path: &OsStr) -> Vec<PathBuf> {
- let mut seen = BTreeSet::new();
- let mut candidates = Vec::new();
- for name in ["wasm32-unknown-unknown-clang", "clang"] {
- for candidate in executable_matches(name, path) {
- let key = fs::canonicalize(&candidate).unwrap_or_else(|_| candidate.clone());
- if seen.insert(key) {
- candidates.push(candidate);
- }
- }
- }
- candidates
-}
-
-fn verify_wasm_c_compiler(path: &Path, cflags: &ResolvedWasmCFlags) -> Result<(), String> {
- let tempdir = tempfile::tempdir()
- .map_err(|error| format!("failed to create C probe tempdir: {error}"))?;
- let source_path = tempdir.path().join("wasm_probe.c");
- let object_path = tempdir.path().join("wasm_probe.o");
- fs::write(
- &source_path,
- "int radroots_wasm_probe(void) { return 0; }\n",
- )
- .map_err(|error| format!("failed to write C compiler probe: {error}"))?;
- let output = Command::new(path)
- .args(&cflags.args)
- .arg("-c")
- .arg(&source_path)
- .arg("-o")
- .arg(&object_path)
- .stdout(Stdio::piped())
- .stderr(Stdio::piped())
- .output()
- .map_err(|error| format!("failed to run {}: {error}", path.display()))?;
- if output.status.success() && object_path.is_file() {
- return Ok(());
- }
- let stderr = String::from_utf8_lossy(&output.stderr);
- Err(format!(
- "{} rejected `{WASM_CFLAGS_ENV}={}` for {}: {}",
- path.display(),
- cflags.value,
- WASM_TARGET,
- stderr.trim()
- ))
-}
-
-fn prepend_path(command: &mut Command, prefix: &Path) {
- let existing = env::var_os("PATH").unwrap_or_default();
- let mut paths = vec![prefix.to_path_buf()];
- paths.extend(env::split_paths(&existing));
- if let Ok(joined) = env::join_paths(paths) {
- command.env("PATH", joined);
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::{
- env,
- ffi::OsStr,
- fs,
- path::{Path, PathBuf},
- process::Command,
- time::{SystemTime, UNIX_EPOCH},
- };
-
- use crate::package_matrix::wasm_package_specs;
-
- use super::{
- ResolvedWasmCFlags, WASM_C_COMPILER_ENV, WASM_C_TARGET_ARG, WASM_CFLAGS_ENV, WASM_TARGET,
- WasmCCompiler, remove_wasm_pack_gitignore, resolve_path_tool_from_path,
- resolve_wasm_c_compiler_with, resolve_wasm_cflags_from_str, rust_toolchain_channel_from,
- rustup_target_list_args, selected_specs, target_list_contains, validate_target_list,
- wasm_pack_args, wasm_package_requires_c_compiler,
- };
-
- #[test]
- fn selects_all_specs_by_default() {
- assert_eq!(selected_specs(&[]).expect("all specs").len(), 3);
- }
-
- #[test]
- fn selects_one_spec_by_key() {
- let specs = selected_specs(&["--package".to_owned(), "replica_store".to_owned()])
- .expect("replica store spec");
- assert_eq!(specs[0].package_name, "@radroots/replica-store-wasm");
- }
-
- #[test]
- fn rejects_unknown_spec_key() {
- assert!(selected_specs(&["--package".to_owned(), "missing".to_owned()]).is_err());
- }
-
- #[test]
- fn wasm_pack_arguments_disable_package_manifest_generation() {
- let args = wasm_pack_args(wasm_package_specs()[0]);
- assert!(args.contains(&"--no-pack"));
- }
-
- #[test]
- fn removes_wasm_pack_generated_gitignore() {
- let root = test_root("wasm_pack_gitignore");
- let dist_dir = root.join("dist");
- fs::create_dir_all(&dist_dir).expect("create dist");
- fs::write(dist_dir.join(".gitignore"), "*\n").expect("write ignore");
-
- remove_wasm_pack_gitignore(&dist_dir, wasm_package_specs()[0]).expect("remove ignore");
-
- assert!(!dist_dir.join(".gitignore").exists());
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn refuses_unexpected_wasm_pack_gitignore_contents() {
- let root = test_root("custom_gitignore");
- let dist_dir = root.join("dist");
- fs::create_dir_all(&dist_dir).expect("create dist");
- fs::write(dist_dir.join(".gitignore"), "!keep\n").expect("write ignore");
-
- let error = remove_wasm_pack_gitignore(&dist_dir, wasm_package_specs()[0])
- .expect_err("custom ignore rejected");
-
- assert!(error.contains("unexpected wasm-pack ignore file"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn path_tool_resolution_reports_missing_tools() {
- let error = resolve_path_tool_from_path("wasm-pack", std::ffi::OsStr::new(""))
- .expect_err("missing");
- assert!(error.contains("missing wasm-pack"));
- }
-
- #[test]
- fn path_tool_resolution_reports_ambiguous_tools() {
- let root = test_root("ambiguous_wasm_pack");
- let first = root.join("first");
- let second = root.join("second");
- fs::create_dir_all(&first).expect("create first dir");
- fs::create_dir_all(&second).expect("create second dir");
- write_executable(first.join("wasm-pack"));
- write_executable(second.join("wasm-pack"));
- let path = env::join_paths([first, second]).expect("join path");
-
- let error =
- resolve_path_tool_from_path("wasm-pack", &path).expect_err("ambiguous wasm-pack");
-
- assert!(error.contains("ambiguous wasm-pack"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn target_list_parser_requires_exact_target() {
- assert!(target_list_contains(
- "aarch64-apple-darwin\nwasm32-unknown-unknown\n",
- "wasm32-unknown-unknown"
- ));
- assert!(!target_list_contains(
- "wasm32-unknown-emscripten\n",
- "wasm32-unknown-unknown"
- ));
- }
-
- #[test]
- fn rustup_target_list_args_are_bound_to_selected_toolchain() {
- assert_eq!(
- rustup_target_list_args("1.97.0"),
- ["target", "list", "--installed", "--toolchain", "1.97.0"]
- );
- }
-
- #[test]
- fn missing_wasm_target_error_names_selected_toolchain() {
- let error = validate_target_list("aarch64-apple-darwin\n", WASM_TARGET, "1.97.0")
- .expect_err("missing target");
-
- assert!(error.contains("wasm32-unknown-unknown"));
- assert!(error.contains("--toolchain 1.97.0"));
- }
-
- #[test]
- fn parses_rust_toolchain_channel() {
- let channel = rust_toolchain_channel_from(
- r#"[toolchain]
-channel = "1.97.0"
-"#,
- )
- .expect("channel");
-
- assert_eq!(channel, "1.97.0");
- }
-
- #[test]
- fn rejects_missing_wasm_c_compiler() {
- let error = resolve_wasm_c_compiler_with(
- None,
- OsStr::new(""),
- Vec::new(),
- wasm_cflags(None),
- |_, _| Ok(()),
- )
- .expect_err("missing compiler");
-
- assert!(error.contains(WASM_C_COMPILER_ENV));
- assert!(error.contains(WASM_C_TARGET_ARG));
- }
-
- #[test]
- fn rejects_unsuitable_wasm_c_compiler_candidates() {
- let root = test_root("unsuitable_wasm_c_compiler");
- fs::create_dir_all(&root).expect("create root");
- let compiler = root.join("clang");
- write_executable(compiler.clone());
-
- let error = resolve_wasm_c_compiler_with(
- None,
- OsStr::new(""),
- vec![compiler],
- wasm_cflags(None),
- |_, _| Err("target unsupported".to_owned()),
- )
- .expect_err("unsuitable compiler");
-
- assert!(error.contains("missing suitable WASM C compiler"));
- assert!(error.contains(WASM_C_COMPILER_ENV));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn accepts_verified_wasm_c_compiler_candidate() {
- let root = test_root("verified_wasm_c_compiler");
- fs::create_dir_all(&root).expect("create root");
- let compiler = root.join("clang");
- write_executable(compiler.clone());
-
- let resolved = resolve_wasm_c_compiler_with(
- None,
- OsStr::new(""),
- vec![compiler.clone()],
- wasm_cflags(Some("-O2")),
- |path: &Path, cflags: &ResolvedWasmCFlags| {
- assert_eq!(path, compiler);
- assert_eq!(cflags.value, format!("{WASM_C_TARGET_ARG} -O2"));
- Ok(())
- },
- )
- .expect("compiler");
-
- assert_eq!(resolved.path, compiler);
- assert_eq!(resolved.cflags.value, format!("{WASM_C_TARGET_ARG} -O2"));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn absent_wasm_cflags_resolve_to_required_target() {
- let resolved = resolve_wasm_cflags_from_str(None).expect("cflags");
-
- assert_eq!(resolved.value, WASM_C_TARGET_ARG);
- assert_eq!(resolved.args, [WASM_C_TARGET_ARG.to_owned()]);
- }
-
- #[test]
- fn present_valid_wasm_cflags_preserve_additional_flags() {
- let value = "-O2 --target=wasm32-unknown-unknown -fvisibility=hidden";
- let resolved = resolve_wasm_cflags_from_str(Some(value)).expect("cflags");
-
- assert_eq!(resolved.value, value);
- assert_eq!(
- resolved.args,
- [
- "-O2".to_owned(),
- WASM_C_TARGET_ARG.to_owned(),
- "-fvisibility=hidden".to_owned()
- ]
- );
- }
-
- #[test]
- fn missing_target_wasm_cflags_prepend_required_target() {
- let resolved = resolve_wasm_cflags_from_str(Some(" -O2 -fPIC ")).expect("target prepended");
-
- assert_eq!(resolved.value, format!("{WASM_C_TARGET_ARG} -O2 -fPIC"));
- assert_eq!(
- resolved.args,
- [
- WASM_C_TARGET_ARG.to_owned(),
- "-O2".to_owned(),
- "-fPIC".to_owned()
- ]
- );
- }
-
- #[test]
- fn conflicting_target_wasm_cflags_are_rejected() {
- let error = resolve_wasm_cflags_from_str(Some("--target=wasm32-wasip1 -O2"))
- .expect_err("conflicting target");
-
- assert!(error.contains(WASM_CFLAGS_ENV));
- assert!(error.contains("--target=wasm32-wasip1"));
- assert!(error.contains(WASM_C_TARGET_ARG));
- }
-
- #[test]
- fn separated_target_wasm_cflags_are_rejected() {
- let error = resolve_wasm_cflags_from_str(Some("--target wasm32-unknown-unknown"))
- .expect_err("separated target");
-
- assert!(error.contains("--target wasm32-unknown-unknown"));
- assert!(error.contains(WASM_C_TARGET_ARG));
- }
-
- #[test]
- fn wasm_c_compiler_command_uses_resolved_cflags() {
- let compiler = WasmCCompiler {
- path: PathBuf::from("clang"),
- cflags: wasm_cflags(Some("-O2")),
- };
- let mut command = Command::new("wasm-pack");
-
- compiler.apply_to_command(&mut command);
-
- assert_eq!(
- command_env(&command, WASM_C_COMPILER_ENV),
- Some(std::ffi::OsString::from("clang"))
- );
- assert_eq!(
- command_env(&command, WASM_CFLAGS_ENV),
- Some(std::ffi::OsString::from(format!("{WASM_C_TARGET_ARG} -O2")))
- );
- }
-
- #[test]
- fn event_codec_wasm_requires_c_compiler() {
- assert!(wasm_package_requires_c_compiler(wasm_package_specs()[0]));
- assert!(!wasm_package_requires_c_compiler(wasm_package_specs()[1]));
- }
-
- fn wasm_cflags(value: Option<&str>) -> ResolvedWasmCFlags {
- resolve_wasm_cflags_from_str(value).expect("resolved cflags")
- }
-
- fn command_env(command: &Command, name: &str) -> Option<std::ffi::OsString> {
- command.get_envs().find_map(|(key, value)| {
- if key == OsStr::new(name) {
- value.map(std::ffi::OsStr::to_os_string)
- } else {
- None
- }
- })
- }
-
- fn write_executable(path: PathBuf) {
- fs::write(&path, "#!/bin/sh\n").expect("write executable");
- #[cfg(unix)]
- {
- use std::os::unix::fs::PermissionsExt;
- let mut permissions = fs::metadata(&path).expect("metadata").permissions();
- permissions.set_mode(0o755);
- fs::set_permissions(&path, permissions).expect("set executable permissions");
- }
- }
-
- fn test_root(name: &str) -> PathBuf {
- let stamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .expect("system time")
- .as_nanos();
- env::temp_dir().join(format!("radroots_sdk_xtask_{name}_{stamp}"))
- }
-}
diff --git a/tools/xtask/src/wasm_declarations.rs b/tools/xtask/src/wasm_declarations.rs
@@ -1,1147 +0,0 @@
-use std::path::Path;
-
-use dto_bindgen_backend_ts::{
- TypeScriptDeclaration, TypeScriptModule, TypeScriptParameter, TypeScriptProperty,
- TypeScriptType, TypeScriptValue,
-};
-
-use crate::package_matrix::WasmPackageSpec;
-
-const WASM_DECLARATION_HEADER: &str =
- "// @generated by cargo xtask generate wasm via dto_bindgen\n// Do not edit by hand.\n";
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct WasmDeclarationFile {
- pub relative_path: String,
- pub contents: String,
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-struct WasmDeclarationInventory {
- public_functions: &'static [WasmPublicFunction],
- support_exports: &'static [WasmSupportExport],
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-struct WasmPublicFunction {
- name: &'static str,
- parameter: Option<&'static str>,
- return_kind: WasmReturnKind,
- low_level: LowLevelSignature,
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum WasmReturnKind {
- String,
- Any,
- Void,
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-struct LowLevelSignature {
- parameters: usize,
- returns: usize,
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-struct WasmSupportExport {
- name: &'static str,
- export_type: WasmSupportExportType,
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum WasmSupportExportType {
- Memory,
- Table,
- Function(LowLevelSignature),
- NumberFunction(usize),
-}
-
-macro_rules! public_function {
- ($name:literal, $parameter:expr, $return_kind:ident, $low_parameters:literal, $low_returns:literal) => {
- WasmPublicFunction {
- name: $name,
- parameter: $parameter,
- return_kind: WasmReturnKind::$return_kind,
- low_level: LowLevelSignature {
- parameters: $low_parameters,
- returns: $low_returns,
- },
- }
- };
-}
-
-macro_rules! memory_export {
- () => {
- WasmSupportExport {
- name: "memory",
- export_type: WasmSupportExportType::Memory,
- }
- };
-}
-
-macro_rules! table_export {
- ($name:literal) => {
- WasmSupportExport {
- name: $name,
- export_type: WasmSupportExportType::Table,
- }
- };
-}
-
-macro_rules! void_function_export {
- ($name:literal, $parameters:literal) => {
- WasmSupportExport {
- name: $name,
- export_type: WasmSupportExportType::Function(LowLevelSignature {
- parameters: $parameters,
- returns: 0,
- }),
- }
- };
-}
-
-macro_rules! number_function_export {
- ($name:literal, $parameters:literal) => {
- WasmSupportExport {
- name: $name,
- export_type: WasmSupportExportType::NumberFunction($parameters),
- }
- };
-}
-
-pub fn write_declaration_files(root: &Path, spec: WasmPackageSpec) -> Result<(), String> {
- let package_dir = root.join(spec.package_dir);
- for file in declaration_files(spec)? {
- crate::fs::write_if_changed(&package_dir.join(&file.relative_path), &file.contents)?;
- }
- Ok(())
-}
-
-pub fn declaration_files(spec: WasmPackageSpec) -> Result<Vec<WasmDeclarationFile>, String> {
- let inventory = inventory(spec)?;
- Ok(vec![
- WasmDeclarationFile {
- relative_path: format!("dist/{}.d.ts", spec.out_name),
- contents: render_declaration_module(primary_module(spec, inventory)),
- },
- WasmDeclarationFile {
- relative_path: format!("dist/{}_bg.wasm.d.ts", spec.out_name),
- contents: render_declaration_module(sidecar_module(spec, inventory)),
- },
- ])
-}
-
-fn inventory(spec: WasmPackageSpec) -> Result<WasmDeclarationInventory, String> {
- match spec.key {
- "event_codec" => Ok(WasmDeclarationInventory {
- public_functions: EVENT_CODEC_FUNCTIONS,
- support_exports: EVENT_CODEC_SUPPORT_EXPORTS,
- }),
- "replica_store" => Ok(WasmDeclarationInventory {
- public_functions: REPLICA_STORE_FUNCTIONS,
- support_exports: SQL_WASM_SUPPORT_EXPORTS,
- }),
- "replica_sync" => Ok(WasmDeclarationInventory {
- public_functions: REPLICA_SYNC_FUNCTIONS,
- support_exports: SQL_WASM_SUPPORT_EXPORTS,
- }),
- _ => Err(format!(
- "missing wasm declaration inventory for {}",
- spec.key
- )),
- }
-}
-
-fn primary_module(spec: WasmPackageSpec, inventory: WasmDeclarationInventory) -> TypeScriptModule {
- let mut module = TypeScriptModule::new(format!("dist/{}.d.ts", spec.out_name));
- for function in inventory.public_functions {
- module.push_declaration(TypeScriptDeclaration::function(
- function.name,
- public_parameters(function.parameter),
- public_return_type(function.return_kind),
- ));
- }
- module.push_declaration(TypeScriptDeclaration::type_alias(
- "InitInput",
- init_input_type(),
- ));
- module.push_declaration(TypeScriptDeclaration::type_alias(
- "InitOutput",
- init_output_type(inventory),
- ));
- module.push_declaration(TypeScriptDeclaration::type_alias(
- "SyncInitInput",
- sync_init_input_type(),
- ));
- module.push_declaration(TypeScriptDeclaration::function(
- "initSync",
- vec![TypeScriptParameter::new(
- "module",
- TypeScriptType::union(vec![
- TypeScriptType::object(vec![TypeScriptProperty::new(
- "module",
- TypeScriptType::named("SyncInitInput"),
- )]),
- TypeScriptType::named("SyncInitInput"),
- ]),
- )],
- TypeScriptType::named("InitOutput"),
- ));
- module.push_declaration(TypeScriptDeclaration::default_function(
- "__wbg_init",
- vec![
- TypeScriptParameter::new(
- "module_or_path",
- TypeScriptType::union(vec![
- TypeScriptType::object(vec![TypeScriptProperty::new(
- "module_or_path",
- TypeScriptType::union(vec![
- TypeScriptType::named("InitInput"),
- promise_of(TypeScriptType::named("InitInput")),
- ]),
- )]),
- TypeScriptType::named("InitInput"),
- promise_of(TypeScriptType::named("InitInput")),
- ]),
- )
- .optional(),
- ],
- promise_of(TypeScriptType::named("InitOutput")),
- ));
- module
-}
-
-fn sidecar_module(spec: WasmPackageSpec, inventory: WasmDeclarationInventory) -> TypeScriptModule {
- let mut module = TypeScriptModule::new(format!("dist/{}_bg.wasm.d.ts", spec.out_name));
- for export in inventory.support_exports {
- if matches!(export.export_type, WasmSupportExportType::Memory) {
- module.push_declaration(support_export_declaration(*export));
- }
- }
- for function in inventory.public_functions {
- module.push_declaration(TypeScriptDeclaration::constant(
- function.name,
- Some(low_level_function_type(function.low_level)),
- TypeScriptValue::string(""),
- ));
- }
- for export in inventory.support_exports {
- if !matches!(export.export_type, WasmSupportExportType::Memory) {
- module.push_declaration(support_export_declaration(*export));
- }
- }
- module
-}
-
-fn render_declaration_module(module: TypeScriptModule) -> String {
- let mut rendered = String::from(WASM_DECLARATION_HEADER);
- rendered.push_str(&module.render_declaration());
- rendered
-}
-
-fn public_parameters(parameter: Option<&'static str>) -> Vec<TypeScriptParameter> {
- parameter
- .map(|name| vec![TypeScriptParameter::new(name, TypeScriptType::String)])
- .unwrap_or_default()
-}
-
-fn public_return_type(kind: WasmReturnKind) -> TypeScriptType {
- match kind {
- WasmReturnKind::String => TypeScriptType::String,
- WasmReturnKind::Any => TypeScriptType::Any,
- WasmReturnKind::Void => TypeScriptType::Void,
- }
-}
-
-fn init_input_type() -> TypeScriptType {
- TypeScriptType::union(vec![
- TypeScriptType::named("RequestInfo"),
- TypeScriptType::named("URL"),
- TypeScriptType::named("Response"),
- TypeScriptType::named("BufferSource"),
- TypeScriptType::named("WebAssembly.Module"),
- ])
-}
-
-fn sync_init_input_type() -> TypeScriptType {
- TypeScriptType::union(vec![
- TypeScriptType::named("BufferSource"),
- TypeScriptType::named("WebAssembly.Module"),
- ])
-}
-
-fn init_output_type(inventory: WasmDeclarationInventory) -> TypeScriptType {
- let mut properties = vec![TypeScriptProperty::new("memory", memory_type()).readonly()];
- properties.extend(inventory.public_functions.iter().map(|function| {
- TypeScriptProperty::new(function.name, low_level_function_type(function.low_level))
- .readonly()
- }));
- properties.extend(
- inventory
- .support_exports
- .iter()
- .filter(|export| !matches!(export.export_type, WasmSupportExportType::Memory))
- .map(|export| {
- TypeScriptProperty::new(export.name, support_export_type(*export)).readonly()
- }),
- );
- TypeScriptType::object(properties)
-}
-
-fn support_export_declaration(export: WasmSupportExport) -> TypeScriptDeclaration {
- TypeScriptDeclaration::constant(
- export.name,
- Some(support_export_type(export)),
- TypeScriptValue::string(""),
- )
-}
-
-fn support_export_type(export: WasmSupportExport) -> TypeScriptType {
- match export.export_type {
- WasmSupportExportType::Memory => memory_type(),
- WasmSupportExportType::Table => TypeScriptType::named("WebAssembly.Table"),
- WasmSupportExportType::Function(signature) => low_level_function_type(signature),
- WasmSupportExportType::NumberFunction(parameters) => {
- low_level_number_function_type(parameters)
- }
- }
-}
-
-fn memory_type() -> TypeScriptType {
- TypeScriptType::named("WebAssembly.Memory")
-}
-
-fn low_level_number_function_type(parameters: usize) -> TypeScriptType {
- TypeScriptType::function(number_parameters(parameters), TypeScriptType::Number)
-}
-
-fn low_level_function_type(signature: LowLevelSignature) -> TypeScriptType {
- TypeScriptType::function(
- number_parameters(signature.parameters),
- low_level_return_type(signature.returns),
- )
-}
-
-fn number_parameters(count: usize) -> Vec<TypeScriptParameter> {
- (0..count)
- .map(|index| {
- TypeScriptParameter::new(low_level_parameter_name(index), TypeScriptType::Number)
- })
- .collect()
-}
-
-fn low_level_parameter_name(index: usize) -> String {
- match index {
- 0 => "a".to_owned(),
- 1 => "b".to_owned(),
- 2 => "c".to_owned(),
- 3 => "d".to_owned(),
- _ => format!("arg{index}"),
- }
-}
-
-fn low_level_return_type(count: usize) -> TypeScriptType {
- match count {
- 0 => TypeScriptType::Void,
- 1 => TypeScriptType::Number,
- _ => TypeScriptType::tuple(vec![TypeScriptType::Number; count]),
- }
-}
-
-fn promise_of(type_expr: TypeScriptType) -> TypeScriptType {
- TypeScriptType::generic("Promise", vec![type_expr])
-}
-
-const EVENT_CODEC_FUNCTIONS: &[WasmPublicFunction] = &[
- public_function!("article_tags", Some("article_json"), String, 2, 4),
- public_function!("coop_tags", Some("coop_json"), String, 2, 4),
- public_function!("document_tags", Some("document_json"), String, 2, 4),
- public_function!("farm_crdt_change_tags", Some("input_json"), String, 2, 4),
- public_function!("farm_file_metadata_tags", Some("file_json"), String, 2, 4),
- public_function!("farm_tags", Some("farm_json"), String, 2, 4),
- public_function!(
- "farm_workspace_manifest_tags",
- Some("workspace_json"),
- String,
- 2,
- 4
- ),
- public_function!("file_metadata_tags", Some("metadata_json"), String, 2, 4),
- public_function!("follow_tags", Some("follow_json"), String, 2, 4),
- public_function!("generic_repost_tags", Some("repost_json"), String, 2, 4),
- public_function!("gift_wrap_tags", Some("gift_wrap_json"), String, 2, 4),
- public_function!("group_admins_tags", Some("group_json"), String, 2, 4),
- public_function!("group_create_group_tags", Some("group_json"), String, 2, 4),
- public_function!("group_create_invite_tags", Some("group_json"), String, 2, 4),
- public_function!("group_delete_event_tags", Some("group_json"), String, 2, 4),
- public_function!("group_delete_group_tags", Some("group_json"), String, 2, 4),
- public_function!("group_edit_metadata_tags", Some("group_json"), String, 2, 4),
- public_function!("group_join_request_tags", Some("group_json"), String, 2, 4),
- public_function!("group_leave_request_tags", Some("group_json"), String, 2, 4),
- public_function!("group_members_tags", Some("group_json"), String, 2, 4),
- public_function!("group_metadata_tags", Some("group_json"), String, 2, 4),
- public_function!("group_put_user_tags", Some("group_json"), String, 2, 4),
- public_function!("group_remove_user_tags", Some("group_json"), String, 2, 4),
- public_function!("group_roles_tags", Some("group_json"), String, 2, 4),
- public_function!("http_auth_tags", Some("auth_json"), String, 2, 4),
- public_function!("job_feedback_tags", Some("job_json"), String, 2, 4),
- public_function!("job_request_tags", Some("job_json"), String, 2, 4),
- public_function!("job_result_tags", Some("job_json"), String, 2, 4),
- public_function!("knowledge_claim_tags", Some("claim_json"), String, 2, 4),
- public_function!(
- "knowledge_field_report_tags",
- Some("report_json"),
- String,
- 2,
- 4
- ),
- public_function!(
- "knowledge_relation_tags",
- Some("relation_json"),
- String,
- 2,
- 4
- ),
- public_function!("knowledge_review_tags", Some("review_json"), String, 2, 4),
- public_function!("knowledge_source_tags", Some("source_json"), String, 2, 4),
- public_function!("list_set_tags", Some("list_json"), String, 2, 4),
- public_function!("list_tags", Some("list_json"), String, 2, 4),
- public_function!(
- "operational_listing_tags",
- Some("listing_json"),
- String,
- 2,
- 4
- ),
- public_function!(
- "operational_listing_tags_full",
- Some("listing_json"),
- String,
- 2,
- 4
- ),
- public_function!("message_file_tags", Some("message_json"), String, 2, 4),
- public_function!("message_tags", Some("message_json"), String, 2, 4),
- public_function!("plot_tags", Some("plot_json"), String, 2, 4),
- public_function!("reaction_tags", Some("reaction_json"), String, 2, 4),
- public_function!("relay_auth_tags", Some("auth_json"), String, 2, 4),
- public_function!("report_tags", Some("report_json"), String, 2, 4),
- public_function!("repost_tags", Some("repost_json"), String, 2, 4),
- public_function!("seal_tags", Some("seal_json"), String, 2, 4),
- public_function!(
- "social_ask_build_authored_draft",
- Some("input_json"),
- String,
- 2,
- 4
- ),
- public_function!(
- "social_comment_build_authored_draft",
- Some("input_json"),
- String,
- 2,
- 4
- ),
- public_function!(
- "social_photo_update_build_authored_draft",
- Some("input_json"),
- String,
- 2,
- 4
- ),
- public_function!(
- "social_update_build_authored_draft",
- Some("input_json"),
- String,
- 2,
- 4
- ),
- public_function!("wiki_article_tags", Some("article_json"), String, 2, 4),
- public_function!(
- "wiki_merge_request_tags",
- Some("request_json"),
- String,
- 2,
- 4
- ),
- public_function!("wiki_redirect_tags", Some("redirect_json"), String, 2, 4),
- public_function!(
- "verify_and_decode_event_json",
- Some("event_json"),
- String,
- 2,
- 4
- ),
- public_function!("contract_manifest_json", None, String, 0, 4),
-];
-
-const EVENT_CODEC_SUPPORT_EXPORTS: &[WasmSupportExport] = &[
- memory_export!(),
- table_export!("__wbindgen_externrefs"),
- number_function_export!("__wbindgen_malloc", 2),
- number_function_export!("__wbindgen_realloc", 4),
- void_function_export!("__externref_table_dealloc", 1),
- void_function_export!("__wbindgen_free", 3),
- void_function_export!("__wbindgen_start", 0),
-];
-
-const REPLICA_STORE_FUNCTIONS: &[WasmPublicFunction] = &[
- public_function!("replica_store_export_begin", None, Any, 0, 3),
- public_function!("replica_store_export_finish", None, Void, 0, 2),
- public_function!("replica_store_export_json", None, Any, 0, 3),
- public_function!("replica_store_farm_create", Some("opts_json"), Any, 2, 3),
- public_function!("replica_store_farm_delete", Some("opts_json"), Any, 2, 3),
- public_function!("replica_store_farm_find_many", Some("opts_json"), Any, 2, 3),
- public_function!("replica_store_farm_find_one", Some("opts_json"), Any, 2, 3),
- public_function!("replica_store_farm_update", Some("opts_json"), Any, 2, 3),
- public_function!(
- "replica_store_farm_gcs_location_create",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_gcs_location_delete",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_gcs_location_find_many",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_gcs_location_find_one",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_gcs_location_update",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_member_claim_create",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_member_claim_delete",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_member_claim_find_many",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_member_claim_find_one",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_member_claim_update",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_member_create",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_member_delete",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_member_find_many",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_member_find_one",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_member_update",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_tag_create",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_tag_delete",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_tag_find_many",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_tag_find_one",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_farm_tag_update",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_gcs_location_create",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_gcs_location_delete",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_gcs_location_find_many",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_gcs_location_find_one",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_gcs_location_update",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!("replica_store_import_json", Some("dump_json"), Void, 2, 2),
- public_function!(
- "replica_store_log_error_create",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_log_error_delete",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_log_error_find_many",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_log_error_find_one",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_log_error_update",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_media_image_create",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_media_image_delete",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_media_image_find_many",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_media_image_find_one",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_media_image_update",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_nostr_event_head_create",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_nostr_event_head_delete",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_nostr_event_head_find_many",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_nostr_event_head_find_one",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_nostr_event_head_update",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_nostr_profile_create",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_nostr_profile_delete",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_nostr_profile_find_many",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_nostr_profile_find_one",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_nostr_profile_relay_set",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_nostr_profile_relay_unset",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_nostr_profile_update",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_nostr_relay_create",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_nostr_relay_delete",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_nostr_relay_find_many",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_nostr_relay_find_one",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_nostr_relay_update",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!("replica_store_plot_create", Some("opts_json"), Any, 2, 3),
- public_function!("replica_store_plot_delete", Some("opts_json"), Any, 2, 3),
- public_function!("replica_store_plot_find_many", Some("opts_json"), Any, 2, 3),
- public_function!("replica_store_plot_find_one", Some("opts_json"), Any, 2, 3),
- public_function!("replica_store_plot_update", Some("opts_json"), Any, 2, 3),
- public_function!(
- "replica_store_plot_gcs_location_create",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_plot_gcs_location_delete",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_plot_gcs_location_find_many",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_plot_gcs_location_find_one",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_plot_gcs_location_update",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_plot_tag_create",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_plot_tag_delete",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_plot_tag_find_many",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_plot_tag_find_one",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_plot_tag_update",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!("replica_store_reset_database", None, Void, 0, 2),
- public_function!("replica_store_run_migrations", None, Void, 0, 2),
- public_function!(
- "replica_store_trade_product_create",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_trade_product_delete",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_trade_product_find_many",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_trade_product_find_one",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_trade_product_location_set",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_trade_product_location_unset",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_trade_product_media_set",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_trade_product_media_unset",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
- public_function!(
- "replica_store_trade_product_update",
- Some("opts_json"),
- Any,
- 2,
- 3
- ),
-];
-
-const REPLICA_SYNC_FUNCTIONS: &[WasmPublicFunction] = &[
- public_function!("replica_sync_ingest_event", Some("event_json"), Any, 2, 3),
- public_function!("replica_sync_sync_all", Some("request_json"), Any, 2, 3),
-];
-
-const SQL_WASM_SUPPORT_EXPORTS: &[WasmSupportExport] = &[
- memory_export!(),
- number_function_export!("__wbindgen_malloc", 2),
- number_function_export!("__wbindgen_realloc", 4),
- void_function_export!("__wbindgen_exn_store", 1),
- number_function_export!("__externref_table_alloc", 0),
- table_export!("__wbindgen_externrefs"),
- void_function_export!("__externref_table_dealloc", 1),
- void_function_export!("__wbindgen_start", 0),
-];
-
-#[cfg(test)]
-mod tests {
- use crate::package_matrix::wasm_package_specs;
-
- use super::{WASM_DECLARATION_HEADER, declaration_files};
-
- #[test]
- fn declaration_files_are_dto_owned_and_cover_primary_and_sidecar_paths() {
- for spec in wasm_package_specs() {
- let files = declaration_files(*spec).expect("declaration files");
- assert_eq!(files.len(), 2);
- assert!(
- files
- .iter()
- .any(|file| file.relative_path == format!("dist/{}.d.ts", spec.out_name))
- );
- assert!(
- files.iter().any(
- |file| file.relative_path == format!("dist/{}_bg.wasm.d.ts", spec.out_name)
- )
- );
- for file in files {
- assert!(file.contents.starts_with(WASM_DECLARATION_HEADER));
- assert!(!file.contents.contains("tslint:disable"));
- assert!(!file.contents.contains("eslint-disable"));
- }
- }
- }
-
- #[test]
- fn generated_declarations_include_public_wasm_entrypoints() {
- let specs = wasm_package_specs();
- let events = declaration_files(specs[0]).expect("event declarations");
- assert!(
- events[0]
- .contents
- .contains("export function operational_listing_tags")
- );
- assert!(
- events[1]
- .contents
- .contains("export declare const operational_listing_tags")
- );
- for authored in [
- "social_ask_build_authored_draft",
- "social_comment_build_authored_draft",
- "social_photo_update_build_authored_draft",
- "social_update_build_authored_draft",
- ] {
- assert!(
- events[0]
- .contents
- .contains(&format!("export function {authored}"))
- );
- assert!(
- events[1]
- .contents
- .contains(&format!("export declare const {authored}"))
- );
- }
- for retired in [
- "calendar_date_event_tags",
- "calendar_event_rsvp_tags",
- "calendar_tags",
- "calendar_time_event_tags",
- ] {
- assert!(!events[0].contents.contains(retired));
- assert!(!events[1].contents.contains(retired));
- }
- for retired in ["comment_tags", "post_tags"] {
- assert!(
- !events[0]
- .contents
- .contains(&format!("export function {retired}("))
- );
- assert!(
- !events[1]
- .contents
- .contains(&format!("export declare const {retired}:"))
- );
- }
-
- let replica_store = declaration_files(specs[1]).expect("replica store declarations");
- assert!(
- replica_store[0]
- .contents
- .contains("export function replica_store_farm_create")
- );
- assert!(
- replica_store[0]
- .contents
- .contains("export function replica_store_farm_update")
- );
- assert!(
- replica_store[0]
- .contents
- .contains("export function replica_store_plot_update")
- );
- assert!(
- replica_store[1]
- .contents
- .contains("export declare const replica_store_farm_create")
- );
- assert!(
- replica_store[1]
- .contents
- .contains("export declare const replica_store_farm_update")
- );
- assert!(
- replica_store[1]
- .contents
- .contains("export declare const replica_store_plot_update")
- );
-
- let replica_sync = declaration_files(specs[2]).expect("replica sync declarations");
- assert!(
- replica_sync[0]
- .contents
- .contains("export function replica_sync_sync_all")
- );
- assert!(
- replica_sync[1]
- .contents
- .contains("export declare const replica_sync_sync_all")
- );
- }
-}
diff --git a/tools/xtask/tests/fixtures/api-leakage/adr-exception.rs b/tools/xtask/tests/fixtures/api-leakage/adr-exception.rs
@@ -1,3 +0,0 @@
-pub fn temporary_client() -> reqwest::Client {
- unreachable!()
-}
diff --git a/tools/xtask/tests/fixtures/api-leakage/allowed-concrete-adapter.rs b/tools/xtask/tests/fixtures/api-leakage/allowed-concrete-adapter.rs
@@ -1,3 +0,0 @@
-pub fn protocol_event(event: nostr::Event) -> nostr::Event {
- event
-}
diff --git a/tools/xtask/tests/fixtures/api-leakage/generic-renamed-tokio.rs b/tools/xtask/tests/fixtures/api-leakage/generic-renamed-tokio.rs
@@ -1,5 +0,0 @@
-use tokio::runtime::Handle as Executor;
-
-pub fn executor() -> Executor {
- unreachable!()
-}
diff --git a/tools/xtask/tests/fixtures/api-leakage/generic-sqlx.rs b/tools/xtask/tests/fixtures/api-leakage/generic-sqlx.rs
@@ -1,5 +0,0 @@
-use sqlx::SqlitePool;
-
-pub struct StorageHandle {
- pub pool: SqlitePool,
-}
diff --git a/tools/xtask/tests/fixtures/api-leakage/private-implementation.rs b/tools/xtask/tests/fixtures/api-leakage/private-implementation.rs
@@ -1,11 +0,0 @@
-use reqwest::Client;
-
-pub struct Adapter {
- client: Client,
-}
-
-impl Adapter {
- fn client(&self) -> &Client {
- &self.client
- }
-}
diff --git a/tools/xtask/tests/fixtures/dependency-boundaries/domain-to-storage.json b/tools/xtask/tests/fixtures/dependency-boundaries/domain-to-storage.json
@@ -1,46 +0,0 @@
-{
- "packages": [
- {
- "id": "fixture#radroots_event@0.1.0",
- "name": "radroots_event",
- "dependencies": [
- {
- "name": "radroots_storage",
- "rename": "storage_alias",
- "kind": null,
- "features": ["memory"],
- "target": "cfg(unix)",
- "uses_default_features": false
- }
- ]
- },
- {
- "id": "fixture#radroots_storage@0.1.0",
- "name": "radroots_storage",
- "dependencies": []
- }
- ],
- "resolve": {
- "nodes": [
- {
- "id": "fixture#radroots_event@0.1.0",
- "deps": [
- {
- "name": "storage_alias",
- "pkg": "fixture#radroots_storage@0.1.0",
- "dep_kinds": [
- {
- "kind": null,
- "target": "cfg(unix)"
- }
- ]
- }
- ]
- },
- {
- "id": "fixture#radroots_storage@0.1.0",
- "deps": []
- }
- ]
- }
-}
diff --git a/tools/xtask/tests/fixtures/dependency-boundaries/service-to-sdk.json b/tools/xtask/tests/fixtures/dependency-boundaries/service-to-sdk.json
@@ -1,46 +0,0 @@
-{
- "packages": [
- {
- "id": "fixture#radroots_sync@0.1.0",
- "name": "radroots_sync",
- "dependencies": [
- {
- "name": "radroots_sdk",
- "rename": "client_engine",
- "kind": null,
- "features": ["memory"],
- "target": null,
- "uses_default_features": false
- }
- ]
- },
- {
- "id": "fixture#radroots_sdk@0.1.0",
- "name": "radroots_sdk",
- "dependencies": []
- }
- ],
- "resolve": {
- "nodes": [
- {
- "id": "fixture#radroots_sync@0.1.0",
- "deps": [
- {
- "name": "client_engine",
- "pkg": "fixture#radroots_sdk@0.1.0",
- "dep_kinds": [
- {
- "kind": null,
- "target": null
- }
- ]
- }
- ]
- },
- {
- "id": "fixture#radroots_sdk@0.1.0",
- "deps": []
- }
- ]
- }
-}
diff --git a/tools/xtask/tests/fixtures/dependency-boundaries/spi-to-adapter.json b/tools/xtask/tests/fixtures/dependency-boundaries/spi-to-adapter.json
@@ -1,46 +0,0 @@
-{
- "packages": [
- {
- "id": "fixture#radroots_transport@0.1.0",
- "name": "radroots_transport",
- "dependencies": [
- {
- "name": "radroots_nostr",
- "rename": "protocol_adapter",
- "kind": "build",
- "features": ["events"],
- "target": "cfg(target_arch = \"wasm32\")",
- "uses_default_features": false
- }
- ]
- },
- {
- "id": "fixture#radroots_nostr@0.1.0",
- "name": "radroots_nostr",
- "dependencies": []
- }
- ],
- "resolve": {
- "nodes": [
- {
- "id": "fixture#radroots_transport@0.1.0",
- "deps": [
- {
- "name": "protocol_adapter",
- "pkg": "fixture#radroots_nostr@0.1.0",
- "dep_kinds": [
- {
- "kind": "build",
- "target": "cfg(target_arch = \"wasm32\")"
- }
- ]
- }
- ]
- },
- {
- "id": "fixture#radroots_nostr@0.1.0",
- "deps": []
- }
- ]
- }
-}
diff --git a/tools/xtask/tests/fixtures/dependency-boundaries/valid-downward.json b/tools/xtask/tests/fixtures/dependency-boundaries/valid-downward.json
@@ -1,46 +0,0 @@
-{
- "packages": [
- {
- "id": "fixture#radroots_transport_nostr@0.1.0",
- "name": "radroots_transport_nostr",
- "dependencies": [
- {
- "name": "radroots_transport",
- "rename": "transport_spi",
- "kind": "dev",
- "features": [],
- "target": "cfg(unix)",
- "uses_default_features": false
- }
- ]
- },
- {
- "id": "fixture#radroots_transport@0.1.0",
- "name": "radroots_transport",
- "dependencies": []
- }
- ],
- "resolve": {
- "nodes": [
- {
- "id": "fixture#radroots_transport_nostr@0.1.0",
- "deps": [
- {
- "name": "transport_spi",
- "pkg": "fixture#radroots_transport@0.1.0",
- "dep_kinds": [
- {
- "kind": "dev",
- "target": "cfg(unix)"
- }
- ]
- }
- ]
- },
- {
- "id": "fixture#radroots_transport@0.1.0",
- "deps": []
- }
- ]
- }
-}