lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

contracts.rs (23035B)


      1 use std::{
      2     collections::{BTreeMap, BTreeSet},
      3     fs,
      4     path::{Path, PathBuf},
      5 };
      6 
      7 use serde::Deserialize;
      8 
      9 use crate::sdk_generation::package_matrix::{
     10     PackageSpec, WasmPackageSpec, package_specs, wasm_package_specs,
     11 };
     12 
     13 #[derive(Debug, Deserialize)]
     14 #[serde(deny_unknown_fields)]
     15 struct ExportContract {
     16     language: LanguageContract,
     17     packages: BTreeMap<String, String>,
     18     artifacts: Option<ExportArtifacts>,
     19     runtime: RuntimeContract,
     20 }
     21 
     22 #[derive(Debug, Deserialize)]
     23 #[serde(deny_unknown_fields)]
     24 struct PackageContract {
     25     language: LanguageContract,
     26     sdk: SdkPackageContract,
     27     rollout: RolloutContract,
     28     operations: BTreeMap<String, String>,
     29     shared_types: BTreeMap<String, String>,
     30     artifacts: Option<SdkArtifacts>,
     31     npm_packages: Option<BTreeMap<String, NpmPackageContract>>,
     32 }
     33 
     34 #[derive(Debug, Deserialize)]
     35 #[serde(deny_unknown_fields)]
     36 struct LanguageContract {
     37     id: String,
     38     repository: String,
     39 }
     40 
     41 #[derive(Debug, Deserialize)]
     42 #[serde(deny_unknown_fields)]
     43 struct RuntimeContract {
     44     networking: String,
     45     signing: String,
     46     deterministic_codec: String,
     47 }
     48 
     49 #[derive(Debug, Deserialize)]
     50 #[serde(deny_unknown_fields)]
     51 struct ExportArtifacts {
     52     models_dir: String,
     53     constants_dir: String,
     54     wasm_dist_dir: Option<String>,
     55     manifest_file: String,
     56 }
     57 
     58 #[derive(Debug, Deserialize)]
     59 #[serde(deny_unknown_fields)]
     60 struct SdkPackageContract {
     61     package: Option<String>,
     62     package_family: Option<String>,
     63     module_format: Option<String>,
     64     deterministic_codec: String,
     65     signing: String,
     66     networking: String,
     67 }
     68 
     69 #[derive(Debug, Deserialize)]
     70 #[serde(deny_unknown_fields)]
     71 struct NpmPackageContract {
     72     kind: String,
     73     crate_name: String,
     74     crate_dir: String,
     75     package: String,
     76     package_dir: String,
     77     out_name: Option<String>,
     78     out_dir: Option<String>,
     79 }
     80 
     81 #[derive(Debug)]
     82 struct ExpectedNpmPackage {
     83     kind: &'static str,
     84     crate_name: &'static str,
     85     crate_dir: &'static str,
     86     package: &'static str,
     87     package_dir: &'static str,
     88     out_name: Option<&'static str>,
     89     out_dir: Option<&'static str>,
     90 }
     91 
     92 #[derive(Debug, Deserialize)]
     93 #[serde(deny_unknown_fields)]
     94 struct RolloutContract {
     95     stage: String,
     96     order: u32,
     97 }
     98 
     99 #[derive(Debug, Deserialize)]
    100 #[serde(deny_unknown_fields)]
    101 struct SdkArtifacts {
    102     models_dir: String,
    103     runtime_dir: String,
    104     wasm_dist_dir: String,
    105     manifest_file: String,
    106 }
    107 
    108 pub fn validate_sdk_contracts(root: &Path) -> Result<(), String> {
    109     radroots_protocol::runtime::v1::validate_catalog(radroots_protocol::runtime::v1::CATALOG)
    110         .map_err(|error| error.to_string())?;
    111     let exports = load_contract_dir::<ExportContract>(&root.join("contracts").join("exports"))?;
    112     let packages = load_contract_dir::<PackageContract>(&root.join("contracts").join("packages"))?;
    113     if exports.is_empty() {
    114         return Err("contracts/exports must define at least one language".to_owned());
    115     }
    116     if packages.is_empty() {
    117         return Err("contracts/packages must define at least one language".to_owned());
    118     }
    119 
    120     let mut export_packages = BTreeMap::new();
    121     let mut export_languages = BTreeSet::new();
    122     for export in &exports {
    123         validate_language(&export.language, "exports")?;
    124         validate_non_empty_map(&export.packages, "exports packages")?;
    125         validate_runtime(
    126             &export.runtime.networking,
    127             &export.runtime.signing,
    128             &export.runtime.deterministic_codec,
    129             &format!("exports {}", export.language.id),
    130         )?;
    131         let artifacts = export
    132             .artifacts
    133             .as_ref()
    134             .ok_or_else(|| format!("exports {} artifacts are required", export.language.id))?;
    135         validate_non_empty(&artifacts.models_dir, "exports artifacts.models_dir")?;
    136         validate_non_empty(&artifacts.constants_dir, "exports artifacts.constants_dir")?;
    137         validate_non_empty(&artifacts.manifest_file, "exports artifacts.manifest_file")?;
    138         if export.language.id == "ts" {
    139             validate_non_empty(
    140                 artifacts.wasm_dist_dir.as_deref().unwrap_or(""),
    141                 "exports ts artifacts.wasm_dist_dir",
    142             )?;
    143         }
    144         if !export_languages.insert(export.language.id.clone()) {
    145             return Err(format!("duplicate exports language {}", export.language.id));
    146         }
    147         let packages = export
    148             .packages
    149             .values()
    150             .cloned()
    151             .collect::<BTreeSet<String>>();
    152         if export.language.id != "ts" && packages.len() != 1 {
    153             return Err(format!(
    154                 "exports {} must resolve to one curated package",
    155                 export.language.id
    156             ));
    157         }
    158         export_packages.insert(export.language.id.clone(), packages);
    159     }
    160 
    161     let mut package_languages = BTreeSet::new();
    162     let mut operation_keys: Option<BTreeSet<String>> = None;
    163     let mut shared_type_keys: Option<BTreeSet<String>> = None;
    164     let mut rollout_orders = BTreeMap::new();
    165     for package in &packages {
    166         validate_language(&package.language, "packages")?;
    167         if let Some(package_name) = package.sdk.package.as_deref() {
    168             validate_non_empty(package_name, "packages sdk.package")?;
    169         }
    170         if let Some(package_family) = package.sdk.package_family.as_deref() {
    171             validate_non_empty(package_family, "packages sdk.package_family")?;
    172         }
    173         validate_runtime(
    174             &package.sdk.networking,
    175             &package.sdk.signing,
    176             &package.sdk.deterministic_codec,
    177             &format!("packages {}", package.language.id),
    178         )?;
    179         if let Some(module_format) = package.sdk.module_format.as_deref() {
    180             validate_non_empty(module_format, "packages sdk.module_format")?;
    181         }
    182         if package.sdk.package.is_none() && package.sdk.package_family.is_none() {
    183             return Err(format!(
    184                 "packages {} sdk.package or sdk.package_family is required",
    185                 package.language.id
    186             ));
    187         }
    188         validate_rollout(&package.language.id, &package.rollout)?;
    189         validate_non_empty_map(&package.operations, "packages operations")?;
    190         validate_non_empty_map(&package.shared_types, "packages shared_types")?;
    191         if package.language.id == "ts" {
    192             let artifacts = package
    193                 .artifacts
    194                 .as_ref()
    195                 .ok_or_else(|| "packages ts artifacts are required".to_owned())?;
    196             validate_non_empty(&artifacts.models_dir, "packages ts artifacts.models_dir")?;
    197             validate_non_empty(&artifacts.runtime_dir, "packages ts artifacts.runtime_dir")?;
    198             validate_non_empty(
    199                 &artifacts.wasm_dist_dir,
    200                 "packages ts artifacts.wasm_dist_dir",
    201             )?;
    202             validate_non_empty(
    203                 &artifacts.manifest_file,
    204                 "packages ts artifacts.manifest_file",
    205             )?;
    206         }
    207         if !package_languages.insert(package.language.id.clone()) {
    208             return Err(format!(
    209                 "duplicate packages language {}",
    210                 package.language.id
    211             ));
    212         }
    213         let Some(packages_for_language) = export_packages.get(&package.language.id) else {
    214             return Err(format!(
    215                 "packages {} is missing a matching export contract",
    216                 package.language.id
    217             ));
    218         };
    219         if package.language.id == "ts" {
    220             let npm_packages = package
    221                 .npm_packages
    222                 .as_ref()
    223                 .ok_or_else(|| "packages ts npm_packages are required".to_owned())?;
    224             validate_ts_npm_packages(package, packages_for_language, npm_packages)?;
    225         } else {
    226             if package.npm_packages.is_some() {
    227                 return Err(format!(
    228                     "packages {} npm_packages is only supported for ts",
    229                     package.language.id
    230                 ));
    231             }
    232             let sdk_package = package.sdk.package.as_ref().ok_or_else(|| {
    233                 format!("packages {} sdk.package is required", package.language.id)
    234             })?;
    235             let expected = [sdk_package.clone()].into_iter().collect::<BTreeSet<_>>();
    236             if packages_for_language != &expected {
    237                 return Err(format!(
    238                     "exports {} must resolve to package {}",
    239                     package.language.id, sdk_package
    240                 ));
    241             }
    242         }
    243         let current_operations = package.operations.keys().cloned().collect::<BTreeSet<_>>();
    244         match &operation_keys {
    245             Some(expected) if expected != &current_operations => {
    246                 return Err(format!(
    247                     "packages {} operations must match the shared operation set",
    248                     package.language.id
    249                 ));
    250             }
    251             None => operation_keys = Some(current_operations),
    252             _ => {}
    253         }
    254         let current_shared_types = package
    255             .shared_types
    256             .keys()
    257             .cloned()
    258             .collect::<BTreeSet<_>>();
    259         match &shared_type_keys {
    260             Some(expected) if expected != &current_shared_types => {
    261                 return Err(format!(
    262                     "packages {} shared_types must match the shared type set",
    263                     package.language.id
    264                 ));
    265             }
    266             None => shared_type_keys = Some(current_shared_types),
    267             _ => {}
    268         }
    269         rollout_orders.insert(package.language.id.clone(), package.rollout.order);
    270     }
    271 
    272     if export_languages != package_languages {
    273         return Err("contracts/exports and contracts/packages languages must match".to_owned());
    274     }
    275     if rollout_orders.get("ts") != Some(&1) {
    276         return Err("packages ts rollout.order must be 1".to_owned());
    277     }
    278     Ok(())
    279 }
    280 
    281 fn validate_ts_npm_packages(
    282     package: &PackageContract,
    283     export_packages: &BTreeSet<String>,
    284     npm_packages: &BTreeMap<String, NpmPackageContract>,
    285 ) -> Result<(), String> {
    286     if package.sdk.package.is_some() {
    287         return Err(
    288             "packages ts sdk.package must not be set for multi-package npm output".to_owned(),
    289         );
    290     }
    291     let package_family = package
    292         .sdk
    293         .package_family
    294         .as_deref()
    295         .ok_or_else(|| "packages ts sdk.package_family is required".to_owned())?;
    296     if package_family != "@radroots" {
    297         return Err("packages ts sdk.package_family must be @radroots".to_owned());
    298     }
    299     let expected = expected_ts_npm_packages();
    300     let expected_keys = expected.keys().cloned().collect::<BTreeSet<_>>();
    301     let actual_keys = npm_packages.keys().cloned().collect::<BTreeSet<_>>();
    302     if actual_keys != expected_keys {
    303         let missing = expected_keys
    304             .difference(&actual_keys)
    305             .cloned()
    306             .collect::<Vec<_>>();
    307         let extra = actual_keys
    308             .difference(&expected_keys)
    309             .cloned()
    310             .collect::<Vec<_>>();
    311         return Err(format!(
    312             "packages ts npm_packages must match package matrix: missing {:?}, extra {:?}",
    313             missing, extra
    314         ));
    315     }
    316     for (key, expected_package) in expected {
    317         let actual = npm_packages
    318             .get(&key)
    319             .ok_or_else(|| format!("packages ts npm package {key} is missing"))?;
    320         validate_npm_package(&key, actual, &expected_package)?;
    321     }
    322     let expected_names = npm_packages
    323         .values()
    324         .map(|package| package.package.clone())
    325         .collect::<BTreeSet<_>>();
    326     if export_packages != &expected_names {
    327         return Err("exports ts package set must match TypeScript npm package matrix".to_owned());
    328     }
    329     Ok(())
    330 }
    331 
    332 fn expected_ts_npm_packages() -> BTreeMap<String, ExpectedNpmPackage> {
    333     let mut expected = BTreeMap::new();
    334     for spec in package_specs() {
    335         expected.insert(spec.key.to_owned(), expected_from_package_spec(*spec));
    336     }
    337     for spec in wasm_package_specs() {
    338         expected.insert(spec.key.to_owned(), expected_from_wasm_package_spec(*spec));
    339     }
    340     expected
    341 }
    342 
    343 fn expected_from_package_spec(spec: PackageSpec) -> ExpectedNpmPackage {
    344     ExpectedNpmPackage {
    345         kind: "bindings",
    346         crate_name: spec.crate_name,
    347         crate_dir: spec.crate_dir,
    348         package: spec.package_name,
    349         package_dir: spec.package_dir,
    350         out_name: None,
    351         out_dir: None,
    352     }
    353 }
    354 
    355 fn expected_from_wasm_package_spec(spec: WasmPackageSpec) -> ExpectedNpmPackage {
    356     ExpectedNpmPackage {
    357         kind: "wasm",
    358         crate_name: spec.crate_name,
    359         crate_dir: spec.crate_dir,
    360         package: spec.package_name,
    361         package_dir: spec.package_dir,
    362         out_name: Some(spec.out_name),
    363         out_dir: Some(spec.out_dir),
    364     }
    365 }
    366 
    367 fn validate_npm_package(
    368     key: &str,
    369     actual: &NpmPackageContract,
    370     expected: &ExpectedNpmPackage,
    371 ) -> Result<(), String> {
    372     validate_npm_field(key, "kind", &actual.kind, expected.kind)?;
    373     validate_npm_field(key, "crate_name", &actual.crate_name, expected.crate_name)?;
    374     validate_npm_field(key, "crate_dir", &actual.crate_dir, expected.crate_dir)?;
    375     validate_npm_field(key, "package", &actual.package, expected.package)?;
    376     validate_npm_field(
    377         key,
    378         "package_dir",
    379         &actual.package_dir,
    380         expected.package_dir,
    381     )?;
    382     validate_optional_npm_field(
    383         key,
    384         "out_name",
    385         actual.out_name.as_deref(),
    386         expected.out_name,
    387     )?;
    388     validate_optional_npm_field(key, "out_dir", actual.out_dir.as_deref(), expected.out_dir)?;
    389     Ok(())
    390 }
    391 
    392 fn validate_npm_field(key: &str, field: &str, actual: &str, expected: &str) -> Result<(), String> {
    393     validate_non_empty(actual, &format!("packages ts npm package {key} {field}"))?;
    394     if actual != expected {
    395         return Err(format!(
    396             "packages ts npm package {key} {field} must be {expected}"
    397         ));
    398     }
    399     Ok(())
    400 }
    401 
    402 fn validate_optional_npm_field(
    403     key: &str,
    404     field: &str,
    405     actual: Option<&str>,
    406     expected: Option<&str>,
    407 ) -> Result<(), String> {
    408     match (actual, expected) {
    409         (Some(actual), Some(expected)) => validate_npm_field(key, field, actual, expected),
    410         (None, None) => Ok(()),
    411         (Some(_), None) => Err(format!(
    412             "packages ts npm package {key} {field} must not be set"
    413         )),
    414         (None, Some(expected)) => Err(format!(
    415             "packages ts npm package {key} {field} must be {expected}"
    416         )),
    417     }
    418 }
    419 
    420 fn load_contract_dir<T>(dir: &Path) -> Result<Vec<T>, String>
    421 where
    422     T: for<'de> Deserialize<'de>,
    423 {
    424     let read_dir =
    425         fs::read_dir(dir).map_err(|error| format!("failed to read {}: {error}", dir.display()))?;
    426     let mut entries = read_dir
    427         .collect::<Result<Vec<_>, _>>()
    428         .map_err(|error| format!("failed to read {} entry: {error}", dir.display()))?;
    429     entries.sort_by_key(|entry| entry.file_name());
    430     let mut contracts = Vec::new();
    431     for entry in entries {
    432         let path = entry.path();
    433         if path.extension().and_then(|extension| extension.to_str()) != Some("toml") {
    434             continue;
    435         }
    436         contracts.push(parse_toml(&path)?);
    437     }
    438     Ok(contracts)
    439 }
    440 
    441 fn parse_toml<T>(path: &PathBuf) -> Result<T, String>
    442 where
    443     T: for<'de> Deserialize<'de>,
    444 {
    445     let raw = fs::read_to_string(path)
    446         .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
    447     toml::from_str(&raw).map_err(|error| format!("failed to parse {}: {error}", path.display()))
    448 }
    449 
    450 fn validate_language(language: &LanguageContract, family: &str) -> Result<(), String> {
    451     validate_non_empty(&language.id, &format!("{family} language.id"))?;
    452     validate_non_empty(
    453         &language.repository,
    454         &format!("{family} language.repository"),
    455     )
    456 }
    457 
    458 fn validate_runtime(
    459     networking: &str,
    460     signing: &str,
    461     deterministic_codec: &str,
    462     family: &str,
    463 ) -> Result<(), String> {
    464     validate_non_empty(networking, &format!("{family} networking"))?;
    465     validate_non_empty(signing, &format!("{family} signing"))?;
    466     validate_non_empty(
    467         deterministic_codec,
    468         &format!("{family} deterministic_codec"),
    469     )
    470 }
    471 
    472 fn validate_rollout(language: &str, rollout: &RolloutContract) -> Result<(), String> {
    473     validate_non_empty(&rollout.stage, "packages rollout.stage")?;
    474     if !matches!(rollout.stage.as_str(), "active" | "next" | "deferred") {
    475         return Err(format!("packages {language} rollout.stage is invalid"));
    476     }
    477     if rollout.order == 0 {
    478         return Err(format!(
    479             "packages {language} rollout.order must be greater than zero"
    480         ));
    481     }
    482     Ok(())
    483 }
    484 
    485 fn validate_non_empty(value: &str, field: &str) -> Result<(), String> {
    486     if value.trim().is_empty() || value.trim() != value {
    487         return Err(format!("{field} must be non-empty"));
    488     }
    489     Ok(())
    490 }
    491 
    492 fn validate_non_empty_map(map: &BTreeMap<String, String>, field: &str) -> Result<(), String> {
    493     if map.is_empty() {
    494         return Err(format!("{field} must not be empty"));
    495     }
    496     for (key, value) in map {
    497         validate_non_empty(key, field)?;
    498         validate_non_empty(value, field)?;
    499     }
    500     Ok(())
    501 }
    502 
    503 #[cfg(test)]
    504 mod tests {
    505     use std::{
    506         fs,
    507         time::{SystemTime, UNIX_EPOCH},
    508     };
    509 
    510     use super::validate_sdk_contracts;
    511 
    512     #[test]
    513     fn rejects_mismatched_language_sets() {
    514         let root = test_root("language_mismatch");
    515         write_contract(&root, "contracts/exports/ts.toml", EXPORT_TS);
    516         let error = validate_sdk_contracts(&root).expect_err("missing packages should fail");
    517         assert!(error.contains("contracts/packages"));
    518         let _ = fs::remove_dir_all(root);
    519     }
    520 
    521     #[test]
    522     fn rejects_package_export_mismatch() {
    523         let root = test_root("package_mismatch");
    524         write_contract(
    525             &root,
    526             "contracts/exports/ts.toml",
    527             EXPORT_TS
    528                 .replace("@radroots/core-bindings", "@radroots/other")
    529                 .as_str(),
    530         );
    531         write_contract(&root, "contracts/packages/ts.toml", PACKAGE_TS);
    532         let error = validate_sdk_contracts(&root).expect_err("mismatch should fail");
    533         assert!(error.contains("exports ts package set"));
    534         let _ = fs::remove_dir_all(root);
    535     }
    536 
    537     #[test]
    538     fn rejects_ts_package_matrix_drift() {
    539         let root = test_root("ts_matrix_drift");
    540         write_contract(&root, "contracts/exports/ts.toml", EXPORT_TS);
    541         write_contract(
    542             &root,
    543             "contracts/packages/ts.toml",
    544             PACKAGE_TS
    545                 .replace(
    546                     "package = \"@radroots/core-bindings\"",
    547                     "package = \"@radroots/other\"",
    548                 )
    549                 .as_str(),
    550         );
    551         let error = validate_sdk_contracts(&root).expect_err("mismatch should fail");
    552         assert!(error.contains("packages ts npm package core package"));
    553         let _ = fs::remove_dir_all(root);
    554     }
    555 
    556     fn test_root(name: &str) -> std::path::PathBuf {
    557         let stamp = SystemTime::now()
    558             .duration_since(UNIX_EPOCH)
    559             .expect("time")
    560             .as_nanos();
    561         std::env::temp_dir().join(format!("radroots_sdk_contracts_{name}_{stamp}"))
    562     }
    563 
    564     fn write_contract(root: &std::path::Path, relative: &str, contents: &str) {
    565         let path = root.join(relative);
    566         fs::create_dir_all(path.parent().expect("parent")).expect("create parent");
    567         fs::write(path, contents).expect("write contract");
    568     }
    569 
    570     const EXPORT_TS: &str = r#"[language]
    571 id = "ts"
    572 repository = "sdk-typescript"
    573 
    574 [packages]
    575 "radroots_core" = "@radroots/core-bindings"
    576 "radroots_event" = "@radroots/event-bindings"
    577 "radroots_identity" = "@radroots/identity-bindings"
    578 "radroots_replica_schema" = "@radroots/replica-schema-bindings"
    579 "radroots_trade" = "@radroots/trade-bindings"
    580 "radroots_event_codec_wasm" = "@radroots/event-codec-wasm"
    581 "radroots_replica_store_wasm" = "@radroots/replica-store-wasm"
    582 "radroots_replica_sync_wasm" = "@radroots/replica-sync-wasm"
    583 
    584 [artifacts]
    585 models_dir = "src/generated"
    586 constants_dir = "src/generated"
    587 wasm_dist_dir = "dist"
    588 manifest_file = "export-manifest.json"
    589 
    590 [runtime]
    591 networking = "native"
    592 signing = "native"
    593 deterministic_codec = "wasm"
    594 "#;
    595 
    596     const PACKAGE_TS: &str = r#"[language]
    597 id = "ts"
    598 repository = "sdk-typescript"
    599 
    600 [sdk]
    601 package_family = "@radroots"
    602 module_format = "esm"
    603 deterministic_codec = "wasm"
    604 signing = "native"
    605 networking = "native"
    606 
    607 [npm_packages.core]
    608 kind = "bindings"
    609 crate_name = "radroots_core_bindings"
    610 crate_dir = "crates/core_bindings"
    611 package = "@radroots/core-bindings"
    612 package_dir = "packages/core-bindings"
    613 
    614 [npm_packages.event]
    615 kind = "bindings"
    616 crate_name = "radroots_event_bindings"
    617 crate_dir = "crates/event_bindings"
    618 package = "@radroots/event-bindings"
    619 package_dir = "packages/event-bindings"
    620 
    621 [npm_packages.identity]
    622 kind = "bindings"
    623 crate_name = "radroots_identity_bindings"
    624 crate_dir = "crates/identity_bindings"
    625 package = "@radroots/identity-bindings"
    626 package_dir = "packages/identity-bindings"
    627 
    628 [npm_packages.replica_schema]
    629 kind = "bindings"
    630 crate_name = "radroots_replica_schema_bindings"
    631 crate_dir = "crates/replica_schema_bindings"
    632 package = "@radroots/replica-schema-bindings"
    633 package_dir = "packages/replica-schema-bindings"
    634 
    635 [npm_packages.trade]
    636 kind = "bindings"
    637 crate_name = "radroots_trade_bindings"
    638 crate_dir = "crates/trade_bindings"
    639 package = "@radroots/trade-bindings"
    640 package_dir = "packages/trade-bindings"
    641 
    642 [npm_packages.event_codec]
    643 kind = "wasm"
    644 crate_name = "radroots_event_codec_wasm"
    645 crate_dir = "crates/event_codec_wasm"
    646 package = "@radroots/event-codec-wasm"
    647 package_dir = "packages/event-codec-wasm"
    648 out_name = "radroots_event_codec_wasm"
    649 out_dir = "../../packages/event-codec-wasm/dist"
    650 
    651 [npm_packages.replica_store]
    652 kind = "wasm"
    653 crate_name = "radroots_replica_store_wasm"
    654 crate_dir = "crates/replica_store_wasm"
    655 package = "@radroots/replica-store-wasm"
    656 package_dir = "packages/replica-store-wasm"
    657 out_name = "radroots_replica_store_wasm"
    658 out_dir = "../../packages/replica-store-wasm/dist"
    659 
    660 [npm_packages.replica_sync]
    661 kind = "wasm"
    662 crate_name = "radroots_replica_sync_wasm"
    663 crate_dir = "crates/replica_sync_wasm"
    664 package = "@radroots/replica-sync-wasm"
    665 package_dir = "packages/replica-sync-wasm"
    666 out_name = "radroots_replica_sync_wasm"
    667 out_dir = "../../packages/replica-sync-wasm/dist"
    668 
    669 [rollout]
    670 stage = "active"
    671 order = 1
    672 
    673 [operations]
    674 "farm.build_draft" = "farm.buildDraft"
    675 
    676 [shared_types]
    677 "RadrootsNip01EventWireParts" = "RadrootsNip01EventWireParts"
    678 "RadrootsNip01EventWireDto" = "RadrootsNip01EventWireDto"
    679 "RadrootsEventDraft" = "RadrootsEventDraft"
    680 "RadrootsSignedEventDto" = "RadrootsSignedEventDto"
    681 "RadrootsVerifiedSignedEventDto" = "RadrootsVerifiedSignedEventDto"
    682 "RadrootsEventEnvelopeDto" = "RadrootsEventEnvelopeDto"
    683 
    684 [artifacts]
    685 models_dir = "src/generated"
    686 runtime_dir = "src/runtime"
    687 wasm_dist_dir = "dist"
    688 manifest_file = "export-manifest.json"
    689 "#;
    690 }