lib

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

commit f793ab1c7654f58a10a8f05e67fba58ccc4e8762
parent 31311d6b52a7bb7cee12708662ae170c19db0a39
Author: triesap <tyson@radroots.org>
Date:   Tue, 14 Jul 2026 18:56:47 +0000

contract: add public malformed corpus

Diffstat:
MCargo.lock | 1+
Mcontracts/conformance/vectors/event/nip01_wire.v1.json | 40++++++++++++++++++++++++++++++++++++++++
Acontracts/conformance/vectors/mesh/frame_cbor.v1.json | 47+++++++++++++++++++++++++++++++++++++++++++++++
Acontracts/conformance/vectors/replica_schema/json_models.v1.json | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontracts/conformance/vectors/transport/target_uri.v1.json | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/mesh/Cargo.toml | 3+++
Mcrates/mesh/tests/mesh.rs | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/replica_schema/tests/query_bind_values.rs | 52++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/transport/tests/transport.rs | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/architecture/embedded_transport_contract.md | 38++++++++++++++++++++++++++++++++++++++
Adocs/release/crates_io_policy.md | 41+++++++++++++++++++++++++++++++++++++++++
Adocs/release/public_validation_model.md | 50++++++++++++++++++++++++++++++++++++++++++++++++++
12 files changed, 578 insertions(+), 2 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -4384,6 +4384,7 @@ version = "0.1.0-alpha.2" dependencies = [ "radroots_transport", "serde", + "serde_json", ] [[package]] diff --git a/contracts/conformance/vectors/event/nip01_wire.v1.json b/contracts/conformance/vectors/event/nip01_wire.v1.json @@ -36,6 +36,46 @@ "expected": { "error": "event_id_mismatch" } + }, + { + "id": "nip01_wire_root_not_object_004", + "kind": "event.nip01_wire.invalid", + "input": { + "raw_json": "[]" + }, + "expected": { + "error": "root_not_object" + } + }, + { + "id": "nip01_wire_missing_content_005", + "kind": "event.nip01_wire.invalid", + "input": { + "raw_json": "{\"id\":\"2a15e33622a155ae231b28bebe390869e67a0e228f77ecfcd652b1ce180a9dde\",\"pubkey\":\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"created_at\":1700000001,\"kind\":0,\"tags\":[],\"sig\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\"}" + }, + "expected": { + "error": "missing_content" + } + }, + { + "id": "nip01_wire_empty_tag_006", + "kind": "event.nip01_wire.invalid", + "input": { + "raw_json": "{\"id\":\"2a15e33622a155ae231b28bebe390869e67a0e228f77ecfcd652b1ce180a9dde\",\"pubkey\":\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"created_at\":1700000001,\"kind\":0,\"tags\":[[]],\"content\":\"{}\",\"sig\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\"}" + }, + "expected": { + "error": "empty_tag" + } + }, + { + "id": "nip01_wire_noncanonical_pubkey_007", + "kind": "event.nip01_wire.invalid", + "input": { + "raw_json": "{\"id\":\"2a15e33622a155ae231b28bebe390869e67a0e228f77ecfcd652b1ce180a9dde\",\"pubkey\":\"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\",\"created_at\":1700000001,\"kind\":0,\"tags\":[],\"content\":\"{}\",\"sig\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\"}" + }, + "expected": { + "error": "noncanonical_pubkey" + } } ] } diff --git a/contracts/conformance/vectors/mesh/frame_cbor.v1.json b/contracts/conformance/vectors/mesh/frame_cbor.v1.json @@ -0,0 +1,47 @@ +{ + "suite": "mesh", + "contract_version": "0.1.0", + "vectors": [ + { + "id": "mesh_frame_cbor_default_001", + "kind": "mesh.frame_cbor.valid", + "input": { + "hex": "a70001010002656c6f63616c03696d6573736167652d3104182a0519ea6006a0" + }, + "expected": { + "message_id": "message-1", + "scope": "local" + } + }, + { + "id": "mesh_frame_cbor_unsupported_version_002", + "kind": "mesh.frame_cbor.invalid", + "input": { + "hex": "a70002010002656c6f63616c03696d6573736167652d3104182a0519ea6006a0" + }, + "expected": { + "error": "unsupported_version" + } + }, + { + "id": "mesh_frame_cbor_wrong_key_order_003", + "kind": "mesh.frame_cbor.invalid", + "input": { + "hex": "a70001020002656c6f63616c03696d6573736167652d3104182a0519ea6006a0" + }, + "expected": { + "error": "invalid_cbor" + } + }, + { + "id": "mesh_frame_cbor_payload_bytes_004", + "kind": "mesh.frame_cbor.invalid", + "input": { + "hex": "a70001010002656c6f63616c03696d6573736167652d3104182a0519ea600643010203" + }, + "expected": { + "error": "payload_transmission_forbidden" + } + } + ] +} diff --git a/contracts/conformance/vectors/replica_schema/json_models.v1.json b/contracts/conformance/vectors/replica_schema/json_models.v1.json @@ -0,0 +1,115 @@ +{ + "suite": "replica_schema", + "contract_version": "0.1.0", + "vectors": [ + { + "id": "replica_schema_farm_json_valid_001", + "kind": "replica_schema.farm_json.valid", + "input": { + "json": { + "id": "farm-alpha", + "created_at": "2026-07-14T00:00:00Z", + "updated_at": "2026-07-14T00:00:00Z", + "d_tag": "farm-alpha", + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Radroots Fixture Farm", + "about": null, + "website": null, + "picture": null, + "banner": null, + "location_primary": null, + "location_city": "Fixture City", + "location_region": "Fixture Region", + "location_country": "Fixture Country" + } + }, + "expected": { + "id": "farm-alpha" + } + }, + { + "id": "replica_schema_farm_json_missing_required_002", + "kind": "replica_schema.farm_json.invalid", + "input": { + "json": { + "id": "farm-alpha", + "created_at": "2026-07-14T00:00:00Z", + "updated_at": "2026-07-14T00:00:00Z", + "d_tag": "farm-alpha", + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111" + } + }, + "expected": { + "error": "missing_required_field" + } + }, + { + "id": "replica_schema_trade_product_json_valid_003", + "kind": "replica_schema.trade_product_json.valid", + "input": { + "json": { + "id": "trade-product-alpha", + "created_at": "2026-07-14T00:00:00Z", + "updated_at": "2026-07-14T00:00:00Z", + "key": "fixture-carrot", + "category": "produce", + "title": "Fixture Carrots", + "summary": "Synthetic fixture product", + "process": "fresh", + "lot": "lot-alpha", + "profile": "farm-alpha", + "year": 2026, + "qty_amt": 12.5, + "qty_amt_exact": "12.5", + "qty_unit": "kg", + "qty_label": null, + "qty_avail": 12, + "price_amt": 6.25, + "price_amt_exact": "6.25", + "price_currency": "USD", + "price_qty_amt": 1.0, + "price_qty_amt_exact": "1.0", + "price_qty_unit": "kg", + "listing_addr": null, + "primary_bin_id": null, + "verified_primary_bin_id": null, + "notes": null + } + }, + "expected": { + "id": "trade-product-alpha" + } + }, + { + "id": "replica_schema_trade_product_json_wrong_type_004", + "kind": "replica_schema.trade_product_json.invalid", + "input": { + "json": { + "id": "trade-product-alpha", + "created_at": "2026-07-14T00:00:00Z", + "updated_at": "2026-07-14T00:00:00Z", + "key": "fixture-carrot", + "category": "produce", + "title": "Fixture Carrots", + "summary": "Synthetic fixture product", + "process": "fresh", + "lot": "lot-alpha", + "profile": "farm-alpha", + "year": "2026", + "qty_amt": 12.5, + "qty_amt_exact": "12.5", + "qty_unit": "kg", + "price_amt": 6.25, + "price_amt_exact": "6.25", + "price_currency": "USD", + "price_qty_amt": 1.0, + "price_qty_amt_exact": "1.0", + "price_qty_unit": "kg" + } + }, + "expected": { + "error": "wrong_type" + } + } + ] +} diff --git a/contracts/conformance/vectors/transport/target_uri.v1.json b/contracts/conformance/vectors/transport/target_uri.v1.json @@ -0,0 +1,76 @@ +{ + "suite": "transport", + "contract_version": "0.1.0", + "vectors": [ + { + "id": "transport_target_uri_generic_canonical_001", + "kind": "transport.target_uri.valid", + "input": { + "uri": "MESH://Node.Example/path?q=1#frag" + }, + "expected": { + "canonical_uri": "mesh://node.example/path?q=1#frag" + } + }, + { + "id": "transport_target_uri_empty_002", + "kind": "transport.target_uri.invalid", + "input": { + "uri": " " + }, + "expected": { + "error": "empty_target_uri" + } + }, + { + "id": "transport_target_uri_whitespace_003", + "kind": "transport.target_uri.invalid", + "input": { + "uri": "bad target" + }, + "expected": { + "error": "invalid_target_uri" + } + }, + { + "id": "transport_target_uri_bad_scheme_004", + "kind": "transport.target_uri.invalid", + "input": { + "uri": "1bad:target" + }, + "expected": { + "error": "invalid_target_uri" + } + }, + { + "id": "transport_nostr_relay_canonical_005", + "kind": "transport.nostr_relay_target.valid", + "input": { + "uri": " WSS://Relay.Example/ " + }, + "expected": { + "canonical_uri": "wss://relay.example" + } + }, + { + "id": "transport_nostr_relay_query_006", + "kind": "transport.nostr_relay_target.invalid", + "input": { + "uri": "wss://relay.example?subscription=1" + }, + "expected": { + "error": "invalid_target_uri" + } + }, + { + "id": "transport_nostr_relay_nonlocal_ws_007", + "kind": "transport.nostr_relay_target.invalid", + "input": { + "uri": "ws://relay.example" + }, + "expected": { + "error": "invalid_target_uri" + } + } + ] +} diff --git a/crates/mesh/Cargo.toml b/crates/mesh/Cargo.toml @@ -19,5 +19,8 @@ serde = ["dep:serde"] radroots_transport = { workspace = true, default-features = false } serde = { workspace = true, optional = true } +[dev-dependencies] +serde_json = { workspace = true, features = ["std"] } + [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/mesh/tests/mesh.rs b/crates/mesh/tests/mesh.rs @@ -5,6 +5,7 @@ use radroots_mesh::{ RadrootsMeshPayloadPolicy, RadrootsMeshPolicyDenyReason, RadrootsMeshPrivacyClass, RadrootsMeshScope, decode_mesh_frame_cbor, encode_mesh_frame_cbor, }; +use serde_json::Value; fn default_frame() -> RadrootsMeshFrame { RadrootsMeshFrame::new( @@ -438,3 +439,68 @@ fn decoder_rejects_malformed_cbor_shapes() { RadrootsMeshError::InvalidCbor ); } + +#[test] +fn checked_in_mesh_cbor_vectors_match_decoder_behavior() { + let vectors = include_str!("../../../contracts/conformance/vectors/mesh/frame_cbor.v1.json"); + let document: Value = serde_json::from_str(vectors).expect("mesh cbor vector json"); + let entries = document + .get("vectors") + .and_then(Value::as_array) + .expect("mesh cbor vectors"); + + for entry in entries { + let kind = entry.get("kind").and_then(Value::as_str).expect("kind"); + let hex = entry + .get("input") + .and_then(|input| input.get("hex")) + .and_then(Value::as_str) + .expect("input hex"); + let bytes = decode_hex(hex); + match kind { + "mesh.frame_cbor.valid" => { + let frame = decode_mesh_frame_cbor(bytes.as_slice()).expect("mesh frame"); + let expected = entry.get("expected").expect("expected"); + assert_eq!( + frame.message_id, + expected + .get("message_id") + .and_then(Value::as_str) + .expect("message id") + ); + assert_eq!( + frame.scope_id.label(), + expected + .get("scope") + .and_then(Value::as_str) + .expect("scope") + ); + } + "mesh.frame_cbor.invalid" => { + assert!(decode_mesh_frame_cbor(bytes.as_slice()).is_err()); + } + other => panic!("unknown mesh cbor vector kind {other}"), + } + } +} + +fn decode_hex(value: &str) -> Vec<u8> { + assert_eq!(value.len() % 2, 0, "hex fixture length must be even"); + value + .as_bytes() + .chunks_exact(2) + .map(|chunk| { + let high = hex_nibble(chunk[0]); + let low = hex_nibble(chunk[1]); + (high << 4) | low + }) + .collect() +} + +fn hex_nibble(byte: u8) -> u8 { + match byte { + b'0'..=b'9' => byte - b'0', + b'a'..=b'f' => byte - b'a' + 10, + _ => panic!("hex fixture contains non-lowercase-hex byte"), + } +} diff --git a/crates/replica_schema/tests/query_bind_values.rs b/crates/replica_schema/tests/query_bind_values.rs @@ -1,4 +1,4 @@ -use radroots_replica_schema::farm::FarmQueryBindValues; +use radroots_replica_schema::farm::{Farm, FarmQueryBindValues}; use radroots_replica_schema::farm_gcs_location::FarmGcsLocationQueryBindValues; use radroots_replica_schema::farm_member::FarmMemberQueryBindValues; use radroots_replica_schema::farm_member_claim::FarmMemberClaimQueryBindValues; @@ -15,7 +15,7 @@ use radroots_replica_schema::plot_tag::PlotTagQueryBindValues; use radroots_replica_schema::result::{ ReplicaSchemaError, ReplicaSchemaResult, ReplicaSchemaResultList, ReplicaSchemaResultPass, }; -use radroots_replica_schema::trade_product::TradeProductQueryBindValues; +use radroots_replica_schema::trade_product::{TradeProduct, TradeProductQueryBindValues}; use serde_json::Value; macro_rules! assert_query_bind_values { @@ -386,3 +386,51 @@ fn schema_result_wrappers_cover_constructors_and_status_labels() { assert_eq!(ReplicaSchemaResultPass::new(true).status_label(), "pass"); assert_eq!(ReplicaSchemaResultPass::new(false).status_label(), "fail"); } + +#[test] +fn checked_in_replica_schema_json_vectors_match_model_behavior() { + let vectors = + include_str!("../../../contracts/conformance/vectors/replica_schema/json_models.v1.json"); + let document: Value = serde_json::from_str(vectors).expect("replica schema vector json"); + let entries = document + .get("vectors") + .and_then(Value::as_array) + .expect("replica schema vectors"); + + for entry in entries { + let kind = entry.get("kind").and_then(Value::as_str).expect("kind"); + let input_json = entry + .get("input") + .and_then(|input| input.get("json")) + .expect("input json") + .clone(); + let expected = entry.get("expected").expect("expected"); + match kind { + "replica_schema.farm_json.valid" => { + let farm: Farm = serde_json::from_value(input_json).expect("farm json"); + assert_eq!( + farm.id, + expected.get("id").and_then(Value::as_str).expect("farm id") + ); + } + "replica_schema.farm_json.invalid" => { + assert!(serde_json::from_value::<Farm>(input_json).is_err()); + } + "replica_schema.trade_product_json.valid" => { + let product: TradeProduct = + serde_json::from_value(input_json).expect("trade product json"); + assert_eq!( + product.id, + expected + .get("id") + .and_then(Value::as_str) + .expect("trade product id") + ); + } + "replica_schema.trade_product_json.invalid" => { + assert!(serde_json::from_value::<TradeProduct>(input_json).is_err()); + } + other => panic!("unknown replica schema vector kind {other}"), + } + } +} diff --git a/crates/transport/tests/transport.rs b/crates/transport/tests/transport.rs @@ -10,6 +10,7 @@ use radroots_transport::{ RadrootsTransportTargetFingerprint, RadrootsTransportTargetLabel, RadrootsTransportTargetReceipt, RadrootsTransportTargetSet, RadrootsTransportTargetUri, }; +use serde_json::Value; fn opaque_payload() -> RadrootsTransportPayload { RadrootsTransportPayload::opaque_bytes("transport-test-payload", b"transport payload") @@ -596,6 +597,56 @@ fn transport_kind_and_target_parsers_cover_negative_edges() { } #[test] +fn checked_in_transport_target_uri_vectors_match_parser_behavior() { + let vectors = + include_str!("../../../contracts/conformance/vectors/transport/target_uri.v1.json"); + let document: Value = serde_json::from_str(vectors).expect("transport target vector json"); + let entries = document + .get("vectors") + .and_then(Value::as_array) + .expect("transport target vectors"); + + for entry in entries { + let kind = entry.get("kind").and_then(Value::as_str).expect("kind"); + let raw_uri = entry + .get("input") + .and_then(|input| input.get("uri")) + .and_then(Value::as_str) + .expect("input uri"); + let expected = entry.get("expected").expect("expected"); + match kind { + "transport.target_uri.valid" => { + let target = RadrootsTransportTargetUri::parse(raw_uri).expect("target URI"); + assert_eq!( + target.as_str(), + expected + .get("canonical_uri") + .and_then(Value::as_str) + .expect("canonical uri") + ); + } + "transport.target_uri.invalid" => { + assert!(RadrootsTransportTargetUri::parse(raw_uri).is_err()); + } + "transport.nostr_relay_target.valid" => { + let target = RadrootsTransportTarget::nostr_relay(raw_uri).expect("relay target"); + assert_eq!( + target.uri.as_str(), + expected + .get("canonical_uri") + .and_then(Value::as_str) + .expect("canonical uri") + ); + } + "transport.nostr_relay_target.invalid" => { + assert!(RadrootsTransportTarget::nostr_relay(raw_uri).is_err()); + } + other => panic!("unknown transport target vector kind {other}"), + } + } +} + +#[test] fn reticulum_transport_targets_require_exact_preview_endpoint() { let target = RadrootsTransportTarget::reticulum_preview().expect("exact Reticulum preview endpoint"); diff --git a/docs/architecture/embedded_transport_contract.md b/docs/architecture/embedded_transport_contract.md @@ -0,0 +1,38 @@ +# Embedded Transport Contract + +Radroots public transport code separates transport-neutral delivery contracts from concrete +transport adapters. Public APIs must describe targets, payloads, delivery status, and satisfaction +policy without smuggling a relay-only model into generic transport surfaces. + +## Contract boundaries + +`radroots_transport` owns transport-neutral value types: + +- `RadrootsTransportKind` +- `RadrootsTransportTarget` +- `RadrootsTransportTargetUri` +- `RadrootsTransportTargetSet` +- `RadrootsTransportPayload` +- `RadrootsTransportSatisfactionPolicy` +- `RadrootsTransportDeliveryReceipt` + +Nostr-specific behavior belongs in Nostr-owned crates and NIP-specific modules. Mesh behavior +belongs in mesh-owned crates. Generic transport code may name Nostr only when it is modeling an +explicit Nostr target or a Nostr adapter boundary. + +## Embedded and preview transports + +The Reticulum preview endpoint is intentionally explicit. The only accepted preview endpoint is the +checked-in `RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI` value, and preview delivery remains unavailable +for real payload transfer until a concrete transport implementation is added. + +Mesh frame CBOR is a source-level contract for local and embedded transport experimentation. The +MVP frame shape is fixed by `radroots_mesh` tests and conformance vectors, and real payload bytes +are rejected by the preview policy. + +## Malformed input posture + +Transport parsers are part of the release validation surface. Malformed transport target URIs, +unsupported mesh CBOR shapes, payload-bearing preview frames, and invalid replica JSON models must +fail through checked-in tests and conformance vectors. Runtime callers should depend on those +fallible constructors and parser results instead of accepting unchecked target strings or raw bytes. diff --git a/docs/release/crates_io_policy.md b/docs/release/crates_io_policy.md @@ -0,0 +1,41 @@ +# Crates.io Policy + +Public crate publication must use registry-resolved public dependencies. Release preflight must +fail closed while required DTO tooling is unavailable from crates.io. + +## Publishable crate boundary + +Crates that declare `publish = ["crates-io"]` are candidates for public registry publication after +their source, contracts, tests, and dependency graph pass the repo-local release preflight. + +Crates that declare `publish = false` are public source crates but are not candidates for registry +publication from this repo-local policy surface. + +## DTO tooling gate + +The DTO crates `dto_bindgen` and `dto_bindgen_core` are currently required by public crate surfaces +that expose generated DTO metadata. Until the required DTO crates are available from crates.io, +`cargo xtask release preflight` must report that registry availability as a publication blocker. + +That blocker must not be bypassed by: + +- git dependencies +- local path dependencies +- vendored DTO copies +- retired-name reexports +- publication-only source rewrites +- release-only feature gates + +The correct state is an explicit preflight failure that names the missing registry dependency. + +## Local validation + +Run the release gate from the nested repo root: + +```bash +cargo xtask release preflight +``` + +If the command fails only because DTO tooling is unavailable from crates.io, record that exact +blocker in closeout evidence. Any additional failure must be treated as a separate source or +contract issue and fixed directly. diff --git a/docs/release/public_validation_model.md b/docs/release/public_validation_model.md @@ -0,0 +1,50 @@ +# Public Validation Model + +This repository treats public source validation as a fail-closed contract. A change is ready for +public release consideration only when source formatting, crate tests, contract validation, +conformance vectors, and release preflight all agree with the checked-in contracts. + +## Validation surfaces + +The public validation surface is source-controlled in this repo: + +- `Cargo.toml` defines the workspace members and crate dependency posture. +- `contracts/**` defines operation metadata, conformance vectors, event contracts, and release + contract inputs. +- `crates/**/tests/**` exercises crate-local public APIs and malformed-input behavior. +- `tools/xtask/**` owns repo-local contract validation and release preflight commands. + +The standard source validation entrypoints are: + +```bash +cargo fmt --all -- --check +cargo test --workspace --all-features +cargo xtask contract validate +cargo xtask release preflight +``` + +Focused hardening slices may run narrower crate commands first, but final release consideration +must return to the workspace and xtask validation surface. + +## Conformance vectors + +Stable public vectors live under `contracts/conformance/vectors/**`. These files are test inputs, +not generated output. They are deliberately small and inspectable so malformed input expectations +remain reviewable in source control. + +The malformed corpus currently covers: + +- event wire JSON in `contracts/conformance/vectors/event/nip01_wire.v1.json` +- transport target URI parsing in `contracts/conformance/vectors/transport/target_uri.v1.json` +- mesh frame CBOR in `contracts/conformance/vectors/mesh/frame_cbor.v1.json` +- replica schema JSON in `contracts/conformance/vectors/replica_schema/json_models.v1.json` + +Each corpus has at least one canonical valid case and malformed cases that must fail through the +same public parser or model type used by downstream consumers. + +## Failure posture + +Validation failures are release blockers. The expected DTO crates.io blocker documented in +`docs/release/crates_io_policy.md` is still a blocker; it is recorded distinctly so release tooling +does not silently substitute git dependencies, local path dependencies, vendored copies, +retired-name reexports, or publication-only workarounds.