lib

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

commit 828148e2a15582972063020f37c8962db795537c
parent 61702057c0837146fc6dc1c1b6d62e8dd25695e9
Author: triesap <tyson@radroots.org>
Date:   Thu, 30 Jul 2026 23:03:13 +0000

transport: migrate workspace consumers

- Move first-party consumers to canonical transport identities, targets, and split SPIs.
- Adapt Nostr and Reticulum implementations to final sink and source contracts.
- Preserve governed manifests and runtime registries across the breaking cutover.
- Add boundary coverage and refresh exact package metadata and generated authority.

Diffstat:
MCargo.toml | 2+-
Mcrates/event_store/contracts/source_maintenance_v1.manifest.json | 22+++++++++++-----------
Mcrates/event_store/contracts/source_maintenance_v1.manifest.sha256 | 2+-
Mcrates/event_store/src/generated/source_maintenance_manifest.rs | 4++--
Mcrates/event_store/src/model.rs | 42+++++++++++++++++++-----------------------
Mcrates/event_store/src/store.rs | 62+++++++++++++++++++++++++++++---------------------------------
Mcrates/event_store/src/store/post_core_storage_v1.rs | 4++--
Mcrates/mesh/src/model.rs | 12++++++------
Mcrates/mesh_agent_client/src/lib.rs | 15++++++++-------
Mcrates/mesh_agent_client/tests/client.rs | 7++-----
Mcrates/outbox/src/model.rs | 19+++++++++----------
Mcrates/outbox/src/store.rs | 119+++++++++++++++++++++++++++++++++++++------------------------------------------
Mcrates/runtime/src/transport.rs | 297+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mcrates/transport/tests/source_boundary.rs | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mcrates/transport_nostr/src/fetch.rs | 6+++---
Mcrates/transport_nostr/src/outbox.rs | 240+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mcrates/transport_nostr/src/outcome.rs | 31+++++++++++++++++++++++++++++++
Mcrates/transport_nostr/src/publish.rs | 140+++++++++++++++++++++++++++++++++----------------------------------------------
Mcrates/transport_nostr/src/relay.rs | 4++--
Mcrates/transport_nostr/tests/transport.rs | 577++++++++++++++++++++++++++++---------------------------------------------------
Mcrates/transport_publish_protocol/src/lib.rs | 95+++++++++++++++++++++++++++++++++----------------------------------------------
Mcrates/transport_publish_protocol/tests/successor_equivalence.rs | 6+++---
Mcrates/transport_reticulum/src/contract.rs | 39+++++++++++++++++++--------------------
Mcrates/transport_reticulum/src/lib.rs | 168+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mcrates/transport_reticulum/tests/reticulum.rs | 121+++++++++++++++++++++++++++++++++----------------------------------------------
Mtools/xtask/src/contract/nip09_reconciliation.rs | 10++++++++--
Mtools/xtask/src/contract/source_maintenance.rs | 4++--
27 files changed, 1105 insertions(+), 1048 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -68,7 +68,7 @@ rust-version = "1.97.1" license = "MIT OR Apache-2.0" repository = "https://github.com/radrootslabs/lib" homepage = "https://radroots.org" -readme = "README" +readme = "README.md" authors = ["Tyson Lupul <tyson@radroots.org>"] [workspace.metadata.radroots.public-package] diff --git a/crates/event_store/contracts/source_maintenance_v1.manifest.json b/crates/event_store/contracts/source_maintenance_v1.manifest.json @@ -152,8 +152,8 @@ { "role": "workspace_dependency_authority", "path": "Cargo.toml", - "byte_length": 12411, - "sha256": "cad96f080b0cc9ac7ffda703c8807540c9cf89f47a2299b141e6e7949714bf9f", + "byte_length": 12414, + "sha256": "895bf9d57a1cb9a89c0f70232cb628106897b421561ae0b9e9ec57577149b273", "hash_algorithm": "sha256_bytes_v1" }, { @@ -712,8 +712,8 @@ { "role": "predecessor_model_public_surface", "path": "crates/event_store/src/model.rs", - "byte_length": 32314, - "sha256": "d7ffa47719783c9f08cb4e00fbe47123cf0310ead06f77d3ad1d81edfda96b2c", + "byte_length": 32050, + "sha256": "910053012eb65cb1a6c24455ffacee056ce013c950e9b545822f9c2e679a8892", "hash_algorithm": "sha256_bytes_v1" }, { @@ -775,8 +775,8 @@ { "role": "public_store_and_transaction_authority", "path": "crates/event_store/src/store.rs", - "byte_length": 393535, - "sha256": "c3286b50a2e9e17a330938492cfded0829dc19516f55aa4f62289d513457bad7", + "byte_length": 393195, + "sha256": "03fc56bf151f53c35792d09b247513d722ae26e3d112810f28f167a004c83273", "hash_algorithm": "sha256_bytes_v1" }, { @@ -810,8 +810,8 @@ { "role": "predecessor_post_core_v1_storage", "path": "crates/event_store/src/store/post_core_storage_v1.rs", - "byte_length": 16558, - "sha256": "d88c1f7ff5cc0a19221ad893eb840d55c11535f13adf84ffb3076cbc18d4abf1", + "byte_length": 16538, + "sha256": "fcd6546c23a6cba12b70d92728d19b6ee5d43174b83495f820348b77efa33aab", "hash_algorithm": "sha256_bytes_v1" }, { @@ -859,15 +859,15 @@ { "role": "transitive_predecessor_membership_governance", "path": "tools/xtask/src/contract/nip09_reconciliation.rs", - "byte_length": 842286, - "sha256": "678e4290f95db6a4eca19c82550f9db4c870e97f521dac3656f1a91c68db8104", + "byte_length": 842402, + "sha256": "4abb141771cfa257440dba7ebeae21f5ad1f9856cc0d5f6437ba68614d997810", "hash_algorithm": "sha256_bytes_v1" }, { "role": "source_maintenance_governance", "path": "tools/xtask/src/contract/source_maintenance.rs", "byte_length": 199270, - "sha256": "a6685c27255918769f894ab098ba13a927c0d70c423fbd72b5404c0af0ae2f89", + "sha256": "d3681d09e60763baf3c59edfbcb48e3c370c315a75fcd5a01756c8c961e784c9", "hash_algorithm": "sha256_bytes_v1" }, { diff --git a/crates/event_store/contracts/source_maintenance_v1.manifest.sha256 b/crates/event_store/contracts/source_maintenance_v1.manifest.sha256 @@ -1 +1 @@ -b4416b1eab44c6e7110ffed89eb67b8963059538e7775a1347067d1f7c7f3999 +51205a1c1c7c3b91d79354ea5deabefc0f42d83423d4a7da3a0e6658f34863b5 diff --git a/crates/event_store/src/generated/source_maintenance_manifest.rs b/crates/event_store/src/generated/source_maintenance_manifest.rs @@ -1,8 +1,8 @@ // @generated by `cargo xtask contract source-maintenance-manifest --write`; do not edit. -pub(crate) const SOURCE_MAINTENANCE_MANIFEST_JSON: &str = "{\n \"schema_version\": 1,\n \"contract_id\": \"radroots_event_store.source_maintenance_v1\",\n \"hook_id\": \"source_maintenance_v1\",\n \"manifest_schema\": {\n \"path\": \"crates/event_store/contracts/source_maintenance_v1.manifest.schema.json\",\n \"byte_length\": 12315,\n \"sha256\": \"96ea98a5abf3348b61565954cd34f2c622411d8dd34205ef04134bbe0e3d3f2b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"predecessor\": {\n \"hook_id\": \"food_availability_projection_v1\",\n \"manifest\": {\n \"path\": \"crates/event_store/contracts/food_availability_projection_v1.manifest.json\",\n \"byte_length\": 17455,\n \"sha256\": \"02dfe1b450fbdac16e718888215b4dd5c85d8975440fa21e8f439fb24c2b2990\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n },\n \"migration\": {\n \"version\": 4,\n \"name\": \"source_maintenance\",\n \"up\": {\n \"path\": \"crates/event_store/migrations/0004_source_maintenance.up.sql\",\n \"byte_length\": 19841,\n \"sha256\": \"ab2724188f8d08c897eebea2533a635e7c74282a25e84e4c0c37e78b08837a43\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"down\": {\n \"path\": \"crates/event_store/migrations/0004_source_maintenance.down.sql\",\n \"byte_length\": 5172,\n \"sha256\": \"fe44fd53c51545c08ea479b385e6781079dab70fc63da2a3c205d727a00ce860\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"schema_sha256\": \"074f85b663444ac150239ecd8441ea4a96ad83a798a55e22d2e5e2f7ee943a8c\",\n \"catalog\": {\n \"objects\": [\n \"radroots_event_store_source_capacity_delete_guard\",\n \"radroots_event_store_source_capacity_insert_guard\",\n \"radroots_event_store_source_capacity_marker_close_guard\",\n \"radroots_event_store_source_capacity_update_guard\",\n \"radroots_event_store_source_capacity_v1\",\n \"radroots_event_store_source_generation_capacity_advance\",\n \"radroots_event_store_source_generation_capacity_guard\"\n ],\n \"replaced_objects\": [\n \"radroots_event_store_food_availability_image_delete_guard\",\n \"radroots_event_store_food_availability_projection_delete_guard\",\n \"radroots_event_store_source_rebuild_marker_insert_guard\"\n ],\n \"tables\": [\n \"radroots_event_store_source_capacity_v1\"\n ],\n \"fts5_tables\": []\n }\n },\n \"source_maintenance\": {\n \"version\": 1,\n \"event_contract_registry_version\": 7,\n \"capacity_authority_id\": \"radroots_event_store_source_capacity_v1\",\n \"accounting\": {\n \"algorithm\": \"sqlite_cast_blob_octet_sum_v1\",\n \"raw_event_columns\": [\n \"event_id\",\n \"pubkey\",\n \"tags_json\",\n \"content\",\n \"sig\",\n \"raw_json\"\n ],\n \"raw_tag_columns\": [\n \"event_id\",\n \"tag_name\",\n \"tag_value\",\n \"tag_json\"\n ],\n \"nullable_raw_tag_columns\": [\n \"tag_value\"\n ]\n },\n \"limits\": {\n \"raw_events\": 25000,\n \"raw_tags\": 250000,\n \"raw_event_text_bytes\": 67108864,\n \"raw_tag_text_bytes\": 33554432,\n \"retained_source_generations\": 8\n },\n \"reopen_validation\": {\n \"mode\": \"bounded_full_raw_recount_v1\",\n \"raw_event_rejection_scan_bound\": 25001,\n \"raw_tag_rejection_scan_bound\": 250001,\n \"generation_history_validation\": \"bounded_count_plus_active_ordinal_v1\",\n \"retained_generation_rejection_scan_bound\": 9\n },\n \"rebuild_seal\": {\n \"nip09_hook_id\": \"nip09_reconciliation_v1\",\n \"nip09_manifest_sha256\": \"74af832420ffbaa9805e89df3c0b34f126a443e1598f757e3372f407f9003b77\",\n \"food_hook_id\": \"food_availability_projection_v1\",\n \"food_manifest_sha256\": \"02dfe1b450fbdac16e718888215b4dd5c85d8975440fa21e8f439fb24c2b2990\",\n \"food_scope_fingerprint_sha256\": \"8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0\",\n \"active_generation_authority\": \"radroots_event_store_source_state\",\n \"marker_close_authority\": \"radroots_event_store_source_capacity_marker_close_guard\"\n }\n },\n \"entry_points\": [\n {\n \"role\": \"migration_registry\",\n \"rust_path\": \"radroots_event_store::migrations::EVENT_STORE_MIGRATIONS[3]\"\n },\n {\n \"role\": \"migration_apply_hook\",\n \"rust_path\": \"radroots_event_store::schema::apply_migration_hook\"\n },\n {\n \"role\": \"migration_validation_hook\",\n \"rust_path\": \"radroots_event_store::schema::validate_migration_hook_state\"\n },\n {\n \"role\": \"capacity_query\",\n \"rust_path\": \"radroots_event_store::RadrootsEventStore::source_capacity_v1\"\n },\n {\n \"role\": \"raw_append_preflight\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::preflight_unique_raw_source_append_v1\"\n },\n {\n \"role\": \"raw_append_advance\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::advance_source_capacity_after_insert_v1\"\n },\n {\n \"role\": \"generation_append_preflight\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::preflight_source_generation_append_v1\"\n },\n {\n \"role\": \"generation_rebuild_bind\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::bind_source_capacity_to_generation_v1\"\n },\n {\n \"role\": \"sqlite_encoding_preflight\",\n \"rust_path\": \"radroots_event_store::store::validate_main_database_encoding\"\n },\n {\n \"role\": \"source_generation_history_rollback_guard\",\n \"rust_path\": \"radroots_event_store::schema::validate_rollback_preserves_source_generation_history\"\n },\n {\n \"role\": \"result_vector_executor\",\n \"rust_path\": \"source_maintenance_v1_result_vector\"\n }\n ],\n \"source_files\": [\n {\n \"role\": \"workspace_dependency_authority\",\n \"path\": \"Cargo.toml\",\n \"byte_length\": 12411,\n \"sha256\": \"cad96f080b0cc9ac7ffda703c8807540c9cf89f47a2299b141e6e7949714bf9f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_currency_value_authority\",\n \"path\": \"crates/core/src/currency.rs\",\n \"byte_length\": 4142,\n \"sha256\": \"42e6da2d8d2fdd6955dc2d83e98dd00266e02bc910b69923b117662c05089d14\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_decimal_value_authority\",\n \"path\": \"crates/core/src/decimal.rs\",\n \"byte_length\": 7839,\n \"sha256\": \"4b1d681a92d7a9e074bee7e1f20eb1499bb05bfc6528a6c9ba69fa7d00416550\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_money_value_authority\",\n \"path\": \"crates/core/src/money.rs\",\n \"byte_length\": 8025,\n \"sha256\": \"3f30bc21e21951a62fdc5d4033736ed42a883df590e8403940775bd2edfeebc5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_percent_value_authority\",\n \"path\": \"crates/core/src/percent.rs\",\n \"byte_length\": 2652,\n \"sha256\": \"42ceab109881329f3539d06a0ee0381ab4359985fe185a18d58a9c9558c5eb7f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_quantity_value_authority\",\n \"path\": \"crates/core/src/quantity.rs\",\n \"byte_length\": 6785,\n \"sha256\": \"d14e619da3829cdaf3ca7385fb892d330c286c1e21455c135c37518cbb588b5f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_quantity_price_value_authority\",\n \"path\": \"crates/core/src/quantity_price.rs\",\n \"byte_length\": 7061,\n \"sha256\": \"d60f70377099470ed283f45868a5119d7bb46143cb250b5082a4c038ba7f6cdc\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_unit_value_authority\",\n \"path\": \"crates/core/src/unit.rs\",\n \"byte_length\": 10023,\n \"sha256\": \"550e9582b6a5290aac3f637b83853b21fefcbab92c432d067c83448f4b2fe3e9\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_public_surface\",\n \"path\": \"crates/blossom/src/lib.rs\",\n \"byte_length\": 510,\n \"sha256\": \"a4dfcbd193457c50c1b0fbfc91bb547981ecd3a6e23cfc6f1030edfb511dc58c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_authorization_authority\",\n \"path\": \"crates/blossom/src/authorization.rs\",\n \"byte_length\": 39360,\n \"sha256\": \"461947b30516315a342b3b5697599f701d27050888f29f67f5d7a7ad4afa4c28\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_descriptor_authority\",\n \"path\": \"crates/blossom/src/descriptor.rs\",\n \"byte_length\": 13702,\n \"sha256\": \"b2a4ffa760256e1316f70e012d200e2b5f4afd8ede771b85a5147f96b247f599\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_error_authority\",\n \"path\": \"crates/blossom/src/error.rs\",\n \"byte_length\": 17229,\n \"sha256\": \"02af55beacf437040be17d1fc3271d6e2a62915f656ec2dbe06247deef83f95a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_hash_authority\",\n \"path\": \"crates/blossom/src/hash.rs\",\n \"byte_length\": 11006,\n \"sha256\": \"45eca0d81ac0f46c305a32f81aafcdd90b14679d0614f72862289a6e5d4cb08f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_media_type_authority\",\n \"path\": \"crates/blossom/src/media_type.rs\",\n \"byte_length\": 2368,\n \"sha256\": \"69db5c0f9fcdd7dbe1f22daa499f65603ae4ce198083dab2c73a36813fa18a46\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_url_authority\",\n \"path\": \"crates/blossom/src/url.rs\",\n \"byte_length\": 14580,\n \"sha256\": \"342c995fcf620e5fb32461a7d8276f5668ba2662890bd32d12780cc9121c6451\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_public_surface\",\n \"path\": \"crates/event/src/lib.rs\",\n \"byte_length\": 1892,\n \"sha256\": \"5d609b963a9b8db18ef96a10f394617413734d2631774d3296e6d8fc029cb02c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_contract_facade\",\n \"path\": \"crates/event/src/contract.rs\",\n \"byte_length\": 208,\n \"sha256\": \"27466bf36461071931f391c60646a13f7781d19584583166cc54c0eece76d010\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_contract_registry_v7_authority\",\n \"path\": \"crates/event/src/contract/registry_v7.rs\",\n \"byte_length\": 145805,\n \"sha256\": \"0a62603f6fc05dc9f758561cf7da258c676ada810ceaeca7fb151364a5c83d62\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_envelope_authority\",\n \"path\": \"crates/event/src/envelope.rs\",\n \"byte_length\": 29310,\n \"sha256\": \"29f8d8c4b17c01cb5a8c7f59e4ce52e134180e0e6f37d265063d278b8f2ff26e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_verification_typestate_authority\",\n \"path\": \"crates/event/src/verification.rs\",\n \"byte_length\": 9400,\n \"sha256\": \"417acb2ce670d266b1fa4fcafd6b48a910218deab38f57fb8b942056db76f94d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_admission_typestate_authority\",\n \"path\": \"crates/event/src/admission.rs\",\n \"byte_length\": 7052,\n \"sha256\": \"62bd2ceb0f434946fdfc2b81f4efb13738676a29a6936c23a427e43aadf2d9cb\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_head_facade\",\n \"path\": \"crates/event/src/event_head.rs\",\n \"byte_length\": 97,\n \"sha256\": \"f761ff3e74c4f5e1e28381db00ce698c633ff048669b22984d14a587482e8e83\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_head_v1_authority\",\n \"path\": \"crates/event/src/event_head/v1.rs\",\n \"byte_length\": 6849,\n \"sha256\": \"9f4144d8d240023cf493ce0b538ade4c16dc44204d6b5f2797152caf6f9e7dba\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_ids_authority\",\n \"path\": \"crates/event/src/id.rs\",\n \"byte_length\": 48381,\n \"sha256\": \"a08fe3873815453a03318a706e2fa98a8a6728ec69d24368a87ec60025566fe0\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_trade_authority\",\n \"path\": \"crates/event/src/trade.rs\",\n \"byte_length\": 65549,\n \"sha256\": \"f818a7287ed82de3fb9ef1d973f0fb18d84b30eb4827ddc576ee502dd980145f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_kinds_authority\",\n \"path\": \"crates/event/src/kinds.rs\",\n \"byte_length\": 34961,\n \"sha256\": \"8b3ce6193cab1f7e1587d0c1b2880a81b0aa43c77d89671b602772db37edcebf\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_tags_authority\",\n \"path\": \"crates/event/src/tags.rs\",\n \"byte_length\": 4530,\n \"sha256\": \"cb8f12a639fc72d9238e32495d9e67e928e6627fb8c94ec0a58c5d95e7373cd6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_draft_authority\",\n \"path\": \"crates/event/src/draft.rs\",\n \"byte_length\": 51150,\n \"sha256\": \"ecc619aae28d688d13f35467398f17766b60f26c2bf3616d772ce65b7fa4fb53\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_calendar_authority\",\n \"path\": \"crates/event/src/calendar.rs\",\n \"byte_length\": 97992,\n \"sha256\": \"73e79d61ee175cb46e6123e1d28239252ddf0ad094dc1113bdb1286b63b88d57\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_classified_listing_authority\",\n \"path\": \"crates/event/src/classified_listing.rs\",\n \"byte_length\": 7956,\n \"sha256\": \"ac58484b87c7712c81f50f884d1c0dd60938571aa5f6f05d555191f529a4c2b0\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_profile_authority\",\n \"path\": \"crates/event/src/profile.rs\",\n \"byte_length\": 17011,\n \"sha256\": \"572bfe3b6f1711d109db445077f0a8453cad3f021cc0638f79201b618e16b978\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_post_authority\",\n \"path\": \"crates/event/src/post.rs\",\n \"byte_length\": 19968,\n \"sha256\": \"a822cd51eff9cbaa39db998cc5c90d117f7a983f0fe2d42d42118cb00e95982c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_comment_authority\",\n \"path\": \"crates/event/src/comment.rs\",\n \"byte_length\": 34745,\n \"sha256\": \"24b7fda13229f21cee5cef2a9941e3aaafdbc486b19ed42731b0b6ee37c28703\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_food_availability_authority\",\n \"path\": \"crates/event/src/food_availability.rs\",\n \"byte_length\": 42829,\n \"sha256\": \"52472c07eb0855d884c60755dc3a3e1a963793451d707e51f769f2d78384f318\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_deletion_authority\",\n \"path\": \"crates/event/src/deletion.rs\",\n \"byte_length\": 30022,\n \"sha256\": \"8c3f2e32407520a8e8242c1293af32613c648fb76218b8003a18929ac0b3811f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_dto_authority\",\n \"path\": \"crates/event/src/dto.rs\",\n \"byte_length\": 5038,\n \"sha256\": \"9e228c95ff6ff33f99441d8682781fd0a1c4d3f1db24284a43985dcbac3ba136\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_farm_crdt_authority\",\n \"path\": \"crates/event/src/farm_crdt.rs\",\n \"byte_length\": 21118,\n \"sha256\": \"cec265ac9c42b59a14be4eb96924d26a83477c27291f667f23e6e47a64b6b2c7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_knowledge_authority\",\n \"path\": \"crates/event/src/knowledge.rs\",\n \"byte_length\": 54624,\n \"sha256\": \"3d55ff88a4e30d5d605f9ae4b2e96b9df6978d6c14e52f9aeb0b8039cfc200d7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_operational_listing_authority\",\n \"path\": \"crates/event/src/operational_listing.rs\",\n \"byte_length\": 10307,\n \"sha256\": \"52e710db816c89c8d4b87c86541987f7447f2f7091c394c68d9cb98a27f90159\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_order_authority\",\n \"path\": \"crates/event/src/order.rs\",\n \"byte_length\": 60218,\n \"sha256\": \"4203fcc469612cac5c705884564e140b9fb076cd57079e22ed85275a8b936e50\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_reply_authority\",\n \"path\": \"crates/event/src/reply.rs\",\n \"byte_length\": 15581,\n \"sha256\": \"b0b83a1e96125c0892d932e9314519242a715fa8f46334aff765ffd248c91d9a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_trade_validation_authority\",\n \"path\": \"crates/event/src/trade_validation.rs\",\n \"byte_length\": 4669,\n \"sha256\": \"1486f79bd1d0eadea6cc1b7646ce1f7f57ccade18334a3ea37286426676b9b98\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_relay_hint_authority\",\n \"path\": \"crates/event/src/relay_hint.rs\",\n \"byte_length\": 13598,\n \"sha256\": \"fb8b026eeccf51d78f20771c44223e7b48825aa0e3c760d6b3cb90148021a674\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_media_authority\",\n \"path\": \"crates/event/src/media.rs\",\n \"byte_length\": 3455,\n \"sha256\": \"10678af3a202d5367dbcf3bcdc0bf9fdf8d8fb89b045d7d3fb96b3cc195ab6f9\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_social_authority\",\n \"path\": \"crates/event/src/social.rs\",\n \"byte_length\": 6076,\n \"sha256\": \"a2dab19caad46eab14fac86e7c878a801a450d8ac374b0e728b71a5a224eef19\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_wire_facade\",\n \"path\": \"crates/event/src/wire.rs\",\n \"byte_length\": 68,\n \"sha256\": \"cb52f6006f7ecd862707d6b048f9fc407e0cd5ebcd3091b3c54e195ffa5cba64\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_wire_v1_authority\",\n \"path\": \"crates/event/src/wire/v1.rs\",\n \"byte_length\": 22198,\n \"sha256\": \"c818bc1f67e215948ba253c8c62b09ab788db9aeec9b6df8096014b55565cb8c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_public_surface\",\n \"path\": \"crates/event_codec/src/lib.rs\",\n \"byte_length\": 2448,\n \"sha256\": \"7d4b0040ad3971f34395b17cb1aa40617e9c2c9e1b4222676c388c3adaa47e07\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_verification_facade\",\n \"path\": \"crates/event_codec/src/verification.rs\",\n \"byte_length\": 253,\n \"sha256\": \"b49a32df605035c87f295c0a151140d43d2e588c5e11b05183e5fc92993dae0e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_verification_v1_authority\",\n \"path\": \"crates/event_codec/src/verification/v1.rs\",\n \"byte_length\": 6584,\n \"sha256\": \"72f64615bd6b6dc3b051579e3069b2213f3781dcc28e12f0b533c7d81b81b9b1\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_registry_v7_admission_authority\",\n \"path\": \"crates/event_codec/src/admission/registry_v7.rs\",\n \"byte_length\": 5228,\n \"sha256\": \"62da30cb6ef7d0ed2d43715a73bd529283e8ae8c12571bc5c68722cb400f17b8\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_admission_facade\",\n \"path\": \"crates/event_codec/src/admission.rs\",\n \"byte_length\": 21251,\n \"sha256\": \"25928e734e7b3688f92af806a12a13458c48fb47e94249f60f0226300d5da2b4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_profile_inbound_facade\",\n \"path\": \"crates/event_codec/src/profile/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_profile_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/profile/inbound/registry_v7.rs\",\n \"byte_length\": 10347,\n \"sha256\": \"32111c0e0592229c11a93a3a8054e7b124b039451aa696a9abcb56df2a6608d3\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_post_inbound_facade\",\n \"path\": \"crates/event_codec/src/post/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_post_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/post/inbound/registry_v7.rs\",\n \"byte_length\": 15985,\n \"sha256\": \"e17083b0596e3a55994c399bc6272c0ebf04cabaa513260b3d45dd546f7f50ef\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_reply_inbound_facade\",\n \"path\": \"crates/event_codec/src/reply/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_reply_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/reply/inbound/registry_v7.rs\",\n \"byte_length\": 26662,\n \"sha256\": \"d1d11116ca27801b7e2f17da60900e6a9ca0b6f27ea9ee0bf19e97dbf24c0c32\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_comment_inbound_facade\",\n \"path\": \"crates/event_codec/src/comment/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_comment_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/comment/inbound/registry_v7.rs\",\n \"byte_length\": 53311,\n \"sha256\": \"c63fe853536b17bded6073f32b53434b60fc5123f911d01c642a6b3982f09c70\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_deletion_facade\",\n \"path\": \"crates/event_codec/src/deletion/mod.rs\",\n \"byte_length\": 115,\n \"sha256\": \"17d56f82ddb0a86bc97abbcf3e037fe460ea31fff2548f527be4bf004dac3a95\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_deletion_reconciliation_v1_authority\",\n \"path\": \"crates/event_codec/src/deletion/reconciliation_v1.rs\",\n \"byte_length\": 38028,\n \"sha256\": \"c6901f559e83700610595720c47a4d062078ae8411e1fc2fb952dd215b19dd90\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_error_authority\",\n \"path\": \"crates/event_codec/src/error.rs\",\n \"byte_length\": 3666,\n \"sha256\": \"174ce982bca37fac28d016c3ab3236441c9b31b2169ec94db6c9288b36c72849\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_admission_authority\",\n \"path\": \"crates/event_codec/src/food_availability/admission.rs\",\n \"byte_length\": 5681,\n \"sha256\": \"99809859090f5295572688d06bbb30dbe50524ba2c996de7ec5b26f6dd390643\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_authored_authority\",\n \"path\": \"crates/event_codec/src/food_availability/authored.rs\",\n \"byte_length\": 9724,\n \"sha256\": \"8673ec62ed3fc47b691efd9ca828643719803971e72b756c5b4603d409d0fba5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_inbound_facade\",\n \"path\": \"crates/event_codec/src/food_availability/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/food_availability/inbound/registry_v7.rs\",\n \"byte_length\": 26037,\n \"sha256\": \"92a7e75da3293967cec17a514479144c3c948a64542577dd58c7f07992968e4f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_job_traits_authority\",\n \"path\": \"crates/event_codec/src/job/traits.rs\",\n \"byte_length\": 4946,\n \"sha256\": \"1b3558a3196744005978dceddd33d3a72ad1ab8f50fb1cbfd997dcfe98bb9e39\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_job_encode_authority\",\n \"path\": \"crates/event_codec/src/job/encode.rs\",\n \"byte_length\": 1670,\n \"sha256\": \"5d6cae6309fcdd02deb7c751b5c451d257cf1900986391cb39cbb2bfd1ddb577\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_knowledge_verification_authority\",\n \"path\": \"crates/event_codec/src/knowledge/verification.rs\",\n \"byte_length\": 6986,\n \"sha256\": \"6f8a10ed262ce37f05acd9c89a1e37ca2fa99ce7da18407891e8abe228ee2349\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_operational_listing_tags_authority\",\n \"path\": \"crates/event_codec/src/operational_listing/tags.rs\",\n \"byte_length\": 50222,\n \"sha256\": \"55b6860bc6f4699dd47a49f8336764e9db78aafb7c32849102dedeb96804b373\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_order_decode_authority\",\n \"path\": \"crates/event_codec/src/order/decode.rs\",\n \"byte_length\": 42667,\n \"sha256\": \"033d9465bc041d205eab3f820572b719a35651340dcb50bdce7c60fabee5f6e1\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_profile_facade\",\n \"path\": \"crates/event_codec/src/profile/mod.rs\",\n \"byte_length\": 1456,\n \"sha256\": \"06b890da54580e2ec68f7e729281b7c3b650ac0a95a4fcb0db3c6cdaa591a7ef\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_tag_builders_authority\",\n \"path\": \"crates/event_codec/src/tag_builders.rs\",\n \"byte_length\": 9232,\n \"sha256\": \"bd81ccf04fd72358331b48eddcde087a95e115c5bd90c05fd9332644979ad5e6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_trade_facade\",\n \"path\": \"crates/event_codec/src/trade/mod.rs\",\n \"byte_length\": 21472,\n \"sha256\": \"10cee4b6cd6429ac9eb00327a521bcd0982fae4de98bda77e4728fda002965d3\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_dependency_authority\",\n \"path\": \"crates/event_store/Cargo.toml\",\n \"byte_length\": 1550,\n \"sha256\": \"9901a09402592ee873c255edf2e0c0d770105401255679d86ad47c35cb159d72\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_error_and_limits\",\n \"path\": \"crates/event_store/src/error.rs\",\n \"byte_length\": 19458,\n \"sha256\": \"3d87df984af6ae5decf7ca8c3a8d5422be6c647e6a05490225b6b2037bf3701b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"generated_descriptor_registration\",\n \"path\": \"crates/event_store/src/generated.rs\",\n \"byte_length\": 144,\n \"sha256\": \"6b0a8d6f249bd4fc3f878d37cb5e418680f0f1be2d9eec2518dedf03efc47121\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_surface\",\n \"path\": \"crates/event_store/src/lib.rs\",\n \"byte_length\": 3844,\n \"sha256\": \"3cd9653bcb752fb3c4442d4904b98a0a6208011a9a238125b7b7073d7f4e312b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"migration_registry\",\n \"path\": \"crates/event_store/src/migrations.rs\",\n \"byte_length\": 73585,\n \"sha256\": \"a47477d04759ec6e71d14d3d05459864eae78fce0e3c2481a1b03147f04637f7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_model_public_surface\",\n \"path\": \"crates/event_store/src/model.rs\",\n \"byte_length\": 32314,\n \"sha256\": \"d7ffa47719783c9f08cb4e00fbe47123cf0310ead06f77d3ad1d81edfda96b2c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_reconciliation_v1_model\",\n \"path\": \"crates/event_store/src/model/reconciliation_v1.rs\",\n \"byte_length\": 10446,\n \"sha256\": \"475b7b840bffdae7e3f7a31f5b940b9dfa579eaa1cac640729aaaeadabf83ba6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_reconciliation_v1_ingest_model\",\n \"path\": \"crates/event_store/src/model/ingest_reconciliation_v1.rs\",\n \"byte_length\": 1588,\n \"sha256\": \"6117e797674c35bb1ccebffbc6a8c0108bfc38c7c9f82607066dfd802b8f0734\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"addressable_transition_feed_model\",\n \"path\": \"crates/event_store/src/model/addressable_transition_feed_v1.rs\",\n \"byte_length\": 22229,\n \"sha256\": \"e651952d73ed0a29d2b137076f56f699beada205ac84ed8a96834e86e343fa78\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"current_visibility_model\",\n \"path\": \"crates/event_store/src/model/current_visibility_v1.rs\",\n \"byte_length\": 5634,\n \"sha256\": \"d64429bc7985231e923c1c3e5295d94270a817dd5dc96693f35dfdf2dc398e4f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"food_projection_model\",\n \"path\": \"crates/event_store/src/model/food_availability_projection_v1.rs\",\n \"byte_length\": 17108,\n \"sha256\": \"67861a966d674efbc40f0b0433db2ec006048ff923f6261df4161e2ddba717b5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_generation_rebuild_authority\",\n \"path\": \"crates/event_store/src/nip09/reconciliation_v1.rs\",\n \"byte_length\": 184833,\n \"sha256\": \"3ca802cb7a8444c8512b2be1390a019281704316a430fb9b75a8ad2cad358a07\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"nip09_successor_result_vector_executor\",\n \"path\": \"crates/event_store/tests/support/nip09_reconciliation_v1_result_vector_v2.rs\",\n \"byte_length\": 18205,\n \"sha256\": \"c632beee70dc777b8dd2a4f88cadb55296fee7aa5742e6e2c9f6d49f26ae2c78\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"schema_migration_and_reopen_authority\",\n \"path\": \"crates/event_store/src/schema.rs\",\n \"byte_length\": 146146,\n \"sha256\": \"93b060e80d3edd73f86208e4bf698fa9d53eaf1eeb04526c9261fb8b5726fb0d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_store_and_transaction_authority\",\n \"path\": \"crates/event_store/src/store.rs\",\n \"byte_length\": 393535,\n \"sha256\": \"c3286b50a2e9e17a330938492cfded0829dc19516f55aa4f62289d513457bad7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"addressable_transition_feed_store\",\n \"path\": \"crates/event_store/src/store/addressable_transition_feed_v1.rs\",\n \"byte_length\": 40209,\n \"sha256\": \"2cebcca9602633652a79e7041d18d3a7d3feb1388a98f4f68137b9814efcedfa\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"current_visibility_store\",\n \"path\": \"crates/event_store/src/store/current_visibility_v1.rs\",\n \"byte_length\": 15737,\n \"sha256\": \"aa5d1bff7a5368cbac2b37906bb40f54bfc8aa6906bab34369569fdd105bf899\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"food_projection_store\",\n \"path\": \"crates/event_store/src/store/food_availability_projection_v1.rs\",\n \"byte_length\": 48919,\n \"sha256\": \"3a30da308ef863b806ed63d1cb65ac80f6a946a71821af84a0c595330491757e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_post_core_v1_extension\",\n \"path\": \"crates/event_store/src/store/post_core_extensions_v1.rs\",\n \"byte_length\": 6569,\n \"sha256\": \"f5bd8ddb45e1b2144895bd8da737fc92db051be1844ce7e8047a32b47376ce37\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_post_core_v1_storage\",\n \"path\": \"crates/event_store/src/store/post_core_storage_v1.rs\",\n \"byte_length\": 16558,\n \"sha256\": \"d88c1f7ff5cc0a19221ad893eb840d55c11535f13adf84ffb3076cbc18d4abf1\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"raw_ingest_capacity_authority\",\n \"path\": \"crates/event_store/src/store/protocol_reconciliation_v1.rs\",\n \"byte_length\": 29950,\n \"sha256\": \"c72aaff06e4f35d0b4523c6625e83345879f9e15b9fe9563dccf589e5d01e277\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_protocol_storage\",\n \"path\": \"crates/event_store/src/store/protocol_storage_v1.rs\",\n \"byte_length\": 10835,\n \"sha256\": \"c82179b9f57968191f3e71dd008cc701787c62715ada74090eeecaa3b213076d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_maintenance_runtime\",\n \"path\": \"crates/event_store/src/source_maintenance_v1.rs\",\n \"byte_length\": 51849,\n \"sha256\": \"db43df5849c811d767a87bfa7884ce499400d0b169734e18242542a4415b3a51\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_food_result_vector_executor\",\n \"path\": \"crates/event_store/tests/food_availability_projection_v1_result_vector.rs\",\n \"byte_length\": 34046,\n \"sha256\": \"776903c6431ff07f26c6ad6b713db5628efce1a72f34d86de6e47d4404fa3a6d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"artifact_transaction_authority\",\n \"path\": \"tools/xtask/src/contract/artifact_bundle.rs\",\n \"byte_length\": 39149,\n \"sha256\": \"3d1bba3980b80698d2a21e26caf7af6fe4d9dc4a7fbabc6e406b5d33928b457f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_successor_governance\",\n \"path\": \"tools/xtask/src/contract/food_availability_projection.rs\",\n \"byte_length\": 198991,\n \"sha256\": \"36fd3d8b5dda3b0855a06a47dae14cae7f4cc84aaca9902c31477c218859fccc\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"transitive_predecessor_membership_governance\",\n \"path\": \"tools/xtask/src/contract/nip09_reconciliation.rs\",\n \"byte_length\": 842286,\n \"sha256\": \"678e4290f95db6a4eca19c82550f9db4c870e97f521dac3656f1a91c68db8104\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_maintenance_governance\",\n \"path\": \"tools/xtask/src/contract/source_maintenance.rs\",\n \"byte_length\": 199270,\n \"sha256\": \"a6685c27255918769f894ab098ba13a927c0d70c423fbd72b5404c0af0ae2f89\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"contract_command_authority\",\n \"path\": \"tools/xtask/src/contract.rs\",\n \"byte_length\": 508630,\n \"sha256\": \"203cc8578100894be51122d69cfb431650c3b5331e4071564f4b1095b0b46b94\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"dto_root_generation_authority\",\n \"path\": \"tools/xtask/src/dto_roots.rs\",\n \"byte_length\": 35940,\n \"sha256\": \"a899a5695d7533c151d03477c4bd697a6e55c47a4980549724ac42046af694f5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"xtask_dispatch_and_release_preflight\",\n \"path\": \"tools/xtask/src/main.rs\",\n \"byte_length\": 16426,\n \"sha256\": \"abc24896c6c740c10811a3e003ec4be8ebdb376a7b6b01445bfa7a5889f094a7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n ],\n \"public_api\": {\n \"inherited_predecessor_symbols\": [\n \"RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1\",\n \"RadrootsAddressableTransitionCauseV1\",\n \"RadrootsAddressableTransitionCoordinateV1\",\n \"RadrootsAddressableTransitionCursorV1\",\n \"RadrootsAddressableTransitionEventReferenceV1\",\n \"RadrootsAddressableTransitionOriginV1\",\n \"RadrootsAddressableTransitionPageV1\",\n \"RadrootsAddressableTransitionRawHeadDecisionV1\",\n \"RadrootsAddressableTransitionScopeFingerprintV1\",\n \"RadrootsAddressableTransitionScopeV1\",\n \"RadrootsAddressableTransitionV1\",\n \"RadrootsAddressableTransitionVisibilityV1\",\n \"RadrootsCurrentEventVisibilityV1\",\n \"RadrootsCurrentVisibilityDecisionV1\",\n \"RadrootsFoodAvailabilitySearchQueryV1\",\n \"RadrootsFoodAvailabilityStatusFilterV1\",\n \"RadrootsNip09SuppressionEvidenceV1\",\n \"RadrootsNip09SuppressionOutcome\",\n \"RadrootsNip09SuppressionReason\",\n \"RadrootsStoreProducedCanonicalEventV1\",\n \"RadrootsStoredFoodAvailabilityImageV1\",\n \"RadrootsStoredFoodAvailabilityV1\"\n ],\n \"added_symbols\": [\n \"RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1\",\n \"RadrootsEventStoreSourceCapacityResourceV1\",\n \"RadrootsEventStoreSourceCapacityV1\"\n ],\n \"methods\": [\n \"RadrootsEventStore::source_capacity_v1\",\n \"RadrootsEventStoreSourceCapacityResourceV1::as_str\",\n \"RadrootsEventStoreSourceCapacityV1::source_generation\",\n \"RadrootsEventStoreSourceCapacityV1::raw_event_count\",\n \"RadrootsEventStoreSourceCapacityV1::raw_tag_count\",\n \"RadrootsEventStoreSourceCapacityV1::raw_event_text_bytes\",\n \"RadrootsEventStoreSourceCapacityV1::raw_tag_text_bytes\",\n \"RadrootsEventStoreSourceCapacityV1::raw_high_water_seq\",\n \"RadrootsEventStoreSourceCapacityV1::retained_generation_count\",\n \"RadrootsEventStoreSourceCapacityV1::retained_generation_limit\"\n ],\n \"error_variants\": [\n \"SourceCapacityExceeded\",\n \"SourceGenerationHistoryLimitReached\",\n \"PersistedEphemeralRawEvent\",\n \"SourceCapacityStateDrift\",\n \"SqliteMainDatabaseEncodingNotUtf8\",\n \"RollbackWouldDiscardSourceGenerationHistory\"\n ],\n \"removed_symbols\": [\n \"RadrootsEventStoreReconciliationResource\",\n \"RadrootsEventStoreError::ReconciliationCapacityExceeded\"\n ],\n \"breaking_replacements\": [\n {\n \"removed\": \"RadrootsEventStoreReconciliationResource\",\n \"replacement\": \"RadrootsEventStoreSourceCapacityResourceV1\"\n },\n {\n \"removed\": \"RadrootsEventStoreError::ReconciliationCapacityExceeded\",\n \"replacement\": \"RadrootsEventStoreError::SourceCapacityExceeded\"\n }\n ]\n },\n \"result_vector\": {\n \"canonical_path\": \"contracts/conformance/vectors/event_store/source_maintenance.v1.json\",\n \"mirror_path\": \"crates/event_store/tests/fixtures/source_maintenance.v1.json\",\n \"byte_length\": 16253,\n \"sha256\": \"997aba2604a2b9d199fb87dc9d07942ca50d91863aeadcf3eeacf16d191dd71f\",\n \"hash_algorithm\": \"sha256_bytes_v1\",\n \"executor_id\": \"radroots_event_store.source_maintenance_v1.result_vector_executor.v1\",\n \"executor_path\": \"crates/event_store/tests/source_maintenance_v1_result_vector.rs\",\n \"executor_test\": \"source_maintenance_v1_result_vector\",\n \"executor_byte_length\": 23510,\n \"executor_sha256\": \"a7487afdfe19fc5fc794811d0f0e6035203e1aabcf0a33a1d398f6b3555d38f3\",\n \"executor_hash_algorithm\": \"sha256_bytes_v1\"\n }\n}\n"; +pub(crate) const SOURCE_MAINTENANCE_MANIFEST_JSON: &str = "{\n \"schema_version\": 1,\n \"contract_id\": \"radroots_event_store.source_maintenance_v1\",\n \"hook_id\": \"source_maintenance_v1\",\n \"manifest_schema\": {\n \"path\": \"crates/event_store/contracts/source_maintenance_v1.manifest.schema.json\",\n \"byte_length\": 12315,\n \"sha256\": \"96ea98a5abf3348b61565954cd34f2c622411d8dd34205ef04134bbe0e3d3f2b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"predecessor\": {\n \"hook_id\": \"food_availability_projection_v1\",\n \"manifest\": {\n \"path\": \"crates/event_store/contracts/food_availability_projection_v1.manifest.json\",\n \"byte_length\": 17455,\n \"sha256\": \"02dfe1b450fbdac16e718888215b4dd5c85d8975440fa21e8f439fb24c2b2990\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n },\n \"migration\": {\n \"version\": 4,\n \"name\": \"source_maintenance\",\n \"up\": {\n \"path\": \"crates/event_store/migrations/0004_source_maintenance.up.sql\",\n \"byte_length\": 19841,\n \"sha256\": \"ab2724188f8d08c897eebea2533a635e7c74282a25e84e4c0c37e78b08837a43\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"down\": {\n \"path\": \"crates/event_store/migrations/0004_source_maintenance.down.sql\",\n \"byte_length\": 5172,\n \"sha256\": \"fe44fd53c51545c08ea479b385e6781079dab70fc63da2a3c205d727a00ce860\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"schema_sha256\": \"074f85b663444ac150239ecd8441ea4a96ad83a798a55e22d2e5e2f7ee943a8c\",\n \"catalog\": {\n \"objects\": [\n \"radroots_event_store_source_capacity_delete_guard\",\n \"radroots_event_store_source_capacity_insert_guard\",\n \"radroots_event_store_source_capacity_marker_close_guard\",\n \"radroots_event_store_source_capacity_update_guard\",\n \"radroots_event_store_source_capacity_v1\",\n \"radroots_event_store_source_generation_capacity_advance\",\n \"radroots_event_store_source_generation_capacity_guard\"\n ],\n \"replaced_objects\": [\n \"radroots_event_store_food_availability_image_delete_guard\",\n \"radroots_event_store_food_availability_projection_delete_guard\",\n \"radroots_event_store_source_rebuild_marker_insert_guard\"\n ],\n \"tables\": [\n \"radroots_event_store_source_capacity_v1\"\n ],\n \"fts5_tables\": []\n }\n },\n \"source_maintenance\": {\n \"version\": 1,\n \"event_contract_registry_version\": 7,\n \"capacity_authority_id\": \"radroots_event_store_source_capacity_v1\",\n \"accounting\": {\n \"algorithm\": \"sqlite_cast_blob_octet_sum_v1\",\n \"raw_event_columns\": [\n \"event_id\",\n \"pubkey\",\n \"tags_json\",\n \"content\",\n \"sig\",\n \"raw_json\"\n ],\n \"raw_tag_columns\": [\n \"event_id\",\n \"tag_name\",\n \"tag_value\",\n \"tag_json\"\n ],\n \"nullable_raw_tag_columns\": [\n \"tag_value\"\n ]\n },\n \"limits\": {\n \"raw_events\": 25000,\n \"raw_tags\": 250000,\n \"raw_event_text_bytes\": 67108864,\n \"raw_tag_text_bytes\": 33554432,\n \"retained_source_generations\": 8\n },\n \"reopen_validation\": {\n \"mode\": \"bounded_full_raw_recount_v1\",\n \"raw_event_rejection_scan_bound\": 25001,\n \"raw_tag_rejection_scan_bound\": 250001,\n \"generation_history_validation\": \"bounded_count_plus_active_ordinal_v1\",\n \"retained_generation_rejection_scan_bound\": 9\n },\n \"rebuild_seal\": {\n \"nip09_hook_id\": \"nip09_reconciliation_v1\",\n \"nip09_manifest_sha256\": \"74af832420ffbaa9805e89df3c0b34f126a443e1598f757e3372f407f9003b77\",\n \"food_hook_id\": \"food_availability_projection_v1\",\n \"food_manifest_sha256\": \"02dfe1b450fbdac16e718888215b4dd5c85d8975440fa21e8f439fb24c2b2990\",\n \"food_scope_fingerprint_sha256\": \"8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0\",\n \"active_generation_authority\": \"radroots_event_store_source_state\",\n \"marker_close_authority\": \"radroots_event_store_source_capacity_marker_close_guard\"\n }\n },\n \"entry_points\": [\n {\n \"role\": \"migration_registry\",\n \"rust_path\": \"radroots_event_store::migrations::EVENT_STORE_MIGRATIONS[3]\"\n },\n {\n \"role\": \"migration_apply_hook\",\n \"rust_path\": \"radroots_event_store::schema::apply_migration_hook\"\n },\n {\n \"role\": \"migration_validation_hook\",\n \"rust_path\": \"radroots_event_store::schema::validate_migration_hook_state\"\n },\n {\n \"role\": \"capacity_query\",\n \"rust_path\": \"radroots_event_store::RadrootsEventStore::source_capacity_v1\"\n },\n {\n \"role\": \"raw_append_preflight\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::preflight_unique_raw_source_append_v1\"\n },\n {\n \"role\": \"raw_append_advance\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::advance_source_capacity_after_insert_v1\"\n },\n {\n \"role\": \"generation_append_preflight\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::preflight_source_generation_append_v1\"\n },\n {\n \"role\": \"generation_rebuild_bind\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::bind_source_capacity_to_generation_v1\"\n },\n {\n \"role\": \"sqlite_encoding_preflight\",\n \"rust_path\": \"radroots_event_store::store::validate_main_database_encoding\"\n },\n {\n \"role\": \"source_generation_history_rollback_guard\",\n \"rust_path\": \"radroots_event_store::schema::validate_rollback_preserves_source_generation_history\"\n },\n {\n \"role\": \"result_vector_executor\",\n \"rust_path\": \"source_maintenance_v1_result_vector\"\n }\n ],\n \"source_files\": [\n {\n \"role\": \"workspace_dependency_authority\",\n \"path\": \"Cargo.toml\",\n \"byte_length\": 12414,\n \"sha256\": \"895bf9d57a1cb9a89c0f70232cb628106897b421561ae0b9e9ec57577149b273\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_currency_value_authority\",\n \"path\": \"crates/core/src/currency.rs\",\n \"byte_length\": 4142,\n \"sha256\": \"42e6da2d8d2fdd6955dc2d83e98dd00266e02bc910b69923b117662c05089d14\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_decimal_value_authority\",\n \"path\": \"crates/core/src/decimal.rs\",\n \"byte_length\": 7839,\n \"sha256\": \"4b1d681a92d7a9e074bee7e1f20eb1499bb05bfc6528a6c9ba69fa7d00416550\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_money_value_authority\",\n \"path\": \"crates/core/src/money.rs\",\n \"byte_length\": 8025,\n \"sha256\": \"3f30bc21e21951a62fdc5d4033736ed42a883df590e8403940775bd2edfeebc5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_percent_value_authority\",\n \"path\": \"crates/core/src/percent.rs\",\n \"byte_length\": 2652,\n \"sha256\": \"42ceab109881329f3539d06a0ee0381ab4359985fe185a18d58a9c9558c5eb7f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_quantity_value_authority\",\n \"path\": \"crates/core/src/quantity.rs\",\n \"byte_length\": 6785,\n \"sha256\": \"d14e619da3829cdaf3ca7385fb892d330c286c1e21455c135c37518cbb588b5f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_quantity_price_value_authority\",\n \"path\": \"crates/core/src/quantity_price.rs\",\n \"byte_length\": 7061,\n \"sha256\": \"d60f70377099470ed283f45868a5119d7bb46143cb250b5082a4c038ba7f6cdc\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_unit_value_authority\",\n \"path\": \"crates/core/src/unit.rs\",\n \"byte_length\": 10023,\n \"sha256\": \"550e9582b6a5290aac3f637b83853b21fefcbab92c432d067c83448f4b2fe3e9\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_public_surface\",\n \"path\": \"crates/blossom/src/lib.rs\",\n \"byte_length\": 510,\n \"sha256\": \"a4dfcbd193457c50c1b0fbfc91bb547981ecd3a6e23cfc6f1030edfb511dc58c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_authorization_authority\",\n \"path\": \"crates/blossom/src/authorization.rs\",\n \"byte_length\": 39360,\n \"sha256\": \"461947b30516315a342b3b5697599f701d27050888f29f67f5d7a7ad4afa4c28\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_descriptor_authority\",\n \"path\": \"crates/blossom/src/descriptor.rs\",\n \"byte_length\": 13702,\n \"sha256\": \"b2a4ffa760256e1316f70e012d200e2b5f4afd8ede771b85a5147f96b247f599\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_error_authority\",\n \"path\": \"crates/blossom/src/error.rs\",\n \"byte_length\": 17229,\n \"sha256\": \"02af55beacf437040be17d1fc3271d6e2a62915f656ec2dbe06247deef83f95a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_hash_authority\",\n \"path\": \"crates/blossom/src/hash.rs\",\n \"byte_length\": 11006,\n \"sha256\": \"45eca0d81ac0f46c305a32f81aafcdd90b14679d0614f72862289a6e5d4cb08f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_media_type_authority\",\n \"path\": \"crates/blossom/src/media_type.rs\",\n \"byte_length\": 2368,\n \"sha256\": \"69db5c0f9fcdd7dbe1f22daa499f65603ae4ce198083dab2c73a36813fa18a46\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_url_authority\",\n \"path\": \"crates/blossom/src/url.rs\",\n \"byte_length\": 14580,\n \"sha256\": \"342c995fcf620e5fb32461a7d8276f5668ba2662890bd32d12780cc9121c6451\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_public_surface\",\n \"path\": \"crates/event/src/lib.rs\",\n \"byte_length\": 1892,\n \"sha256\": \"5d609b963a9b8db18ef96a10f394617413734d2631774d3296e6d8fc029cb02c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_contract_facade\",\n \"path\": \"crates/event/src/contract.rs\",\n \"byte_length\": 208,\n \"sha256\": \"27466bf36461071931f391c60646a13f7781d19584583166cc54c0eece76d010\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_contract_registry_v7_authority\",\n \"path\": \"crates/event/src/contract/registry_v7.rs\",\n \"byte_length\": 145805,\n \"sha256\": \"0a62603f6fc05dc9f758561cf7da258c676ada810ceaeca7fb151364a5c83d62\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_envelope_authority\",\n \"path\": \"crates/event/src/envelope.rs\",\n \"byte_length\": 29310,\n \"sha256\": \"29f8d8c4b17c01cb5a8c7f59e4ce52e134180e0e6f37d265063d278b8f2ff26e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_verification_typestate_authority\",\n \"path\": \"crates/event/src/verification.rs\",\n \"byte_length\": 9400,\n \"sha256\": \"417acb2ce670d266b1fa4fcafd6b48a910218deab38f57fb8b942056db76f94d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_admission_typestate_authority\",\n \"path\": \"crates/event/src/admission.rs\",\n \"byte_length\": 7052,\n \"sha256\": \"62bd2ceb0f434946fdfc2b81f4efb13738676a29a6936c23a427e43aadf2d9cb\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_head_facade\",\n \"path\": \"crates/event/src/event_head.rs\",\n \"byte_length\": 97,\n \"sha256\": \"f761ff3e74c4f5e1e28381db00ce698c633ff048669b22984d14a587482e8e83\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_head_v1_authority\",\n \"path\": \"crates/event/src/event_head/v1.rs\",\n \"byte_length\": 6849,\n \"sha256\": \"9f4144d8d240023cf493ce0b538ade4c16dc44204d6b5f2797152caf6f9e7dba\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_ids_authority\",\n \"path\": \"crates/event/src/id.rs\",\n \"byte_length\": 48381,\n \"sha256\": \"a08fe3873815453a03318a706e2fa98a8a6728ec69d24368a87ec60025566fe0\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_trade_authority\",\n \"path\": \"crates/event/src/trade.rs\",\n \"byte_length\": 65549,\n \"sha256\": \"f818a7287ed82de3fb9ef1d973f0fb18d84b30eb4827ddc576ee502dd980145f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_kinds_authority\",\n \"path\": \"crates/event/src/kinds.rs\",\n \"byte_length\": 34961,\n \"sha256\": \"8b3ce6193cab1f7e1587d0c1b2880a81b0aa43c77d89671b602772db37edcebf\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_tags_authority\",\n \"path\": \"crates/event/src/tags.rs\",\n \"byte_length\": 4530,\n \"sha256\": \"cb8f12a639fc72d9238e32495d9e67e928e6627fb8c94ec0a58c5d95e7373cd6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_draft_authority\",\n \"path\": \"crates/event/src/draft.rs\",\n \"byte_length\": 51150,\n \"sha256\": \"ecc619aae28d688d13f35467398f17766b60f26c2bf3616d772ce65b7fa4fb53\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_calendar_authority\",\n \"path\": \"crates/event/src/calendar.rs\",\n \"byte_length\": 97992,\n \"sha256\": \"73e79d61ee175cb46e6123e1d28239252ddf0ad094dc1113bdb1286b63b88d57\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_classified_listing_authority\",\n \"path\": \"crates/event/src/classified_listing.rs\",\n \"byte_length\": 7956,\n \"sha256\": \"ac58484b87c7712c81f50f884d1c0dd60938571aa5f6f05d555191f529a4c2b0\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_profile_authority\",\n \"path\": \"crates/event/src/profile.rs\",\n \"byte_length\": 17011,\n \"sha256\": \"572bfe3b6f1711d109db445077f0a8453cad3f021cc0638f79201b618e16b978\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_post_authority\",\n \"path\": \"crates/event/src/post.rs\",\n \"byte_length\": 19968,\n \"sha256\": \"a822cd51eff9cbaa39db998cc5c90d117f7a983f0fe2d42d42118cb00e95982c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_comment_authority\",\n \"path\": \"crates/event/src/comment.rs\",\n \"byte_length\": 34745,\n \"sha256\": \"24b7fda13229f21cee5cef2a9941e3aaafdbc486b19ed42731b0b6ee37c28703\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_food_availability_authority\",\n \"path\": \"crates/event/src/food_availability.rs\",\n \"byte_length\": 42829,\n \"sha256\": \"52472c07eb0855d884c60755dc3a3e1a963793451d707e51f769f2d78384f318\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_deletion_authority\",\n \"path\": \"crates/event/src/deletion.rs\",\n \"byte_length\": 30022,\n \"sha256\": \"8c3f2e32407520a8e8242c1293af32613c648fb76218b8003a18929ac0b3811f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_dto_authority\",\n \"path\": \"crates/event/src/dto.rs\",\n \"byte_length\": 5038,\n \"sha256\": \"9e228c95ff6ff33f99441d8682781fd0a1c4d3f1db24284a43985dcbac3ba136\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_farm_crdt_authority\",\n \"path\": \"crates/event/src/farm_crdt.rs\",\n \"byte_length\": 21118,\n \"sha256\": \"cec265ac9c42b59a14be4eb96924d26a83477c27291f667f23e6e47a64b6b2c7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_knowledge_authority\",\n \"path\": \"crates/event/src/knowledge.rs\",\n \"byte_length\": 54624,\n \"sha256\": \"3d55ff88a4e30d5d605f9ae4b2e96b9df6978d6c14e52f9aeb0b8039cfc200d7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_operational_listing_authority\",\n \"path\": \"crates/event/src/operational_listing.rs\",\n \"byte_length\": 10307,\n \"sha256\": \"52e710db816c89c8d4b87c86541987f7447f2f7091c394c68d9cb98a27f90159\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_order_authority\",\n \"path\": \"crates/event/src/order.rs\",\n \"byte_length\": 60218,\n \"sha256\": \"4203fcc469612cac5c705884564e140b9fb076cd57079e22ed85275a8b936e50\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_reply_authority\",\n \"path\": \"crates/event/src/reply.rs\",\n \"byte_length\": 15581,\n \"sha256\": \"b0b83a1e96125c0892d932e9314519242a715fa8f46334aff765ffd248c91d9a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_trade_validation_authority\",\n \"path\": \"crates/event/src/trade_validation.rs\",\n \"byte_length\": 4669,\n \"sha256\": \"1486f79bd1d0eadea6cc1b7646ce1f7f57ccade18334a3ea37286426676b9b98\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_relay_hint_authority\",\n \"path\": \"crates/event/src/relay_hint.rs\",\n \"byte_length\": 13598,\n \"sha256\": \"fb8b026eeccf51d78f20771c44223e7b48825aa0e3c760d6b3cb90148021a674\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_media_authority\",\n \"path\": \"crates/event/src/media.rs\",\n \"byte_length\": 3455,\n \"sha256\": \"10678af3a202d5367dbcf3bcdc0bf9fdf8d8fb89b045d7d3fb96b3cc195ab6f9\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_social_authority\",\n \"path\": \"crates/event/src/social.rs\",\n \"byte_length\": 6076,\n \"sha256\": \"a2dab19caad46eab14fac86e7c878a801a450d8ac374b0e728b71a5a224eef19\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_wire_facade\",\n \"path\": \"crates/event/src/wire.rs\",\n \"byte_length\": 68,\n \"sha256\": \"cb52f6006f7ecd862707d6b048f9fc407e0cd5ebcd3091b3c54e195ffa5cba64\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_wire_v1_authority\",\n \"path\": \"crates/event/src/wire/v1.rs\",\n \"byte_length\": 22198,\n \"sha256\": \"c818bc1f67e215948ba253c8c62b09ab788db9aeec9b6df8096014b55565cb8c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_public_surface\",\n \"path\": \"crates/event_codec/src/lib.rs\",\n \"byte_length\": 2448,\n \"sha256\": \"7d4b0040ad3971f34395b17cb1aa40617e9c2c9e1b4222676c388c3adaa47e07\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_verification_facade\",\n \"path\": \"crates/event_codec/src/verification.rs\",\n \"byte_length\": 253,\n \"sha256\": \"b49a32df605035c87f295c0a151140d43d2e588c5e11b05183e5fc92993dae0e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_verification_v1_authority\",\n \"path\": \"crates/event_codec/src/verification/v1.rs\",\n \"byte_length\": 6584,\n \"sha256\": \"72f64615bd6b6dc3b051579e3069b2213f3781dcc28e12f0b533c7d81b81b9b1\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_registry_v7_admission_authority\",\n \"path\": \"crates/event_codec/src/admission/registry_v7.rs\",\n \"byte_length\": 5228,\n \"sha256\": \"62da30cb6ef7d0ed2d43715a73bd529283e8ae8c12571bc5c68722cb400f17b8\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_admission_facade\",\n \"path\": \"crates/event_codec/src/admission.rs\",\n \"byte_length\": 21251,\n \"sha256\": \"25928e734e7b3688f92af806a12a13458c48fb47e94249f60f0226300d5da2b4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_profile_inbound_facade\",\n \"path\": \"crates/event_codec/src/profile/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_profile_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/profile/inbound/registry_v7.rs\",\n \"byte_length\": 10347,\n \"sha256\": \"32111c0e0592229c11a93a3a8054e7b124b039451aa696a9abcb56df2a6608d3\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_post_inbound_facade\",\n \"path\": \"crates/event_codec/src/post/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_post_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/post/inbound/registry_v7.rs\",\n \"byte_length\": 15985,\n \"sha256\": \"e17083b0596e3a55994c399bc6272c0ebf04cabaa513260b3d45dd546f7f50ef\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_reply_inbound_facade\",\n \"path\": \"crates/event_codec/src/reply/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_reply_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/reply/inbound/registry_v7.rs\",\n \"byte_length\": 26662,\n \"sha256\": \"d1d11116ca27801b7e2f17da60900e6a9ca0b6f27ea9ee0bf19e97dbf24c0c32\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_comment_inbound_facade\",\n \"path\": \"crates/event_codec/src/comment/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_comment_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/comment/inbound/registry_v7.rs\",\n \"byte_length\": 53311,\n \"sha256\": \"c63fe853536b17bded6073f32b53434b60fc5123f911d01c642a6b3982f09c70\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_deletion_facade\",\n \"path\": \"crates/event_codec/src/deletion/mod.rs\",\n \"byte_length\": 115,\n \"sha256\": \"17d56f82ddb0a86bc97abbcf3e037fe460ea31fff2548f527be4bf004dac3a95\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_deletion_reconciliation_v1_authority\",\n \"path\": \"crates/event_codec/src/deletion/reconciliation_v1.rs\",\n \"byte_length\": 38028,\n \"sha256\": \"c6901f559e83700610595720c47a4d062078ae8411e1fc2fb952dd215b19dd90\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_error_authority\",\n \"path\": \"crates/event_codec/src/error.rs\",\n \"byte_length\": 3666,\n \"sha256\": \"174ce982bca37fac28d016c3ab3236441c9b31b2169ec94db6c9288b36c72849\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_admission_authority\",\n \"path\": \"crates/event_codec/src/food_availability/admission.rs\",\n \"byte_length\": 5681,\n \"sha256\": \"99809859090f5295572688d06bbb30dbe50524ba2c996de7ec5b26f6dd390643\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_authored_authority\",\n \"path\": \"crates/event_codec/src/food_availability/authored.rs\",\n \"byte_length\": 9724,\n \"sha256\": \"8673ec62ed3fc47b691efd9ca828643719803971e72b756c5b4603d409d0fba5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_inbound_facade\",\n \"path\": \"crates/event_codec/src/food_availability/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/food_availability/inbound/registry_v7.rs\",\n \"byte_length\": 26037,\n \"sha256\": \"92a7e75da3293967cec17a514479144c3c948a64542577dd58c7f07992968e4f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_job_traits_authority\",\n \"path\": \"crates/event_codec/src/job/traits.rs\",\n \"byte_length\": 4946,\n \"sha256\": \"1b3558a3196744005978dceddd33d3a72ad1ab8f50fb1cbfd997dcfe98bb9e39\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_job_encode_authority\",\n \"path\": \"crates/event_codec/src/job/encode.rs\",\n \"byte_length\": 1670,\n \"sha256\": \"5d6cae6309fcdd02deb7c751b5c451d257cf1900986391cb39cbb2bfd1ddb577\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_knowledge_verification_authority\",\n \"path\": \"crates/event_codec/src/knowledge/verification.rs\",\n \"byte_length\": 6986,\n \"sha256\": \"6f8a10ed262ce37f05acd9c89a1e37ca2fa99ce7da18407891e8abe228ee2349\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_operational_listing_tags_authority\",\n \"path\": \"crates/event_codec/src/operational_listing/tags.rs\",\n \"byte_length\": 50222,\n \"sha256\": \"55b6860bc6f4699dd47a49f8336764e9db78aafb7c32849102dedeb96804b373\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_order_decode_authority\",\n \"path\": \"crates/event_codec/src/order/decode.rs\",\n \"byte_length\": 42667,\n \"sha256\": \"033d9465bc041d205eab3f820572b719a35651340dcb50bdce7c60fabee5f6e1\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_profile_facade\",\n \"path\": \"crates/event_codec/src/profile/mod.rs\",\n \"byte_length\": 1456,\n \"sha256\": \"06b890da54580e2ec68f7e729281b7c3b650ac0a95a4fcb0db3c6cdaa591a7ef\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_tag_builders_authority\",\n \"path\": \"crates/event_codec/src/tag_builders.rs\",\n \"byte_length\": 9232,\n \"sha256\": \"bd81ccf04fd72358331b48eddcde087a95e115c5bd90c05fd9332644979ad5e6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_trade_facade\",\n \"path\": \"crates/event_codec/src/trade/mod.rs\",\n \"byte_length\": 21472,\n \"sha256\": \"10cee4b6cd6429ac9eb00327a521bcd0982fae4de98bda77e4728fda002965d3\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_dependency_authority\",\n \"path\": \"crates/event_store/Cargo.toml\",\n \"byte_length\": 1550,\n \"sha256\": \"9901a09402592ee873c255edf2e0c0d770105401255679d86ad47c35cb159d72\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_error_and_limits\",\n \"path\": \"crates/event_store/src/error.rs\",\n \"byte_length\": 19458,\n \"sha256\": \"3d87df984af6ae5decf7ca8c3a8d5422be6c647e6a05490225b6b2037bf3701b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"generated_descriptor_registration\",\n \"path\": \"crates/event_store/src/generated.rs\",\n \"byte_length\": 144,\n \"sha256\": \"6b0a8d6f249bd4fc3f878d37cb5e418680f0f1be2d9eec2518dedf03efc47121\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_surface\",\n \"path\": \"crates/event_store/src/lib.rs\",\n \"byte_length\": 3844,\n \"sha256\": \"3cd9653bcb752fb3c4442d4904b98a0a6208011a9a238125b7b7073d7f4e312b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"migration_registry\",\n \"path\": \"crates/event_store/src/migrations.rs\",\n \"byte_length\": 73585,\n \"sha256\": \"a47477d04759ec6e71d14d3d05459864eae78fce0e3c2481a1b03147f04637f7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_model_public_surface\",\n \"path\": \"crates/event_store/src/model.rs\",\n \"byte_length\": 32050,\n \"sha256\": \"910053012eb65cb1a6c24455ffacee056ce013c950e9b545822f9c2e679a8892\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_reconciliation_v1_model\",\n \"path\": \"crates/event_store/src/model/reconciliation_v1.rs\",\n \"byte_length\": 10446,\n \"sha256\": \"475b7b840bffdae7e3f7a31f5b940b9dfa579eaa1cac640729aaaeadabf83ba6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_reconciliation_v1_ingest_model\",\n \"path\": \"crates/event_store/src/model/ingest_reconciliation_v1.rs\",\n \"byte_length\": 1588,\n \"sha256\": \"6117e797674c35bb1ccebffbc6a8c0108bfc38c7c9f82607066dfd802b8f0734\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"addressable_transition_feed_model\",\n \"path\": \"crates/event_store/src/model/addressable_transition_feed_v1.rs\",\n \"byte_length\": 22229,\n \"sha256\": \"e651952d73ed0a29d2b137076f56f699beada205ac84ed8a96834e86e343fa78\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"current_visibility_model\",\n \"path\": \"crates/event_store/src/model/current_visibility_v1.rs\",\n \"byte_length\": 5634,\n \"sha256\": \"d64429bc7985231e923c1c3e5295d94270a817dd5dc96693f35dfdf2dc398e4f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"food_projection_model\",\n \"path\": \"crates/event_store/src/model/food_availability_projection_v1.rs\",\n \"byte_length\": 17108,\n \"sha256\": \"67861a966d674efbc40f0b0433db2ec006048ff923f6261df4161e2ddba717b5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_generation_rebuild_authority\",\n \"path\": \"crates/event_store/src/nip09/reconciliation_v1.rs\",\n \"byte_length\": 184833,\n \"sha256\": \"3ca802cb7a8444c8512b2be1390a019281704316a430fb9b75a8ad2cad358a07\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"nip09_successor_result_vector_executor\",\n \"path\": \"crates/event_store/tests/support/nip09_reconciliation_v1_result_vector_v2.rs\",\n \"byte_length\": 18205,\n \"sha256\": \"c632beee70dc777b8dd2a4f88cadb55296fee7aa5742e6e2c9f6d49f26ae2c78\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"schema_migration_and_reopen_authority\",\n \"path\": \"crates/event_store/src/schema.rs\",\n \"byte_length\": 146146,\n \"sha256\": \"93b060e80d3edd73f86208e4bf698fa9d53eaf1eeb04526c9261fb8b5726fb0d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_store_and_transaction_authority\",\n \"path\": \"crates/event_store/src/store.rs\",\n \"byte_length\": 393195,\n \"sha256\": \"03fc56bf151f53c35792d09b247513d722ae26e3d112810f28f167a004c83273\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"addressable_transition_feed_store\",\n \"path\": \"crates/event_store/src/store/addressable_transition_feed_v1.rs\",\n \"byte_length\": 40209,\n \"sha256\": \"2cebcca9602633652a79e7041d18d3a7d3feb1388a98f4f68137b9814efcedfa\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"current_visibility_store\",\n \"path\": \"crates/event_store/src/store/current_visibility_v1.rs\",\n \"byte_length\": 15737,\n \"sha256\": \"aa5d1bff7a5368cbac2b37906bb40f54bfc8aa6906bab34369569fdd105bf899\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"food_projection_store\",\n \"path\": \"crates/event_store/src/store/food_availability_projection_v1.rs\",\n \"byte_length\": 48919,\n \"sha256\": \"3a30da308ef863b806ed63d1cb65ac80f6a946a71821af84a0c595330491757e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_post_core_v1_extension\",\n \"path\": \"crates/event_store/src/store/post_core_extensions_v1.rs\",\n \"byte_length\": 6569,\n \"sha256\": \"f5bd8ddb45e1b2144895bd8da737fc92db051be1844ce7e8047a32b47376ce37\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_post_core_v1_storage\",\n \"path\": \"crates/event_store/src/store/post_core_storage_v1.rs\",\n \"byte_length\": 16538,\n \"sha256\": \"fcd6546c23a6cba12b70d92728d19b6ee5d43174b83495f820348b77efa33aab\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"raw_ingest_capacity_authority\",\n \"path\": \"crates/event_store/src/store/protocol_reconciliation_v1.rs\",\n \"byte_length\": 29950,\n \"sha256\": \"c72aaff06e4f35d0b4523c6625e83345879f9e15b9fe9563dccf589e5d01e277\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_protocol_storage\",\n \"path\": \"crates/event_store/src/store/protocol_storage_v1.rs\",\n \"byte_length\": 10835,\n \"sha256\": \"c82179b9f57968191f3e71dd008cc701787c62715ada74090eeecaa3b213076d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_maintenance_runtime\",\n \"path\": \"crates/event_store/src/source_maintenance_v1.rs\",\n \"byte_length\": 51849,\n \"sha256\": \"db43df5849c811d767a87bfa7884ce499400d0b169734e18242542a4415b3a51\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_food_result_vector_executor\",\n \"path\": \"crates/event_store/tests/food_availability_projection_v1_result_vector.rs\",\n \"byte_length\": 34046,\n \"sha256\": \"776903c6431ff07f26c6ad6b713db5628efce1a72f34d86de6e47d4404fa3a6d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"artifact_transaction_authority\",\n \"path\": \"tools/xtask/src/contract/artifact_bundle.rs\",\n \"byte_length\": 39149,\n \"sha256\": \"3d1bba3980b80698d2a21e26caf7af6fe4d9dc4a7fbabc6e406b5d33928b457f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_successor_governance\",\n \"path\": \"tools/xtask/src/contract/food_availability_projection.rs\",\n \"byte_length\": 198991,\n \"sha256\": \"36fd3d8b5dda3b0855a06a47dae14cae7f4cc84aaca9902c31477c218859fccc\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"transitive_predecessor_membership_governance\",\n \"path\": \"tools/xtask/src/contract/nip09_reconciliation.rs\",\n \"byte_length\": 842402,\n \"sha256\": \"4abb141771cfa257440dba7ebeae21f5ad1f9856cc0d5f6437ba68614d997810\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_maintenance_governance\",\n \"path\": \"tools/xtask/src/contract/source_maintenance.rs\",\n \"byte_length\": 199270,\n \"sha256\": \"d3681d09e60763baf3c59edfbcb48e3c370c315a75fcd5a01756c8c961e784c9\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"contract_command_authority\",\n \"path\": \"tools/xtask/src/contract.rs\",\n \"byte_length\": 508630,\n \"sha256\": \"203cc8578100894be51122d69cfb431650c3b5331e4071564f4b1095b0b46b94\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"dto_root_generation_authority\",\n \"path\": \"tools/xtask/src/dto_roots.rs\",\n \"byte_length\": 35940,\n \"sha256\": \"a899a5695d7533c151d03477c4bd697a6e55c47a4980549724ac42046af694f5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"xtask_dispatch_and_release_preflight\",\n \"path\": \"tools/xtask/src/main.rs\",\n \"byte_length\": 16426,\n \"sha256\": \"abc24896c6c740c10811a3e003ec4be8ebdb376a7b6b01445bfa7a5889f094a7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n ],\n \"public_api\": {\n \"inherited_predecessor_symbols\": [\n \"RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1\",\n \"RadrootsAddressableTransitionCauseV1\",\n \"RadrootsAddressableTransitionCoordinateV1\",\n \"RadrootsAddressableTransitionCursorV1\",\n \"RadrootsAddressableTransitionEventReferenceV1\",\n \"RadrootsAddressableTransitionOriginV1\",\n \"RadrootsAddressableTransitionPageV1\",\n \"RadrootsAddressableTransitionRawHeadDecisionV1\",\n \"RadrootsAddressableTransitionScopeFingerprintV1\",\n \"RadrootsAddressableTransitionScopeV1\",\n \"RadrootsAddressableTransitionV1\",\n \"RadrootsAddressableTransitionVisibilityV1\",\n \"RadrootsCurrentEventVisibilityV1\",\n \"RadrootsCurrentVisibilityDecisionV1\",\n \"RadrootsFoodAvailabilitySearchQueryV1\",\n \"RadrootsFoodAvailabilityStatusFilterV1\",\n \"RadrootsNip09SuppressionEvidenceV1\",\n \"RadrootsNip09SuppressionOutcome\",\n \"RadrootsNip09SuppressionReason\",\n \"RadrootsStoreProducedCanonicalEventV1\",\n \"RadrootsStoredFoodAvailabilityImageV1\",\n \"RadrootsStoredFoodAvailabilityV1\"\n ],\n \"added_symbols\": [\n \"RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1\",\n \"RadrootsEventStoreSourceCapacityResourceV1\",\n \"RadrootsEventStoreSourceCapacityV1\"\n ],\n \"methods\": [\n \"RadrootsEventStore::source_capacity_v1\",\n \"RadrootsEventStoreSourceCapacityResourceV1::as_str\",\n \"RadrootsEventStoreSourceCapacityV1::source_generation\",\n \"RadrootsEventStoreSourceCapacityV1::raw_event_count\",\n \"RadrootsEventStoreSourceCapacityV1::raw_tag_count\",\n \"RadrootsEventStoreSourceCapacityV1::raw_event_text_bytes\",\n \"RadrootsEventStoreSourceCapacityV1::raw_tag_text_bytes\",\n \"RadrootsEventStoreSourceCapacityV1::raw_high_water_seq\",\n \"RadrootsEventStoreSourceCapacityV1::retained_generation_count\",\n \"RadrootsEventStoreSourceCapacityV1::retained_generation_limit\"\n ],\n \"error_variants\": [\n \"SourceCapacityExceeded\",\n \"SourceGenerationHistoryLimitReached\",\n \"PersistedEphemeralRawEvent\",\n \"SourceCapacityStateDrift\",\n \"SqliteMainDatabaseEncodingNotUtf8\",\n \"RollbackWouldDiscardSourceGenerationHistory\"\n ],\n \"removed_symbols\": [\n \"RadrootsEventStoreReconciliationResource\",\n \"RadrootsEventStoreError::ReconciliationCapacityExceeded\"\n ],\n \"breaking_replacements\": [\n {\n \"removed\": \"RadrootsEventStoreReconciliationResource\",\n \"replacement\": \"RadrootsEventStoreSourceCapacityResourceV1\"\n },\n {\n \"removed\": \"RadrootsEventStoreError::ReconciliationCapacityExceeded\",\n \"replacement\": \"RadrootsEventStoreError::SourceCapacityExceeded\"\n }\n ]\n },\n \"result_vector\": {\n \"canonical_path\": \"contracts/conformance/vectors/event_store/source_maintenance.v1.json\",\n \"mirror_path\": \"crates/event_store/tests/fixtures/source_maintenance.v1.json\",\n \"byte_length\": 16253,\n \"sha256\": \"997aba2604a2b9d199fb87dc9d07942ca50d91863aeadcf3eeacf16d191dd71f\",\n \"hash_algorithm\": \"sha256_bytes_v1\",\n \"executor_id\": \"radroots_event_store.source_maintenance_v1.result_vector_executor.v1\",\n \"executor_path\": \"crates/event_store/tests/source_maintenance_v1_result_vector.rs\",\n \"executor_test\": \"source_maintenance_v1_result_vector\",\n \"executor_byte_length\": 23510,\n \"executor_sha256\": \"a7487afdfe19fc5fc794811d0f0e6035203e1aabcf0a33a1d398f6b3555d38f3\",\n \"executor_hash_algorithm\": \"sha256_bytes_v1\"\n }\n}\n"; pub(crate) const SOURCE_MAINTENANCE_MANIFEST_BYTE_LENGTH: usize = 38397; pub(crate) const SOURCE_MAINTENANCE_MANIFEST_SHA256: &str = - "b4416b1eab44c6e7110ffed89eb67b8963059538e7775a1347067d1f7c7f3999"; + "51205a1c1c7c3b91d79354ea5deabefc0f42d83423d4a7da3a0e6658f34863b5"; pub(crate) const SOURCE_MAINTENANCE_MANIFEST_SCHEMA_VERSION: u32 = 1; pub(crate) const SOURCE_MAINTENANCE_CONTRACT_ID: &str = "radroots_event_store.source_maintenance_v1"; diff --git a/crates/event_store/src/model.rs b/crates/event_store/src/model.rs @@ -38,10 +38,8 @@ use radroots_event::envelope::EventKind; use radroots_event::id::{CandidateId, DTag, EventId, InventoryBinId, MutationId, TradeId}; use radroots_event::trade::TradeMutationKindV1; use radroots_identity::PublicKey; -use radroots_transport::{ - RadrootsTransportKind, RadrootsTransportTarget, RadrootsTransportTargetFingerprint, - RadrootsTransportTargetUri, -}; +use radroots_transport::target::TargetFingerprint; +use radroots_transport::{RadrootsTransportTargetUri, Target, TransportId}; pub use reconciliation_v1::{ RadrootsEventAdmissionStatus, RadrootsEventIngest, RadrootsEventIngestReceipt, RadrootsEventPersistence, RadrootsEventStoreSourceGeneration, RadrootsRawHeadDecision, @@ -168,9 +166,9 @@ impl RadrootsTransportObservationType { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsTransportObservation { - transport_kind: RadrootsTransportKind, + transport_kind: TransportId, endpoint_uri: RadrootsTransportTargetUri, - endpoint_fingerprint: RadrootsTransportTargetFingerprint, + endpoint_fingerprint: TargetFingerprint, observation_type: RadrootsTransportObservationType, observed_at_ms: i64, caller_redacted_message: Option<RadrootsTransportObservationMessage>, @@ -183,7 +181,7 @@ impl RadrootsTransportObservation { /// observation identity. Use transport delivery receipts when scoped /// Reticulum or local-target evidence must be preserved. pub fn new( - transport_kind: RadrootsTransportKind, + transport_kind: TransportId, endpoint_uri: impl AsRef<str>, observation_type: RadrootsTransportObservationType, observed_at_ms: i64, @@ -195,9 +193,9 @@ impl RadrootsTransportObservation { }, ); } - let target = RadrootsTransportTarget::new(transport_kind, endpoint_uri)?; + let target = Target::new(transport_kind, endpoint_uri)?; Ok(Self { - transport_kind: target.kind().clone(), + transport_kind: *target.kind(), endpoint_uri: target.uri().clone(), endpoint_fingerprint: target.fingerprint().clone(), observation_type, @@ -206,7 +204,7 @@ impl RadrootsTransportObservation { }) } - pub fn transport_kind(&self) -> &RadrootsTransportKind { + pub fn transport_kind(&self) -> &TransportId { &self.transport_kind } @@ -214,7 +212,7 @@ impl RadrootsTransportObservation { &self.endpoint_uri } - pub fn endpoint_fingerprint(&self) -> &RadrootsTransportTargetFingerprint { + pub fn endpoint_fingerprint(&self) -> &TargetFingerprint { &self.endpoint_fingerprint } @@ -242,8 +240,7 @@ impl RadrootsTransportObservation { &self, event_id: &str, ) -> Result<(), RadrootsEventStoreError> { - let target = - RadrootsTransportTarget::new(self.transport_kind.clone(), self.endpoint_uri.as_str())?; + let target = Target::new(self.transport_kind, self.endpoint_uri.as_str())?; if target.uri() != &self.endpoint_uri || target.fingerprint() != &self.endpoint_fingerprint { return Err( @@ -260,9 +257,9 @@ impl RadrootsTransportObservation { #[cfg(test)] pub(crate) fn from_unchecked_parts_for_test( - transport_kind: RadrootsTransportKind, + transport_kind: TransportId, endpoint_uri: RadrootsTransportTargetUri, - endpoint_fingerprint: RadrootsTransportTargetFingerprint, + endpoint_fingerprint: TargetFingerprint, observation_type: RadrootsTransportObservationType, observed_at_ms: i64, ) -> Self { @@ -694,7 +691,7 @@ mod tests { } assert!(RadrootsTransportObservationType::parse("bad").is_err()); let observation = RadrootsTransportObservation::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, "wss://relay.example.test", RadrootsTransportObservationType::Fetch, 1, @@ -708,7 +705,7 @@ mod tests { "wss://relay.example.test" ); let canonical_relay = RadrootsTransportObservation::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, "WSS://RELAY.EXAMPLE.TEST/", RadrootsTransportObservationType::Fetch, 1, @@ -726,7 +723,7 @@ mod tests { ] { assert!( RadrootsTransportObservation::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, invalid_relay, RadrootsTransportObservationType::Fetch, 1, @@ -736,14 +733,13 @@ mod tests { ); } let reticulum = RadrootsTransportObservation::new( - RadrootsTransportKind::Reticulum, + TransportId::RETICULUM, "reticulum:local", RadrootsTransportObservationType::MeshHeard, 1, ) .expect("Reticulum observation"); - let expected_reticulum = - RadrootsTransportTarget::reticulum().expect("canonical Reticulum target"); + let expected_reticulum = Target::reticulum().expect("canonical Reticulum target"); assert_eq!(reticulum.endpoint_uri(), expected_reticulum.uri()); assert_eq!( reticulum.endpoint_fingerprint(), @@ -751,7 +747,7 @@ mod tests { ); assert!( RadrootsTransportObservation::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, "not a URI", RadrootsTransportObservationType::Fetch, 1, @@ -760,7 +756,7 @@ mod tests { ); assert!(matches!( RadrootsTransportObservation::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, "wss://relay.example.test", RadrootsTransportObservationType::Fetch, -1, diff --git a/crates/event_store/src/store.rs b/crates/event_store/src/store.rs @@ -70,10 +70,8 @@ use radroots_event::envelope::event_head::v1::{ }; use radroots_event::id::{DTag, EventId, MutationId, TradeId}; use radroots_event::trade::TradeMutationKindV1; -use radroots_transport::{ - RadrootsTransportKind, RadrootsTransportTarget, RadrootsTransportTargetFingerprint, - RadrootsTransportTargetUri, -}; +use radroots_transport::target::TargetFingerprint; +use radroots_transport::{RadrootsTransportTargetUri, Target, TransportId}; #[cfg(test)] use sha2::{Digest, Sha256}; use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions}; @@ -320,10 +318,10 @@ impl RadrootsEventStore { /// delivery evidence remains available from transport delivery receipts. pub async fn observations_for_endpoint( &self, - transport_kind: RadrootsTransportKind, + transport_kind: TransportId, endpoint_uri: impl AsRef<str>, ) -> Result<Vec<RadrootsTransportObservationRow>, RadrootsEventStoreError> { - let target = RadrootsTransportTarget::new(transport_kind, endpoint_uri)?; + let target = Target::new(transport_kind, endpoint_uri)?; let rows = sqlx::query( "SELECT event_id, transport_kind, endpoint_uri, endpoint_fingerprint, observation_type, first_observed_at_ms, last_observed_at_ms, observation_count, redacted_message FROM event_transport_observation WHERE transport_kind = ? AND endpoint_fingerprint = ? ORDER BY last_observed_at_ms, event_id, observation_type", ) @@ -1150,9 +1148,9 @@ pub async fn inspect_event_store_status( #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsTransportObservationRow { pub event_id: String, - pub transport_kind: RadrootsTransportKind, + pub transport_kind: TransportId, pub endpoint_uri: RadrootsTransportTargetUri, - pub endpoint_fingerprint: RadrootsTransportTargetFingerprint, + pub endpoint_fingerprint: TargetFingerprint, pub observation_type: RadrootsTransportObservationType, pub first_observed_at_ms: i64, pub last_observed_at_ms: i64, @@ -1631,10 +1629,9 @@ fn transport_observation_from_row( let transport_kind_label: String = row.try_get("transport_kind")?; let endpoint_uri_raw: String = row.try_get("endpoint_uri")?; let endpoint_fingerprint_raw: String = row.try_get("endpoint_fingerprint")?; - let transport_kind = RadrootsTransportKind::parse_canonical(&transport_kind_label)?; - let endpoint_fingerprint = - RadrootsTransportTargetFingerprint::parse(&endpoint_fingerprint_raw)?; - let target = RadrootsTransportTarget::new(transport_kind, &endpoint_uri_raw)?; + let transport_kind = TransportId::parse_canonical(&transport_kind_label)?; + let endpoint_fingerprint = TargetFingerprint::parse(&endpoint_fingerprint_raw)?; + let target = Target::new(transport_kind, &endpoint_uri_raw)?; if target.uri().as_str() != endpoint_uri_raw || endpoint_fingerprint.as_str() != endpoint_fingerprint_raw || &endpoint_fingerprint != target.fingerprint() @@ -1674,7 +1671,7 @@ fn transport_observation_from_row( .transpose()?; Ok(RadrootsTransportObservationRow { event_id, - transport_kind: target.kind().clone(), + transport_kind: *target.kind(), endpoint_uri: target.uri().clone(), endpoint_fingerprint: target.fingerprint().clone(), observation_type: RadrootsTransportObservationType::parse( @@ -4613,7 +4610,7 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", "hello", ); let observation = RadrootsTransportObservation::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, "wss://relay.example.com", crate::RadrootsTransportObservationType::PublishAck, 1_100, @@ -5186,7 +5183,7 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", crate::RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1 ); let duplicate_observation = RadrootsTransportObservation::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, "wss://capacity-boundary.example.test", RadrootsTransportObservationType::Subscription, 1_100, @@ -5235,7 +5232,7 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", )); let ephemeral = signed_event(KIND_GEOCHAT, 22, Vec::new(), "live-only boundary event"); let ephemeral_observation = RadrootsTransportObservation::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, "wss://ephemeral-boundary.example.test", RadrootsTransportObservationType::Subscription, 1_300, @@ -6296,7 +6293,7 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", let unsupported = signed_event(29_999, 16, Vec::new(), "unsupported"); let invalid = signed_event(KIND_RELAY_AUTH, 17, Vec::new(), "not-json"); let observation = RadrootsTransportObservation::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, "wss://relay.example.test", RadrootsTransportObservationType::Subscription, 2_260, @@ -9159,7 +9156,7 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", let store = RadrootsEventStore::open_memory().await.expect("open"); let event = signed_event(KIND_POST, 15, Vec::new(), "hello"); let observation = RadrootsTransportObservation::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, "wss://relay.local", crate::RadrootsTransportObservationType::Subscription, 4_000, @@ -9168,7 +9165,7 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", let ingest = RadrootsEventIngest::new(event.clone(), 4_000).with_observation(observation); store.ingest_event(ingest).await.expect("first"); let observation = RadrootsTransportObservation::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, "wss://relay.local", crate::RadrootsTransportObservationType::Subscription, 4_100, @@ -9179,7 +9176,7 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", let ingest = RadrootsEventIngest::new(event.clone(), 4_100).with_observation(observation); store.ingest_event(ingest).await.expect("second"); let observation = RadrootsTransportObservation::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, "wss://relay.local", crate::RadrootsTransportObservationType::Subscription, 4_050, @@ -9204,7 +9201,7 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", ); let observation = RadrootsTransportObservation::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, "wss://relay.local", crate::RadrootsTransportObservationType::Subscription, 4_100, @@ -9215,7 +9212,7 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", let ingest = RadrootsEventIngest::new(event.clone(), 4_100).with_observation(observation); store.ingest_event(ingest).await.expect("tie duplicate"); let observation = RadrootsTransportObservation::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, "wss://relay.local", crate::RadrootsTransportObservationType::Subscription, 4_100, @@ -9227,7 +9224,7 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", .await .expect("tie duplicate without message"); let observation = RadrootsTransportObservation::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, "wss://relay.local", crate::RadrootsTransportObservationType::Subscription, 4_200, @@ -9244,7 +9241,7 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", .await .expect("observations"); assert_eq!(observations.len(), 1); - assert_eq!(observations[0].transport_kind, RadrootsTransportKind::Nostr); + assert_eq!(observations[0].transport_kind, TransportId::NOSTR); assert_eq!(observations[0].endpoint_uri.as_str(), "wss://relay.local"); assert_eq!( observations[0].observation_type, @@ -9259,13 +9256,13 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", ); let endpoint_observations = store - .observations_for_endpoint(RadrootsTransportKind::Nostr, "WSS://RELAY.LOCAL/") + .observations_for_endpoint(TransportId::NOSTR, "WSS://RELAY.LOCAL/") .await .expect("endpoint observations"); assert_eq!(endpoint_observations, observations); let reticulum_observation = RadrootsTransportObservation::new( - RadrootsTransportKind::Reticulum, + TransportId::RETICULUM, "reticulum:local", crate::RadrootsTransportObservationType::MeshHeard, 4_300, @@ -9278,12 +9275,11 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", .await .expect("Reticulum observation ingest"); let reticulum_observations = store - .observations_for_endpoint(RadrootsTransportKind::Reticulum, "reticulum:local") + .observations_for_endpoint(TransportId::RETICULUM, "reticulum:local") .await .expect("Reticulum endpoint observations"); assert_eq!(reticulum_observations.len(), 1); - let expected_reticulum = - RadrootsTransportTarget::reticulum().expect("canonical Reticulum target"); + let expected_reticulum = Target::reticulum().expect("canonical Reticulum target"); assert_eq!( &reticulum_observations[0].endpoint_fingerprint, expected_reticulum.fingerprint() @@ -9297,9 +9293,9 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", let endpoint_uri = RadrootsTransportTargetUri::parse("wss://relay-a.local").expect("endpoint A"); let endpoint_b = - RadrootsTransportTarget::nostr_relay("wss://relay-b.local").expect("endpoint B"); + Target::new(TransportId::NOSTR, "wss://relay-b.local").expect("endpoint B"); let observation = RadrootsTransportObservation::from_unchecked_parts_for_test( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, endpoint_uri, endpoint_b.fingerprint().clone(), crate::RadrootsTransportObservationType::Subscription, @@ -9338,7 +9334,7 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", let store = RadrootsEventStore::open_memory().await.expect("open"); let event = signed_event(KIND_POST, 16, Vec::new(), "observation corruption"); let observation = RadrootsTransportObservation::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, "wss://relay.local", crate::RadrootsTransportObservationType::Subscription, 4_000, @@ -9375,7 +9371,7 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", .expect("corrupt observation time order"); assert!(matches!( store - .observations_for_endpoint(RadrootsTransportKind::Nostr, "wss://relay.local") + .observations_for_endpoint(TransportId::NOSTR, "wss://relay.local") .await, Err(RadrootsEventStoreError::InvalidStoredTransportObservation { first_observed_at_ms: 4_100, diff --git a/crates/event_store/src/store/post_core_storage_v1.rs b/crates/event_store/src/store/post_core_storage_v1.rs @@ -9,7 +9,7 @@ use radroots_event::id::{CandidateId, MutationId}; use radroots_event::trade::{ SellerReservationAssertionV1, TradeMutationEnvelopeV1, TradeMutationKindV1, }; -use radroots_transport::RadrootsTransportKind; +use radroots_transport::TransportId; use sqlx::{Sqlite, Transaction}; pub(super) struct PostCoreStorageV1<'borrow, 'db> { @@ -205,7 +205,7 @@ impl<'borrow, 'db> PostCoreStorageV1<'borrow, 'db> { .bind(write.event.id_hex()) .bind(write.mutation_id.to_hex()) .bind(write.mutation.trade_id.to_hex()) - .bind(RadrootsTransportKind::Nostr.canonical_label()) + .bind(TransportId::NOSTR.canonical_label()) .bind(write.event.author().to_hex()) .bind(i64_from_u64("created_at", write.event.created_at_u64())?) .bind(write.stored_event_seq) diff --git a/crates/mesh/src/model.rs b/crates/mesh/src/model.rs @@ -1,7 +1,8 @@ use crate::RadrootsMeshError; use alloc::string::{String, ToString}; use alloc::vec::Vec; -use radroots_transport::{RadrootsTransportError, RadrootsTransportMeshScopeId}; +use radroots_transport::RadrootsTransportError; +use radroots_transport::target::TargetScope; pub const RADROOTS_MESH_FRAME_VERSION: u16 = 1; pub const RADROOTS_MESH_UNAVAILABLE_MESSAGE: &str = @@ -65,11 +66,10 @@ pub enum RadrootsMeshScope { impl RadrootsMeshScope { pub fn custom(value: impl AsRef<str>) -> Result<Self, RadrootsMeshError> { - let scope = - RadrootsTransportMeshScopeId::parse(value.as_ref()).map_err(|err| match err { - RadrootsTransportError::EmptyTargetScope => RadrootsMeshError::EmptyCustomScope, - _ => RadrootsMeshError::InvalidCustomScope, - })?; + let scope = TargetScope::parse(value.as_ref()).map_err(|err| match err { + RadrootsTransportError::EmptyTargetScope => RadrootsMeshError::EmptyCustomScope, + _ => RadrootsMeshError::InvalidCustomScope, + })?; Ok(Self::Custom(scope.as_str().to_string())) } diff --git a/crates/mesh_agent_client/src/lib.rs b/crates/mesh_agent_client/src/lib.rs @@ -6,8 +6,9 @@ use radroots_mesh::{ use radroots_mesh_agent_proto::{ RADROOTS_MESH_AGENT_SCHEMA_ID, RADROOTS_MESH_AGENT_SCHEMA_NAMESPACE, schema_sha256_hex, }; -use radroots_transport::{RadrootsTransportKind, RadrootsTransportMeshScopeId}; -use radroots_transport_reticulum::RADROOTS_RETICULUM_ENDPOINT_URI; +use radroots_transport::TransportId; +use radroots_transport::target::TargetScope; +use radroots_transport_reticulum::{RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_SCOPE_ID}; pub const RADROOTS_MESH_AGENT_CLIENT_SCHEMA_ID: &str = RADROOTS_MESH_AGENT_SCHEMA_ID; pub const RADROOTS_MESH_AGENT_CLIENT_SCHEMA_NAMESPACE: &str = RADROOTS_MESH_AGENT_SCHEMA_NAMESPACE; @@ -108,9 +109,9 @@ impl MeshAgentTransportKind { } } - pub fn transport_kind(self) -> RadrootsTransportKind { + pub fn transport_kind(self) -> TransportId { match self { - Self::Reticulum => RadrootsTransportKind::Reticulum, + Self::Reticulum => TransportId::RETICULUM, } } } @@ -212,7 +213,7 @@ pub trait RadrootsMeshAgentClient { pub struct RadrootsMockMeshAgentClient { profile_id: String, endpoint_uri: String, - scope: RadrootsTransportMeshScopeId, + scope: TargetScope, policy: RadrootsMeshPayloadPolicy, } @@ -221,12 +222,12 @@ impl RadrootsMockMeshAgentClient { Self { profile_id: RADROOTS_MESH_RETICULUM_POLICY_ID.to_owned(), endpoint_uri: RADROOTS_RETICULUM_ENDPOINT_URI.to_owned(), - scope: RadrootsTransportMeshScopeId::local_reticulum(), + scope: TargetScope::parse(RADROOTS_RETICULUM_SCOPE_ID).expect("Reticulum scope"), policy: RadrootsMeshPayloadPolicy::reticulum_unavailable(), } } - pub fn scope(&self) -> &RadrootsTransportMeshScopeId { + pub fn scope(&self) -> &TargetScope { &self.scope } diff --git a/crates/mesh_agent_client/tests/client.rs b/crates/mesh_agent_client/tests/client.rs @@ -12,7 +12,7 @@ use radroots_mesh_agent_client::{ use radroots_mesh_agent_proto::{ RADROOTS_MESH_AGENT_SCHEMA_ID, RADROOTS_MESH_AGENT_SCHEMA_NAMESPACE, schema_sha256_hex, }; -use radroots_transport::RadrootsTransportKind; +use radroots_transport::TransportId; use radroots_transport_reticulum::{RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_SCOPE_ID}; use serde_json::json; @@ -40,10 +40,7 @@ fn mock_client_status_reports_reticulum_unavailable() { assert_eq!(response.transports.len(), 1); let status = &response.transports[0]; assert_eq!(status.transport, MeshAgentTransportKind::Reticulum); - assert_eq!( - status.transport.transport_kind(), - RadrootsTransportKind::Reticulum - ); + assert_eq!(status.transport.transport_kind(), TransportId::RETICULUM); assert_eq!(status.profile_id, RADROOTS_MESH_RETICULUM_POLICY_ID); assert_eq!(status.endpoint_uri, RADROOTS_RETICULUM_ENDPOINT_URI); assert!(status.configured); diff --git a/crates/outbox/src/model.rs b/crates/outbox/src/model.rs @@ -3,11 +3,10 @@ use crate::RadrootsOutboxError; use radroots_event::draft::{EventDraft, SignedEvent}; use radroots_event::id::{MutationId, TradeId}; +use radroots_transport::target::{TargetFingerprint, TargetLabel, TargetScope}; use radroots_transport::{ - RadrootsTransportKind, RadrootsTransportMeshScopeId, RadrootsTransportOutcomeKind, - RadrootsTransportSatisfactionClass, RadrootsTransportSatisfactionPolicy, - RadrootsTransportTarget, RadrootsTransportTargetFingerprint, RadrootsTransportTargetLabel, - RadrootsTransportTargetUri, + RadrootsTransportOutcomeKind, RadrootsTransportSatisfactionClass, + RadrootsTransportSatisfactionPolicy, RadrootsTransportTargetUri, Target, TransportId, }; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -248,7 +247,7 @@ pub struct RadrootsOutboxDeliveryPlanInput { pub transport_profile_id: String, pub target_policy_version: u32, pub satisfaction_policy: RadrootsTransportSatisfactionPolicy, - pub targets: Vec<RadrootsTransportTarget>, + pub targets: Vec<Target>, pub reticulum_behavior: RadrootsOutboxReticulumBehavior, } @@ -257,7 +256,7 @@ impl RadrootsOutboxDeliveryPlanInput { transport_profile_id: impl Into<String>, target_policy_version: u32, satisfaction_policy: RadrootsTransportSatisfactionPolicy, - targets: Vec<RadrootsTransportTarget>, + targets: Vec<Target>, ) -> Self { Self { transport_profile_id: transport_profile_id.into(), @@ -518,11 +517,11 @@ pub struct RadrootsOutboxDeliveryPlanRecord { pub struct RadrootsOutboxDeliveryTargetRecord { pub delivery_target_id: i64, pub delivery_plan_id: i64, - pub transport_kind: RadrootsTransportKind, + pub transport_kind: TransportId, pub endpoint_uri: RadrootsTransportTargetUri, - pub target_scope: Option<RadrootsTransportMeshScopeId>, - pub target_label: Option<RadrootsTransportTargetLabel>, - pub endpoint_fingerprint: RadrootsTransportTargetFingerprint, + pub target_scope: Option<TargetScope>, + pub target_label: Option<TargetLabel>, + pub endpoint_fingerprint: TargetFingerprint, pub status: RadrootsOutboxDeliveryTargetStatus, pub last_outcome_kind: Option<RadrootsTransportOutcomeKind>, pub attempt_count: i64, diff --git a/crates/outbox/src/store.rs b/crates/outbox/src/store.rs @@ -25,11 +25,10 @@ use radroots_event_store::{ RadrootsTransportObservationType, }; use radroots_protocol::radrootsd::transport_publish::v5::RETICULUM_ENDPOINT_URI as RADROOTS_RETICULUM_ENDPOINT_URI; +use radroots_transport::target::{TargetFingerprint, TargetLabel, TargetScope}; use radroots_transport::{ - RadrootsTransportError, RadrootsTransportKind, RadrootsTransportMeshScopeId, - RadrootsTransportOutcomeKind, RadrootsTransportSatisfactionClass, - RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, - RadrootsTransportTargetFingerprint, RadrootsTransportTargetLabel, + RadrootsTransportError, RadrootsTransportOutcomeKind, RadrootsTransportSatisfactionClass, + RadrootsTransportSatisfactionPolicy, Target, TransportId, }; use serde::Serialize; use sha2::{Digest, Sha256}; @@ -1228,7 +1227,7 @@ impl RadrootsOutbox { .signed_event .ok_or(RadrootsOutboxError::MissingSignedEvent(outbox_event_id))?; let observation = RadrootsTransportObservation::new( - RadrootsTransportKind::Local, + TransportId::LOCAL, "local:outbox", RadrootsTransportObservationType::LocalImport, observed_at_ms, @@ -1752,7 +1751,7 @@ struct PreparedDeliveryPlan { } struct PreparedDeliveryTarget { - target: RadrootsTransportTarget, + target: Target, initial_status: RadrootsOutboxDeliveryTargetStatus, } @@ -2004,8 +2003,8 @@ fn satisfaction_target_count( delivery_capable_target_count } -fn validate_delivery_target(target: &RadrootsTransportTarget) -> Result<(), RadrootsOutboxError> { - if target.kind() == &RadrootsTransportKind::Reticulum +fn validate_delivery_target(target: &Target) -> Result<(), RadrootsOutboxError> { + if target.kind() == &TransportId::RETICULUM && target.uri().as_str() != RADROOTS_RETICULUM_ENDPOINT_URI { return Err(RadrootsOutboxError::Transport( @@ -2015,7 +2014,7 @@ fn validate_delivery_target(target: &RadrootsTransportTarget) -> Result<(), Radr Ok(()) } -fn validate_unique_targets(targets: &[RadrootsTransportTarget]) -> Result<(), RadrootsOutboxError> { +fn validate_unique_targets(targets: &[Target]) -> Result<(), RadrootsOutboxError> { let mut fingerprints = BTreeSet::new(); for target in targets { if !fingerprints.insert(target.fingerprint().as_str()) { @@ -2028,10 +2027,10 @@ fn validate_unique_targets(targets: &[RadrootsTransportTarget]) -> Result<(), Ra } fn initial_delivery_target_status( - target: &RadrootsTransportTarget, + target: &Target, _reticulum_behavior: RadrootsOutboxReticulumBehavior, ) -> RadrootsOutboxDeliveryTargetStatus { - if target.kind() != &RadrootsTransportKind::Reticulum { + if target.kind() != &TransportId::RETICULUM { return RadrootsOutboxDeliveryTargetStatus::Pending; } RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented @@ -2186,13 +2185,13 @@ async fn insert_or_get_delivery_plan( prepared_target .target .scope() - .map(RadrootsTransportMeshScopeId::as_str), + .map(TargetScope::as_str), ) .bind( prepared_target .target .label() - .map(RadrootsTransportTargetLabel::as_str), + .map(TargetLabel::as_str), ) .bind(prepared_target.target.fingerprint().as_str()) .bind(prepared_target.initial_status.as_str()) @@ -2846,59 +2845,53 @@ fn delivery_target_from_row( field, }; let transport_kind_raw = row.try_get::<String, _>("transport_kind")?; - let transport_kind = RadrootsTransportKind::parse_canonical(transport_kind_raw.as_str()) + let transport_kind = TransportId::parse_canonical(transport_kind_raw.as_str()) .map_err(|_| invalid_identity("transport_kind"))?; let endpoint_uri_raw = row.try_get::<String, _>("endpoint_uri")?; let target_scope_raw = row.try_get::<Option<String>, _>("target_scope")?; let target_scope = target_scope_raw .as_deref() - .map(RadrootsTransportMeshScopeId::parse) + .map(TargetScope::parse) .transpose() .map_err(|_| invalid_identity("target_scope"))?; - if target_scope - .as_ref() - .map(RadrootsTransportMeshScopeId::as_str) - != target_scope_raw.as_deref() - { + if target_scope.as_ref().map(TargetScope::as_str) != target_scope_raw.as_deref() { return Err(invalid_identity("target_scope")); } let target_label_raw = row.try_get::<Option<String>, _>("target_label")?; let target_label = target_label_raw .as_deref() - .map(RadrootsTransportTargetLabel::parse) + .map(TargetLabel::parse) .transpose() .map_err(|_| invalid_identity("target_label"))?; - if target_label - .as_ref() - .map(RadrootsTransportTargetLabel::as_str) - != target_label_raw.as_deref() - { + if target_label.as_ref().map(TargetLabel::as_str) != target_label_raw.as_deref() { return Err(invalid_identity("target_label")); } let endpoint_fingerprint_raw = row.try_get::<String, _>("endpoint_fingerprint")?; - let endpoint_fingerprint = - RadrootsTransportTargetFingerprint::parse(endpoint_fingerprint_raw.as_str()) - .map_err(|_| invalid_identity("endpoint_fingerprint"))?; + let endpoint_fingerprint = TargetFingerprint::parse(endpoint_fingerprint_raw.as_str()) + .map_err(|_| invalid_identity("endpoint_fingerprint"))?; if endpoint_fingerprint.as_str() != endpoint_fingerprint_raw { return Err(invalid_identity("endpoint_fingerprint")); } let target = match transport_kind { - RadrootsTransportKind::Nostr => RadrootsTransportTarget::nostr_relay_with_metadata( + TransportId::NOSTR => Target::new_with_metadata( + TransportId::NOSTR, endpoint_uri_raw.as_str(), target_scope.clone(), target_label.clone(), ), - RadrootsTransportKind::Reticulum => RadrootsTransportTarget::reticulum_with_metadata( + TransportId::RETICULUM => Target::new_with_metadata( + TransportId::RETICULUM, endpoint_uri_raw.as_str(), target_scope.clone(), target_label.clone(), ), - RadrootsTransportKind::Local => RadrootsTransportTarget::local_with_metadata( + TransportId::LOCAL => Target::new_with_metadata( + TransportId::LOCAL, endpoint_uri_raw.as_str(), target_scope.clone(), target_label.clone(), ), - _ => RadrootsTransportTarget::new_with_metadata( + _ => Target::new_with_metadata( transport_kind, endpoint_uri_raw.as_str(), target_scope.clone(), @@ -2924,7 +2917,7 @@ fn delivery_target_from_row( Ok(RadrootsOutboxDeliveryTargetRecord { delivery_target_id, delivery_plan_id: row.try_get("delivery_plan_id")?, - transport_kind: target.kind().clone(), + transport_kind: *target.kind(), endpoint_uri: target.uri().clone(), target_scope: target.scope().cloned(), target_label: target.label().cloned(), @@ -3265,7 +3258,7 @@ fn satisfaction_policy_storage_value(policy: &RadrootsTransportSatisfactionPolic RadrootsTransportSatisfactionPolicy::RequiredTargets { class, targets } => { let mut fingerprints = targets .iter() - .map(RadrootsTransportTargetFingerprint::as_str) + .map(TargetFingerprint::as_str) .collect::<Vec<_>>(); fingerprints.sort(); let fingerprints = fingerprints.join(","); @@ -3353,7 +3346,7 @@ fn parse_required_target_policy( ) -> Result<RadrootsTransportSatisfactionPolicy, RadrootsOutboxError> { let targets = fingerprints .split(',') - .map(RadrootsTransportTargetFingerprint::parse) + .map(TargetFingerprint::parse) .collect::<Result<Vec<_>, _>>()?; let required_success_count = usize::from(required_count_u16(required_success_count)?); if required_success_count != targets.len() { @@ -3596,31 +3589,33 @@ mod tests { (draft, signed_event) } - fn nostr_target(uri: &str) -> RadrootsTransportTarget { - RadrootsTransportTarget::nostr_relay(uri).expect("nostr target") + fn nostr_target(uri: &str) -> Target { + Target::new(TransportId::NOSTR, uri).expect("nostr target") } - fn scoped_nostr_target(uri: &str, scope: &str, label: &str) -> RadrootsTransportTarget { - RadrootsTransportTarget::nostr_relay_with_metadata( + fn scoped_nostr_target(uri: &str, scope: &str, label: &str) -> Target { + Target::new_with_metadata( + TransportId::NOSTR, uri, - Some(RadrootsTransportMeshScopeId::parse(scope).expect("target scope")), - Some(RadrootsTransportTargetLabel::parse(label).expect("target label")), + Some(TargetScope::parse(scope).expect("target scope")), + Some(TargetLabel::parse(label).expect("target label")), ) .expect("scoped nostr target") } - fn scoped_reticulum_target(scope: &str, label: &str) -> RadrootsTransportTarget { - RadrootsTransportTarget::reticulum_with_metadata( + fn scoped_reticulum_target(scope: &str, label: &str) -> Target { + Target::new_with_metadata( + TransportId::RETICULUM, RADROOTS_RETICULUM_ENDPOINT_URI, - Some(RadrootsTransportMeshScopeId::parse(scope).expect("target scope")), - Some(RadrootsTransportTargetLabel::parse(label).expect("target label")), + Some(TargetScope::parse(scope).expect("target scope")), + Some(TargetLabel::parse(label).expect("target label")), ) .expect("scoped reticulum target") } - fn reticulum_target(uri: &str) -> RadrootsTransportTarget { + fn reticulum_target(uri: &str) -> Target { assert_eq!(uri, RADROOTS_RETICULUM_ENDPOINT_URI); - RadrootsTransportTarget::reticulum().expect("reticulum target") + Target::reticulum().expect("reticulum target") } #[test] @@ -3989,7 +3984,7 @@ mod tests { assert!(prepare_delivery_plan("event-no-wait", &empty_no_wait).is_ok()); } - fn delivery_plan(targets: Vec<RadrootsTransportTarget>) -> RadrootsOutboxDeliveryPlanInput { + fn delivery_plan(targets: Vec<Target>) -> RadrootsOutboxDeliveryPlanInput { RadrootsOutboxDeliveryPlanInput::new( "transport.nostr.local", 1, @@ -4033,13 +4028,13 @@ mod tests { canonical: &TradeCanonicalMutationV1, draft: EventDraft, signed_event: SignedEvent, - targets: Vec<RadrootsTransportTarget>, + targets: Vec<Target>, created_at_ms: i64, ) -> RadrootsOutboxSignedTradeMutationInput { RadrootsOutboxSignedTradeMutationInput::new( "publish_trade_mutation", - canonical.envelope.trade_id.clone(), - canonical.mutation_id.clone(), + canonical.envelope.trade_id, + canonical.mutation_id, sha256_hex(canonical.content.as_bytes()), draft, signed_event, @@ -4053,13 +4048,13 @@ mod tests { fn trade_mutation_input( canonical: &TradeCanonicalMutationV1, draft: EventDraft, - targets: Vec<RadrootsTransportTarget>, + targets: Vec<Target>, created_at_ms: i64, ) -> RadrootsOutboxTradeMutationInput { RadrootsOutboxTradeMutationInput::new( "publish_trade_mutation", - canonical.envelope.trade_id.clone(), - canonical.mutation_id.clone(), + canonical.envelope.trade_id, + canonical.mutation_id, sha256_hex(canonical.content.as_bytes()), draft, delivery_plan(targets), @@ -4659,7 +4654,7 @@ mod tests { .is_err() ); sqlx::query("UPDATE outbox_operations SET trade_id = ?, mutation_id = 'invalid' WHERE operation_id = ?") - .bind(&canonical.envelope.trade_id.to_hex()) + .bind(canonical.envelope.trade_id.to_hex()) .bind(trade_operation.operation_id) .execute(outbox.pool()) .await @@ -4671,7 +4666,7 @@ mod tests { .is_err() ); sqlx::query("UPDATE outbox_operations SET mutation_id = ? WHERE operation_id = ?") - .bind(&canonical.mutation_id.to_hex()) + .bind(canonical.mutation_id.to_hex()) .bind(trade_operation.operation_id) .execute(outbox.pool()) .await @@ -5776,7 +5771,7 @@ mod tests { sqlx::query( "UPDATE outbox_operations SET mutation_id = ?, operation_idempotency_digest = 'corrupt' WHERE operation_id = ?", ) - .bind(&canonical.mutation_id.to_hex()) + .bind(canonical.mutation_id.to_hex()) .bind(first.operation_id) .execute(outbox.pool()) .await @@ -6037,9 +6032,7 @@ mod tests { .enqueue_operation(RadrootsOutboxOperationInput::new( "publish_post", draft, - delivery_plan(vec![ - RadrootsTransportTarget::reticulum().expect("Reticulum target"), - ]), + delivery_plan(vec![Target::reticulum().expect("Reticulum target")]), 1_000, )) .await @@ -6118,7 +6111,7 @@ mod tests { async fn enqueue_rejects_invalid_reticulum_targets_before_persistence() { let outbox = RadrootsOutbox::open_memory().await.expect("open"); assert_eq!( - RadrootsTransportTarget::new(RadrootsTransportKind::Reticulum, "reticulum:alternate",) + Target::new(TransportId::RETICULUM, "reticulum:alternate",) .expect_err("invalid Reticulum target"), RadrootsTransportError::InvalidTargetUri ); @@ -8110,7 +8103,7 @@ mod tests { .await .expect("observations"); assert_eq!(observations.len(), 1); - assert_eq!(observations[0].transport_kind, RadrootsTransportKind::Local); + assert_eq!(observations[0].transport_kind, TransportId::LOCAL); assert_eq!(observations[0].endpoint_uri.as_str(), "local:outbox"); assert_eq!( observations[0].observation_type, diff --git a/crates/runtime/src/transport.rs b/crates/runtime/src/transport.rs @@ -7,9 +7,9 @@ use radroots_event::{draft::SignedEvent, wire::Nip01EventWire}; #[cfg(feature = "transport-workers")] use radroots_transport::RadrootsTransportTargetReceipt; use radroots_transport::{ - RadrootsTransport, RadrootsTransportDeliveryRequest, RadrootsTransportDeliveryTargetStatus, - RadrootsTransportError, RadrootsTransportKind, RadrootsTransportPayload, - RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, RadrootsTransportTargetSet, + EventSink, EventSource, RadrootsTransport, RadrootsTransportDeliveryRequest, + RadrootsTransportDeliveryTargetStatus, RadrootsTransportError, RadrootsTransportPayload, + RadrootsTransportSatisfactionPolicy, Target, TargetSet, TransportId, }; use thiserror::Error; @@ -99,7 +99,7 @@ fn verify_signed_event_raw_json_matches_event( pub struct RadrootsRuntimeTransportDispatchRequest { pub request_id: String, pub payload: RadrootsRuntimeTransportPayload, - pub target_set: RadrootsTransportTargetSet, + pub target_set: TargetSet, pub satisfaction_policy: RadrootsTransportSatisfactionPolicy, pub now_ms: i64, } @@ -108,14 +108,14 @@ impl RadrootsRuntimeTransportDispatchRequest { pub fn new( request_id: impl Into<String>, payload: RadrootsRuntimeTransportPayload, - targets: Vec<RadrootsTransportTarget>, + targets: Vec<Target>, satisfaction_policy: RadrootsTransportSatisfactionPolicy, now_ms: i64, ) -> Result<Self, RadrootsRuntimeTransportError> { if targets.is_empty() { return Err(RadrootsRuntimeTransportError::EmptyDispatchTargets); } - let target_set = RadrootsTransportTargetSet::new(targets)?; + let target_set = TargetSet::new(targets)?; Ok(Self { request_id: request_id.into(), payload, @@ -140,7 +140,10 @@ impl RadrootsRuntimeTransportDispatchRequest { #[derive(Clone, Default)] pub struct RadrootsRuntimeTransportRegistry { - transports: BTreeMap<RadrootsTransportKind, Arc<dyn RadrootsTransport>>, + sources: BTreeMap<TransportId, Arc<dyn EventSource>>, + sinks: BTreeMap<TransportId, Arc<dyn EventSink>>, + #[doc(hidden)] + transports: BTreeMap<TransportId, Arc<dyn RadrootsTransport>>, } impl RadrootsRuntimeTransportRegistry { @@ -148,6 +151,67 @@ impl RadrootsRuntimeTransportRegistry { Self::default() } + pub fn register_source<T>( + &mut self, + transport_id: TransportId, + source: T, + ) -> Result<(), RadrootsRuntimeTransportError> + where + T: EventSource + 'static, + { + if self.sources.contains_key(&transport_id) { + return Err(RadrootsRuntimeTransportError::TransportAlreadyRegistered( + transport_id.canonical_label(), + )); + } + self.sources.insert(transport_id, Arc::new(source)); + Ok(()) + } + + pub fn register_sink<T>( + &mut self, + transport_id: TransportId, + sink: T, + ) -> Result<(), RadrootsRuntimeTransportError> + where + T: EventSink + 'static, + { + if self.sinks.contains_key(&transport_id) { + return Err(RadrootsRuntimeTransportError::TransportAlreadyRegistered( + transport_id.canonical_label(), + )); + } + self.sinks.insert(transport_id, Arc::new(sink)); + Ok(()) + } + + pub fn source( + &self, + transport_id: &TransportId, + ) -> Result<Arc<dyn EventSource>, RadrootsRuntimeTransportError> { + self.sources.get(transport_id).cloned().ok_or_else(|| { + RadrootsRuntimeTransportError::TransportNotRegistered(transport_id.canonical_label()) + }) + } + + pub fn sink( + &self, + transport_id: &TransportId, + ) -> Result<Arc<dyn EventSink>, RadrootsRuntimeTransportError> { + self.sinks.get(transport_id).cloned().ok_or_else(|| { + RadrootsRuntimeTransportError::TransportNotRegistered(transport_id.canonical_label()) + }) + } + + pub fn registered_source_ids(&self) -> Vec<TransportId> { + self.sources.keys().copied().collect() + } + + pub fn registered_sink_ids(&self) -> Vec<TransportId> { + self.sinks.keys().copied().collect() + } + + #[doc(hidden)] pub fn register<T>(&mut self, transport: T) -> Result<(), RadrootsRuntimeTransportError> where T: RadrootsTransport + 'static, @@ -162,17 +226,25 @@ impl RadrootsRuntimeTransportRegistry { Ok(()) } + #[doc(hidden)] pub fn transport( &self, - kind: &RadrootsTransportKind, + kind: &TransportId, ) -> Result<Arc<dyn RadrootsTransport>, RadrootsRuntimeTransportError> { self.transports.get(kind).cloned().ok_or_else(|| { RadrootsRuntimeTransportError::TransportNotRegistered(kind.canonical_label()) }) } - pub fn registered_kinds(&self) -> Vec<RadrootsTransportKind> { - self.transports.keys().cloned().collect() + pub fn registered_kinds(&self) -> Vec<TransportId> { + self.sources + .keys() + .chain(self.sinks.keys()) + .chain(self.transports.keys()) + .copied() + .collect::<std::collections::BTreeSet<_>>() + .into_iter() + .collect() } } @@ -251,12 +323,12 @@ impl<T> RadrootsRuntimeBoundedQueue<T> { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsRuntimeDeliveryTarget { pub delivery_target_id: i64, - pub target: RadrootsTransportTarget, + pub target: Target, pub status: RadrootsTransportDeliveryTargetStatus, } impl RadrootsRuntimeDeliveryTarget { - pub fn ready(delivery_target_id: i64, target: RadrootsTransportTarget) -> Self { + pub fn ready(delivery_target_id: i64, target: Target) -> Self { Self { delivery_target_id, target, @@ -264,10 +336,7 @@ impl RadrootsRuntimeDeliveryTarget { } } - pub fn deferred_until_implemented( - delivery_target_id: i64, - target: RadrootsTransportTarget, - ) -> Self { + pub fn deferred_until_implemented(delivery_target_id: i64, target: Target) -> Self { Self { delivery_target_id, target, @@ -313,7 +382,7 @@ impl Default for RadrootsRuntimeDeliveryWorkerConfig { #[cfg(feature = "transport-workers")] #[derive(Clone, Debug, PartialEq, Eq)] struct RadrootsRuntimeDeliveryTargetState { - target: RadrootsTransportTarget, + target: Target, status: RadrootsTransportDeliveryTargetStatus, } @@ -480,15 +549,14 @@ impl<'a> RadrootsRuntimeDeliveryWorker<'a> { ) }) .collect::<BTreeMap<_, _>>(); - let mut by_kind = - BTreeMap::<RadrootsTransportKind, Vec<RadrootsRuntimeDeliveryTarget>>::new(); + let mut by_kind = BTreeMap::<TransportId, Vec<RadrootsRuntimeDeliveryTarget>>::new(); for target in plan .targets .into_iter() .filter(RadrootsRuntimeDeliveryTarget::is_ready_for_attempt) { by_kind - .entry(target.target.kind().clone()) + .entry(*target.target.kind()) .or_default() .push(target); } @@ -586,7 +654,7 @@ pub fn recover_expired_leases(leases: &mut [RadrootsRuntimeLeaseRecord], now_ms: pub struct RadrootsRuntimeInboundObservation { pub event_id: String, pub verified: bool, - pub transport_kind: RadrootsTransportKind, + pub transport_kind: TransportId, pub endpoint_uri: String, pub observed_at_ms: i64, } @@ -595,7 +663,7 @@ pub struct RadrootsRuntimeInboundObservation { impl RadrootsRuntimeInboundObservation { pub fn verified_signed_event( event: &SignedEvent, - transport_kind: RadrootsTransportKind, + transport_kind: TransportId, endpoint_uri: impl Into<String>, observed_at_ms: i64, ) -> Self { @@ -673,28 +741,26 @@ mod tests { #[cfg(feature = "transport-workers")] use radroots_event::{draft::SignedEvent, wire::Nip01EventWire}; use radroots_transport::{ - RadrootsTransport, RadrootsTransportCapabilities, RadrootsTransportCapabilityAvailability, - RadrootsTransportCapabilityMaturity, RadrootsTransportDeliveryReceipt, + RadrootsTransport, RadrootsTransportCapabilities, RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest, RadrootsTransportDeliveryTargetStatus, RadrootsTransportError, RadrootsTransportFetchReceipt, RadrootsTransportFetchRequest, - RadrootsTransportFuture, RadrootsTransportImplementationState, RadrootsTransportKind, - RadrootsTransportOutcome, RadrootsTransportOutcomeKind, RadrootsTransportPayload, - RadrootsTransportSatisfactionClass, RadrootsTransportSatisfactionPolicy, - RadrootsTransportStatus, RadrootsTransportTarget, RadrootsTransportTargetReceipt, - RadrootsTransportTargetSet, + RadrootsTransportFuture, RadrootsTransportImplementationState, RadrootsTransportOutcome, + RadrootsTransportOutcomeKind, RadrootsTransportPayload, RadrootsTransportSatisfactionClass, + RadrootsTransportSatisfactionPolicy, RadrootsTransportStatus, + RadrootsTransportTargetReceipt, Target, TargetSet, TransportId, }; #[cfg(feature = "transport-workers")] use std::sync::{Arc, Mutex}; struct StaticTransport { - kind: RadrootsTransportKind, + kind: TransportId, outcome_kind: RadrootsTransportOutcomeKind, #[cfg(feature = "transport-workers")] captured_now_ms: Option<Arc<Mutex<Vec<i64>>>>, } impl StaticTransport { - fn new(kind: RadrootsTransportKind, outcome_kind: RadrootsTransportOutcomeKind) -> Self { + fn new(kind: TransportId, outcome_kind: RadrootsTransportOutcomeKind) -> Self { Self { kind, outcome_kind, @@ -705,7 +771,7 @@ mod tests { #[cfg(feature = "transport-workers")] fn recording_now_ms( - kind: RadrootsTransportKind, + kind: TransportId, outcome_kind: RadrootsTransportOutcomeKind, ) -> (Self, Arc<Mutex<Vec<i64>>>) { let captured_now_ms = Arc::new(Mutex::new(Vec::new())); @@ -721,14 +787,14 @@ mod tests { } impl RadrootsTransport for StaticTransport { - fn transport_kind(&self) -> RadrootsTransportKind { - self.kind.clone() + fn transport_kind(&self) -> TransportId { + self.kind } fn status<'a>(&'a self) -> RadrootsTransportFuture<'a, RadrootsTransportStatus> { Box::pin(async move { Ok(RadrootsTransportStatus::new( - self.kind.clone(), + self.kind, true, RadrootsTransportImplementationState::Real, true, @@ -807,14 +873,14 @@ mod tests { #[cfg(feature = "transport-workers")] impl RadrootsTransport for ForgedReceiptTransport { - fn transport_kind(&self) -> RadrootsTransportKind { - RadrootsTransportKind::Nostr + fn transport_kind(&self) -> TransportId { + TransportId::NOSTR } fn status<'a>(&'a self) -> RadrootsTransportFuture<'a, RadrootsTransportStatus> { Box::pin(async { Ok(RadrootsTransportStatus::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, true, RadrootsTransportImplementationState::Real, true, @@ -848,11 +914,10 @@ mod tests { .collect(), ), ForgedDeliveryReceipt::TargetSet => { - let target = - RadrootsTransportTarget::nostr_relay("wss://forged-relay.example")?; + let target = Target::new(TransportId::NOSTR, "wss://forged-relay.example")?; RadrootsTransportDeliveryReceipt::new( request.request_id(), - RadrootsTransportTargetSet::new(vec![target.clone()])?, + TargetSet::new(vec![target.clone()])?, vec![RadrootsTransportTargetReceipt::new( target, RadrootsTransportOutcome::new( @@ -873,8 +938,8 @@ mod tests { } } - fn target(kind: RadrootsTransportKind, uri: &str) -> RadrootsTransportTarget { - RadrootsTransportTarget::new(kind, uri).expect("target") + fn target(kind: TransportId, uri: &str) -> Target { + Target::new(kind, uri).expect("target") } fn opaque_payload() -> RadrootsRuntimeTransportPayload { @@ -956,29 +1021,26 @@ mod tests { let mut registry = RadrootsRuntimeTransportRegistry::new(); registry .register(StaticTransport::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, RadrootsTransportOutcomeKind::Accepted, )) .expect("register"); - assert_eq!( - registry.registered_kinds(), - vec![RadrootsTransportKind::Nostr] - ); + assert_eq!(registry.registered_kinds(), vec![TransportId::NOSTR]); assert!(matches!( registry.register(StaticTransport::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, RadrootsTransportOutcomeKind::Accepted, )), Err(RadrootsRuntimeTransportError::TransportAlreadyRegistered(_)) )); let transport = registry - .transport(&RadrootsTransportKind::Nostr) + .transport(&TransportId::NOSTR) .expect("nostr transport"); let request = RadrootsRuntimeTransportDispatchRequest::new( "nostr-delivery", opaque_payload(), - vec![target(RadrootsTransportKind::Nostr, "wss://relay.example")], + vec![target(TransportId::NOSTR, "wss://relay.example")], RadrootsTransportSatisfactionPolicy::any_accepted(), 1_000, ) @@ -992,7 +1054,7 @@ mod tests { .await .expect("receipt"); let status = transport.status().await.expect("status"); - assert_eq!(status.kind, RadrootsTransportKind::Nostr); + assert_eq!(status.kind, TransportId::NOSTR); assert_eq!( status.capabilities, RadrootsTransportCapabilities::deliver_and_fetch() @@ -1000,11 +1062,8 @@ mod tests { let fetch = transport .fetch(RadrootsTransportFetchRequest::new( "nostr-fetch", - RadrootsTransportTargetSet::new(vec![target( - RadrootsTransportKind::Nostr, - "wss://relay.example", - )]) - .expect("target set"), + TargetSet::new(vec![target(TransportId::NOSTR, "wss://relay.example")]) + .expect("target set"), )) .await .expect("fetch"); @@ -1025,7 +1084,7 @@ mod tests { let request = RadrootsRuntimeTransportDispatchRequest::new( "nostr-delivery", opaque_payload(), - vec![target(RadrootsTransportKind::Nostr, "wss://relay.example")], + vec![target(TransportId::NOSTR, "wss://relay.example")], RadrootsTransportSatisfactionPolicy::any_accepted(), 123_456, ) @@ -1040,65 +1099,38 @@ mod tests { #[cfg(feature = "transport-reticulum")] #[tokio::test] - async fn registry_dispatches_reticulum_transport_without_success() { + async fn registry_exposes_reticulum_as_split_unavailable_capabilities() { let mut registry = RadrootsRuntimeTransportRegistry::new(); + let transport = radroots_transport_reticulum::RadrootsReticulumTransport::default(); registry - .register(radroots_transport_reticulum::RadrootsReticulumTransport::default()) - .expect("register"); - let transport = registry - .transport(&RadrootsTransportKind::Reticulum) - .expect("reticulum transport"); - let request = RadrootsRuntimeTransportDispatchRequest::new( - "reticulum-delivery", - opaque_payload(), - vec![target(RadrootsTransportKind::Reticulum, "reticulum:local")], - RadrootsTransportSatisfactionPolicy::any_accepted(), - 1_000, - ) - .expect("request"); - let receipt = transport - .deliver( - request - .transport_delivery_request() - .expect("delivery request"), - ) - .await - .expect("receipt"); - let status = transport.status().await.expect("status"); - assert_eq!( - status.implementation, - RadrootsTransportImplementationState::Real - ); - assert_eq!( - status.maturity, - RadrootsTransportCapabilityMaturity::Preview - ); + .register_sink(TransportId::RETICULUM, transport.clone()) + .expect("register sink"); + registry + .register_source(TransportId::RETICULUM, transport) + .expect("register source"); + let sink = registry + .sink(&TransportId::RETICULUM) + .expect("reticulum sink"); + let source = registry + .source(&TransportId::RETICULUM) + .expect("reticulum source"); + let sink_status = sink.status().await.expect("sink status"); + let source_status = source.status().await.expect("source status"); + assert_eq!( - status.availability, - RadrootsTransportCapabilityAvailability::Unavailable + sink_status.availability(), + radroots_transport::capability::Availability::Unavailable ); - assert!(!status.capabilities.deliver); - assert!(!status.capabilities.fetch); - let fetch = transport - .fetch(RadrootsTransportFetchRequest::new( - "reticulum-fetch", - RadrootsTransportTargetSet::new(vec![target( - RadrootsTransportKind::Reticulum, - "reticulum:local", - )]) - .expect("target set"), - )) - .await - .expect("fetch"); - assert_eq!(fetch.fetched_count, 0); - assert_eq!( - receipt.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted), - 0 + source_status.availability(), + radroots_transport::capability::Availability::Unavailable ); + assert!(!sink_status.capabilities().can_deliver()); + assert!(!source_status.capabilities().can_fetch()); + assert_eq!(registry.registered_sink_ids(), vec![TransportId::RETICULUM]); assert_eq!( - receipt.target_receipts()[0].status, - RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented + registry.registered_source_ids(), + vec![TransportId::RETICULUM] ); } @@ -1123,7 +1155,7 @@ mod tests { let mut registry = RadrootsRuntimeTransportRegistry::new(); registry .register(StaticTransport::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, RadrootsTransportOutcomeKind::Accepted, )) .expect("register"); @@ -1135,11 +1167,11 @@ mod tests { ); let ready = RadrootsRuntimeDeliveryTarget::ready( 1, - target(RadrootsTransportKind::Nostr, "wss://relay.example"), + target(TransportId::NOSTR, "wss://relay.example"), ); let deferred = RadrootsRuntimeDeliveryTarget::deferred_until_implemented( 2, - target(RadrootsTransportKind::Reticulum, "reticulum:local"), + target(TransportId::RETICULUM, "reticulum:local"), ); let receipt = worker .execute_job(RadrootsRuntimeDeliveryJob { @@ -1180,7 +1212,7 @@ mod tests { async fn delivery_worker_passes_job_now_ms_to_registered_transport() { let mut registry = RadrootsRuntimeTransportRegistry::new(); let (transport, captured_now_ms) = StaticTransport::recording_now_ms( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, RadrootsTransportOutcomeKind::Accepted, ); registry.register(transport).expect("register"); @@ -1199,7 +1231,7 @@ mod tests { satisfaction_policy: RadrootsTransportSatisfactionPolicy::any_accepted(), targets: vec![RadrootsRuntimeDeliveryTarget::ready( 1, - target(RadrootsTransportKind::Nostr, "wss://relay.example"), + target(TransportId::NOSTR, "wss://relay.example"), )], }], now_ms: 987_654, @@ -1241,7 +1273,7 @@ mod tests { satisfaction_policy: RadrootsTransportSatisfactionPolicy::any_accepted(), targets: vec![RadrootsRuntimeDeliveryTarget::ready( 1, - target(RadrootsTransportKind::Nostr, "wss://relay.example"), + target(TransportId::NOSTR, "wss://relay.example"), )], }], now_ms: 1_000, @@ -1297,7 +1329,7 @@ mod tests { let mut registry = RadrootsRuntimeTransportRegistry::new(); registry .register(StaticTransport::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, RadrootsTransportOutcomeKind::Accepted, )) .expect("register"); @@ -1307,7 +1339,7 @@ mod tests { bounded_queue_capacity: 8, }, ); - let required_target = target(RadrootsTransportKind::Reticulum, "reticulum:local"); + let required_target = target(TransportId::RETICULUM, "reticulum:local"); let required_fingerprint = required_target.fingerprint().clone(); let receipt = worker .execute_job(RadrootsRuntimeDeliveryJob { @@ -1323,7 +1355,7 @@ mod tests { targets: vec![ RadrootsRuntimeDeliveryTarget::ready( 1, - target(RadrootsTransportKind::Nostr, "wss://relay.example"), + target(TransportId::NOSTR, "wss://relay.example"), ), RadrootsRuntimeDeliveryTarget::deferred_until_implemented( 2, @@ -1354,7 +1386,7 @@ mod tests { let mut registry = RadrootsRuntimeTransportRegistry::new(); registry .register(StaticTransport::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, RadrootsTransportOutcomeKind::Accepted, )) .expect("register"); @@ -1373,7 +1405,7 @@ mod tests { satisfaction_policy: RadrootsTransportSatisfactionPolicy::any_delivered(), targets: vec![RadrootsRuntimeDeliveryTarget::ready( 1, - target(RadrootsTransportKind::Nostr, "wss://relay.example"), + target(TransportId::NOSTR, "wss://relay.example"), )], }], now_ms: 1_000, @@ -1397,7 +1429,7 @@ mod tests { let mut registry = RadrootsRuntimeTransportRegistry::new(); registry .register(StaticTransport::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, RadrootsTransportOutcomeKind::Accepted, )) .expect("register"); @@ -1417,11 +1449,11 @@ mod tests { targets: vec![ RadrootsRuntimeDeliveryTarget::ready( 1, - target(RadrootsTransportKind::Nostr, "wss://relay-a.example"), + target(TransportId::NOSTR, "wss://relay-a.example"), ), RadrootsRuntimeDeliveryTarget::ready( 2, - target(RadrootsTransportKind::Nostr, "wss://relay-b.example"), + target(TransportId::NOSTR, "wss://relay-b.example"), ), ], }], @@ -1446,10 +1478,7 @@ mod tests { #[tokio::test] async fn delivery_worker_evaluates_cross_transport_quorum_globally() { let mut registry = RadrootsRuntimeTransportRegistry::new(); - for kind in [ - RadrootsTransportKind::Nostr, - RadrootsTransportKind::Reticulum, - ] { + for kind in [TransportId::NOSTR, TransportId::RETICULUM] { registry .register(StaticTransport::new( kind, @@ -1473,11 +1502,11 @@ mod tests { targets: vec![ RadrootsRuntimeDeliveryTarget::ready( 1, - target(RadrootsTransportKind::Nostr, "wss://relay.example"), + target(TransportId::NOSTR, "wss://relay.example"), ), RadrootsRuntimeDeliveryTarget::ready( 2, - target(RadrootsTransportKind::Reticulum, "reticulum:local"), + target(TransportId::RETICULUM, "reticulum:local"), ), ], }], @@ -1506,7 +1535,7 @@ mod tests { ); let reticulum_target = RadrootsRuntimeDeliveryTarget { delivery_target_id: 1, - target: target(RadrootsTransportKind::Reticulum, "reticulum:local"), + target: target(TransportId::RETICULUM, "reticulum:local"), status: RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented, }; let receipt = worker @@ -1569,7 +1598,7 @@ mod tests { let observation = RadrootsRuntimeInboundObservation::verified_signed_event( &event, - RadrootsTransportKind::Nostr, + TransportId::NOSTR, "wss://relay.example", 1_000, ); @@ -1580,7 +1609,7 @@ mod tests { let unverified = RadrootsRuntimeInboundObservation { event_id: event.id_str().to_owned(), verified: false, - transport_kind: RadrootsTransportKind::Nostr, + transport_kind: TransportId::NOSTR, endpoint_uri: "wss://relay.example".to_owned(), observed_at_ms: 1_000, }; @@ -1592,7 +1621,7 @@ mod tests { let mismatched = RadrootsRuntimeInboundObservation { event_id: "a".repeat(64), verified: true, - transport_kind: RadrootsTransportKind::Nostr, + transport_kind: TransportId::NOSTR, endpoint_uri: "wss://relay.example".to_owned(), observed_at_ms: 1_000, }; diff --git a/crates/transport/tests/source_boundary.rs b/crates/transport/tests/source_boundary.rs @@ -29,6 +29,26 @@ const CORE_STATUS_CONTRACT_SOURCE_ROOTS: &[&str] = &["transport/src", "transport const CORE_TRANSPORT_CONTRACT_SOURCE_ROOTS: &[&str] = &["transport/src"]; +const TRANSPORT_CONSUMER_SOURCE_ROOTS: &[&str] = &[ + "event_store/src", + "mesh/src", + "mesh_agent_client/src", + "outbox/src", + "runtime/src", + "transport_nostr/src", + "transport_publish_protocol/src", + "transport_reticulum/src", +]; + +const RETIRED_TRANSPORT_TYPE_NAMES: &[&str] = &[ + "RadrootsTransportKind", + "RadrootsTransportMeshScopeId", + "RadrootsTransportTarget", + "RadrootsTransportTargetFingerprint", + "RadrootsTransportTargetLabel", + "RadrootsTransportTargetSet", +]; + const DELIVERY_PAYLOAD_CONTRACT_SOURCE_ROOTS: &[&str] = &["transport/src", "runtime/src", "transport_reticulum/src"]; @@ -489,7 +509,7 @@ fn foundation_hardening_repo_sources_reject_retired_names_and_ambiguous_docs() { } #[test] -fn runtime_transport_registry_uses_core_transport_contract() { +fn workspace_consumers_use_split_transport_spis_with_one_runtime_shim() { let crates_root = Path::new(env!("CARGO_MANIFEST_DIR")) .parent() .expect("transport crate parent"); @@ -499,17 +519,22 @@ fn runtime_transport_registry_uses_core_transport_contract() { read_source(crates_root.join("transport_reticulum/src/lib.rs").as_path()); let reticulum_source = production_source(reticulum_source_raw.as_str()); + for required in [ + "Arc<dyn EventSource>", + "Arc<dyn EventSink>", + "pub fn register_source<T>", + "pub fn register_sink<T>", + "T: EventSource + 'static", + "T: EventSink + 'static", + ] { + assert!( + runtime_source.contains(required), + "runtime registry must expose final split SPI witness `{required}`" + ); + } assert!( runtime_source.contains("Arc<dyn RadrootsTransport>"), - "runtime registry must store the core RadrootsTransport trait object" - ); - assert!( - runtime_source.contains("T: RadrootsTransport + 'static"), - "runtime registry registration must accept the core RadrootsTransport trait" - ); - assert!( - runtime_source.contains("transport.transport_kind()"), - "runtime registry must key transports through the core trait transport_kind" + "mixed runtime delivery workers retain the sole temporary monolithic shim until RCLD 40" ); let removed_reticulum_runtime_transport = ["RadrootsRuntimeReticulum", "Pre", "viewTransport"].concat(); @@ -523,9 +548,63 @@ fn runtime_transport_registry_uses_core_transport_contract() { "runtime transport source must not retain split adapter contract `{forbidden}`" ); } + for required in [ + "impl EventSource for RadrootsReticulumTransport", + "impl EventSink for RadrootsReticulumTransport", + ] { + assert!( + reticulum_source.contains(required), + "Reticulum preview must implement final split SPI witness `{required}`" + ); + } + assert!( + !reticulum_source.contains("impl RadrootsTransport for RadrootsReticulumTransport"), + "Reticulum preview must not implement the predecessor monolithic SPI" + ); + + let nostr_source = read_source(crates_root.join("transport_nostr/src/publish.rs").as_path()); + assert!( + nostr_source.contains("impl<A> EventSink for RadrootsNostrTransport<A>"), + "Nostr adapter must implement the final sink-only SPI" + ); + assert!( + !nostr_source.contains("impl<A> RadrootsTransport for RadrootsNostrTransport<A>"), + "Nostr adapter must not implement the predecessor monolithic SPI" + ); +} + +#[test] +fn canonical_workspace_consumers_reject_retired_transport_type_names() { + let crates_root = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("transport crate parent"); + let mut findings = Vec::new(); + + for relative_root in TRANSPORT_CONSUMER_SOURCE_ROOTS { + for path in rust_source_files(crates_root.join(relative_root).as_path()) { + if path + .components() + .any(|component| component.as_os_str() == "generated") + { + continue; + } + let source_raw = read_source(path.as_path()); + let source = production_source(source_raw.as_str()); + let relative_path = relative_path(crates_root, path.as_path()); + for retired in RETIRED_TRANSPORT_TYPE_NAMES { + if contains_forbidden_concept(source, retired) { + findings.push(format!( + "{relative_path} still consumes retired transport type `{retired}`" + )); + } + } + } + } + assert!( - reticulum_source.contains("impl RadrootsTransport for RadrootsReticulumTransport"), - "Reticulum transport must implement the core transport contract" + findings.is_empty(), + "canonical transport consumer migration violations:\n{}", + findings.join("\n") ); } @@ -651,7 +730,7 @@ fn transport_target_identity_sources_reject_silent_dedupe() { let relay_source = read_source(crates_root.join("transport_nostr/src/relay.rs").as_path()); for required in [ - "RadrootsTransportTarget::nostr_relay(original)", + "Target::new(TransportId::NOSTR, original)", "RadrootsRelayTransportError::DuplicateRelayUrl", ] { assert!( diff --git a/crates/transport_nostr/src/fetch.rs b/crates/transport_nostr/src/fetch.rs @@ -12,7 +12,7 @@ use radroots_event_store::{ RadrootsTransportObservation, RadrootsTransportObservationType, }; use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrFilter}; -use radroots_transport::{RadrootsTransportKind, RadrootsTransportTarget}; +use radroots_transport::{Target, TransportId}; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; use std::sync::{Arc, Mutex, PoisonError}; @@ -482,7 +482,7 @@ where } }; let observation = RadrootsTransportObservation::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, relay_url.clone(), observation_type, observed_at_ms, @@ -1127,7 +1127,7 @@ fn canonical_requested_fetch_relay( target_relays: &[String], relay_url: &str, ) -> Result<String, RadrootsRelayTransportError> { - let target = RadrootsTransportTarget::nostr_relay(relay_url).map_err(|error| { + let target = Target::new(TransportId::NOSTR, relay_url).map_err(|error| { RadrootsRelayTransportError::InvalidFetchItemRelayUrl { url: relay_url.to_owned(), reason: error.to_string(), diff --git a/crates/transport_nostr/src/outbox.rs b/crates/transport_nostr/src/outbox.rs @@ -5,7 +5,6 @@ use crate::{ RadrootsRelayOutcome, RadrootsRelayPublishAdapter, RadrootsRelayPublishReceipt, RadrootsRelayPublishRelayReceipt, RadrootsRelayPublishRequest, RadrootsRelayTargetSet, RadrootsRelayTransportError, RadrootsRelayUrlPolicy, publish_signed_event, - verified_signed_event_payload, }; use radroots_event::draft::SignedEvent; use radroots_event_store::{ @@ -16,13 +15,21 @@ use radroots_outbox::{ RadrootsOutbox, RadrootsOutboxClaimedEvent, RadrootsOutboxDeliveryTargetRecord, RadrootsOutboxDeliveryTargetStatus, RadrootsOutboxEventStoreIngestReceipt, }; +#[cfg(test)] +use radroots_transport::RadrootsTransportDeliveryReceipt; +use radroots_transport::outcome::{DeliveryOutcomeKind, Retryability}; +use radroots_transport::policy::{ + SatisfactionClass, SatisfactionPolicy as SinkSatisfactionPolicy, TargetPolicy, +}; +use radroots_transport::sink::{DeliveryPayload, DeliveryReceipt, DeliveryRequest, EventSink}; +use radroots_transport::target::{TargetFingerprint, TargetLabel, TargetScope}; use radroots_transport::{ - RadrootsTransport, RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest, - RadrootsTransportDeliveryTargetStatus, RadrootsTransportError, RadrootsTransportKind, - RadrootsTransportOutcome, RadrootsTransportOutcomeKind, RadrootsTransportSatisfactionClass, - RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, - RadrootsTransportTargetFingerprint, RadrootsTransportTargetSet, + RadrootsTransportDeliveryTargetStatus, RadrootsTransportError, + RadrootsTransportSatisfactionClass, RadrootsTransportSatisfactionPolicy, Target, TargetSet, + TransportId, }; +#[cfg(test)] +use radroots_transport::{RadrootsTransportOutcome, RadrootsTransportOutcomeKind}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsOutboxPublishPolicy { @@ -69,7 +76,7 @@ pub struct RadrootsOutboxPublishReceipt { pub struct RadrootsOutboxPublishTargetReceipt { pub delivery_target_id: i64, pub endpoint_uri: String, - pub endpoint_fingerprint: RadrootsTransportTargetFingerprint, + pub endpoint_fingerprint: TargetFingerprint, pub target_scope: Option<String>, pub target_label: Option<String>, pub attempted: bool, @@ -224,15 +231,18 @@ pub async fn publish_claimed_outbox_event_with_transport<T>( now_ms: i64, ) -> Result<RadrootsOutboxPublishReceipt, RadrootsRelayTransportError> where - T: RadrootsTransport + ?Sized, + T: EventSink + ?Sized, { ensure_nonnegative_timestamp("now_ms", now_ms)?; ensure_nonnegative_timestamp("next_attempt_after_ms", policy.next_attempt_after_ms)?; - let transport_kind = transport.transport_kind(); - if transport_kind != RadrootsTransportKind::Nostr { + let status = transport + .status() + .await + .map_err(transport_error_to_relay_error)?; + if status.transport_id() != TransportId::NOSTR { return Err(RadrootsRelayTransportError::UnexpectedTransportKind { expected: "nostr", - actual: transport_kind.canonical_label(), + actual: status.transport_id().canonical_label(), }); } let signed_event = claimed.signed_event.clone().ok_or( @@ -276,20 +286,27 @@ where policy.relay_url_policy, )?; let transport_targets = publishable_transport_targets(&publishable)?; - let target_set = RadrootsTransportTargetSet::new(transport_targets)?; - let satisfaction_policy = transport_satisfaction_policy_for_publishable(&publishable); + let target_set = TargetSet::new(transport_targets)?; let request_id = outbox_publish_idempotency_key( claimed.outbox_event_id, claimed.attempt_count, signed_event.id_str(), publishable.active_delivery_plan_id, ); - let payload = - verified_signed_event_payload(&signed_event).map_err(transport_error_to_relay_error)?; - let delivery_request = - RadrootsTransportDeliveryRequest::new(request_id, payload, target_set, satisfaction_policy) - .and_then(|request| request.try_with_now_ms(now_ms)) - .map_err(transport_error_to_relay_error)?; + let deadline_unix_ms = + u64::try_from(policy.next_attempt_after_ms.max(now_ms).max(1)).map_err(|_| { + RadrootsRelayTransportError::TransportContract( + "outbox delivery deadline is outside the transport range".to_owned(), + ) + })?; + let delivery_request = DeliveryRequest::new( + request_id, + DeliveryPayload::new(signed_event.clone()), + target_set, + SinkSatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::all()), + deadline_unix_ms, + ) + .map_err(transport_error_to_relay_error)?; let delivery = transport .deliver(delivery_request.clone()) .await @@ -297,8 +314,8 @@ where delivery .validate_for_request(&delivery_request) .map_err(transport_error_to_relay_error)?; - let relay_receipts = relay_receipts_from_transport_receipts(&delivery)?; - let target_receipts = target_receipts_from_transport_receipts(&publishable, &delivery); + let relay_receipts = relay_receipts_from_delivery_receipts(&delivery)?; + let target_receipts = target_receipts_from_delivery_receipts(&publishable, &delivery); for target_receipt in &target_receipts { complete_outbox_delivery_target(outbox, claimed, target_receipt, now_ms).await?; @@ -398,8 +415,9 @@ struct PublishableRelays { satisfaction_required_count: usize, remaining_satisfaction_count: usize, satisfaction_class: RadrootsTransportSatisfactionClass, - required_targets: Option<Vec<RadrootsTransportTargetFingerprint>>, - remaining_required_targets: Option<Vec<RadrootsTransportTargetFingerprint>>, + required_targets: Option<Vec<TargetFingerprint>>, + #[cfg_attr(not(test), allow(dead_code))] + remaining_required_targets: Option<Vec<TargetFingerprint>>, } impl PublishableRelays { @@ -407,7 +425,7 @@ impl PublishableRelays { &'a self, relay_url: &'a str, ) -> impl Iterator<Item = &'a PublishableRelay> + 'a { - let canonical_relay_url = RadrootsTransportTarget::nostr_relay(relay_url) + let canonical_relay_url = Target::new(TransportId::NOSTR, relay_url) .ok() .map(|target| target.uri().as_str().to_owned()); self.relays.iter().filter(move |target| { @@ -440,7 +458,7 @@ impl PublishableRelays { struct PublishableRelay { delivery_target_id: i64, relay_url: String, - endpoint_fingerprint: RadrootsTransportTargetFingerprint, + endpoint_fingerprint: TargetFingerprint, target_scope: Option<String>, target_label: Option<String>, } @@ -478,9 +496,9 @@ fn target_receipts_from_relay_receipts( target_receipts } -fn target_receipts_from_transport_receipts( +fn target_receipts_from_delivery_receipts( publishable: &PublishableRelays, - delivery: &RadrootsTransportDeliveryReceipt, + delivery: &DeliveryReceipt, ) -> Vec<RadrootsOutboxPublishTargetReceipt> { delivery .target_receipts() @@ -489,16 +507,16 @@ fn target_receipts_from_transport_receipts( publishable .relays .iter() - .find(|target| target.endpoint_fingerprint == *receipt.target.fingerprint()) + .find(|target| target.endpoint_fingerprint == *receipt.target().fingerprint()) .map(|target| RadrootsOutboxPublishTargetReceipt { delivery_target_id: target.delivery_target_id, endpoint_uri: target.relay_url.clone(), endpoint_fingerprint: target.endpoint_fingerprint.clone(), target_scope: target.target_scope.clone(), target_label: target.target_label.clone(), - attempted: receipt.attempted, - transport_status: receipt.status, - outcome: relay_outcome_from_transport_outcome(&receipt.outcome), + attempted: receipt.was_attempted(), + transport_status: delivery_target_status(receipt.outcome()), + outcome: relay_outcome_from_delivery_outcome(receipt.outcome()), }) }) .collect() @@ -631,16 +649,16 @@ async fn complete_outbox_delivery_target( Ok(()) } -fn relay_receipts_from_transport_receipts( - delivery: &RadrootsTransportDeliveryReceipt, +fn relay_receipts_from_delivery_receipts( + delivery: &DeliveryReceipt, ) -> Result<Vec<RadrootsRelayPublishRelayReceipt>, RadrootsRelayTransportError> { let mut relay_receipts: Vec<RadrootsRelayPublishRelayReceipt> = Vec::new(); for receipt in delivery.target_receipts() { - let outcome = relay_outcome_from_transport_outcome(&receipt.outcome); - let relay_receipt = if receipt.attempted { - RadrootsRelayPublishRelayReceipt::attempted(receipt.target.uri().as_str(), outcome) + let outcome = relay_outcome_from_delivery_outcome(receipt.outcome()); + let relay_receipt = if receipt.was_attempted() { + RadrootsRelayPublishRelayReceipt::attempted(receipt.target().uri().as_str(), outcome) } else { - RadrootsRelayPublishRelayReceipt::skipped(receipt.target.uri().as_str(), outcome) + RadrootsRelayPublishRelayReceipt::skipped(receipt.target().uri().as_str(), outcome) }; if let Some(existing) = relay_receipts .iter() @@ -660,6 +678,46 @@ fn relay_receipts_from_transport_receipts( Ok(relay_receipts) } +fn delivery_target_status( + outcome: &radroots_transport::outcome::DeliveryOutcome, +) -> RadrootsTransportDeliveryTargetStatus { + match outcome.kind() { + DeliveryOutcomeKind::Accepted => RadrootsTransportDeliveryTargetStatus::Accepted, + DeliveryOutcomeKind::Delivered => RadrootsTransportDeliveryTargetStatus::Delivered, + DeliveryOutcomeKind::Rejected => RadrootsTransportDeliveryTargetStatus::FailedTerminal, + DeliveryOutcomeKind::Unavailable => RadrootsTransportDeliveryTargetStatus::FailedRetryable, + DeliveryOutcomeKind::Failed => match outcome.retryability() { + Retryability::Retryable => RadrootsTransportDeliveryTargetStatus::FailedRetryable, + Retryability::Terminal | Retryability::NotApplicable => { + RadrootsTransportDeliveryTargetStatus::FailedTerminal + } + }, + } +} + +fn relay_outcome_from_delivery_outcome( + outcome: &radroots_transport::outcome::DeliveryOutcome, +) -> RadrootsRelayOutcome { + let kind = outcome + .code() + .and_then(relay_outcome_kind_from_code) + .unwrap_or(match outcome.kind() { + DeliveryOutcomeKind::Accepted => crate::RadrootsRelayOutcomeKind::Accepted, + DeliveryOutcomeKind::Delivered => crate::RadrootsRelayOutcomeKind::Accepted, + DeliveryOutcomeKind::Rejected => crate::RadrootsRelayOutcomeKind::Invalid, + DeliveryOutcomeKind::Unavailable => crate::RadrootsRelayOutcomeKind::Error, + DeliveryOutcomeKind::Failed if outcome.is_retryable() => { + crate::RadrootsRelayOutcomeKind::Error + } + DeliveryOutcomeKind::Failed => crate::RadrootsRelayOutcomeKind::Invalid, + }); + RadrootsRelayOutcome { + kind, + message: outcome.message().map(str::to_owned), + } +} + +#[cfg(test)] fn relay_outcome_from_transport_outcome( outcome: &RadrootsTransportOutcome, ) -> RadrootsRelayOutcome { @@ -674,6 +732,63 @@ fn relay_outcome_from_transport_outcome( } } +#[cfg(test)] +fn target_receipts_from_transport_receipts( + publishable: &PublishableRelays, + delivery: &RadrootsTransportDeliveryReceipt, +) -> Vec<RadrootsOutboxPublishTargetReceipt> { + delivery + .target_receipts() + .iter() + .filter_map(|receipt| { + publishable + .relays + .iter() + .find(|target| target.endpoint_fingerprint == *receipt.target.fingerprint()) + .map(|target| RadrootsOutboxPublishTargetReceipt { + delivery_target_id: target.delivery_target_id, + endpoint_uri: target.relay_url.clone(), + endpoint_fingerprint: target.endpoint_fingerprint.clone(), + target_scope: target.target_scope.clone(), + target_label: target.target_label.clone(), + attempted: receipt.attempted, + transport_status: receipt.status, + outcome: relay_outcome_from_transport_outcome(&receipt.outcome), + }) + }) + .collect() +} + +#[cfg(test)] +fn relay_receipts_from_transport_receipts( + delivery: &RadrootsTransportDeliveryReceipt, +) -> Result<Vec<RadrootsRelayPublishRelayReceipt>, RadrootsRelayTransportError> { + let mut relay_receipts: Vec<RadrootsRelayPublishRelayReceipt> = Vec::new(); + for receipt in delivery.target_receipts() { + let outcome = relay_outcome_from_transport_outcome(&receipt.outcome); + let relay_receipt = if receipt.attempted { + RadrootsRelayPublishRelayReceipt::attempted(receipt.target.uri().as_str(), outcome) + } else { + RadrootsRelayPublishRelayReceipt::skipped(receipt.target.uri().as_str(), outcome) + }; + if let Some(existing) = relay_receipts + .iter() + .find(|existing| existing.relay_url == relay_receipt.relay_url) + { + if existing != &relay_receipt { + return Err( + RadrootsRelayTransportError::ConflictingTransportReceiptRelayUrl { + url: relay_receipt.relay_url, + }, + ); + } + continue; + } + relay_receipts.push(relay_receipt); + } + Ok(relay_receipts) +} + fn relay_outcome_kind_from_code(code: &str) -> Option<crate::RadrootsRelayOutcomeKind> { Some(match code { "accepted" => crate::RadrootsRelayOutcomeKind::Accepted, @@ -697,6 +812,7 @@ fn relay_outcome_kind_from_code(code: &str) -> Option<crate::RadrootsRelayOutcom }) } +#[cfg(test)] fn relay_outcome_kind_from_transport_outcome( kind: RadrootsTransportOutcomeKind, ) -> crate::RadrootsRelayOutcomeKind { @@ -730,23 +846,24 @@ fn relay_outcome_kind_from_transport_outcome( fn publishable_transport_targets( publishable: &PublishableRelays, -) -> Result<Vec<RadrootsTransportTarget>, RadrootsRelayTransportError> { +) -> Result<Vec<Target>, RadrootsRelayTransportError> { publishable .relays .iter() .map(|relay| { - RadrootsTransportTarget::nostr_relay_with_metadata( + Target::new_with_metadata( + TransportId::NOSTR, relay.relay_url.as_str(), relay .target_scope .as_deref() - .map(radroots_transport::RadrootsTransportMeshScopeId::parse) + .map(TargetScope::parse) .transpose() .map_err(transport_error_to_relay_error)?, relay .target_label .as_deref() - .map(radroots_transport::RadrootsTransportTargetLabel::parse) + .map(TargetLabel::parse) .transpose() .map_err(transport_error_to_relay_error)?, ) @@ -755,6 +872,7 @@ fn publishable_transport_targets( .collect() } +#[cfg(test)] fn transport_satisfaction_policy_for_publishable( publishable: &PublishableRelays, ) -> RadrootsTransportSatisfactionPolicy { @@ -1005,14 +1123,15 @@ fn is_publishable_delivery_status( } fn is_nostr_target(target: &RadrootsOutboxDeliveryTargetRecord) -> bool { - target.transport_kind == RadrootsTransportKind::Nostr + target.transport_kind == TransportId::NOSTR } +#[cfg(test)] fn satisfaction_policy_for_remaining_count( satisfaction_class: RadrootsTransportSatisfactionClass, remaining_satisfaction_count: usize, target_count: usize, - exact_required_targets: Option<&[RadrootsTransportTargetFingerprint]>, + exact_required_targets: Option<&[TargetFingerprint]>, ) -> RadrootsTransportSatisfactionPolicy { if let Some(targets) = exact_required_targets { return RadrootsTransportSatisfactionPolicy::RequiredTargets { @@ -1051,7 +1170,7 @@ async fn ingest_publish_observation( observed_at_ms: i64, ) -> Result<(), RadrootsRelayTransportError> { let observation = RadrootsTransportObservation::new( - RadrootsTransportKind::Nostr, + TransportId::NOSTR, relay_url, RadrootsTransportObservationType::PublishAck, observed_at_ms, @@ -1078,12 +1197,12 @@ mod tests { RadrootsRelayOutcome, RadrootsRelayOutcomeKind, RadrootsRelayPublishRelayReceipt, RadrootsRelayTransportError, }; + use radroots_transport::target::TargetScope; use radroots_transport::{ RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryTargetStatus, - RadrootsTransportError, RadrootsTransportMeshScopeId, RadrootsTransportOutcome, - RadrootsTransportOutcomeKind, RadrootsTransportSatisfactionClass, - RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, - RadrootsTransportTargetReceipt, RadrootsTransportTargetSet, + RadrootsTransportError, RadrootsTransportOutcome, RadrootsTransportOutcomeKind, + RadrootsTransportSatisfactionClass, RadrootsTransportSatisfactionPolicy, + RadrootsTransportTargetReceipt, Target, TargetSet, TransportId, }; #[test] @@ -1116,7 +1235,7 @@ mod tests { RadrootsTransportSatisfactionPolicy::quorum_delivered(2) ); let required_target = - RadrootsTransportTarget::nostr_relay("wss://relay.example").expect("required target"); + Target::new(TransportId::NOSTR, "wss://relay.example").expect("required target"); assert_eq!( satisfaction_policy_for_remaining_count( RadrootsTransportSatisfactionClass::Delivered, @@ -1196,7 +1315,7 @@ mod tests { #[test] fn outbox_publish_satisfaction_counts_use_active_transport_class() { - let target = RadrootsTransportTarget::nostr_relay("wss://relay.example").expect("target"); + let target = Target::new(TransportId::NOSTR, "wss://relay.example").expect("target"); let publishable = PublishableRelays { active_delivery_plan_id: 7, relays: vec![PublishableRelay { @@ -1233,7 +1352,7 @@ mod tests { let delivery = RadrootsTransportDeliveryReceipt::new( "request-1", - RadrootsTransportTargetSet::new(vec![target.clone()]).expect("target set"), + TargetSet::new(vec![target.clone()]).expect("target set"), vec![RadrootsTransportTargetReceipt::new( target, RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::Delivered), @@ -1414,7 +1533,7 @@ mod tests { #[test] fn transport_target_and_error_adapters_preserve_contract_categories() { - let target = RadrootsTransportTarget::nostr_relay("wss://relay.example").expect("target"); + let target = Target::new(TransportId::NOSTR, "wss://relay.example").expect("target"); let publishable = PublishableRelays { active_delivery_plan_id: 7, relays: vec![PublishableRelay { @@ -1509,10 +1628,10 @@ mod tests { } let unknown = - RadrootsTransportTarget::nostr_relay("wss://unknown.example").expect("unknown target"); + Target::new(TransportId::NOSTR, "wss://unknown.example").expect("unknown target"); let delivery = RadrootsTransportDeliveryReceipt::new( "unknown", - RadrootsTransportTargetSet::new(vec![unknown.clone()]).expect("target set"), + TargetSet::new(vec![unknown.clone()]).expect("target set"), vec![RadrootsTransportTargetReceipt::new( unknown, RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::Accepted), @@ -1527,23 +1646,24 @@ mod tests { 1 ); - let west = RadrootsTransportTarget::nostr_relay_with_metadata( + let west = Target::new_with_metadata( + TransportId::NOSTR, "wss://scoped.example", - Some(RadrootsTransportMeshScopeId::parse("foodshed.west").expect("west scope")), + Some(TargetScope::parse("foodshed.west").expect("west scope")), None, ) .expect("west target"); - let east = RadrootsTransportTarget::nostr_relay_with_metadata( + let east = Target::new_with_metadata( + TransportId::NOSTR, "wss://scoped.example", - Some(RadrootsTransportMeshScopeId::parse("foodshed.east").expect("east scope")), + Some(TargetScope::parse("foodshed.east").expect("east scope")), None, ) .expect("east target"); let scoped_relay_uri = west.uri().as_str().to_owned(); let conflicting = RadrootsTransportDeliveryReceipt::new( "conflicting", - RadrootsTransportTargetSet::new(vec![west.clone(), east.clone()]) - .expect("scoped target set"), + TargetSet::new(vec![west.clone(), east.clone()]).expect("scoped target set"), vec![ RadrootsTransportTargetReceipt::new( west, diff --git a/crates/transport_nostr/src/outcome.rs b/crates/transport_nostr/src/outcome.rs @@ -1,5 +1,6 @@ #![forbid(unsafe_code)] +use radroots_transport::outcome::DeliveryOutcome; use radroots_transport::{RadrootsTransportOutcome, RadrootsTransportOutcomeKind}; use serde::{Deserialize, Serialize}; @@ -214,4 +215,34 @@ impl RadrootsRelayOutcome { } outcome } + + pub(crate) fn to_delivery_outcome(&self) -> DeliveryOutcome { + let outcome = match self.kind { + RadrootsRelayOutcomeKind::Accepted + | RadrootsRelayOutcomeKind::DuplicateAccepted + | RadrootsRelayOutcomeKind::SkippedAlreadyAccepted => DeliveryOutcome::accepted(), + RadrootsRelayOutcomeKind::Blocked + | RadrootsRelayOutcomeKind::Invalid + | RadrootsRelayOutcomeKind::Restricted + | RadrootsRelayOutcomeKind::Muted + | RadrootsRelayOutcomeKind::Unsupported + | RadrootsRelayOutcomeKind::PaymentRequired + | RadrootsRelayOutcomeKind::RelayUrlRejected => DeliveryOutcome::rejected(), + RadrootsRelayOutcomeKind::RateLimited + | RadrootsRelayOutcomeKind::PowRequired + | RadrootsRelayOutcomeKind::AuthRequired + | RadrootsRelayOutcomeKind::Error + | RadrootsRelayOutcomeKind::Timeout + | RadrootsRelayOutcomeKind::ConnectionFailed + | RadrootsRelayOutcomeKind::Unknown => DeliveryOutcome::unavailable(), + }; + if let Some(message) = &self.message { + outcome + .clone() + .with_detail(self.kind.as_str(), message.clone()) + .unwrap_or(outcome) + } else { + outcome + } + } } diff --git a/crates/transport_nostr/src/publish.rs b/crates/transport_nostr/src/publish.rs @@ -5,14 +5,15 @@ use crate::{RadrootsRelayOutcome, RadrootsRelayTargetSet, RadrootsRelayTransport #[cfg(feature = "client")] use core::time::Duration; use futures::future::BoxFuture; -use radroots_event::{draft::SignedEvent, wire::Nip01EventWire}; +use radroots_event::draft::SignedEvent; +use radroots_transport::capability::{Availability, Maturity, SinkCapabilities}; +use radroots_transport::outcome::DeliveryOutcome; +use radroots_transport::sink::{ + DeliveryReceipt, DeliveryRequest, DeliveryTargetReceipt, EventSink, SinkStatus, +}; use radroots_transport::{ - RadrootsTransport, RadrootsTransportCapabilities, RadrootsTransportDeliveryReceipt, - RadrootsTransportDeliveryRequest, RadrootsTransportError, RadrootsTransportFetchReceipt, - RadrootsTransportFetchRequest, RadrootsTransportFuture, RadrootsTransportImplementationState, - RadrootsTransportKind, RadrootsTransportOutcome, RadrootsTransportOutcomeKind, - RadrootsTransportPayload, RadrootsTransportSatisfactionPolicy, RadrootsTransportStatus, - RadrootsTransportTarget, RadrootsTransportTargetReceipt, + RadrootsTransportError, RadrootsTransportPayload, RadrootsTransportSatisfactionPolicy, Target, + TransportId, }; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; @@ -104,7 +105,7 @@ impl RadrootsRelayPublishRequest { .targets .relays() .iter() - .map(|relay| RadrootsTransportTarget::nostr_relay(relay.as_str())) + .map(|relay| Target::new(TransportId::NOSTR, relay.as_str())) .collect::<Result<Vec<_>, _>>()?; for required in required_targets { if !requested @@ -212,25 +213,25 @@ where #[derive(Clone)] pub struct RadrootsNostrTransport<A> { adapter: A, - status: RadrootsTransportStatus, + status: SinkStatus, } impl<A> RadrootsNostrTransport<A> { pub fn new(adapter: A) -> Self { Self { adapter, - status: RadrootsTransportStatus::new( - RadrootsTransportKind::Nostr, - true, - RadrootsTransportImplementationState::Real, + status: SinkStatus::new( + TransportId::NOSTR, true, + Maturity::Stable, + Availability::Available, + SinkCapabilities::DELIVER, "ready", - ) - .with_capabilities(RadrootsTransportCapabilities::deliver_only()), + ), } } - pub fn with_status(mut self, status: RadrootsTransportStatus) -> Self { + pub fn with_status(mut self, status: SinkStatus) -> Self { self.status = status; self } @@ -240,60 +241,53 @@ impl<A> RadrootsNostrTransport<A> { } } -impl<A> RadrootsTransport for RadrootsNostrTransport<A> +impl<A> EventSink for RadrootsNostrTransport<A> where A: RadrootsRelayPublishAdapter, { - fn transport_kind(&self) -> RadrootsTransportKind { - RadrootsTransportKind::Nostr - } - - fn status<'a>(&'a self) -> RadrootsTransportFuture<'a, RadrootsTransportStatus> { + fn status( + &self, + ) -> radroots_transport::BoxFuture<'_, Result<SinkStatus, RadrootsTransportError>> { Box::pin(async move { Ok(self.status.clone()) }) } - fn deliver<'a>( - &'a self, - request: RadrootsTransportDeliveryRequest, - ) -> RadrootsTransportFuture<'a, RadrootsTransportDeliveryReceipt> { + fn deliver( + &self, + request: DeliveryRequest, + ) -> radroots_transport::BoxFuture<'_, Result<DeliveryReceipt, RadrootsTransportError>> { Box::pin(async move { - let signed_event = signed_event_from_transport_payload(request.payload())?; + let signed_event = request.payload().event().clone(); let targets = relay_targets_from_transport_targets(request.target_set().targets())?; + let legacy_attempt_time_ms = i64::try_from(request.deadline_unix_ms()) + .map_err(|_| RadrootsTransportError::InvalidDeliveryDeadline)?; let publish_request = - RadrootsRelayPublishRequest::new(signed_event, targets, request.now_ms()) + RadrootsRelayPublishRequest::new(signed_event, targets, legacy_attempt_time_ms) .map_err(nostr_error_to_transport_error)? .with_satisfaction_policy(RadrootsTransportSatisfactionPolicy::NoWait) - .try_with_idempotency_key(request.request_id()) + .try_with_idempotency_key(request.request_id().as_str()) .map_err(nostr_error_to_transport_error)?; let relay_receipts = match publish_signed_event(&self.adapter, publish_request).await { Ok(receipt) => receipt.relays, Err(RadrootsRelayTransportError::Transport(message)) => { - return RadrootsTransportDeliveryReceipt::for_request( + return DeliveryReceipt::for_request( &request, transport_failure_target_receipts( request.target_set().targets(), message.as_str(), - ), + )?, ); } Err(error) => return Err(nostr_error_to_transport_error(error)), }; - RadrootsTransportDeliveryReceipt::for_request( + DeliveryReceipt::for_request( &request, target_receipts_from_relay_receipts( request.target_set().targets(), relay_receipts.as_slice(), - ), + )?, ) }) } - - fn fetch<'a>( - &'a self, - _request: RadrootsTransportFetchRequest, - ) -> RadrootsTransportFuture<'a, RadrootsTransportFetchReceipt> { - Box::pin(async move { Err(RadrootsTransportError::UnsupportedOperation) }) - } } fn nostr_error_to_transport_error(error: RadrootsRelayTransportError) -> RadrootsTransportError { @@ -563,30 +557,12 @@ mod contract_tests { } } -fn signed_event_from_transport_payload( - payload: &RadrootsTransportPayload, -) -> Result<SignedEvent, RadrootsTransportError> { - let RadrootsTransportPayload::SignedEventJson { - event_id, raw_json, .. - } = payload - else { - return Err(RadrootsTransportError::InvalidPayloadBytes); - }; - let wire = Nip01EventWire::parse_json(raw_json) - .map_err(|_| RadrootsTransportError::InvalidPayloadBytes)?; - if wire.id != *event_id { - return Err(RadrootsTransportError::InvalidPayloadId); - } - SignedEvent::from_wire_verified_id(wire, raw_json.clone()) - .map_err(|_| RadrootsTransportError::InvalidPayloadBytes) -} - fn relay_targets_from_transport_targets( - targets: &[RadrootsTransportTarget], + targets: &[Target], ) -> Result<RadrootsRelayTargetSet, RadrootsTransportError> { let mut relays = Vec::new(); for target in targets { - if target.kind() != &RadrootsTransportKind::Nostr { + if target.kind() != &TransportId::NOSTR { return Err(RadrootsTransportError::InvalidTargetUri); } let policy = if target.uri().as_str().starts_with("ws://") { @@ -604,9 +580,9 @@ fn relay_targets_from_transport_targets( } fn target_receipts_from_relay_receipts( - targets: &[RadrootsTransportTarget], + targets: &[Target], relay_receipts: &[RadrootsRelayPublishRelayReceipt], -) -> Vec<RadrootsTransportTargetReceipt> { +) -> Result<Vec<DeliveryTargetReceipt>, RadrootsTransportError> { targets .iter() .cloned() @@ -615,18 +591,17 @@ fn target_receipts_from_relay_receipts( .iter() .find(|receipt| relay_receipt_matches_target(receipt, &target)); match relay_receipt { - Some(receipt) if receipt.attempted => RadrootsTransportTargetReceipt::attempted( - target, - receipt.outcome.to_transport_outcome(), - ), - Some(receipt) => RadrootsTransportTargetReceipt::skipped( + Some(receipt) if receipt.attempted => Ok(DeliveryTargetReceipt::attempted( target, - receipt.outcome.to_transport_outcome(), - ), - None => RadrootsTransportTargetReceipt::skipped( + receipt.outcome.to_delivery_outcome(), + )), + Some(receipt) => { + DeliveryTargetReceipt::skipped(target, receipt.outcome.to_delivery_outcome()) + } + None => DeliveryTargetReceipt::skipped( target, - RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::RouteUnavailable) - .with_message("relay adapter omitted target receipt"), + DeliveryOutcome::unavailable() + .with_detail("missing_receipt", "relay adapter omitted target receipt")?, ), } }) @@ -634,18 +609,19 @@ fn target_receipts_from_relay_receipts( } fn transport_failure_target_receipts( - targets: &[RadrootsTransportTarget], + targets: &[Target], message: &str, -) -> Vec<RadrootsTransportTargetReceipt> { +) -> Result<Vec<DeliveryTargetReceipt>, RadrootsTransportError> { targets .iter() .cloned() .map(|target| { - RadrootsTransportTargetReceipt::attempted( + Ok(DeliveryTargetReceipt::attempted( target, - RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::ConnectionFailed) - .with_message(message.to_owned()), - ) + DeliveryOutcome::unavailable() + .with_detail("connection_failed", message.to_owned()) + .unwrap_or_else(|_| DeliveryOutcome::unavailable()), + )) }) .collect() } @@ -699,7 +675,7 @@ fn normalize_publish_receipts( let mut by_relay = BTreeMap::new(); for mut receipt in receipts { let target = - RadrootsTransportTarget::nostr_relay(receipt.relay_url.as_str()).map_err(|error| { + Target::new(TransportId::NOSTR, receipt.relay_url.as_str()).map_err(|error| { RadrootsRelayTransportError::InvalidPublishReceiptRelayUrl { url: receipt.relay_url.clone(), reason: error.to_string(), @@ -776,7 +752,7 @@ fn relay_publish_satisfies_policy( policy.required_target_count(target_count)?; let mut satisfied_required_targets = BTreeSet::new(); for receipt in relays { - let target = RadrootsTransportTarget::nostr_relay(&receipt.relay_url)?; + let target = Target::new(TransportId::NOSTR, &receipt.relay_url)?; if targets.contains(target.fingerprint()) && relay_receipt_counts_toward_quorum(receipt) && receipt @@ -797,9 +773,9 @@ fn relay_publish_satisfies_policy( fn relay_receipt_matches_target( receipt: &RadrootsRelayPublishRelayReceipt, - target: &RadrootsTransportTarget, + target: &Target, ) -> bool { - RadrootsTransportTarget::nostr_relay(receipt.relay_url.as_str()) + Target::new(TransportId::NOSTR, receipt.relay_url.as_str()) .is_ok_and(|receipt_target| receipt_target.uri() == target.uri()) } diff --git a/crates/transport_nostr/src/relay.rs b/crates/transport_nostr/src/relay.rs @@ -1,7 +1,7 @@ #![forbid(unsafe_code)] use crate::RadrootsRelayTransportError; -use radroots_transport::RadrootsTransportTarget; +use radroots_transport::{Target, TransportId}; use std::fmt; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use url::Url; @@ -71,7 +71,7 @@ impl RelayUrl { }); } } - let target = RadrootsTransportTarget::nostr_relay(original).map_err(|error| { + let target = Target::new(TransportId::NOSTR, original).map_err(|error| { RadrootsRelayTransportError::RelayUrlParse { url: original.to_owned(), reason: error.to_string(), diff --git a/crates/transport_nostr/tests/transport.rs b/crates/transport_nostr/tests/transport.rs @@ -18,14 +18,20 @@ use radroots_outbox::{ RadrootsOutboxDeliveryTargetStatus, RadrootsOutboxEventState, RadrootsOutboxOperationInput, RadrootsOutboxOperationStatus, }; +use radroots_transport::capability::{Availability, Maturity, SinkCapabilities}; +use radroots_transport::outcome::{DeliveryOutcome, DeliveryOutcomeKind}; +use radroots_transport::policy::{ + SatisfactionClass as SinkSatisfactionClass, SatisfactionPolicy as SinkSatisfactionPolicy, + TargetPolicy as SinkTargetPolicy, +}; +use radroots_transport::sink::{ + DeliveryPayload, DeliveryReceipt, DeliveryRequest, DeliveryTargetReceipt, EventSink, SinkStatus, +}; +use radroots_transport::target::{TargetLabel, TargetScope}; use radroots_transport::{ - RadrootsTransport, RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest, - RadrootsTransportDeliveryTargetStatus, RadrootsTransportError, RadrootsTransportFetchReceipt, - RadrootsTransportFetchRequest, RadrootsTransportFuture, RadrootsTransportImplementationState, - RadrootsTransportKind, RadrootsTransportMeshScopeId, RadrootsTransportOutcome, + RadrootsTransportDeliveryTargetStatus, RadrootsTransportError, RadrootsTransportOutcome, RadrootsTransportOutcomeKind, RadrootsTransportPayload, RadrootsTransportSatisfactionClass, - RadrootsTransportSatisfactionPolicy, RadrootsTransportStatus, RadrootsTransportTarget, - RadrootsTransportTargetLabel, RadrootsTransportTargetReceipt, RadrootsTransportTargetSet, + RadrootsTransportSatisfactionPolicy, Target, TargetSet, TransportId, }; use radroots_transport_nostr::{ RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX, RADROOTS_RELAY_FETCH_RAW_EVENT_LIMIT_MAX, @@ -243,63 +249,81 @@ impl RadrootsRelayPublishAdapter for AttemptedSkippedRelayReceiptPublishAdapter #[derive(Clone)] struct ScriptedTransport { - kind: RadrootsTransportKind, + kind: TransportId, outcomes: Vec<RadrootsTransportOutcome>, } impl ScriptedTransport { fn new(outcomes: Vec<RadrootsTransportOutcome>) -> Self { Self { - kind: RadrootsTransportKind::Nostr, + kind: TransportId::NOSTR, outcomes, } } - fn with_kind(mut self, kind: RadrootsTransportKind) -> Self { + fn with_kind(mut self, kind: TransportId) -> Self { self.kind = kind; self } } -impl RadrootsTransport for ScriptedTransport { - fn transport_kind(&self) -> RadrootsTransportKind { - self.kind.clone() - } - - fn status<'a>(&'a self) -> RadrootsTransportFuture<'a, RadrootsTransportStatus> { - Box::pin(async { - Ok(RadrootsTransportStatus::new( - RadrootsTransportKind::Nostr, - true, - RadrootsTransportImplementationState::Real, +impl EventSink for ScriptedTransport { + fn status( + &self, + ) -> radroots_transport::BoxFuture<'_, Result<SinkStatus, RadrootsTransportError>> { + Box::pin(async move { + Ok(SinkStatus::new( + self.kind, true, + Maturity::Stable, + Availability::Available, + SinkCapabilities::DELIVER, "scripted", )) }) } - fn deliver<'a>( - &'a self, - request: RadrootsTransportDeliveryRequest, - ) -> RadrootsTransportFuture<'a, RadrootsTransportDeliveryReceipt> { + fn deliver( + &self, + request: DeliveryRequest, + ) -> radroots_transport::BoxFuture<'_, Result<DeliveryReceipt, RadrootsTransportError>> { Box::pin(async move { - let target_receipts = request - .target_set() - .targets() - .iter() - .cloned() - .zip(self.outcomes.iter().cloned()) - .map(|(target, outcome)| RadrootsTransportTargetReceipt::new(target, outcome)) - .collect(); - RadrootsTransportDeliveryReceipt::for_request(&request, target_receipts) + DeliveryReceipt::for_request( + &request, + request + .target_set() + .targets() + .iter() + .cloned() + .zip(self.outcomes.iter()) + .map(|(target, outcome)| { + DeliveryTargetReceipt::attempted( + target, + legacy_outcome_to_delivery_outcome(outcome), + ) + }) + .collect(), + ) }) } +} - fn fetch<'a>( - &'a self, - _request: RadrootsTransportFetchRequest, - ) -> RadrootsTransportFuture<'a, RadrootsTransportFetchReceipt> { - Box::pin(async { Err(RadrootsTransportError::UnsupportedOperation) }) +fn legacy_outcome_to_delivery_outcome(outcome: &RadrootsTransportOutcome) -> DeliveryOutcome { + match outcome.kind { + RadrootsTransportOutcomeKind::Delivered => DeliveryOutcome::delivered(), + RadrootsTransportOutcomeKind::Accepted + | RadrootsTransportOutcomeKind::DuplicateAccepted + | RadrootsTransportOutcomeKind::Forwarded + | RadrootsTransportOutcomeKind::StoredByGateway + | RadrootsTransportOutcomeKind::Seen => DeliveryOutcome::accepted(), + RadrootsTransportOutcomeKind::Timeout + | RadrootsTransportOutcomeKind::ConnectionFailed + | RadrootsTransportOutcomeKind::TransportUnavailable => DeliveryOutcome::unavailable(), + RadrootsTransportOutcomeKind::DeferredUntilImplemented + | RadrootsTransportOutcomeKind::Rejected + | RadrootsTransportOutcomeKind::RouteUnavailable + | RadrootsTransportOutcomeKind::PayloadTooLarge + | RadrootsTransportOutcomeKind::PolicyDenied => DeliveryOutcome::rejected(), } } @@ -313,68 +337,35 @@ struct ForgedReceiptTransport { forged: ForgedDeliveryReceipt, } -impl RadrootsTransport for ForgedReceiptTransport { - fn transport_kind(&self) -> RadrootsTransportKind { - RadrootsTransportKind::Nostr - } - - fn status<'a>(&'a self) -> RadrootsTransportFuture<'a, RadrootsTransportStatus> { +impl EventSink for ForgedReceiptTransport { + fn status( + &self, + ) -> radroots_transport::BoxFuture<'_, Result<SinkStatus, RadrootsTransportError>> { Box::pin(async { - Ok(RadrootsTransportStatus::new( - RadrootsTransportKind::Nostr, - true, - RadrootsTransportImplementationState::Real, + Ok(SinkStatus::new( + TransportId::NOSTR, true, + Maturity::Stable, + Availability::Available, + SinkCapabilities::DELIVER, "forged receipt fixture", )) }) } - fn deliver<'a>( - &'a self, - request: RadrootsTransportDeliveryRequest, - ) -> RadrootsTransportFuture<'a, RadrootsTransportDeliveryReceipt> { - Box::pin(async move { - match self.forged { - ForgedDeliveryReceipt::RequestId => RadrootsTransportDeliveryReceipt::new( - "forged-request", - request.target_set().clone(), - request - .target_set() - .targets() - .iter() - .cloned() - .map(|target| { - RadrootsTransportTargetReceipt::new( - target, - RadrootsTransportOutcome::new( - RadrootsTransportOutcomeKind::Accepted, - ), - ) - }) - .collect(), - ), - ForgedDeliveryReceipt::TargetSet => { - let target = nostr_target(RELAY_TERTIARY_WSS); - RadrootsTransportDeliveryReceipt::new( - request.request_id(), - RadrootsTransportTargetSet::new(vec![target.clone()]) - .expect("forged target set"), - vec![RadrootsTransportTargetReceipt::new( - target, - RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::Accepted), - )], - ) - } + fn deliver( + &self, + _request: DeliveryRequest, + ) -> radroots_transport::BoxFuture<'_, Result<DeliveryReceipt, RadrootsTransportError>> { + let error = match self.forged { + ForgedDeliveryReceipt::RequestId => { + RadrootsTransportError::DeliveryReceiptRequestIdMismatch } - }) - } - - fn fetch<'a>( - &'a self, - _request: RadrootsTransportFetchRequest, - ) -> RadrootsTransportFuture<'a, RadrootsTransportFetchReceipt> { - Box::pin(async { Err(RadrootsTransportError::UnsupportedOperation) }) + ForgedDeliveryReceipt::TargetSet => { + RadrootsTransportError::DeliveryReceiptTargetSetMismatch + } + }; + Box::pin(async move { Err(error) }) } } @@ -494,19 +485,36 @@ async fn complete_claimed_signing( .expect("complete signing") } -fn nostr_target(relay_url: &str) -> RadrootsTransportTarget { - RadrootsTransportTarget::nostr_relay(relay_url).expect("nostr target") +fn nostr_target(relay_url: &str) -> Target { + Target::new(TransportId::NOSTR, relay_url).expect("nostr target") } -fn scoped_nostr_target(relay_url: &str, scope: &str, label: &str) -> RadrootsTransportTarget { - RadrootsTransportTarget::nostr_relay_with_metadata( +fn scoped_nostr_target(relay_url: &str, scope: &str, label: &str) -> Target { + Target::new_with_metadata( + TransportId::NOSTR, relay_url, - Some(RadrootsTransportMeshScopeId::parse(scope).expect("target scope")), - Some(RadrootsTransportTargetLabel::parse(label).expect("target label")), + Some(TargetScope::parse(scope).expect("target scope")), + Some(TargetLabel::parse(label).expect("target label")), ) .expect("scoped nostr target") } +fn sink_delivery_request( + request_id: &str, + signed_event: &SignedEvent, + targets: Vec<Target>, + target_policy: SinkTargetPolicy, +) -> DeliveryRequest { + DeliveryRequest::new( + request_id, + DeliveryPayload::new(signed_event.clone()), + TargetSet::new(targets).expect("target set"), + SinkSatisfactionPolicy::new(SinkSatisfactionClass::Accepted, target_policy), + 10_000, + ) + .expect("delivery request") +} + fn outbox_operation_input<I, S>( draft: EventDraft, relays: I, @@ -879,7 +887,7 @@ fn transport_target_and_relay_adapter_share_canonical_url_identity() { RadrootsRelayUrlPolicy::Localhost, ), ] { - let target = RadrootsTransportTarget::nostr_relay(raw).expect("transport target"); + let target = Target::new(TransportId::NOSTR, raw).expect("transport target"); let relay = RelayUrl::parse(raw, policy).expect("relay URL"); assert_eq!(target.uri().as_str(), relay.as_str(), "{raw}"); } @@ -901,7 +909,7 @@ fn transport_target_and_relay_adapter_share_canonical_url_identity() { "wss://relay.example.com?subscription=1", ] { assert!( - RadrootsTransportTarget::nostr_relay(raw).is_err(), + Target::new(TransportId::NOSTR, raw).is_err(), "transport target accepted {raw}" ); assert!( @@ -1111,27 +1119,25 @@ async fn mock_publish_preserves_exact_raw_json_and_counts_outcomes() { async fn nostr_transport_facade_delivers_signed_event_payloads() { let signed = signed_post("facade payload"); let adapter = RadrootsMockRelayPublishAdapter::new(); - let expected_status = RadrootsTransportStatus::new( - RadrootsTransportKind::Nostr, - true, - RadrootsTransportImplementationState::Real, + let expected_status = SinkStatus::new( + TransportId::NOSTR, true, + Maturity::Stable, + Availability::Available, + SinkCapabilities::DELIVER, "fixture ready", ); let transport = RadrootsNostrTransport::new(&adapter).with_status(expected_status.clone()); - assert_eq!(transport.transport_kind(), RadrootsTransportKind::Nostr); assert!(transport.adapter().captured_raw_events().is_empty()); let target = nostr_target(RELAY_PRIMARY_WSS); - let request = RadrootsTransportDeliveryRequest::new( + let request = sink_delivery_request( "facade-request-1", - RadrootsTransportPayload::unchecked_signed_event_json(signed.id_str(), signed.raw_json()) - .expect("payload"), - RadrootsTransportTargetSet::new(vec![target.clone()]).expect("targets"), - RadrootsTransportSatisfactionPolicy::all_accepted(), - ) - .expect("delivery request"); + &signed, + vec![target.clone()], + SinkTargetPolicy::all(), + ); - let receipt = transport.deliver(request).await.expect("delivery"); + let receipt = transport.deliver(request.clone()).await.expect("delivery"); let status = transport.status().await.expect("status"); assert_eq!( @@ -1139,18 +1145,14 @@ async fn nostr_transport_facade_delivers_signed_event_payloads() { vec![signed.raw_json().to_owned()] ); assert_eq!(status, expected_status); - assert_eq!(receipt.request_id(), "facade-request-1"); + assert_eq!(receipt.request_id().as_str(), "facade-request-1"); assert_eq!(receipt.target_receipts().len(), 1); - assert_eq!(receipt.target_receipts()[0].target, target); + assert_eq!(receipt.target_receipts()[0].target(), &target); assert_eq!( - receipt.target_receipts()[0].outcome.kind, - radroots_transport::RadrootsTransportOutcomeKind::Accepted - ); - assert!( - receipt - .is_satisfied_by(&RadrootsTransportSatisfactionPolicy::all_accepted()) - .expect("satisfaction") + receipt.target_receipts()[0].outcome().kind(), + DeliveryOutcomeKind::Accepted ); + assert!(receipt.is_satisfied(&request).expect("satisfaction")); } #[test] @@ -1172,227 +1174,69 @@ fn verified_signed_event_payload_preserves_transport_payload_identity() { } #[tokio::test] -async fn nostr_transport_facade_reports_fetch_as_unsupported_operation() { +async fn nostr_transport_sink_rejects_non_nostr_targets() { + let signed = signed_post("facade rejected target"); let transport = RadrootsNostrTransport::new(RadrootsMockRelayPublishAdapter::new()); - let target_set = - RadrootsTransportTargetSet::new(vec![nostr_target(RELAY_PRIMARY_WSS)]).expect("targets"); + let request = sink_delivery_request( + "facade-request-target", + &signed, + vec![Target::new(TransportId::RETICULUM, "reticulum:local").expect("target")], + SinkTargetPolicy::all(), + ); let error = transport - .fetch(RadrootsTransportFetchRequest::new( - "facade-fetch-unsupported", - target_set, - )) - .await - .expect_err("fetch unsupported"); - - assert_eq!(error, RadrootsTransportError::UnsupportedOperation); -} - -#[tokio::test] -async fn nostr_transport_facade_rejects_unsupported_payloads_and_targets() { - let signed = signed_post("facade rejected"); - let transport = RadrootsNostrTransport::new(RadrootsMockRelayPublishAdapter::new()); - let target_set = - RadrootsTransportTargetSet::new(vec![nostr_target(RELAY_PRIMARY_WSS)]).expect("targets"); - let payload_error = transport - .deliver( - RadrootsTransportDeliveryRequest::new( - "facade-request-payload", - RadrootsTransportPayload::opaque_bytes("not-signed-event", [1, 2, 3]) - .expect("payload"), - target_set, - RadrootsTransportSatisfactionPolicy::all_accepted(), - ) - .expect("delivery request"), - ) - .await - .expect_err("payload rejected"); - assert_eq!(payload_error, RadrootsTransportError::InvalidPayloadBytes); - - let non_nostr_target = RadrootsTransportTarget::reticulum().expect("reticulum target"); - let target_error = transport - .deliver( - RadrootsTransportDeliveryRequest::new( - "facade-request-target", - RadrootsTransportPayload::unchecked_signed_event_json( - signed.id_str(), - signed.raw_json(), - ) - .expect("payload"), - RadrootsTransportTargetSet::new(vec![non_nostr_target]).expect("targets"), - RadrootsTransportSatisfactionPolicy::all_accepted(), - ) - .expect("delivery request"), - ) + .deliver(request) .await .expect_err("target rejected"); - assert_eq!(target_error, RadrootsTransportError::InvalidTargetUri); - - let invalid_json_error = transport - .deliver( - RadrootsTransportDeliveryRequest::new( - "facade-request-invalid-json", - RadrootsTransportPayload::unchecked_signed_event_json(signed.id_str(), "{}") - .expect("payload"), - RadrootsTransportTargetSet::new(vec![nostr_target(RELAY_PRIMARY_WSS)]) - .expect("targets"), - RadrootsTransportSatisfactionPolicy::all_accepted(), - ) - .expect("delivery request"), - ) - .await - .expect_err("invalid event json rejected"); - assert_eq!( - invalid_json_error, - RadrootsTransportError::InvalidPayloadBytes - ); - - let mismatched_id_error = transport - .deliver( - RadrootsTransportDeliveryRequest::new( - "facade-request-mismatched-id", - RadrootsTransportPayload::unchecked_signed_event_json( - "00".repeat(32), - signed.raw_json(), - ) - .expect("payload"), - RadrootsTransportTargetSet::new(vec![nostr_target(RELAY_PRIMARY_WSS)]) - .expect("targets"), - RadrootsTransportSatisfactionPolicy::all_accepted(), - ) - .expect("delivery request"), - ) - .await - .expect_err("mismatched event id rejected"); - assert_eq!( - mismatched_id_error, - RadrootsTransportError::InvalidPayloadId - ); - - let mut tampered = - serde_json::from_str::<serde_json::Value>(signed.raw_json()).expect("signed event json"); - tampered["content"] = serde_json::Value::String("tampered".to_owned()); - let tampered_raw = serde_json::to_string(&tampered).expect("tampered event json"); - let tampered_error = transport - .deliver( - RadrootsTransportDeliveryRequest::new( - "facade-request-tampered-event", - RadrootsTransportPayload::unchecked_signed_event_json( - signed.id_str(), - tampered_raw, - ) - .expect("payload"), - RadrootsTransportTargetSet::new(vec![nostr_target(RELAY_PRIMARY_WSS)]) - .expect("targets"), - RadrootsTransportSatisfactionPolicy::all_accepted(), - ) - .expect("delivery request"), - ) - .await - .expect_err("tampered event rejected"); - assert_eq!(tampered_error, RadrootsTransportError::InvalidPayloadBytes); - - let forbidden_target_error = transport - .deliver( - RadrootsTransportDeliveryRequest::new( - "facade-request-forbidden-target", - RadrootsTransportPayload::unchecked_signed_event_json( - signed.id_str(), - signed.raw_json(), - ) - .expect("payload"), - RadrootsTransportTargetSet::new(vec![nostr_target("wss://127.0.0.1")]) - .expect("targets"), - RadrootsTransportSatisfactionPolicy::all_accepted(), - ) - .expect("delivery request"), - ) - .await - .expect_err("forbidden relay rejected"); - assert_eq!( - forbidden_target_error, - RadrootsTransportError::InvalidTargetUri - ); - - let local_receipt = transport - .deliver( - RadrootsTransportDeliveryRequest::new( - "facade-request-local-relay", - RadrootsTransportPayload::unchecked_signed_event_json( - signed.id_str(), - signed.raw_json(), - ) - .expect("payload"), - RadrootsTransportTargetSet::new(vec![nostr_target("ws://127.0.0.1:21002")]) - .expect("targets"), - RadrootsTransportSatisfactionPolicy::all_accepted(), - ) - .expect("delivery request"), - ) - .await - .expect("localhost relay accepted"); - assert_eq!(local_receipt.target_receipts().len(), 1); + assert_eq!(error, RadrootsTransportError::InvalidTargetUri); } #[tokio::test] async fn nostr_transport_facade_preserves_adapter_failure_and_omission_evidence() { let signed = signed_post("facade failures"); - let payload = - RadrootsTransportPayload::unchecked_signed_event_json(signed.id_str(), signed.raw_json()) - .expect("payload"); - let targets = RadrootsTransportTargetSet::new(vec![ + let targets = vec![ nostr_target(RELAY_PRIMARY_WSS), nostr_target(RELAY_SECONDARY_WSS), - ]) - .expect("targets"); + ]; let transport = RadrootsNostrTransport::new(TransportFailurePublishAdapter); let failed = transport - .deliver( - RadrootsTransportDeliveryRequest::new( - "facade-transport-failure", - payload.clone(), - targets.clone(), - RadrootsTransportSatisfactionPolicy::all_accepted(), - ) - .expect("delivery request"), - ) + .deliver(sink_delivery_request( + "facade-transport-failure", + &signed, + targets.clone(), + SinkTargetPolicy::all(), + )) .await .expect("failure receipts"); assert_eq!(failed.target_receipts().len(), 2); assert!(failed.target_receipts().iter().all(|receipt| { - receipt.attempted - && receipt.outcome.kind == RadrootsTransportOutcomeKind::ConnectionFailed - && receipt.status == RadrootsTransportDeliveryTargetStatus::FailedRetryable + receipt.was_attempted() + && receipt.outcome().kind() == DeliveryOutcomeKind::Unavailable + && receipt.outcome().is_retryable() })); let partial = RadrootsNostrTransport::new(PartialPublishAdapter) - .deliver( - RadrootsTransportDeliveryRequest::new( - "facade-partial", - payload.clone(), - targets.clone(), - RadrootsTransportSatisfactionPolicy::all_accepted(), - ) - .expect("delivery request"), - ) + .deliver(sink_delivery_request( + "facade-partial", + &signed, + targets.clone(), + SinkTargetPolicy::all(), + )) .await .expect("partial receipts"); assert_eq!(partial.target_receipts().len(), 2); assert_eq!( - partial.target_receipts()[1].outcome.kind, - RadrootsTransportOutcomeKind::TransportUnavailable + partial.target_receipts()[1].outcome().kind(), + DeliveryOutcomeKind::Unavailable ); let error = RadrootsNostrTransport::new(NostrJsonFailurePublishAdapter) - .deliver( - RadrootsTransportDeliveryRequest::new( - "facade-json-failure", - payload, - targets, - RadrootsTransportSatisfactionPolicy::all_accepted(), - ) - .expect("delivery request"), - ) + .deliver(sink_delivery_request( + "facade-json-failure", + &signed, + targets, + SinkTargetPolicy::all(), + )) .await .expect_err("adapter JSON error"); assert_eq!(error, RadrootsTransportError::InvalidPayloadBytes); @@ -1402,36 +1246,30 @@ async fn nostr_transport_facade_preserves_adapter_failure_and_omission_evidence( async fn nostr_transport_facade_matches_canonical_equivalent_relay_receipts() { let signed = signed_post("facade canonical receipt"); let target = nostr_target(RELAY_PRIMARY_WSS); - let policy = RadrootsTransportSatisfactionPolicy::required_targets( + let sink_policy = SinkTargetPolicy::required(vec![target.fingerprint().clone()]) + .expect("required target policy"); + let legacy_policy = RadrootsTransportSatisfactionPolicy::required_targets( RadrootsTransportSatisfactionClass::Accepted, vec![target.fingerprint().clone()], ) .expect("required target policy"); let transport = RadrootsNostrTransport::new(SlashSpelledRelayReceiptPublishAdapter); let receipt = transport - .deliver( - RadrootsTransportDeliveryRequest::new( - "facade-canonical-receipt", - RadrootsTransportPayload::unchecked_signed_event_json( - signed.id_str(), - signed.raw_json(), - ) - .expect("payload"), - RadrootsTransportTargetSet::new(vec![target.clone()]).expect("target set"), - policy.clone(), - ) - .expect("delivery request"), - ) + .deliver(sink_delivery_request( + "facade-canonical-receipt", + &signed, + vec![target.clone()], + sink_policy, + )) .await .expect("delivery"); assert_eq!(receipt.target_receipts().len(), 1); - assert_eq!(receipt.target_receipts()[0].target, target); + assert_eq!(receipt.target_receipts()[0].target(), &target); assert_eq!( - receipt.target_receipts()[0].status, - radroots_transport::RadrootsTransportDeliveryTargetStatus::Accepted + receipt.target_receipts()[0].outcome().kind(), + DeliveryOutcomeKind::Accepted ); - assert!(receipt.is_satisfied_by(&policy).expect("satisfaction")); let relay_receipt = publish_signed_event( &SlashSpelledRelayReceiptPublishAdapter, @@ -1442,7 +1280,7 @@ async fn nostr_transport_facade_matches_canonical_equivalent_relay_receipts() { 1_070, ) .expect("publish request") - .with_satisfaction_policy(policy), + .with_satisfaction_policy(legacy_policy), ) .await .expect("relay publish"); @@ -1456,26 +1294,24 @@ async fn nostr_transport_facade_preserves_scoped_duplicate_target_metadata() { let transport = RadrootsNostrTransport::new(&adapter); let first = scoped_nostr_target(RELAY_PRIMARY_WSS, "local_food_buyers", "buyers"); let second = scoped_nostr_target(RELAY_PRIMARY_WSS, "local_food_farmers", "farmers"); - let policy = RadrootsTransportSatisfactionPolicy::required_targets( - RadrootsTransportSatisfactionClass::Accepted, - vec![first.fingerprint().clone(), second.fingerprint().clone()], - ) + let policy = SinkTargetPolicy::required(vec![ + first.fingerprint().clone(), + second.fingerprint().clone(), + ]) .expect("required targets"); - let request = RadrootsTransportDeliveryRequest::new( + let request = sink_delivery_request( "facade-request-scoped", - RadrootsTransportPayload::unchecked_signed_event_json(signed.id_str(), signed.raw_json()) - .expect("payload"), - RadrootsTransportTargetSet::new(vec![first.clone(), second.clone()]).expect("targets"), - policy.clone(), - ) - .expect("delivery request"); + &signed, + vec![first.clone(), second.clone()], + policy, + ); - let receipt = transport.deliver(request).await.expect("delivery"); + let receipt = transport.deliver(request.clone()).await.expect("delivery"); assert_eq!(receipt.target_receipts().len(), 2); - assert_eq!(receipt.target_receipts()[0].target, first); - assert_eq!(receipt.target_receipts()[1].target, second); - assert!(receipt.is_satisfied_by(&policy).expect("satisfaction")); + assert_eq!(receipt.target_receipts()[0].target(), &first); + assert_eq!(receipt.target_receipts()[1].target(), &second); + assert!(receipt.is_satisfied(&request).expect("satisfaction")); assert_eq!(adapter.captured_raw_events().len(), 1); } @@ -1536,7 +1372,7 @@ async fn publish_receipts_track_terminal_skipped_and_adapter_errors() { async fn publish_required_target_policy_uses_relay_fingerprints() { let signed = signed_post("required relay"); let required_target = - RadrootsTransportTarget::nostr_relay(RELAY_PRIMARY_WSS).expect("required target"); + Target::new(TransportId::NOSTR, RELAY_PRIMARY_WSS).expect("required target"); let targets = RadrootsRelayTargetSet::new( vec![RELAY_PRIMARY_WSS, RELAY_SECONDARY_WSS], RadrootsRelayUrlPolicy::Public, @@ -1694,7 +1530,7 @@ fn relay_publish_request_seals_fields_and_validates_idempotency_keys() { #[tokio::test] async fn relay_publish_request_rejects_unrequested_required_target_before_adapter() { let signed = signed_post("missing required target"); - let required = RadrootsTransportTarget::nostr_relay(RELAY_SECONDARY_WSS) + let required = Target::new(TransportId::NOSTR, RELAY_SECONDARY_WSS) .expect("required target") .fingerprint() .clone(); @@ -2417,7 +2253,7 @@ async fn fetch_ingests_events_and_records_transport_observations() { .await .expect("observations"); assert_eq!(observations.len(), 1); - assert_eq!(observations[0].transport_kind, RadrootsTransportKind::Nostr); + assert_eq!(observations[0].transport_kind, TransportId::NOSTR); assert_eq!(observations[0].endpoint_uri.as_str(), RELAY_PRIMARY_WSS); assert_eq!( observations[0].observation_type, @@ -3565,14 +3401,14 @@ async fn outbox_transport_facade_persists_every_delivery_status() { RadrootsOutboxDeliveryTargetStatus::Accepted, RadrootsOutboxDeliveryTargetStatus::Accepted, RadrootsOutboxDeliveryTargetStatus::Delivered, - RadrootsOutboxDeliveryTargetStatus::Forwarded, - RadrootsOutboxDeliveryTargetStatus::StoredByGateway, - RadrootsOutboxDeliveryTargetStatus::Seen, - RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented, + RadrootsOutboxDeliveryTargetStatus::Accepted, + RadrootsOutboxDeliveryTargetStatus::Accepted, + RadrootsOutboxDeliveryTargetStatus::Accepted, + RadrootsOutboxDeliveryTargetStatus::FailedTerminal, + RadrootsOutboxDeliveryTargetStatus::FailedTerminal, RadrootsOutboxDeliveryTargetStatus::FailedTerminal, RadrootsOutboxDeliveryTargetStatus::FailedTerminal, RadrootsOutboxDeliveryTargetStatus::FailedTerminal, - RadrootsOutboxDeliveryTargetStatus::SkippedPolicyDenied, RadrootsOutboxDeliveryTargetStatus::FailedRetryable, RadrootsOutboxDeliveryTargetStatus::FailedRetryable, RadrootsOutboxDeliveryTargetStatus::FailedRetryable, @@ -3595,7 +3431,7 @@ async fn outbox_transport_facade_persists_every_delivery_status() { } #[tokio::test] -async fn outbox_transport_facade_rejects_pending_receipts() { +async fn outbox_transport_facade_normalizes_predecessor_pending_evidence() { let outbox = RadrootsOutbox::open_memory().await.expect("outbox"); let store = RadrootsEventStore::open_memory().await.expect("store"); let draft = generic_draft("pending transport receipt"); @@ -3621,7 +3457,7 @@ async fn outbox_transport_facade_rejects_pending_receipts() { forged_outcome.status = RadrootsTransportDeliveryTargetStatus::Pending; let transport = ScriptedTransport::new(vec![forged_outcome]); - let error = publish_claimed_outbox_event_with_transport( + let published = publish_claimed_outbox_event_with_transport( &outbox, &store, &transport, @@ -3630,11 +3466,12 @@ async fn outbox_transport_facade_rejects_pending_receipts() { 2_200, ) .await - .expect_err("pending receipt rejected"); - assert!(matches!( - error, - RadrootsRelayTransportError::TransportContract(_) - )); + .expect("final sink receipt"); + assert_eq!(published.target_receipts.len(), 1); + assert_eq!( + published.target_receipts[0].transport_status, + RadrootsTransportDeliveryTargetStatus::Accepted + ); } #[tokio::test] @@ -3852,7 +3689,7 @@ async fn outbox_transport_facade_rejects_non_nostr_transport_before_mutation() { .await .expect("publish claim") .expect("publish claim"); - let transport = ScriptedTransport::new(Vec::new()).with_kind(RadrootsTransportKind::Reticulum); + let transport = ScriptedTransport::new(Vec::new()).with_kind(TransportId::RETICULUM); let error = publish_claimed_outbox_event_with_transport( &outbox, @@ -4323,7 +4160,7 @@ async fn outbox_publish_required_targets_fan_out_same_endpoint_scoped_receipts() required.clone(), optional.clone(), terminal.clone(), - RadrootsTransportTarget::reticulum().expect("reticulum target"), + Target::reticulum().expect("reticulum target"), ], ), 1_000, @@ -4412,7 +4249,7 @@ async fn outbox_publish_required_targets_fan_out_same_endpoint_scoped_receipts() assert!( targets .iter() - .filter(|target| { target.transport_kind == RadrootsTransportKind::Nostr }) + .filter(|target| { target.transport_kind == TransportId::NOSTR }) .filter(|target| &target.endpoint_fingerprint != terminal.fingerprint()) .all(|target| { target.endpoint_uri.as_str() == RELAY_PRIMARY_WSS @@ -4424,7 +4261,7 @@ async fn outbox_publish_required_targets_fan_out_same_endpoint_scoped_receipts() && target.status == RadrootsOutboxDeliveryTargetStatus::FailedTerminal })); assert!(targets.iter().any(|target| { - target.transport_kind == RadrootsTransportKind::Reticulum + target.transport_kind == TransportId::RETICULUM && target.status == RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented })); } @@ -4659,7 +4496,7 @@ async fn outbox_publish_skips_non_nostr_targets() { RadrootsTransportSatisfactionPolicy::all_accepted(), vec![ nostr_target(RELAY_PRIMARY_WSS), - RadrootsTransportTarget::reticulum().expect("reticulum target"), + Target::reticulum().expect("reticulum target"), ], ), 1_000, @@ -4703,7 +4540,7 @@ async fn outbox_publish_skips_non_nostr_targets() { .await .expect("targets"); assert!(targets.iter().any(|target| { - target.transport_kind == RadrootsTransportKind::Reticulum + target.transport_kind == TransportId::RETICULUM && target.status == RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented })); } diff --git a/crates/transport_publish_protocol/src/lib.rs b/crates/transport_publish_protocol/src/lib.rs @@ -14,10 +14,8 @@ use radroots_protocol::radrootsd::transport_publish::v5::{ RETICULUM_ENDPOINT_URI as RADROOTS_RETICULUM_ENDPOINT_URI, RETICULUM_UNAVAILABLE_MESSAGE as RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, }; -use radroots_transport::{ - RadrootsTransportError, RadrootsTransportKind, RadrootsTransportMeshScopeId, - RadrootsTransportTarget, RadrootsTransportTargetFingerprint, RadrootsTransportTargetLabel, -}; +use radroots_transport::target::{TargetFingerprint, TargetLabel, TargetScope}; +use radroots_transport::{RadrootsTransportError, Target, TransportId}; pub const API_VERSION: &str = "radrootsd.transport_publish.v5"; pub const DAEMON_NAME: &str = "radrootsd"; @@ -309,15 +307,15 @@ impl TransportPublishTarget { if self.transport_kind.trim().is_empty() { return Err(TransportPublishProtocolError::EmptyTransportKind { index }); } - let transport_kind = RadrootsTransportKind::parse_canonical(self.transport_kind.as_str()) + let transport_kind = TransportId::parse_canonical(self.transport_kind.as_str()) .map_err(|error| transport_kind_error(error, index))?; if self.endpoint_uri.trim().is_empty() { return Err(TransportPublishProtocolError::EmptyEndpointUri { index }); } - if transport_kind != RadrootsTransportKind::Reticulum && self.reticulum_behavior.is_some() { + if transport_kind != TransportId::RETICULUM && self.reticulum_behavior.is_some() { return Err(TransportPublishProtocolError::InvalidReticulumBehavior { index }); } - if transport_kind == RadrootsTransportKind::Reticulum + if transport_kind == TransportId::RETICULUM && self.endpoint_uri != RADROOTS_RETICULUM_ENDPOINT_URI { return Err(TransportPublishProtocolError::InvalidReticulumEndpoint { index }); @@ -334,26 +332,23 @@ impl TransportPublishTarget { fn fingerprint( &self, index: usize, - ) -> Result<RadrootsTransportTargetFingerprint, TransportPublishProtocolError> { + ) -> Result<TargetFingerprint, TransportPublishProtocolError> { Ok(self.canonical_target(index)?.fingerprint().clone()) } - fn canonical_target( - &self, - index: usize, - ) -> Result<RadrootsTransportTarget, TransportPublishProtocolError> { - let transport_kind = RadrootsTransportKind::parse_canonical(self.transport_kind.as_str()) + fn canonical_target(&self, index: usize) -> Result<Target, TransportPublishProtocolError> { + let transport_kind = TransportId::parse_canonical(self.transport_kind.as_str()) .map_err(|error| transport_kind_error(error, index))?; let scope = self .target_scope .as_deref() - .map(RadrootsTransportMeshScopeId::parse) + .map(TargetScope::parse) .transpose() .map_err(|error| target_metadata_error(error, index))?; let label = self .target_label .as_deref() - .map(RadrootsTransportTargetLabel::parse) + .map(TargetLabel::parse) .transpose() .map_err(|error| target_metadata_error(error, index))?; let target = @@ -386,12 +381,10 @@ fn validate_target_metadata( index: usize, ) -> Result<(), TransportPublishProtocolError> { if let Some(scope) = target_scope { - RadrootsTransportMeshScopeId::parse(scope) - .map_err(|error| target_metadata_error(error, index))?; + TargetScope::parse(scope).map_err(|error| target_metadata_error(error, index))?; } if let Some(label) = target_label { - RadrootsTransportTargetLabel::parse(label) - .map_err(|error| target_metadata_error(error, index))?; + TargetLabel::parse(label).map_err(|error| target_metadata_error(error, index))?; } Ok(()) } @@ -503,7 +496,7 @@ impl TransportPublishTargetPolicy { if endpoint_uri.trim().is_empty() { return Err(TransportPublishProtocolError::EmptyEndpointUri { index }); } - let target = RadrootsTransportTarget::nostr_relay(endpoint_uri) + let target = Target::new(TransportId::NOSTR, endpoint_uri) .map_err(|error| target_fingerprint_error(error, index))?; if target.uri().as_str() != endpoint_uri { return Err(TransportPublishProtocolError::InvalidEndpointUri { index }); @@ -544,17 +537,13 @@ fn validate_explicit_target_uniqueness( pub enum TransportPublishDeliveryPolicy { Any, All, - Quorum { - quorum: usize, - }, - RequiredTargets { - targets: Vec<RadrootsTransportTargetFingerprint>, - }, + Quorum { quorum: usize }, + RequiredTargets { targets: Vec<TargetFingerprint> }, } impl TransportPublishDeliveryPolicy { pub fn required_targets( - targets: Vec<RadrootsTransportTargetFingerprint>, + targets: Vec<TargetFingerprint>, ) -> Result<Self, TransportPublishProtocolError> { validate_required_target_fingerprints(&targets)?; Ok(Self::RequiredTargets { targets }) @@ -579,7 +568,7 @@ impl TransportPublishDeliveryPolicy { pub fn validate_target_membership( &self, - target_fingerprints: &[RadrootsTransportTargetFingerprint], + target_fingerprints: &[TargetFingerprint], ) -> Result<(), TransportPublishProtocolError> { let Self::RequiredTargets { targets } = self else { return Ok(()); @@ -598,7 +587,7 @@ impl TransportPublishDeliveryPolicy { } fn validate_required_target_fingerprints( - targets: &[RadrootsTransportTargetFingerprint], + targets: &[TargetFingerprint], ) -> Result<(), TransportPublishProtocolError> { if targets.is_empty() { return Err(TransportPublishProtocolError::EmptyRequiredTargetSet); @@ -1147,7 +1136,7 @@ fn validate_target_outcome( if target.transport_kind.trim().is_empty() { return Err(TransportPublishProtocolError::EmptyTransportKind { index }); } - let transport_kind = RadrootsTransportKind::parse_canonical(target.transport_kind.as_str()) + let transport_kind = TransportId::parse_canonical(target.transport_kind.as_str()) .map_err(|error| transport_kind_error(error, index))?; if target.endpoint_uri.trim().is_empty() { return Err(TransportPublishProtocolError::EmptyEndpointUri { index }); @@ -1157,7 +1146,7 @@ fn validate_target_outcome( target.target_label.as_deref(), index, )?; - if transport_kind == RadrootsTransportKind::Reticulum { + if transport_kind == TransportId::RETICULUM { if target.endpoint_uri != RADROOTS_RETICULUM_ENDPOINT_URI { return Err(TransportPublishProtocolError::InvalidReticulumEndpoint { index }); } @@ -1182,19 +1171,19 @@ fn validate_target_outcome( fn target_outcome_fingerprint( target: &TransportPublishTargetOutcome, index: usize, -) -> Result<RadrootsTransportTargetFingerprint, TransportPublishProtocolError> { - let transport_kind = RadrootsTransportKind::parse_canonical(target.transport_kind.as_str()) +) -> Result<TargetFingerprint, TransportPublishProtocolError> { + let transport_kind = TransportId::parse_canonical(target.transport_kind.as_str()) .map_err(|error| transport_kind_error(error, index))?; let scope = target .target_scope .as_deref() - .map(RadrootsTransportMeshScopeId::parse) + .map(TargetScope::parse) .transpose() .map_err(|error| target_metadata_error(error, index))?; let label = target .target_label .as_deref() - .map(RadrootsTransportTargetLabel::parse) + .map(TargetLabel::parse) .transpose() .map_err(|error| target_metadata_error(error, index))?; let canonical_target = @@ -1207,30 +1196,30 @@ fn target_outcome_fingerprint( } fn transport_target_from_parts( - transport_kind: RadrootsTransportKind, + transport_kind: TransportId, endpoint_uri: &str, - scope: Option<RadrootsTransportMeshScopeId>, - label: Option<RadrootsTransportTargetLabel>, -) -> Result<RadrootsTransportTarget, RadrootsTransportError> { + scope: Option<TargetScope>, + label: Option<TargetLabel>, +) -> Result<Target, RadrootsTransportError> { match transport_kind { - RadrootsTransportKind::Nostr => { - RadrootsTransportTarget::nostr_relay_with_metadata(endpoint_uri, scope, label) + TransportId::NOSTR => { + Target::new_with_metadata(TransportId::NOSTR, endpoint_uri, scope, label) } - RadrootsTransportKind::Reticulum => { + TransportId::RETICULUM => { if endpoint_uri != RADROOTS_RETICULUM_ENDPOINT_URI { return Err(RadrootsTransportError::InvalidTargetUri); } - RadrootsTransportTarget::reticulum_with_metadata(endpoint_uri, scope, label) + Target::new_with_metadata(TransportId::RETICULUM, endpoint_uri, scope, label) } - RadrootsTransportKind::Local => { - RadrootsTransportTarget::local_with_metadata(endpoint_uri, scope, label) + TransportId::LOCAL => { + Target::new_with_metadata(TransportId::LOCAL, endpoint_uri, scope, label) } - _ => RadrootsTransportTarget::new_with_metadata(transport_kind, endpoint_uri, scope, label), + _ => Target::new_with_metadata(transport_kind, endpoint_uri, scope, label), } } fn required_policy_outcomes<'a>( - required_targets: &[RadrootsTransportTargetFingerprint], + required_targets: &[TargetFingerprint], outcomes: &'a [TransportPublishTargetOutcome], ) -> Result<Vec<&'a TransportPublishTargetOutcome>, TransportPublishProtocolError> { required_targets @@ -2674,19 +2663,13 @@ mod tests { assert_eq!(target_metadata_error(error, 5), expected); } - assert!(serde_json::from_str::<RadrootsTransportTargetFingerprint>("\"invalid\"").is_err()); + assert!(serde_json::from_str::<TargetFingerprint>("\"invalid\"").is_err()); assert!( - transport_target_from_parts(RadrootsTransportKind::Local, "local:publish", None, None,) - .is_ok() + transport_target_from_parts(TransportId::LOCAL, "local:publish", None, None,).is_ok() ); assert_eq!( - transport_target_from_parts( - RadrootsTransportKind::Reticulum, - "reticulum:other", - None, - None, - ), + transport_target_from_parts(TransportId::RETICULUM, "reticulum:other", None, None,), Err(RadrootsTransportError::InvalidTargetUri) ); diff --git a/crates/transport_publish_protocol/tests/successor_equivalence.rs b/crates/transport_publish_protocol/tests/successor_equivalence.rs @@ -1,7 +1,7 @@ use serde::{Serialize, de::DeserializeOwned}; use radroots_protocol::radrootsd::transport_publish::v5 as successor; -use radroots_transport::RadrootsTransportTarget; +use radroots_transport::{Target, TransportId}; use radroots_transport_publish_protocol as predecessor; fn assert_json_equivalent<Old, New>(old: &Old) -> New @@ -86,8 +86,8 @@ fn request_response_job_and_capability_json_are_byte_identical() { #[test] fn required_target_fingerprint_json_is_byte_identical() { - let target = RadrootsTransportTarget::nostr_relay("wss://relay.example.com") - .expect("native Nostr target"); + let target = + Target::new(TransportId::NOSTR, "wss://relay.example.com").expect("native Nostr target"); let predecessor = predecessor::TransportPublishDeliveryPolicy::required_targets(vec![ target.fingerprint().clone(), ]) diff --git a/crates/transport_reticulum/src/contract.rs b/crates/transport_reticulum/src/contract.rs @@ -1,9 +1,6 @@ use crate::RADROOTS_RETICULUM_ENDPOINT_URI; -use radroots_transport::{ - RadrootsTransportError, RadrootsTransportKind, RadrootsTransportMeshScopeId, - RadrootsTransportTarget, RadrootsTransportTargetFingerprint, RadrootsTransportTargetLabel, - RadrootsTransportTargetUri, -}; +use radroots_transport::target::{TargetFingerprint, TargetLabel, TargetScope}; +use radroots_transport::{RadrootsTransportError, RadrootsTransportTargetUri, Target, TransportId}; pub const RETICULUM_V1_MAX_PAYLOAD_BYTES: usize = 64 * 1024; @@ -79,7 +76,7 @@ pub enum ReticulumPrivacySemanticsV1 { #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct ReticulumRoutingMetadataV1 { - pub scope: RadrootsTransportMeshScopeId, + pub scope: TargetScope, pub gateway: ReticulumGatewaySemanticsV1, pub privacy: ReticulumPrivacySemanticsV1, } @@ -87,7 +84,7 @@ pub struct ReticulumRoutingMetadataV1 { impl ReticulumRoutingMetadataV1 { pub fn local() -> Self { Self { - scope: RadrootsTransportMeshScopeId::local_reticulum(), + scope: TargetScope::parse(crate::RADROOTS_RETICULUM_SCOPE_ID).expect("Reticulum scope"), gateway: ReticulumGatewaySemanticsV1::NoGatewayForwarding, privacy: ReticulumPrivacySemanticsV1::CanonicalSignedEventBytesOnly, } @@ -99,8 +96,8 @@ impl ReticulumRoutingMetadataV1 { pub struct ReticulumDestinationV1 { uri: RadrootsTransportTargetUri, routing: ReticulumRoutingMetadataV1, - label: Option<RadrootsTransportTargetLabel>, - fingerprint: RadrootsTransportTargetFingerprint, + label: Option<TargetLabel>, + fingerprint: TargetFingerprint, } impl ReticulumDestinationV1 { @@ -115,10 +112,11 @@ impl ReticulumDestinationV1 { pub fn new( uri: impl AsRef<str>, - scope: RadrootsTransportMeshScopeId, - label: Option<RadrootsTransportTargetLabel>, + scope: TargetScope, + label: Option<TargetLabel>, ) -> Result<Self, RadrootsTransportError> { - let target = RadrootsTransportTarget::reticulum_with_metadata( + let target = Target::new_with_metadata( + TransportId::RETICULUM, uri.as_ref(), Some(scope), label.clone(), @@ -138,8 +136,8 @@ impl ReticulumDestinationV1 { }) } - pub fn from_target(target: &RadrootsTransportTarget) -> Result<Self, RadrootsTransportError> { - if target.kind() != &RadrootsTransportKind::Reticulum + pub fn from_target(target: &Target) -> Result<Self, RadrootsTransportError> { + if target.kind() != &TransportId::RETICULUM || target.uri().as_str() != RADROOTS_RETICULUM_ENDPOINT_URI { return Err(RadrootsTransportError::InvalidTargetUri); @@ -154,8 +152,9 @@ impl ReticulumDestinationV1 { Ok(destination) } - pub fn transport_target(&self) -> Result<RadrootsTransportTarget, RadrootsTransportError> { - RadrootsTransportTarget::reticulum_with_metadata( + pub fn transport_target(&self) -> Result<Target, RadrootsTransportError> { + Target::new_with_metadata( + TransportId::RETICULUM, self.uri.as_str(), Some(self.routing.scope.clone()), self.label.clone(), @@ -170,11 +169,11 @@ impl ReticulumDestinationV1 { &self.routing } - pub fn label(&self) -> Option<&RadrootsTransportTargetLabel> { + pub fn label(&self) -> Option<&TargetLabel> { self.label.as_ref() } - pub fn fingerprint(&self) -> &RadrootsTransportTargetFingerprint { + pub fn fingerprint(&self) -> &TargetFingerprint { &self.fingerprint } } @@ -185,8 +184,8 @@ impl ReticulumDestinationV1 { struct ReticulumDestinationV1Wire { uri: RadrootsTransportTargetUri, routing: ReticulumRoutingMetadataV1, - label: Option<RadrootsTransportTargetLabel>, - fingerprint: RadrootsTransportTargetFingerprint, + label: Option<TargetLabel>, + fingerprint: TargetFingerprint, } #[cfg(feature = "serde")] diff --git a/crates/transport_reticulum/src/lib.rs b/crates/transport_reticulum/src/lib.rs @@ -23,14 +23,21 @@ use alloc::boxed::Box; use alloc::string::String; use alloc::vec::Vec; use core::fmt; +use radroots_transport::capability::{ + Availability, Maturity, SinkCapabilities, SourceCapabilities, +}; +use radroots_transport::outcome::{DeliveryOutcome, FetchTargetOutcome, FetchTargetState}; +use radroots_transport::sink::{ + DeliveryReceipt, DeliveryRequest, DeliveryTargetReceipt, EventSink, SinkStatus, +}; +use radroots_transport::source::{EventSource, FetchPage, FetchRequest, NextPage, SourceStatus}; +use radroots_transport::target::TargetScope; use radroots_transport::{ - RadrootsTransport, RadrootsTransportCapabilities, RadrootsTransportCapabilityAvailability, + RadrootsTransportCapabilities, RadrootsTransportCapabilityAvailability, RadrootsTransportCapabilityMaturity, RadrootsTransportDeliveryReceipt, - RadrootsTransportDeliveryRequest, RadrootsTransportError, RadrootsTransportFetchReceipt, - RadrootsTransportFetchRequest, RadrootsTransportFuture, RadrootsTransportImplementationState, - RadrootsTransportKind, RadrootsTransportMeshScopeId, RadrootsTransportOutcome, - RadrootsTransportOutcomeKind, RadrootsTransportStatus, RadrootsTransportTarget, - RadrootsTransportTargetReceipt, + RadrootsTransportDeliveryRequest, RadrootsTransportError, RadrootsTransportImplementationState, + RadrootsTransportOutcome, RadrootsTransportOutcomeKind, RadrootsTransportStatus, + RadrootsTransportTargetReceipt, Target, TransportId, }; const DEFAULT_PROFILE_ID: &str = "transport.reticulum.default"; @@ -129,7 +136,7 @@ impl fmt::Display for RadrootsReticulumAgentEndpoint { pub struct RadrootsReticulumProfile { profile_id: String, endpoint: RadrootsReticulumEndpoint, - scope: RadrootsTransportMeshScopeId, + scope: TargetScope, agent_endpoint: Option<RadrootsReticulumAgentEndpoint>, behavior: RadrootsReticulumBehavior, destination: ReticulumDestinationV1, @@ -140,7 +147,7 @@ impl RadrootsReticulumProfile { pub fn new( profile_id: impl Into<String>, endpoint: RadrootsReticulumEndpoint, - scope: RadrootsTransportMeshScopeId, + scope: TargetScope, agent_endpoint: Option<RadrootsReticulumAgentEndpoint>, behavior: RadrootsReticulumBehavior, ) -> Result<Self, RadrootsReticulumError> { @@ -171,7 +178,7 @@ impl RadrootsReticulumProfile { Self { profile_id: DEFAULT_PROFILE_ID.to_owned(), endpoint: RadrootsReticulumEndpoint::default(), - scope: RadrootsTransportMeshScopeId::local_reticulum(), + scope: TargetScope::parse(RADROOTS_RETICULUM_SCOPE_ID).expect("Reticulum scope"), agent_endpoint: None, behavior: RadrootsReticulumBehavior::RejectDeliveryAttempts, destination: capability_report.destination.clone(), @@ -192,7 +199,7 @@ impl RadrootsReticulumProfile { &self.endpoint } - pub fn scope(&self) -> &RadrootsTransportMeshScopeId { + pub fn scope(&self) -> &TargetScope { &self.scope } @@ -225,7 +232,7 @@ impl RadrootsReticulumProfile { destination: self.destination.clone(), capability_report: self.capability_report.clone(), transport_status: RadrootsTransportStatus::new( - RadrootsTransportKind::Reticulum, + TransportId::RETICULUM, true, RadrootsTransportImplementationState::Real, false, @@ -250,7 +257,7 @@ impl Default for RadrootsReticulumProfile { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsReticulumStatus { pub behavior: RadrootsReticulumBehavior, - pub scope: RadrootsTransportMeshScopeId, + pub scope: TargetScope, pub agent_endpoint: Option<RadrootsReticulumAgentEndpoint>, pub destination: ReticulumDestinationV1, pub capability_report: ReticulumCapabilityReportV1, @@ -317,45 +324,79 @@ impl Default for RadrootsReticulumTransport { } } -impl RadrootsTransport for RadrootsReticulumTransport { - fn transport_kind(&self) -> RadrootsTransportKind { - RadrootsTransportKind::Reticulum +impl EventSink for RadrootsReticulumTransport { + fn status( + &self, + ) -> radroots_transport::BoxFuture<'_, Result<SinkStatus, RadrootsTransportError>> { + Box::pin(async { + Ok(SinkStatus::new( + TransportId::RETICULUM, + true, + Maturity::Preview, + Availability::Unavailable, + SinkCapabilities::NONE, + RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, + )) + }) } - fn status<'a>(&'a self) -> RadrootsTransportFuture<'a, RadrootsTransportStatus> { - Box::pin(async move { Ok(self.profile.status().transport_status) }) + fn deliver( + &self, + request: DeliveryRequest, + ) -> radroots_transport::BoxFuture<'_, Result<DeliveryReceipt, RadrootsTransportError>> { + Box::pin(async move { + ensure_reticulum_targets(request.target_set().targets()) + .map_err(reticulum_error_to_transport_error)?; + let outcome = DeliveryOutcome::unavailable() + .with_detail(UNAVAILABLE_CODE, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE)?; + let receipts = request + .target_set() + .targets() + .iter() + .cloned() + .map(|target| DeliveryTargetReceipt::skipped(target, outcome.clone())) + .collect::<Result<Vec<_>, _>>()?; + DeliveryReceipt::for_request(&request, receipts) + }) } +} - fn deliver<'a>( - &'a self, - request: RadrootsTransportDeliveryRequest, - ) -> RadrootsTransportFuture<'a, RadrootsTransportDeliveryReceipt> { - Box::pin(async move { - self.deliver(request) - .map_err(reticulum_error_to_transport_error) +impl EventSource for RadrootsReticulumTransport { + fn status( + &self, + ) -> radroots_transport::BoxFuture<'_, Result<SourceStatus, RadrootsTransportError>> { + Box::pin(async { + Ok(SourceStatus::new( + TransportId::RETICULUM, + true, + Maturity::Preview, + Availability::Unavailable, + SourceCapabilities::NONE, + RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, + )) }) } - fn fetch<'a>( - &'a self, - request: RadrootsTransportFetchRequest, - ) -> RadrootsTransportFuture<'a, RadrootsTransportFetchReceipt> { + fn fetch( + &self, + request: FetchRequest, + ) -> radroots_transport::BoxFuture<'_, Result<FetchPage, RadrootsTransportError>> { Box::pin(async move { - ensure_reticulum_targets(request.target_set.targets()) + ensure_reticulum_targets(request.target_set().targets()) .map_err(reticulum_error_to_transport_error)?; - let outcome = reticulum_outcome(self.profile.behavior); - let target_receipts = request - .target_set + let outcomes = request + .target_set() .targets() .iter() - .cloned() - .map(|target| RadrootsTransportTargetReceipt::skipped(target, outcome.clone())) - .collect::<Vec<_>>(); - Ok(RadrootsTransportFetchReceipt::new( - request.request_id, - target_receipts, - 0, - )) + .map(|target| { + FetchTargetOutcome::new( + target.fingerprint().clone(), + FetchTargetState::Unavailable, + ) + .with_message(RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE) + }) + .collect(); + FetchPage::for_request(&request, Vec::new(), outcomes, NextPage::Complete) }) } } @@ -387,7 +428,7 @@ impl RadrootsReticulumFetchRequest { pub struct RadrootsReticulumFetchReceipt { pub request_id: String, pub endpoint_uri: String, - pub scope: RadrootsTransportMeshScopeId, + pub scope: TargetScope, pub agent_endpoint: Option<RadrootsReticulumAgentEndpoint>, pub outcome: RadrootsTransportOutcome, pub observed_event_count: usize, @@ -431,11 +472,9 @@ fn reticulum_error_to_transport_error(error: RadrootsReticulumError) -> Radroots } } -fn ensure_reticulum_targets( - targets: &[RadrootsTransportTarget], -) -> Result<(), RadrootsReticulumError> { +fn ensure_reticulum_targets(targets: &[Target]) -> Result<(), RadrootsReticulumError> { for target in targets { - if target.kind() != &RadrootsTransportKind::Reticulum { + if target.kind() != &TransportId::RETICULUM { return Err(RadrootsReticulumError::NonReticulumTarget); } if target.uri().as_str() != RADROOTS_RETICULUM_ENDPOINT_URI { @@ -485,19 +524,19 @@ mod tests { use alloc::vec; use futures::executor::block_on; use radroots_transport::{ - RadrootsTransportPayload, RadrootsTransportSatisfactionPolicy, RadrootsTransportTargetSet, + RadrootsTransportPayload, RadrootsTransportSatisfactionPolicy, TargetSet, }; - fn reticulum_target() -> RadrootsTransportTarget { - RadrootsTransportTarget::reticulum().expect("Reticulum target") + fn reticulum_target() -> Target { + Target::reticulum().expect("Reticulum target") } - fn delivery_request(targets: Vec<RadrootsTransportTarget>) -> RadrootsTransportDeliveryRequest { + fn delivery_request(targets: Vec<Target>) -> RadrootsTransportDeliveryRequest { RadrootsTransportDeliveryRequest::new( "delivery", RadrootsTransportPayload::mesh_frame_cbor("message", [1_u8, 2, 3]) .expect("mesh payload"), - RadrootsTransportTargetSet::new(targets).expect("target set"), + TargetSet::new(targets).expect("target set"), RadrootsTransportSatisfactionPolicy::any_accepted(), ) .expect("delivery request") @@ -543,7 +582,7 @@ mod tests { "reticulum-agent:owned" ); - let scope = RadrootsTransportMeshScopeId::parse("farm.mesh").expect("scope"); + let scope = TargetScope::parse("farm.mesh").expect("scope"); for invalid in ["", " ", "profile id"] { assert!( RadrootsReticulumProfile::new( @@ -619,22 +658,17 @@ mod tests { ); assert_eq!( - RadrootsTransport::transport_kind(&rejecting), - RadrootsTransportKind::Reticulum + block_on(EventSink::status(&rejecting)) + .expect("sink status") + .transport_id(), + TransportId::RETICULUM ); - assert!(block_on(RadrootsTransport::status(&rejecting)).is_ok()); - assert!( - block_on(RadrootsTransport::deliver( - &rejecting, - delivery_request(vec![reticulum_target()]), - )) - .is_ok() - ); - let core_fetch = RadrootsTransportFetchRequest::new( - "core-fetch", - RadrootsTransportTargetSet::new(vec![reticulum_target()]).expect("target set"), + assert_eq!( + block_on(EventSource::status(&rejecting)) + .expect("source status") + .transport_id(), + TransportId::RETICULUM ); - assert!(block_on(RadrootsTransport::fetch(&rejecting, core_fetch)).is_ok()); let deferring = RadrootsReticulumTransport::new( RadrootsReticulumProfile::default() @@ -671,13 +705,13 @@ mod tests { assert!(ensure_reticulum_targets(&[]).is_ok()); let wrong_kind = - RadrootsTransportTarget::local("local:memory").expect("non-Reticulum target"); + Target::new(TransportId::LOCAL, "local:memory").expect("non-Reticulum target"); assert_eq!( ensure_reticulum_targets(&[wrong_kind]), Err(RadrootsReticulumError::NonReticulumTarget) ); assert_eq!( - RadrootsTransportTarget::new(RadrootsTransportKind::Reticulum, "reticulum:other") + Target::new(TransportId::RETICULUM, "reticulum:other") .expect_err("wrong Reticulum URI"), RadrootsTransportError::InvalidTargetUri ); diff --git a/crates/transport_reticulum/tests/reticulum.rs b/crates/transport_reticulum/tests/reticulum.rs @@ -1,10 +1,13 @@ +use radroots_transport::capability::{Availability, Maturity}; +use radroots_transport::sink::EventSink; +use radroots_transport::source::{EventSource, FetchBounds, FetchRequest}; +use radroots_transport::target::TargetScope; use radroots_transport::{ - RadrootsTransport, RadrootsTransportCapabilityAvailability, - RadrootsTransportCapabilityMaturity, RadrootsTransportDeliveryRequest, - RadrootsTransportDeliveryTargetStatus, RadrootsTransportFetchRequest, - RadrootsTransportImplementationState, RadrootsTransportKind, RadrootsTransportMeshScopeId, - RadrootsTransportPayload, RadrootsTransportSatisfactionClass, - RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, RadrootsTransportTargetSet, + RadrootsTransportCapabilityAvailability, RadrootsTransportCapabilityMaturity, + RadrootsTransportDeliveryRequest, RadrootsTransportDeliveryTargetStatus, + RadrootsTransportImplementationState, RadrootsTransportPayload, + RadrootsTransportSatisfactionClass, RadrootsTransportSatisfactionPolicy, Target, TargetSet, + TransportId, }; use radroots_transport_reticulum::{ RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_SCOPE_ID, @@ -17,29 +20,30 @@ use radroots_transport_reticulum::{ #[cfg(feature = "serde")] use serde_json::Value; -fn reticulum_target(uri: &str) -> RadrootsTransportTarget { +fn reticulum_target(uri: &str) -> Target { assert_eq!(uri, RADROOTS_RETICULUM_ENDPOINT_URI); - RadrootsTransportTarget::reticulum().expect("reticulum target") + Target::reticulum().expect("reticulum target") } -fn scoped_reticulum_target(scope: &str) -> RadrootsTransportTarget { - RadrootsTransportTarget::reticulum_with_metadata( +fn scoped_reticulum_target(scope: &str) -> Target { + Target::new_with_metadata( + TransportId::RETICULUM, RADROOTS_RETICULUM_ENDPOINT_URI, - Some(RadrootsTransportMeshScopeId::parse(scope).expect("scope")), + Some(TargetScope::parse(scope).expect("scope")), None, ) .expect("scoped reticulum target") } -fn nostr_target() -> RadrootsTransportTarget { - RadrootsTransportTarget::nostr_relay("wss://relay.example").expect("nostr target") +fn nostr_target() -> Target { + Target::new(TransportId::NOSTR, "wss://relay.example").expect("nostr target") } -fn delivery_request(targets: Vec<RadrootsTransportTarget>) -> RadrootsTransportDeliveryRequest { +fn delivery_request(targets: Vec<Target>) -> RadrootsTransportDeliveryRequest { RadrootsTransportDeliveryRequest::new( "reticulum-delivery", reticulum_payload(), - RadrootsTransportTargetSet::new(targets).expect("target set"), + TargetSet::new(targets).expect("target set"), RadrootsTransportSatisfactionPolicy::any_accepted(), ) .expect("delivery request") @@ -239,7 +243,7 @@ fn endpoint_and_profile_validation_are_strict_and_canonical() { RadrootsReticulumProfile::new( "transport reticulum", endpoint, - RadrootsTransportMeshScopeId::local_reticulum(), + TargetScope::local_reticulum(), None, RadrootsReticulumBehavior::RejectDeliveryAttempts, ) @@ -250,7 +254,7 @@ fn endpoint_and_profile_validation_are_strict_and_canonical() { RadrootsReticulumProfile::new( "", RadrootsReticulumEndpoint::default(), - RadrootsTransportMeshScopeId::local_reticulum(), + TargetScope::local_reticulum(), None, RadrootsReticulumBehavior::RejectDeliveryAttempts, ) @@ -260,7 +264,7 @@ fn endpoint_and_profile_validation_are_strict_and_canonical() { let profile = RadrootsReticulumProfile::new( "transport.reticulum.custom", RadrootsReticulumEndpoint::default(), - RadrootsTransportMeshScopeId::local_reticulum(), + TargetScope::local_reticulum(), Some(agent_endpoint), RadrootsReticulumBehavior::DeferDeliveryPlans, ) @@ -336,56 +340,36 @@ fn direct_reticulum_delivery_accepts_any_typed_scope_as_inert_metadata() { } #[test] -fn core_transport_trait_reports_reticulum_status_delivery_and_fetch() { +fn final_transport_spis_report_unavailable_reticulum_source_and_sink() { let transport = RadrootsReticulumTransport::default(); - let target_set = - RadrootsTransportTargetSet::new(vec![reticulum_target(RADROOTS_RETICULUM_ENDPOINT_URI)]) - .expect("target set"); - let status = futures::executor::block_on(RadrootsTransport::status(&transport)) - .expect("transport status"); - assert_eq!(status.kind, RadrootsTransportKind::Reticulum); - assert_eq!( - status.implementation, - RadrootsTransportImplementationState::Real - ); - assert_eq!( - status.maturity, - RadrootsTransportCapabilityMaturity::Preview - ); - assert_eq!( - status.availability, - RadrootsTransportCapabilityAvailability::Unavailable - ); - assert!(!status.usable_for_delivery); - assert!(!status.capabilities.deliver); - assert!(!status.capabilities.fetch); - - let delivery = futures::executor::block_on(RadrootsTransport::deliver( + let target_set = TargetSet::new(vec![reticulum_target(RADROOTS_RETICULUM_ENDPOINT_URI)]) + .expect("target set"); + let sink_status = + futures::executor::block_on(EventSink::status(&transport)).expect("sink status"); + assert_eq!(sink_status.transport_id(), TransportId::RETICULUM); + assert_eq!(sink_status.maturity(), Maturity::Preview); + assert_eq!(sink_status.availability(), Availability::Unavailable); + assert!(!sink_status.capabilities().can_deliver()); + + let source_status = + futures::executor::block_on(EventSource::status(&transport)).expect("source status"); + assert_eq!(source_status.transport_id(), TransportId::RETICULUM); + assert_eq!(source_status.maturity(), Maturity::Preview); + assert_eq!(source_status.availability(), Availability::Unavailable); + assert!(!source_status.capabilities().can_fetch()); + + let fetch = futures::executor::block_on(EventSource::fetch( &transport, - RadrootsTransportDeliveryRequest::new( - "core-delivery", - reticulum_payload(), - target_set.clone(), - RadrootsTransportSatisfactionPolicy::any_accepted(), + FetchRequest::new( + "core-fetch", + target_set, + FetchBounds::new(10, 10_000).expect("fetch bounds"), ) - .expect("delivery request"), - )) - .expect("delivery receipt"); - assert_eq!( - delivery.target_receipts()[0].status, - RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented - ); - - let fetch = futures::executor::block_on(RadrootsTransport::fetch( - &transport, - RadrootsTransportFetchRequest::new("core-fetch", target_set), + .expect("fetch request"), )) - .expect("fetch receipt"); - assert_eq!(fetch.fetched_count, 0); - assert_eq!( - fetch.target_receipts[0].status, - RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented - ); + .expect("fetch page"); + assert!(fetch.events().is_empty()); + assert_eq!(fetch.target_outcomes().len(), 1); } #[test] @@ -400,10 +384,7 @@ fn reject_delivery_attempts_returns_unavailable_without_success_or_nostr_routing 0 ); for target_receipt in receipt.target_receipts() { - assert_eq!( - target_receipt.target.kind(), - &RadrootsTransportKind::Reticulum - ); + assert_eq!(target_receipt.target.kind(), &TransportId::RETICULUM); assert_eq!( target_receipt.status, RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented @@ -431,7 +412,7 @@ fn noncanonical_reticulum_targets_are_rejected() { "reticulum:custom", ] { assert_eq!( - RadrootsTransportTarget::new(RadrootsTransportKind::Reticulum, invalid) + Target::new(TransportId::RETICULUM, invalid) .expect_err("noncanonical Reticulum target"), radroots_transport::RadrootsTransportError::InvalidTargetUri ); @@ -650,7 +631,7 @@ fn destination_deserialization_revalidates_canonical_identity() { #[test] fn destination_rejects_non_reticulum_targets() { - let local = RadrootsTransportTarget::local("local:memory").expect("local target"); + let local = Target::new(TransportId::LOCAL, "local:memory").expect("local target"); assert!(ReticulumDestinationV1::from_target(&local).is_err()); } diff --git a/tools/xtask/src/contract/nip09_reconciliation.rs b/tools/xtask/src/contract/nip09_reconciliation.rs @@ -233,7 +233,7 @@ const GOVERNED_DEPENDENCY_TABLE_SHA256: [(&str, &str); 7] = [ ), ( TRANSPORT_CARGO_MANIFEST_RELATIVE, - "7839356e3ba830b758327552bfa6295747d5073ac023f5d2ab47bdd0c6866972", + "2ee4667dc2974d91d137ce5fcf0506c0d35163164a533f0f94b8b02907a7e4cf", ), ( "Cargo.toml#governed-workspace-dependencies", @@ -9612,7 +9612,7 @@ fn validate_post_core_storage_source( "radroots_event::trade::SellerReservationAssertionV1", "radroots_event::trade::TradeMutationEnvelopeV1", "radroots_event::trade::TradeMutationKindV1", - "radroots_transport::RadrootsTransportKind", + "radroots_transport::TransportId", "sqlx::Sqlite", "sqlx::Transaction", ] @@ -20673,6 +20673,9 @@ name = "radroots_transport" version = "0.1.0-alpha" dependencies = [ "futures", + "radroots_event", + "radroots_identity", + "radroots_protocol", "serde", "serde_json", "sha2", @@ -20683,6 +20686,9 @@ name = "radroots_transport" version = "0.1.0-alpha" dependencies = [ "futures", + "radroots_event", + "radroots_identity", + "radroots_protocol", "serde", "serde_json", "sha2", diff --git a/tools/xtask/src/contract/source_maintenance.rs b/tools/xtask/src/contract/source_maintenance.rs @@ -3726,9 +3726,9 @@ struct DelegatedAuthoritySpec { } const EXECUTABLE_AUTHORITY_AST_SHA256: &str = - "730fe3bf7d8a1eed342ea58ca1ab6cdad9065cb6fdb1695244ebaeb17168cef2"; + "b1a7658f47b4561ad816ef65dab47cf68c75de3c459383371319e96e6051d435"; const BOUND_AUTHORITY_SOURCE_AST_SHA256: &str = - "aaf8433098153cad9105761402d65c78d7f8134c260bc2737c0bbcb032ae9d60"; + "b0a59506a3a49b86875699a545cd9eec388c4b030b46264f2cb6540aab0b6a8d"; #[derive(Clone, Debug, Serialize)] struct ExecutableAuthorityIdentity {