diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 01fcc6479..90fe2df7d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,5 +1,4 @@ { - "rust/noosphere-api": "0.13.0+deprecated", "rust/noosphere-cli": "0.15.0", "rust/noosphere-collections": "0.6.4", "rust/noosphere-core": "0.16.0", @@ -9,6 +8,5 @@ "rust/noosphere": "0.15.0", "rust/noosphere-ipfs": "0.8.0", "rust/noosphere-gateway": "0.9.0", - "rust/noosphere-sphere": "0.11.0+deprecated", "rust/noosphere-common": "0.1.0" -} +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 215726dc0..ab662a1f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3487,32 +3487,6 @@ dependencies = [ "witty-phrase-generator", ] -[[package]] -name = "noosphere-api" -version = "0.13.0+deprecated" -dependencies = [ - "anyhow", - "cid", - "iroh-car", - "libipld-cbor", - "libipld-core", - "noosphere-core", - "noosphere-storage", - "reqwest", - "serde", - "serde_urlencoded", - "thiserror", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", - "ucan", - "ucan-key-support", - "url", - "wasm-bindgen", - "wasm-bindgen-test", -] - [[package]] name = "noosphere-cli" version = "0.15.0" @@ -3776,37 +3750,6 @@ dependencies = [ "void", ] -[[package]] -name = "noosphere-sphere" -version = "0.11.0+deprecated" -dependencies = [ - "anyhow", - "async-stream", - "async-trait", - "bytes", - "cid", - "futures-util", - "iroh-car", - "libipld-cbor", - "libipld-core", - "noosphere-api", - "noosphere-core", - "noosphere-storage", - "serde", - "serde_json", - "thiserror", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", - "ucan", - "ucan-key-support", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test", -] - [[package]] name = "noosphere-storage" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 21fd0c1bf..ba7185d16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,6 @@ [workspace] members = [ "rust/noosphere", - "rust/noosphere-api", "rust/noosphere-cli", "rust/noosphere-collections", "rust/noosphere-common", @@ -10,7 +9,6 @@ members = [ "rust/noosphere-into", "rust/noosphere-ipfs", "rust/noosphere-ns", - "rust/noosphere-sphere", "rust/noosphere-storage", ] diff --git a/release-please-config.json b/release-please-config.json index c0461654b..eb3eb59c7 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -8,9 +8,6 @@ "bump-patch-for-minor-pre-major": true, "last-release-sha": "d1fd253414cf6852653baa08ab1074bde7e57538", "packages": { - "rust/noosphere-api": { - "release-as": "0.13.0+deprecated" - }, "rust/noosphere-common": {}, "rust/noosphere-cli": { "draft": true @@ -21,13 +18,10 @@ "rust/noosphere-ipfs": {}, "rust/noosphere-into": {}, "rust/noosphere-ns": {}, - "rust/noosphere-sphere": { - "release-as": "0.11.0+deprecated" - }, "rust/noosphere-storage": {}, "rust/noosphere": { "draft": true } }, "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" -} +} \ No newline at end of file diff --git a/rust/noosphere-api/CHANGELOG.md b/rust/noosphere-api/CHANGELOG.md deleted file mode 100644 index a02b73cc7..000000000 --- a/rust/noosphere-api/CHANGELOG.md +++ /dev/null @@ -1,375 +0,0 @@ -# Changelog - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.4.0 to 0.5.0 - * noosphere-storage bumped from 0.3.0 to 0.4.0 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.5.0 to 0.5.1 - * noosphere-storage bumped from 0.4.0 to 0.4.1 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.5.1 to 0.6.0 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.6.0 to 0.6.1 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.6.1 to 0.6.2 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.6.2 to 0.6.3 - * noosphere-storage bumped from 0.4.1 to 0.4.2 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.8.0 to 0.9.0 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.9.0 to 0.9.1 - * noosphere-storage bumped from 0.6.0 to 0.6.1 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.9.1 to 0.9.2 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.9.3 to 0.10.0 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.10.0 to 0.10.1 - -* The following workspace dependencies were updated - * dependencies - * noosphere-car bumped from 0.1.1 to 0.1.2 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.12.0 to 0.12.1 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.12.1 to 0.12.2 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.12.2 to 0.12.3 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.13.0 to 0.13.1 - * noosphere-storage bumped from 0.7.0 to 0.7.1 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.15.1 to 0.15.2 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.15.2 to 0.16.0 - * noosphere-storage bumped from 0.8.1 to 0.9.0 - -## [0.12.1](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-api-v0.12.0...noosphere-api-v0.12.1) (2023-08-10) - - -### Features - -* `orb sphere history` and `orb sphere render` ([#576](https://github.com/subconsciousnetwork/noosphere/issues/576)) ([a6f0a74](https://github.com/subconsciousnetwork/noosphere/commit/a6f0a74cde2fc001bfff5c1bed0844ac19fc8258)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.15.0 to 0.15.1 - * noosphere-storage bumped from 0.8.0 to 0.8.1 - -## [0.12.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-api-v0.11.0...noosphere-api-v0.12.0) (2023-08-04) - - -### ⚠ BREAKING CHANGES - -* `orb` uses latest Noosphere capabilities ([#530](https://github.com/subconsciousnetwork/noosphere/issues/530)) - -### Features - -* `orb` uses latest Noosphere capabilities ([#530](https://github.com/subconsciousnetwork/noosphere/issues/530)) ([adfa028](https://github.com/subconsciousnetwork/noosphere/commit/adfa028ebcb2de7ea7492af57239fcc9bfc27955)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.14.0 to 0.15.0 - * noosphere-storage bumped from 0.7.1 to 0.8.0 - -## [0.11.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-api-v0.10.0...noosphere-api-v0.11.0) (2023-07-20) - - -### ⚠ BREAKING CHANGES - -* C FFI to verify authorizations ([#510](https://github.com/subconsciousnetwork/noosphere/issues/510)) - -### Features - -* C FFI to verify authorizations ([#510](https://github.com/subconsciousnetwork/noosphere/issues/510)) ([ed092fc](https://github.com/subconsciousnetwork/noosphere/commit/ed092fc303f89ca4737f5e67681e2ede8189304d)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.13.2 to 0.14.0 - -## [0.10.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-api-v0.9.1...noosphere-api-v0.10.0) (2023-07-19) - - -### ⚠ BREAKING CHANGES - -* Replace `noosphere-car` with `iroh-car` throughout the Noosphere crates. ([#492](https://github.com/subconsciousnetwork/noosphere/issues/492)) - -### Features - -* Replace `noosphere-car` with `iroh-car` throughout the Noosphere crates. ([#492](https://github.com/subconsciousnetwork/noosphere/issues/492)) ([e89d498](https://github.com/subconsciousnetwork/noosphere/commit/e89d49879b3a1d2ce8529e438df7995ae8b4e44f)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.13.1 to 0.13.2 - -## [0.9.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-api-v0.8.3...noosphere-api-v0.9.0) (2023-07-01) - - -### ⚠ BREAKING CHANGES - -* Authorize and revoke APIs ([#420](https://github.com/subconsciousnetwork/noosphere/issues/420)) -* Update to `rs-ucan` 0.4.0, implementing UCAN 0.10ish. ([#449](https://github.com/subconsciousnetwork/noosphere/issues/449)) - -### Features - -* Authorize and revoke APIs ([#420](https://github.com/subconsciousnetwork/noosphere/issues/420)) ([73f016e](https://github.com/subconsciousnetwork/noosphere/commit/73f016e12448c46f95ae7683d91fd6422a925555)) -* Update to `rs-ucan` 0.4.0, implementing UCAN 0.10ish. ([#449](https://github.com/subconsciousnetwork/noosphere/issues/449)) ([8b806c5](https://github.com/subconsciousnetwork/noosphere/commit/8b806c5462b5601a5f8417a6a20769b76b57ee6a)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.12.3 to 0.13.0 - -## [0.8.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-api-v0.7.9...noosphere-api-v0.8.0) (2023-06-08) - - -### ⚠ BREAKING CHANGES - -* Enable incremental sphere replication ([#409](https://github.com/subconsciousnetwork/noosphere/issues/409)) -* Migrate blake2b->blake3 everywhere. ([#400](https://github.com/subconsciousnetwork/noosphere/issues/400)) - -### Features - -* Consolidate `NsRecord` implementation in`LinkRecord`. Fixes [#395](https://github.com/subconsciousnetwork/noosphere/issues/395) ([#399](https://github.com/subconsciousnetwork/noosphere/issues/399)) ([9ee4798](https://github.com/subconsciousnetwork/noosphere/commit/9ee47981232fde00b34bb9458c5b0b2799a610ca)) -* Migrate blake2b->blake3 everywhere. ([#400](https://github.com/subconsciousnetwork/noosphere/issues/400)) ([f9e0aec](https://github.com/subconsciousnetwork/noosphere/commit/f9e0aecd76a7253aba13b1881af32a2e543fb6de)), closes [#386](https://github.com/subconsciousnetwork/noosphere/issues/386) - - -### Bug Fixes - -* Enable incremental sphere replication ([#409](https://github.com/subconsciousnetwork/noosphere/issues/409)) ([8812a1e](https://github.com/subconsciousnetwork/noosphere/commit/8812a1e8c9348301b36b77d6c1a2024432806358)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.11.0 to 0.12.0 - * noosphere-storage bumped from 0.6.3 to 0.7.0 - * noosphere-car bumped from 0.1.2 to 0.2.0 - -## [0.7.8](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-api-v0.7.7...noosphere-api-v0.7.8) (2023-05-08) - - -### Features - -* Enable expired yet valid records in the name system. Update to ucan 0.2.0. ([#360](https://github.com/subconsciousnetwork/noosphere/issues/360)) ([3b0663a](https://github.com/subconsciousnetwork/noosphere/commit/3b0663abc7783a6d33dd47d20caae7597ab93ed0)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.10.2 to 0.11.0 - * noosphere-storage bumped from 0.6.2 to 0.6.3 - -## [0.7.7](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-api-v0.7.6...noosphere-api-v0.7.7) (2023-05-05) - - -### Features - -* Enable expired yet valid records in the name system. Update to ucan 0.2.0. ([#360](https://github.com/subconsciousnetwork/noosphere/issues/360)) ([3b0663a](https://github.com/subconsciousnetwork/noosphere/commit/3b0663abc7783a6d33dd47d20caae7597ab93ed0)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.10.1 to 0.10.2 - -## [0.7.4](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-api-v0.7.3...noosphere-api-v0.7.4) (2023-04-22) - - -### Features - -* Update IPLD-related dependencies ([#327](https://github.com/subconsciousnetwork/noosphere/issues/327)) ([5fdfadb](https://github.com/subconsciousnetwork/noosphere/commit/5fdfadb1656f9d6eef2dbbb8b00a598106bccf00)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.9.2 to 0.9.3 - * noosphere-storage bumped from 0.6.1 to 0.6.2 - * noosphere-car bumped from 0.1.0 to 0.1.1 - -## [0.7.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-api-v0.6.0...noosphere-api-v0.7.0) (2023-03-29) - - -### ⚠ BREAKING CHANGES - -* Traverse the Noosphere vast (#284) - -### Features - -* Traverse the Noosphere vast ([#284](https://github.com/subconsciousnetwork/noosphere/issues/284)) ([43bceaf](https://github.com/subconsciousnetwork/noosphere/commit/43bceafcc838c5b06565780f372bf7b401de288e)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.7.0 to 0.8.0 - * noosphere-storage bumped from 0.5.0 to 0.6.0 - -## [0.6.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-api-v0.5.6...noosphere-api-v0.6.0) (2023-03-14) - - -### ⚠ BREAKING CHANGES - -* Petname resolution and synchronization in spheres and gateways (#253) - -### Features - -* Petname resolution and synchronization in spheres and gateways ([#253](https://github.com/subconsciousnetwork/noosphere/issues/253)) ([f7ddfa7](https://github.com/subconsciousnetwork/noosphere/commit/f7ddfa7b65129efe795c6e3fca58cdc22799127a)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.6.3 to 0.7.0 - * noosphere-storage bumped from 0.4.2 to 0.5.0 - -## [0.5.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-api-v0.4.0...noosphere-api-v0.5.0) (2022-11-30) - - -### ⚠ BREAKING CHANGES - -* Several critical dependencies of this library were updated to new versions that contain breaking changes. - -### Miscellaneous Chores - -* Update IPLD-adjacent dependencies ([#180](https://github.com/subconsciousnetwork/noosphere/issues/180)) ([1a1114b](https://github.com/subconsciousnetwork/noosphere/commit/1a1114b0c6277ea2c0d879e43191e962eb2e462b)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.3.0 to 0.4.0 - * noosphere-storage bumped from 0.2.0 to 0.3.0 - -## [0.4.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-api-v0.3.0...noosphere-api-v0.4.0) (2022-11-29) - - -### ⚠ BREAKING CHANGES - -* The `StorageProvider` trait has been replaced by the `Storage` trait. This new trait allows for distinct backing implementations of `BlockStore` and `KeyValueStore`. -* The `.sphere` directory has a new layout; the files previously used to store metadata have been replaced with database metadata; the `blocks` directory is now called `storage`. At this time the easiest migration path is to initialize a new sphere and copy your existing files into it. - -### Features - -* Re-implement `noosphere-cli` in terms of `noosphere` ([#162](https://github.com/subconsciousnetwork/noosphere/issues/162)) ([1e83bbb](https://github.com/subconsciousnetwork/noosphere/commit/1e83bbb689642b878f4f6909d7dd4a6df56b29f9)) -* Refactor storage interfaces ([#178](https://github.com/subconsciousnetwork/noosphere/issues/178)) ([4db55c4](https://github.com/subconsciousnetwork/noosphere/commit/4db55c4cba56b329a638a4227e7f3247ad8d319c)) -* Syndicate sphere revisions to IPFS Kubo ([#177](https://github.com/subconsciousnetwork/noosphere/issues/177)) ([e269e04](https://github.com/subconsciousnetwork/noosphere/commit/e269e0484b73e0f5507406d57a2c06cf849bee3d)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.2.0 to 0.3.0 - * noosphere-storage bumped from 0.1.0 to 0.2.0 - -## [0.3.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-api-v0.2.0...noosphere-api-v0.3.0) (2022-11-14) - - -### ⚠ BREAKING CHANGES - -* Many APIs that previously asked for bare strings when a DID string was expected now expect a newtype called `Did` that wraps a string. - -### Features - -* `SphereFs` is initialized with key material ([#140](https://github.com/subconsciousnetwork/noosphere/issues/140)) ([af48061](https://github.com/subconsciousnetwork/noosphere/commit/af4806114ca8f7703e0a888c7f369a4a4ed69c00)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.1.0 to 0.2.0 - -## [0.2.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-api-v0.1.0...noosphere-api-v0.2.0) (2022-11-09) - - -### ⚠ BREAKING CHANGES - -* The `noosphere-api` Client now holds an owned key instead of a reference. - -### Features - -* Add `noosphere` crate-based Swift package ([#131](https://github.com/subconsciousnetwork/noosphere/issues/131)) ([e1204c2](https://github.com/subconsciousnetwork/noosphere/commit/e1204c2a5822c3c0dbb7e61bbacffb2c1f49d8d8)) - -## [0.1.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-api-v0.1.0...noosphere-api-v0.1.0) (2022-11-09) - - -### ⚠ BREAKING CHANGES - -* The `noosphere-api` Client now holds an owned key instead of a reference. - -### Features - -* Add `noosphere` crate-based Swift package ([#131](https://github.com/subconsciousnetwork/noosphere/issues/131)) ([e1204c2](https://github.com/subconsciousnetwork/noosphere/commit/e1204c2a5822c3c0dbb7e61bbacffb2c1f49d8d8)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.1.0-alpha.1 to 0.1.0 - * noosphere-storage bumped from 0.1.0-alpha.1 to 0.1.0 - -## [0.1.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-api-v0.1.0-alpha.1...noosphere-api-v0.1.0) (2022-11-03) - - -### Bug Fixes - -* **api:** Use rustls instead of OpenSSL ([1a0625a](https://github.com/subconsciousnetwork/noosphere/commit/1a0625ad79330d35ca137361297318bdbf29137e)) diff --git a/rust/noosphere-api/Cargo.toml b/rust/noosphere-api/Cargo.toml deleted file mode 100644 index 6826f1f27..000000000 --- a/rust/noosphere-api/Cargo.toml +++ /dev/null @@ -1,49 +0,0 @@ -[package] -name = "noosphere-api" -version = "0.13.0+deprecated" -edition = "2021" -description = "Type information pertinent to the REST API of the gateway server that is a part of the Noosphere CLI" -keywords = ["rest", "api", "noosphere", "p2p"] -categories = [ - "web-programming", - "http-client", - "authentication", - "web-assembly" -] -rust-version = "1.60.0" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/noosphere-api" -repository = "https://github.com/subconsciousnetwork/noosphere" -homepage = "https://github.com/subconsciousnetwork/noosphere" -readme = "README.md" - - -[dependencies] -anyhow = { workspace = true } -thiserror = { workspace = true } -cid = { workspace = true } -url = { workspace = true } -serde = { workspace = true } -serde_urlencoded = "~0.7" -tracing = { workspace = true } -noosphere-core = { version = "0.16.0", path = "../noosphere-core" } -noosphere-storage = { version = "0.9.0", path = "../noosphere-storage" } -iroh-car = { workspace = true } -reqwest = { version = "0.11.15", default-features = false, features = ["json", "rustls-tls", "stream"] } -tokio-stream = { workspace = true } -tokio-util = "0.7.7" - -ucan = { workspace = true } -ucan-key-support = { workspace = true } - -libipld-core = { workspace = true } -libipld-cbor = { workspace = true } - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tokio = { workspace = true, features = ["full"] } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = { workspace = true } - -[dev-dependencies] -wasm-bindgen-test = { workspace = true } diff --git a/rust/noosphere-api/README.md b/rust/noosphere-api/README.md deleted file mode 100644 index 3b0cd09a5..000000000 --- a/rust/noosphere-api/README.md +++ /dev/null @@ -1,7 +0,0 @@ -![API Stability: Alpha](https://img.shields.io/badge/API%20Stability-Alpha-red) - -# Noosphere API - -This crate contains type information pertinent to the REST API of the gateway -server that is a part of the Noosphere CLI, as well as a reference client that -can be used on the web or on native targets. diff --git a/rust/noosphere-api/src/client.rs b/rust/noosphere-api/src/client.rs deleted file mode 100644 index 82aca69ec..000000000 --- a/rust/noosphere-api/src/client.rs +++ /dev/null @@ -1,336 +0,0 @@ -use std::str::FromStr; - -use crate::{ - data::{ - FetchParameters, IdentifyResponse, PushBody, PushError, PushResponse, ReplicateParameters, - }, - route::{Route, RouteUrl}, -}; - -use anyhow::{anyhow, Result}; -use cid::Cid; -use iroh_car::CarReader; -use libipld_cbor::DagCborCodec; - -use noosphere_core::{ - authority::{generate_capability, Author, SphereAbility, SphereReference}, - data::{Link, MemoIpld}, -}; -use noosphere_storage::{block_deserialize, block_serialize}; -use reqwest::{header::HeaderMap, Body, StatusCode}; -use tokio_stream::{Stream, StreamExt}; -use tokio_util::io::StreamReader; -use ucan::{ - builder::UcanBuilder, - capability::CapabilityView, - crypto::{did::DidParser, KeyMaterial}, - store::{UcanJwtStore, UcanStore}, - ucan::Ucan, -}; -use url::Url; - -/// A [Client] is a simple, portable HTTP client for the Noosphere gateway REST -/// API. It embodies the intended usage of the REST API, which includes an -/// opening handshake (with associated key verification) and various -/// UCAN-authorized verbs over sphere data. -pub struct Client -where - K: KeyMaterial + Clone + 'static, - S: UcanStore, -{ - pub session: IdentifyResponse, - pub sphere_identity: String, - pub api_base: Url, - pub author: Author, - pub store: S, - client: reqwest::Client, -} - -impl Client -where - K: KeyMaterial + Clone + 'static, - S: UcanStore, -{ - pub async fn identify( - sphere_identity: &str, - api_base: &Url, - author: &Author, - did_parser: &mut DidParser, - store: S, - ) -> Result> { - debug!("Initializing Noosphere API client"); - debug!("Client represents sphere {}", sphere_identity); - debug!("Client targetting API at {}", api_base); - - let client = reqwest::Client::new(); - - let mut url = api_base.clone(); - url.set_path(&Route::Did.to_string()); - - let did_response = client.get(url).send().await?; - - match did_response.status() { - StatusCode::OK => (), - _ => return Err(anyhow!("Unable to look up gateway identity")), - }; - - let gateway_identity = did_response.text().await?; - - let mut url = api_base.clone(); - url.set_path(&Route::Identify.to_string()); - - let (jwt, ucan_headers) = Self::make_bearer_token( - &gateway_identity, - author, - &generate_capability(sphere_identity, SphereAbility::Fetch), - &store, - ) - .await?; - - let identify_response: IdentifyResponse = client - .get(url) - .bearer_auth(jwt) - .headers(ucan_headers) - .send() - .await? - .json() - .await?; - - identify_response.verify(did_parser, &store).await?; - - debug!( - "Handshake succeeded with gateway {}", - identify_response.gateway_identity - ); - - Ok(Client { - session: identify_response, - sphere_identity: sphere_identity.into(), - api_base: api_base.clone(), - author: author.clone(), - store, - client, - }) - } - - async fn make_bearer_token( - gateway_identity: &str, - author: &Author, - capability: &CapabilityView, - store: &S, - ) -> Result<(String, HeaderMap)> { - let mut signable = UcanBuilder::default() - .issued_by(&author.key) - .for_audience(gateway_identity) - .with_lifetime(120) - .claiming_capability(capability) - .with_nonce() - .build()?; - - let mut ucan_headers = HeaderMap::new(); - - let authorization = author.require_authorization()?; - let authorization_cid = Cid::try_from(authorization)?; - - match authorization.as_ucan(store).await { - Ok(ucan) => { - if let Some(ucan_proofs) = ucan.proofs() { - // TODO(ucan-wg/rs-ucan#37): We should integrate a helper for this kind of stuff into rs-ucan - let mut proofs_to_search: Vec = ucan_proofs.clone(); - - debug!("Making bearer token... {:?}", proofs_to_search); - - while let Some(cid_string) = proofs_to_search.pop() { - let cid = Cid::from_str(cid_string.as_str())?; - let jwt = store.require_token(&cid).await?; - let ucan = Ucan::from_str(&jwt)?; - - debug!("Adding UCAN header for {}", cid); - - if let Some(ucan_proofs) = ucan.proofs() { - proofs_to_search.extend(ucan_proofs.clone().into_iter()); - } - - ucan_headers.append("ucan", format!("{cid} {jwt}").parse()?); - } - } - - ucan_headers.append( - "ucan", - format!("{} {}", authorization_cid, ucan.encode()?).parse()?, - ); - } - _ => { - debug!( - "Unable to resolve authorization to a UCAN; it will be used as a blind proof" - ) - } - }; - - // TODO(ucan-wg/rs-ucan#32): This is kind of a hack until we can add proofs by CID - signable - .proofs - .push(Cid::try_from(authorization)?.to_string()); - - let jwt = signable.sign().await?.encode()?; - - // TODO: It is inefficient to send the same UCANs with every request, - // we should probably establish a conventional flow for syncing UCANs - // this way only once when pairing a gateway. For now, this is about the - // same efficiency as what we had before when UCANs were all inlined to - // a single token. - Ok((jwt, ucan_headers)) - } - - /// Replicate content from Noosphere, streaming its blocks from the - /// configured gateway. If the gateway doesn't have the desired content, it - /// will look it up from other sources such as IPFS if they are available. - /// Note that this means this call can potentially block on upstream - /// access to an IPFS node (which, depending on the node's network - /// configuration and peering status, can be quite slow). - pub async fn replicate( - &self, - memo_version: &Cid, - params: Option<&ReplicateParameters>, - ) -> Result)>>> { - let url = Url::try_from(RouteUrl( - &self.api_base, - Route::Replicate(Some(*memo_version)), - params, - ))?; - - debug!("Client replicating {} from {}", memo_version, url); - - let capability = generate_capability(&self.sphere_identity, SphereAbility::Fetch); - - let (token, ucan_headers) = Self::make_bearer_token( - &self.session.gateway_identity, - &self.author, - &capability, - &self.store, - ) - .await?; - - let response = self - .client - .get(url) - .bearer_auth(token) - .headers(ucan_headers) - .send() - .await?; - - Ok( - CarReader::new(StreamReader::new(response.bytes_stream().map( - |item| match item { - Ok(item) => Ok(item), - Err(error) => { - error!("Failed to read CAR stream: {}", error); - Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe)) - } - }, - ))) - .await? - .stream() - .map(|block| match block { - Ok(block) => Ok(block), - Err(error) => Err(anyhow!(error)), - }), - ) - } - - pub async fn fetch( - &self, - params: &FetchParameters, - ) -> Result, impl Stream)>>)>> { - let url = Url::try_from(RouteUrl(&self.api_base, Route::Fetch, Some(params)))?; - - debug!("Client fetching blocks from {}", url); - - let capability = generate_capability(&self.sphere_identity, SphereAbility::Fetch); - let (token, ucan_headers) = Self::make_bearer_token( - &self.session.gateway_identity, - &self.author, - &capability, - &self.store, - ) - .await?; - - let response = self - .client - .get(url) - .bearer_auth(token) - .headers(ucan_headers) - .send() - .await?; - - let reader = CarReader::new(StreamReader::new(response.bytes_stream().map( - |item| match item { - Ok(item) => Ok(item), - Err(error) => { - error!("Failed to read CAR stream: {}", error); - Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe)) - } - }, - ))) - .await?; - - let tip = reader.header().roots().first().cloned(); - - if let Some(tip) = tip { - Ok(match tip.codec() { - // Identity codec = no changes - 0 => None, - _ => Some(( - tip.into(), - reader.stream().map(|block| match block { - Ok(block) => Ok(block), - Err(error) => Err(anyhow!(error)), - }), - )), - }) - } else { - Ok(None) - } - } - - pub async fn push(&self, push_body: &PushBody) -> Result { - let url = Url::try_from(RouteUrl::<()>(&self.api_base, Route::Push, None))?; - debug!( - "Client pushing {} blocks for sphere {} to {}", - push_body.blocks.len(), - push_body.sphere, - url - ); - let capability = generate_capability(&self.sphere_identity, SphereAbility::Push); - let (token, ucan_headers) = Self::make_bearer_token( - &self.session.gateway_identity, - &self.author, - &capability, - &self.store, - ) - .await?; - - let (_, push_body_bytes) = block_serialize::(push_body)?; - - let response = self - .client - .put(url) - .bearer_auth(token) - .headers(ucan_headers) - .header("Content-Type", "application/octet-stream") - .body(Body::from(push_body_bytes)) - .send() - .await - .map_err(|err| PushError::Internal(anyhow!(err)))?; - - if response.status() == StatusCode::CONFLICT { - return Err(PushError::Conflict); - } - - let bytes = response - .bytes() - .await - .map_err(|err| PushError::Internal(anyhow!(err)))?; - Ok(block_deserialize::(bytes.as_ref())?) - } -} diff --git a/rust/noosphere-api/src/data.rs b/rust/noosphere-api/src/data.rs deleted file mode 100644 index aa2d8bfd2..000000000 --- a/rust/noosphere-api/src/data.rs +++ /dev/null @@ -1,261 +0,0 @@ -use std::{fmt::Display, str::FromStr}; - -use anyhow::{anyhow, Result}; -use cid::Cid; -use noosphere_core::{ - authority::{generate_capability, SphereAbility, SPHERE_SEMANTICS}, - data::{Bundle, Did, Jwt, Link, MemoIpld}, - error::NoosphereError, -}; -use noosphere_storage::{base64_decode, base64_encode}; -use reqwest::StatusCode; -use serde::{Deserialize, Deserializer, Serialize}; -use thiserror::Error; -use ucan::{ - chain::ProofChain, - crypto::{did::DidParser, KeyMaterial}, - store::UcanStore, - Ucan, -}; - -pub trait AsQuery { - fn as_query(&self) -> Result>; -} - -impl AsQuery for () { - fn as_query(&self) -> Result> { - Ok(None) - } -} - -// NOTE: Adapted from https://github.com/tokio-rs/axum/blob/7caa4a3a47a31c211d301f3afbc518ea2c07b4de/examples/query-params-with-empty-strings/src/main.rs#L42-L54 -/// Serde deserialization decorator to map empty Strings to None, -fn empty_string_as_none<'de, D, T>(de: D) -> Result, D::Error> -where - D: Deserializer<'de>, - T: FromStr, - T::Err: std::fmt::Display, -{ - let opt = Option::::deserialize(de)?; - match opt.as_deref() { - None | Some("") => Ok(None), - Some(s) => FromStr::from_str(s) - .map_err(serde::de::Error::custom) - .map(Some), - } -} - -/// The query parameters expected for the "replicate" API route -#[derive(Debug, Serialize, Deserialize)] -pub struct ReplicateParameters { - /// This is the last revision of the content that is being fetched that is - /// already fully available to the caller of the API - #[serde(default, deserialize_with = "empty_string_as_none")] - pub since: Option>, -} - -impl AsQuery for ReplicateParameters { - fn as_query(&self) -> Result> { - Ok(self.since.as_ref().map(|since| format!("since={since}"))) - } -} - -/// The query parameters expected for the "fetch" API route -#[derive(Debug, Serialize, Deserialize)] -pub struct FetchParameters { - /// This is the last revision of the "counterpart" sphere that is managed - /// by the API host that the client is fetching from - #[serde(default, deserialize_with = "empty_string_as_none")] - pub since: Option>, -} - -impl AsQuery for FetchParameters { - fn as_query(&self) -> Result> { - Ok(self.since.as_ref().map(|since| format!("since={since}"))) - } -} - -/// The possible responses from the "fetch" API route -#[derive(Debug, Serialize, Deserialize)] -pub enum FetchResponse { - /// There are new revisions to the local and "counterpart" spheres to sync - /// with local history - NewChanges { - /// The tip of the "counterpart" sphere that is managed by the API host - /// that the client is fetching from - tip: Cid, - }, - /// There are no new revisions since the revision specified in the initial - /// fetch request - UpToDate, -} - -/// The body payload expected by the "push" API route -#[derive(Debug, Serialize, Deserialize)] -pub struct PushBody { - /// The DID of the local sphere whose revisions are being pushed - pub sphere: Did, - /// The base revision represented by the payload being pushed; if the - /// entire history is being pushed, then this should be None - pub local_base: Option>, - /// The tip of the history represented by the payload being pushed - pub local_tip: Link, - /// The last received tip of the counterpart sphere - pub counterpart_tip: Option>, - /// A bundle of all the blocks needed to hydrate the revisions from the - /// base to the tip of history as represented by this payload - pub blocks: Bundle, - /// An optional name record to publish to the Noosphere Name System - pub name_record: Option, -} - -/// The possible responses from the "push" API route -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum PushResponse { - /// The new history was accepted - Accepted { - /// This is the new tip of the "counterpart" sphere after accepting - /// the latest history from the local sphere. This is guaranteed to be - /// at least one revision ahead of the latest revision being tracked - /// by the client (because it points to the newly received tip of the - /// local sphere's history) - new_tip: Link, - /// The blocks needed to hydrate the revisions of the "counterpart" - /// sphere history to the tip represented in this response - blocks: Bundle, - }, - /// The history was already known by the API host, so no changes were made - NoChange, -} - -#[derive(Error, Debug)] -pub enum PushError { - #[error("Pushed history conflicts with canonical history")] - Conflict, - #[error("Missing some implied history")] - MissingHistory, - #[error("Replica is up to date")] - UpToDate, - #[error("Internal error")] - Internal(anyhow::Error), -} - -impl From for PushError { - fn from(error: NoosphereError) -> Self { - error.into() - } -} - -impl From for PushError { - fn from(value: anyhow::Error) -> Self { - PushError::Internal(value) - } -} - -impl From for StatusCode { - fn from(error: PushError) -> Self { - match error { - PushError::Conflict => StatusCode::CONFLICT, - PushError::MissingHistory => StatusCode::UNPROCESSABLE_ENTITY, - PushError::UpToDate => StatusCode::BAD_REQUEST, - PushError::Internal(error) => { - error!("Internal: {:?}", error); - StatusCode::INTERNAL_SERVER_ERROR - } - } - } -} - -/// The response from the "identify" API route; this is a signed response that -/// allows the client to verify the authority of the API host -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct IdentifyResponse { - /// The DID of the API host - pub gateway_identity: Did, - /// The DID of the "counterpart" sphere - pub sphere_identity: Did, - /// The signature of the API host over this payload, as base64-encoded bytes - pub signature: String, - /// The proof that the API host was authorized by the "counterpart" sphere - /// in the form of a UCAN JWT - pub proof: String, -} - -impl IdentifyResponse { - pub async fn sign(sphere_identity: &str, key: &K, proof: &Ucan) -> Result - where - K: KeyMaterial, - { - let gateway_identity = Did(key.get_did().await?); - let signature = base64_encode( - &key.sign(&[gateway_identity.as_bytes(), sphere_identity.as_bytes()].concat()) - .await?, - )?; - Ok(IdentifyResponse { - gateway_identity, - sphere_identity: sphere_identity.into(), - signature, - proof: proof.encode()?, - }) - } - - pub fn shares_identity_with(&self, other: &IdentifyResponse) -> bool { - self.gateway_identity == other.gateway_identity - && self.sphere_identity == other.sphere_identity - } - - /// Verifies that the signature scheme on the payload. The signature is made - /// by signing the bytes of the gateway's key DID plus the bytes of the - /// sphere DID that the gateway claims to manage. Remember: this sphere is - /// not the user's sphere, but rather the "counterpart" sphere created and - /// modified by the gateway. Additionally, a proof is given that the gateway - /// has been authorized to modify its own sphere. - /// - /// This verification is intended to check two things: - /// - /// 1. The gateway has control of the key that it represents with its DID - /// 2. The gateway is authorized to modify the sphere it claims to manage - pub async fn verify(&self, did_parser: &mut DidParser, store: &S) -> Result<()> { - let gateway_key = did_parser.parse(&self.gateway_identity)?; - let payload_bytes = [ - self.gateway_identity.as_bytes(), - self.sphere_identity.as_bytes(), - ] - .concat(); - let signature_bytes = base64_decode(&self.signature)?; - - // Verify that the signature is valid - gateway_key.verify(&payload_bytes, &signature_bytes).await?; - - let proof = ProofChain::try_from_token_string(&self.proof, None, did_parser, store).await?; - - if proof.ucan().audience() != self.gateway_identity.as_str() { - return Err(anyhow!("Wrong audience!")); - } - - let capability = generate_capability(&self.sphere_identity, SphereAbility::Push); - let capability_infos = proof.reduce_capabilities(&SPHERE_SEMANTICS); - - for capability_info in capability_infos { - if capability_info.capability.enables(&capability) - && capability_info - .originators - .contains(self.sphere_identity.as_str()) - { - return Ok(()); - } - } - - Err(anyhow!("Not authorized!")) - } -} - -impl Display for IdentifyResponse { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "((Gateway {}), (Sphere {}))", - self.gateway_identity, self.sphere_identity - ) - } -} diff --git a/rust/noosphere-api/src/lib.rs b/rust/noosphere-api/src/lib.rs deleted file mode 100644 index f28bc3eee..000000000 --- a/rust/noosphere-api/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[macro_use] -extern crate tracing; - -pub mod client; -pub mod data; -pub mod route; diff --git a/rust/noosphere-api/src/route.rs b/rust/noosphere-api/src/route.rs deleted file mode 100644 index 5fb9e6e4e..000000000 --- a/rust/noosphere-api/src/route.rs +++ /dev/null @@ -1,53 +0,0 @@ -use anyhow::Result; -use cid::Cid; -use std::fmt::Display; -use url::Url; - -use crate::data::AsQuery; - -pub const API_VERSION: &str = "v0alpha1"; - -pub enum Route { - Fetch, - Push, - Publish, - Did, - Identify, - Replicate(Option), -} - -impl Display for Route { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let fragment = match self { - Route::Fetch => "fetch".into(), - Route::Push => "push".into(), - Route::Publish => "publish".into(), - Route::Did => "did".into(), - Route::Identify => "identify".into(), - Route::Replicate(cid) => match cid { - Some(cid) => format!("replicate/{cid}"), - None => "replicate/:memo".into(), - }, - }; - - write!(f, "/api/{API_VERSION}/{fragment}") - } -} - -pub struct RouteUrl<'a, 'b, Params: AsQuery = ()>(pub &'a Url, pub Route, pub Option<&'b Params>); - -impl<'a, 'b, Params: AsQuery> TryFrom> for Url { - type Error = anyhow::Error; - - fn try_from(value: RouteUrl<'a, 'b, Params>) -> Result { - let RouteUrl(api_base, route, params) = value; - let mut url = api_base.clone(); - url.set_path(&route.to_string()); - if let Some(params) = params { - url.set_query(params.as_query()?.as_deref()); - } else { - url.set_query(None); - } - Ok(url) - } -} diff --git a/rust/noosphere-sphere/CHANGELOG.md b/rust/noosphere-sphere/CHANGELOG.md deleted file mode 100644 index 08df06cbe..000000000 --- a/rust/noosphere-sphere/CHANGELOG.md +++ /dev/null @@ -1,423 +0,0 @@ -# Changelog - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.10.0 to 0.10.1 - * noosphere-api bumped from 0.7.5 to 0.7.6 - * noosphere-ipfs bumped from 0.4.0 to 0.4.1 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.10.1 to 0.10.2 - * noosphere-api bumped from 0.7.6 to 0.7.7 - * noosphere-ipfs bumped from 0.4.1 to 0.4.2 - -* The following workspace dependencies were updated - * dependencies - * noosphere-api bumped from 0.7.8 to 0.7.9 - * noosphere-ipfs bumped from 0.4.3 to 0.4.4 - * noosphere-car bumped from 0.1.1 to 0.1.2 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.12.2 to 0.12.3 - * noosphere-api bumped from 0.8.2 to 0.8.3 - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.13.0 to 0.13.1 - * noosphere-storage bumped from 0.7.0 to 0.7.1 - * noosphere-api bumped from 0.9.0 to 0.9.1 - -## [0.11.0+deprecated](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.10.2...noosphere-sphere-v0.11.0+deprecated) (2023-09-19) - - -### ⚠ BREAKING CHANGES - -* Replace `Bundle` with CAR streams in push ([#624](https://github.com/subconsciousnetwork/noosphere/issues/624)) - -### Features - -* Replace `Bundle` with CAR streams in push ([#624](https://github.com/subconsciousnetwork/noosphere/issues/624)) ([9390797](https://github.com/subconsciousnetwork/noosphere/commit/9390797eb6653fdecd41c3a54225ffd55945bb89)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.15.2 to 0.16.0 - * noosphere-storage bumped from 0.8.1 to 0.9.0 - * noosphere-api bumped from 0.12.2 to 0.13.0+deprecated - * dev-dependencies - * noosphere-core bumped from 0.15.2 to 0.16.0 - -## [0.10.2](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.10.1...noosphere-sphere-v0.10.2) (2023-08-29) - - -### Features - -* Ensure adopted link records are fresher than previous entries. Fixes [#258](https://github.com/subconsciousnetwork/noosphere/issues/258), fixes [#562](https://github.com/subconsciousnetwork/noosphere/issues/562) ([#578](https://github.com/subconsciousnetwork/noosphere/issues/578)) ([36e42fb](https://github.com/subconsciousnetwork/noosphere/commit/36e42fb03424858e7731d10ad0a0cf89826b1354)) - - -### Bug Fixes - -* Increase allowed request body payload size ([#608](https://github.com/subconsciousnetwork/noosphere/issues/608)) ([da83f38](https://github.com/subconsciousnetwork/noosphere/commit/da83f3894d47d606bd148b72db83414a92688cf4)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.15.1 to 0.15.2 - * noosphere-api bumped from 0.12.1 to 0.12.2 - * dev-dependencies - * noosphere-core bumped from 0.15.1 to 0.15.2 - -## [0.10.1](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.10.0...noosphere-sphere-v0.10.1) (2023-08-10) - - -### Features - -* `orb sphere history` and `orb sphere render` ([#576](https://github.com/subconsciousnetwork/noosphere/issues/576)) ([a6f0a74](https://github.com/subconsciousnetwork/noosphere/commit/a6f0a74cde2fc001bfff5c1bed0844ac19fc8258)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.15.0 to 0.15.1 - * noosphere-storage bumped from 0.8.0 to 0.8.1 - * noosphere-api bumped from 0.12.0 to 0.12.1 - * dev-dependencies - * noosphere-core bumped from 0.15.0 to 0.15.1 - -## [0.10.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.9.0...noosphere-sphere-v0.10.0) (2023-08-04) - - -### ⚠ BREAKING CHANGES - -* `orb` uses latest Noosphere capabilities ([#530](https://github.com/subconsciousnetwork/noosphere/issues/530)) - -### Features - -* `orb` uses latest Noosphere capabilities ([#530](https://github.com/subconsciousnetwork/noosphere/issues/530)) ([adfa028](https://github.com/subconsciousnetwork/noosphere/commit/adfa028ebcb2de7ea7492af57239fcc9bfc27955)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.14.0 to 0.15.0 - * noosphere-storage bumped from 0.7.1 to 0.8.0 - * noosphere-api bumped from 0.11.0 to 0.12.0 - -## [0.9.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.8.0...noosphere-sphere-v0.9.0) (2023-07-20) - - -### ⚠ BREAKING CHANGES - -* C FFI to verify authorizations ([#510](https://github.com/subconsciousnetwork/noosphere/issues/510)) - -### Features - -* C FFI to verify authorizations ([#510](https://github.com/subconsciousnetwork/noosphere/issues/510)) ([ed092fc](https://github.com/subconsciousnetwork/noosphere/commit/ed092fc303f89ca4737f5e67681e2ede8189304d)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.13.2 to 0.14.0 - * noosphere-api bumped from 0.10.0 to 0.11.0 - -## [0.8.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.7.1...noosphere-sphere-v0.8.0) (2023-07-19) - - -### ⚠ BREAKING CHANGES - -* Replace `noosphere-car` with `iroh-car` throughout the Noosphere crates. ([#492](https://github.com/subconsciousnetwork/noosphere/issues/492)) - -### Features - -* Replace `noosphere-car` with `iroh-car` throughout the Noosphere crates. ([#492](https://github.com/subconsciousnetwork/noosphere/issues/492)) ([e89d498](https://github.com/subconsciousnetwork/noosphere/commit/e89d49879b3a1d2ce8529e438df7995ae8b4e44f)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.13.1 to 0.13.2 - * noosphere-api bumped from 0.9.1 to 0.10.0 - -## [0.7.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.6.3...noosphere-sphere-v0.7.0) (2023-07-01) - - -### ⚠ BREAKING CHANGES - -* Authorize and revoke APIs ([#420](https://github.com/subconsciousnetwork/noosphere/issues/420)) -* Update to `rs-ucan` 0.4.0, implementing UCAN 0.10ish. ([#449](https://github.com/subconsciousnetwork/noosphere/issues/449)) - -### Features - -* Authorize and revoke APIs ([#420](https://github.com/subconsciousnetwork/noosphere/issues/420)) ([73f016e](https://github.com/subconsciousnetwork/noosphere/commit/73f016e12448c46f95ae7683d91fd6422a925555)) -* Update to `rs-ucan` 0.4.0, implementing UCAN 0.10ish. ([#449](https://github.com/subconsciousnetwork/noosphere/issues/449)) ([8b806c5](https://github.com/subconsciousnetwork/noosphere/commit/8b806c5462b5601a5f8417a6a20769b76b57ee6a)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.12.3 to 0.13.0 - * noosphere-api bumped from 0.8.3 to 0.9.0 - -## [0.6.2](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.6.1...noosphere-sphere-v0.6.2) (2023-06-22) - - -### Bug Fixes - -* Disallow "did:" as petnames, adding self to address book ([#387](https://github.com/subconsciousnetwork/noosphere/issues/387)) ([77920be](https://github.com/subconsciousnetwork/noosphere/commit/77920be9dfdf0983de920394617a58c2395ad506)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.12.1 to 0.12.2 - * noosphere-api bumped from 0.8.1 to 0.8.2 - -## [0.6.1](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.6.0...noosphere-sphere-v0.6.1) (2023-06-09) - - -### Bug Fixes - -* Resolve petnames in correct order ([#412](https://github.com/subconsciousnetwork/noosphere/issues/412)) ([5df3f91](https://github.com/subconsciousnetwork/noosphere/commit/5df3f9187be1d0ef6edd542a9d1268c7cb4ffdb7)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.12.0 to 0.12.1 - * noosphere-api bumped from 0.8.0 to 0.8.1 - -## [0.6.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.5.8...noosphere-sphere-v0.6.0) (2023-06-08) - - -### ⚠ BREAKING CHANGES - -* Enable incremental sphere replication ([#409](https://github.com/subconsciousnetwork/noosphere/issues/409)) -* Migrate blake2b->blake3 everywhere. ([#400](https://github.com/subconsciousnetwork/noosphere/issues/400)) - -### Features - -* Consolidate `NsRecord` implementation in`LinkRecord`. Fixes [#395](https://github.com/subconsciousnetwork/noosphere/issues/395) ([#399](https://github.com/subconsciousnetwork/noosphere/issues/399)) ([9ee4798](https://github.com/subconsciousnetwork/noosphere/commit/9ee47981232fde00b34bb9458c5b0b2799a610ca)) -* Migrate blake2b->blake3 everywhere. ([#400](https://github.com/subconsciousnetwork/noosphere/issues/400)) ([f9e0aec](https://github.com/subconsciousnetwork/noosphere/commit/f9e0aecd76a7253aba13b1881af32a2e543fb6de)), closes [#386](https://github.com/subconsciousnetwork/noosphere/issues/386) - - -### Bug Fixes - -* Enable incremental sphere replication ([#409](https://github.com/subconsciousnetwork/noosphere/issues/409)) ([8812a1e](https://github.com/subconsciousnetwork/noosphere/commit/8812a1e8c9348301b36b77d6c1a2024432806358)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.11.0 to 0.12.0 - * noosphere-storage bumped from 0.6.3 to 0.7.0 - * noosphere-api bumped from 0.7.9 to 0.8.0 - * noosphere-car bumped from 0.1.2 to 0.2.0 - -## [0.5.8](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.5.7...noosphere-sphere-v0.5.8) (2023-05-12) - - -### Features - -* Get petnames assigned to a DID for a sphere ([#384](https://github.com/subconsciousnetwork/noosphere/issues/384)) ([aa1cec7](https://github.com/subconsciousnetwork/noosphere/commit/aa1cec7663b41b5bb0f6ffe3066d944b86153b2a)) -* Validate petnames and slugs, disallow an empty strings. ([#382](https://github.com/subconsciousnetwork/noosphere/issues/382)) ([fdda233](https://github.com/subconsciousnetwork/noosphere/commit/fdda2330d8545a64054bcda5b97e288c6c7ffdaa)) - -## [0.5.7](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.5.6...noosphere-sphere-v0.5.7) (2023-05-11) - - -### Bug Fixes - -* Ensure petname link records are replicated ([#377](https://github.com/subconsciousnetwork/noosphere/issues/377)) ([b5d0204](https://github.com/subconsciousnetwork/noosphere/commit/b5d020423d81ed4e37f33cf6bd0c73b8883b1673)) - -## [0.5.6](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.5.5...noosphere-sphere-v0.5.6) (2023-05-09) - - -### Bug Fixes - -* Removed petnames stay removed ([#373](https://github.com/subconsciousnetwork/noosphere/issues/373)) ([76a4ccf](https://github.com/subconsciousnetwork/noosphere/commit/76a4ccfd80f7855933a122a841f0398ab0bcc03c)) - -## [0.5.4](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.5.3...noosphere-sphere-v0.5.4) (2023-05-08) - - -### Features - -* Make `anyhow` a workspace dependency in `noosphere-sphere` ([254049b](https://github.com/subconsciousnetwork/noosphere/commit/254049b12a1721a4c024e07dbd46b06737d00ee1)) - -## [0.5.3](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.5.2...noosphere-sphere-v0.5.3) (2023-05-08) - - -### Features - -* Enable expired yet valid records in the name system. Update to ucan 0.2.0. ([#360](https://github.com/subconsciousnetwork/noosphere/issues/360)) ([3b0663a](https://github.com/subconsciousnetwork/noosphere/commit/3b0663abc7783a6d33dd47d20caae7597ab93ed0)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.10.2 to 0.11.0 - * noosphere-storage bumped from 0.6.2 to 0.6.3 - * noosphere-api bumped from 0.7.7 to 0.7.8 - * noosphere-ipfs bumped from 0.4.2 to 0.4.3 - -## [0.5.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.4.1...noosphere-sphere-v0.5.0) (2023-05-02) - - -### ⚠ BREAKING CHANGES - -* Revised tracing configuration (#342) - -### Features - -* Revised tracing configuration ([#342](https://github.com/subconsciousnetwork/noosphere/issues/342)) ([c4a4084](https://github.com/subconsciousnetwork/noosphere/commit/c4a4084771680c8e49b3db498a5da422db2adda8)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.9.3 to 0.10.0 - * noosphere-api bumped from 0.7.4 to 0.7.5 - * noosphere-ipfs bumped from 0.3.4 to 0.4.0 - -## [0.4.1](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.4.0...noosphere-sphere-v0.4.1) (2023-04-22) - - -### Features - -* Update IPLD-related dependencies ([#327](https://github.com/subconsciousnetwork/noosphere/issues/327)) ([5fdfadb](https://github.com/subconsciousnetwork/noosphere/commit/5fdfadb1656f9d6eef2dbbb8b00a598106bccf00)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.9.2 to 0.9.3 - * noosphere-storage bumped from 0.6.1 to 0.6.2 - * noosphere-api bumped from 0.7.3 to 0.7.4 - * noosphere-ipfs bumped from 0.3.3 to 0.3.4 - * noosphere-car bumped from 0.1.0 to 0.1.1 - -## [0.4.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.3.2...noosphere-sphere-v0.4.0) (2023-04-19) - - -### ⚠ BREAKING CHANGES - -* Some non-blocking, callback-based C FFI (#322) - -### Features - -* Some non-blocking, callback-based C FFI ([#322](https://github.com/subconsciousnetwork/noosphere/issues/322)) ([693ce40](https://github.com/subconsciousnetwork/noosphere/commit/693ce40143acf99f758a12df2627e265ef105e03)) -* Sphere writes do not block immutable reads ([#321](https://github.com/subconsciousnetwork/noosphere/issues/321)) ([14373c5](https://github.com/subconsciousnetwork/noosphere/commit/14373c5281c091bb41623677571566a2788a7e3f)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.9.1 to 0.9.2 - * noosphere-api bumped from 0.7.2 to 0.7.3 - * noosphere-ipfs bumped from 0.3.2 to 0.3.3 - -## [0.3.2](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.3.1...noosphere-sphere-v0.3.2) (2023-04-13) - - -### Bug Fixes - -* Unreachable petname sequence is not an error ([#310](https://github.com/subconsciousnetwork/noosphere/issues/310)) ([96f2938](https://github.com/subconsciousnetwork/noosphere/commit/96f2938d76f41fe240466bc7cfe397f886aa7e04)) - -## [0.3.1](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.3.0...noosphere-sphere-v0.3.1) (2023-04-10) - - -### Features - -* Dot syntax when traversing by petname ([#306](https://github.com/subconsciousnetwork/noosphere/issues/306)) ([cd87b05](https://github.com/subconsciousnetwork/noosphere/commit/cd87b0533c21bbbd4d82332556e70ecc706a5531)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.9.0 to 0.9.1 - * noosphere-storage bumped from 0.6.0 to 0.6.1 - * noosphere-api bumped from 0.7.1 to 0.7.2 - * noosphere-ipfs bumped from 0.3.1 to 0.3.2 - -## [0.3.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.2.0...noosphere-sphere-v0.3.0) (2023-04-04) - - -### ⚠ BREAKING CHANGES - -* Apply breaking domain concept in anticipation of beta (#298) - -### Miscellaneous Chores - -* Apply breaking domain concept in anticipation of beta ([#298](https://github.com/subconsciousnetwork/noosphere/issues/298)) ([bd34ab4](https://github.com/subconsciousnetwork/noosphere/commit/bd34ab49b2d2c65cffe25657cf4d188d5c79d15f)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.8.0 to 0.9.0 - * noosphere-api bumped from 0.7.0 to 0.7.1 - * noosphere-ipfs bumped from 0.3.0 to 0.3.1 - -## [0.2.0](https://github.com/subconsciousnetwork/noosphere/compare/noosphere-sphere-v0.1.0...noosphere-sphere-v0.2.0) (2023-03-29) - - -### ⚠ BREAKING CHANGES - -* Traverse the Noosphere vast (#284) -* Revise links and gateway (#278) - -### Features - -* Revise links and gateway ([#278](https://github.com/subconsciousnetwork/noosphere/issues/278)) ([4cd2e3a](https://github.com/subconsciousnetwork/noosphere/commit/4cd2e3af8b10cdaae710d87e4b919b5180d10fec)) -* Traverse the Noosphere vast ([#284](https://github.com/subconsciousnetwork/noosphere/issues/284)) ([43bceaf](https://github.com/subconsciousnetwork/noosphere/commit/43bceafcc838c5b06565780f372bf7b401de288e)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.7.0 to 0.8.0 - * noosphere-storage bumped from 0.5.0 to 0.6.0 - * noosphere-api bumped from 0.6.0 to 0.7.0 - * noosphere-ipfs bumped from 0.2.0 to 0.3.0 - -## 0.1.0 (2023-03-14) - - -### ⚠ BREAKING CHANGES - -* Implement C FFI for petname management (#271) -* Petname resolution and synchronization in spheres and gateways (#253) - -### Features - -* Implement C FFI for petname management ([#271](https://github.com/subconsciousnetwork/noosphere/issues/271)) ([d43c628](https://github.com/subconsciousnetwork/noosphere/commit/d43c6283c6b2374de503d70bd46c8df7d0337c3a)) -* Petname resolution and synchronization in spheres and gateways ([#253](https://github.com/subconsciousnetwork/noosphere/issues/253)) ([f7ddfa7](https://github.com/subconsciousnetwork/noosphere/commit/f7ddfa7b65129efe795c6e3fca58cdc22799127a)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * noosphere-core bumped from 0.6.3 to 0.7.0 - * noosphere-storage bumped from 0.4.2 to 0.5.0 - * noosphere-api bumped from 0.5.6 to 0.6.0 diff --git a/rust/noosphere-sphere/Cargo.toml b/rust/noosphere-sphere/Cargo.toml deleted file mode 100644 index 8cbb85b94..000000000 --- a/rust/noosphere-sphere/Cargo.toml +++ /dev/null @@ -1,59 +0,0 @@ -[package] -name = "noosphere-sphere" -version = "0.11.0+deprecated" -edition = "2021" -description = "High-level access to content, address books and other features of spheres" -keywords = [] -categories = [] -rust-version = "1.60.0" -license = "MIT OR Apache-2.0" -documentation = "https://docs.rs/noosphere-sphere" -repository = "https://github.com/subconsciousnetwork/noosphere" -homepage = "https://github.com/subconsciousnetwork/noosphere" -readme = "README.md" - -[features] -default = [] -helpers = [] - -[dependencies] -anyhow = { workspace = true } -cid = { workspace = true } -iroh-car = { workspace = true } -url = { version = "^2", features = ["serde"] } -tracing = { workspace = true } - -noosphere-core = { version = "0.16.0", path = "../noosphere-core" } -noosphere-storage = { version = "0.9.0", path = "../noosphere-storage" } -noosphere-api = { version = "0.13.0+deprecated", path = "../noosphere-api" } - -ucan = { workspace = true } -ucan-key-support = { workspace = true } - -async-trait = "~0.1" -tokio-stream = { workspace = true } -async-stream = { workspace = true } -tokio-util = { version = "0.7.7", features = ["io"] } -futures-util = "0.3.27" -libipld-core = { workspace = true } -libipld-cbor = { workspace = true } -bytes = "^1" -serde_json = { workspace = true } -serde = { workspace = true } -thiserror = { workspace = true } - -[dev-dependencies] -noosphere-core = { version = "0.16.0", path = "../noosphere-core", features = ["helpers"] } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -# TODO: We should eventually support gateway storage as a specialty target only, -# as it is a specialty use-case -tokio = { workspace = true, features = ["sync", "macros"] } -wasm-bindgen = { workspace = true } -wasm-bindgen-futures = "0.4.37" - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tokio = { workspace = true, features = ["full"] } - -[target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen-test = { workspace = true } diff --git a/rust/noosphere-sphere/README.md b/rust/noosphere-sphere/README.md deleted file mode 100644 index 3abf86771..000000000 --- a/rust/noosphere-sphere/README.md +++ /dev/null @@ -1,5 +0,0 @@ -![API Stability: Alpha](https://img.shields.io/badge/API%20Stability-Alpha-red) - -# noosphere-sphere - -High-level access to content, address books and other features of spheres diff --git a/rust/noosphere-sphere/src/authority/mod.rs b/rust/noosphere-sphere/src/authority/mod.rs deleted file mode 100644 index 4c768903e..000000000 --- a/rust/noosphere-sphere/src/authority/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod read; -pub use read::*; - -mod write; -pub use write::*; diff --git a/rust/noosphere-sphere/src/authority/read.rs b/rust/noosphere-sphere/src/authority/read.rs deleted file mode 100644 index d02927473..000000000 --- a/rust/noosphere-sphere/src/authority/read.rs +++ /dev/null @@ -1,190 +0,0 @@ -use std::sync::Arc; - -use anyhow::{anyhow, Result}; -use noosphere_core::{ - authority::{Author, Authorization}, - data::{Did, Link, Mnemonic}, - error::NoosphereError, -}; -use noosphere_storage::Storage; - -use tokio_stream::StreamExt; - -use crate::{HasSphereContext, SphereContextKey}; -use async_trait::async_trait; - -/// Anything that can read the authority section from a sphere should implement -/// [SphereAuthorityRead]. A blanket implementation is provided for anything -/// that implements [HasSphereContext]. -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -pub trait SphereAuthorityRead -where - S: Storage + 'static, - Self: Sized, -{ - /// For a given [Authorization], checks that the authorization and all of its - /// ancester proofs are valid and have not been revoked - async fn verify_authorization( - &self, - authorization: &Authorization, - ) -> Result<(), NoosphereError>; - - /// Look up an [Authorization] by a [Did]. - async fn get_authorization(&self, did: &Did) -> Result>; - - /// Look up all [Authorization]s with the specified name - async fn get_authorizations_by_name(&self, name: &str) -> Result>; - - /// Derive a root sphere key from a mnemonic and return a version of this - /// [SphereAuthorityRead] whose inner [SphereContext]'s [Author] is using - /// that root sphere key. - async fn escalate_authority(&self, mnemonic: &Mnemonic) -> Result; -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl SphereAuthorityRead for C -where - C: HasSphereContext, - S: Storage + 'static, -{ - async fn verify_authorization( - &self, - authorization: &Authorization, - ) -> Result<(), NoosphereError> { - self.to_sphere() - .await? - .verify_authorization(authorization) - .await - } - - async fn get_authorization(&self, did: &Did) -> Result> { - let sphere = self.to_sphere().await?; - let authority = sphere.get_authority().await?; - let delegations = authority.get_delegations().await?; - let delegations_stream = delegations.into_stream().await?; - - tokio::pin!(delegations_stream); - - while let Some((Link { cid, .. }, delegation)) = delegations_stream.try_next().await? { - let ucan = delegation.resolve_ucan(sphere.store()).await?; - let authorized_did = ucan.audience(); - - if authorized_did == did { - return Ok(Some(Authorization::Cid(cid))); - } - } - - Ok(None) - } - - async fn get_authorizations_by_name(&self, name: &str) -> Result> { - let sphere = self.to_sphere().await?; - let authority = sphere.get_authority().await?; - let delegations = authority.get_delegations().await?; - let delegations_stream = delegations.into_stream().await?; - let mut authorizations = Vec::new(); - - tokio::pin!(delegations_stream); - - while let Some((link, delegation)) = delegations_stream.try_next().await? { - if delegation.name == name { - authorizations.push(Authorization::Cid(link.into())); - } - } - - Ok(authorizations) - } - - async fn escalate_authority(&self, mnemonic: &Mnemonic) -> Result { - let root_key: SphereContextKey = Arc::new(Box::new(mnemonic.to_credential()?)); - let root_author = Author { - key: root_key, - authorization: None, - }; - - let root_identity = root_author.did().await?; - let sphere_identity = self.identity().await?; - - if sphere_identity != root_identity { - return Err(anyhow!( - "Provided mnemonic did not produce the expected credential" - )); - } - - Ok(Self::wrap( - self.sphere_context() - .await? - .with_author(&root_author) - .await?, - ) - .await) - } -} - -#[cfg(test)] -mod tests { - use anyhow::Result; - use noosphere_core::data::Did; - - use ucan::crypto::KeyMaterial; - #[cfg(target_arch = "wasm32")] - use wasm_bindgen_test::wasm_bindgen_test; - - #[cfg(target_arch = "wasm32")] - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - - use crate::helpers::{simulated_sphere_context, SimulationAccess}; - use crate::{HasSphereContext, SphereAuthorityRead}; - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_get_an_authorization_by_did() -> Result<()> { - let (sphere_context, _) = - simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; - - let author_did = Did(sphere_context - .sphere_context() - .await? - .author() - .key - .get_did() - .await?); - - let authorization = sphere_context - .get_authorization(&author_did) - .await? - .unwrap(); - - let _ucan = authorization - .as_ucan(sphere_context.sphere_context().await?.db()) - .await?; - - Ok(()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_verify_an_authorization_to_write_to_a_sphere() -> Result<()> { - let (sphere_context, _) = - simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; - - let author_did = Did(sphere_context - .sphere_context() - .await? - .author() - .key - .get_did() - .await?); - - let authorization = sphere_context - .get_authorization(&author_did) - .await? - .unwrap(); - - sphere_context.verify_authorization(&authorization).await?; - - Ok(()) - } -} diff --git a/rust/noosphere-sphere/src/authority/write.rs b/rust/noosphere-sphere/src/authority/write.rs deleted file mode 100644 index 409b22f96..000000000 --- a/rust/noosphere-sphere/src/authority/write.rs +++ /dev/null @@ -1,385 +0,0 @@ -use crate::{internal::SphereContextInternal, HasMutableSphereContext, HasSphereContext}; - -use super::SphereAuthorityRead; -use anyhow::{anyhow, Result}; -use async_trait::async_trait; -use cid::Cid; -use noosphere_core::{ - authority::{generate_capability, Authorization, SphereAbility}, - data::{DelegationIpld, Did, Jwt, Link, RevocationIpld}, - view::SPHERE_LIFETIME, -}; -use noosphere_storage::{Storage, UcanStore}; -use tokio_stream::StreamExt; -use ucan::{builder::UcanBuilder, crypto::KeyMaterial}; - -/// Any type which implements [SphereAuthorityWrite] is able to manipulate the -/// [AuthorityIpld] section of a sphere. This includes authorizing other keys -/// and revoking prior authorizations. -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -pub trait SphereAuthorityWrite: SphereAuthorityRead -where - S: Storage + 'static, -{ - /// Authorize another key by its [Did], associating the authorization with a - /// provided display name - async fn authorize(&mut self, name: &str, identity: &Did) -> Result; - - /// Revoke a previously granted authorization. - /// - /// Note that correctly revoking an authorization requires signing with a credential - /// that is in the chain of authority that ultimately granted the authorization being - /// revoked. Attempting to revoke a credential with any credential that isn't in that - /// chain of authority will fail. - async fn revoke_authorization(&mut self, authorization: &Authorization) -> Result<()>; - - /// Recover authority by revoking all previously delegated authorizations - /// and creating a new one that delegates authority to the specified key - /// (given by its [Did]). - /// - /// Note that correctly recovering authority requires signing with the root - /// sphere credential, so generally can only be performed on a type that - /// implements [SphereAuthorityEscalate] - async fn recover_authority(&mut self, new_owner: &Did) -> Result; -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl SphereAuthorityWrite for C -where - C: HasSphereContext + HasMutableSphereContext, - S: Storage + 'static, -{ - // TODO(#423): We allow optional human-readable names for authorizations, - // but this will bear the consequence of leaking personal information about - // the user (e.g., a list of their authorized devices). We should encrypt - // these names so that they are only readable by the user themselves. - // TODO(#560): We should probably enforce that each [Did] only gets one - // authorization, from a hygeine perspective; elsewhere we need to assume - // multiple authorizations for the same [Did] are possible. - async fn authorize(&mut self, name: &str, identity: &Did) -> Result { - self.assert_write_access().await?; - - let author = self.sphere_context().await?.author().clone(); - let mut sphere = self.to_sphere().await?; - let authorization = author.require_authorization()?; - - self.verify_authorization(authorization).await?; - - let authorization_expiry: Option = { - let ucan = authorization - .as_ucan(&UcanStore(sphere.store().clone())) - .await?; - *ucan.expires_at() - }; - - let mut builder = UcanBuilder::default() - .issued_by(&author.key) - .for_audience(identity) - .claiming_capability(&generate_capability( - &sphere.get_identity().await?, - SphereAbility::Authorize, - )) - .with_nonce(); - - // TODO(ucan-wg/rs-ucan#114): Clean this up when - // `UcanBuilder::with_expiration` accepts `Option` - if let Some(expiry) = authorization_expiry { - builder = builder.with_expiration(expiry); - } - - // TODO(ucan-wg/rs-ucan#32): Clean this up when we can use a CID as an authorization - let mut signable = builder.build()?; - - signable - .proofs - .push(Cid::try_from(authorization)?.to_string()); - - let jwt = signable.sign().await?.encode()?; - - let delegation = DelegationIpld::register(name, &jwt, sphere.store_mut()).await?; - - self.sphere_context_mut() - .await? - .mutation_mut() - .delegations_mut() - .set(&Link::new(delegation.jwt), &delegation); - - Ok(Authorization::Cid(delegation.jwt)) - } - - async fn revoke_authorization(&mut self, authorization: &Authorization) -> Result<()> { - self.assert_write_access().await?; - - let mut sphere_context = self.sphere_context_mut().await?; - - if !sphere_context - .author() - .is_authorizer_of(authorization, sphere_context.db()) - .await? - { - let author_did = sphere_context.author().did().await?; - - return Err(anyhow!( - "{} cannot revoke authorization {} (not a delegating authority)", - author_did, - authorization - )); - } - - let authorization_cid = Link::::from(Cid::try_from(authorization)?); - let delegations = sphere_context - .sphere() - .await? - .get_authority() - .await? - .get_delegations() - .await?; - - if delegations.get(&authorization_cid).await?.is_none() { - return Err(anyhow!( - "No authority has been delegated to the authorization being revoked" - )); - } - - let revocation = - RevocationIpld::revoke(&authorization_cid, &sphere_context.author().key).await?; - - sphere_context - .mutation_mut() - .delegations_mut() - .remove(&authorization_cid); - - sphere_context - .mutation_mut() - .revocations_mut() - .set(&authorization_cid, &revocation); - - // TODO(#424): Recursively remove any sub-delegations here (and revoke them?) - - Ok(()) - } - - async fn recover_authority(&mut self, new_owner: &Did) -> Result { - self.assert_write_access().await?; - - let mut sphere_context = self.sphere_context_mut().await?; - let author_did = Did(sphere_context.author().key.get_did().await?); - let sphere_identity = sphere_context.identity().clone(); - - if author_did != sphere_identity { - return Err(anyhow!( - "Only the root sphere credential can be used to recover authority" - )); - } - - let sphere = sphere_context.sphere().await?; - let authority = sphere.get_authority().await?; - let delegations = authority.get_delegations().await?; - let delegation_stream = delegations.into_stream().await?; - - tokio::pin!(delegation_stream); - - // First: revoke all current authority - while let Some((link, _)) = delegation_stream.try_next().await? { - let revocation = RevocationIpld::revoke(&link, &sphere_context.author().key).await?; - - sphere_context - .mutation_mut() - .delegations_mut() - .remove(&link); - sphere_context - .mutation_mut() - .revocations_mut() - .set(&link, &revocation); - } - - // Then: bless a new owner - let ucan = UcanBuilder::default() - .issued_by(&sphere_context.author().key) - .for_audience(new_owner) - .with_lifetime(SPHERE_LIFETIME) - .with_nonce() - .claiming_capability(&generate_capability( - &sphere_identity, - SphereAbility::Authorize, - )) - .build()? - .sign() - .await?; - - let jwt = ucan.encode()?; - let delegation = DelegationIpld::register("(OWNER)", &jwt, sphere_context.db()).await?; - let link = Link::new(delegation.jwt); - - sphere_context - .mutation_mut() - .delegations_mut() - .set(&link, &delegation); - - Ok(Authorization::Cid(link.into())) - } -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use anyhow::Result; - use noosphere_core::authority::{generate_ed25519_key, Author}; - use noosphere_core::data::Did; - - use tokio::sync::Mutex; - use ucan::crypto::KeyMaterial; - #[cfg(target_arch = "wasm32")] - use wasm_bindgen_test::wasm_bindgen_test; - - #[cfg(target_arch = "wasm32")] - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - - use crate::helpers::{simulated_sphere_context, SimulationAccess}; - use crate::{ - HasMutableSphereContext, HasSphereContext, SphereAuthorityRead, SphereAuthorityWrite, - SphereContextKey, - }; - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_allows_an_authorized_key_to_authorize_other_keys() -> Result<()> { - let (mut sphere_context, _) = - simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; - - let other_key = generate_ed25519_key(); - let other_did = Did(other_key.get_did().await?); - - let other_authorization = sphere_context.authorize("other", &other_did).await?; - sphere_context.save(None).await?; - - assert!(sphere_context - .verify_authorization(&other_authorization) - .await - .is_ok()); - - Ok(()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_implicitly_revokes_transitive_authorizations() -> Result<()> { - let (mut sphere_context, mnemonic) = - simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; - - let other_key: SphereContextKey = Arc::new(Box::new(generate_ed25519_key())); - let other_did = Did(other_key.get_did().await?); - - let other_authorization = sphere_context.authorize("other", &other_did).await?; - sphere_context.save(None).await?; - - let mut sphere_context_with_other_credential = Arc::new(Mutex::new( - sphere_context - .sphere_context() - .await? - .with_author(&Author { - key: other_key.clone(), - authorization: Some(other_authorization.clone()), - }) - .await?, - )); - - let third_key = generate_ed25519_key(); - let third_did = Did(third_key.get_did().await?); - - let third_authorization = sphere_context_with_other_credential - .authorize("third", &third_did) - .await?; - sphere_context_with_other_credential.save(None).await?; - - let mut root_sphere_context = sphere_context.escalate_authority(&mnemonic).await?; - - root_sphere_context - .revoke_authorization(&other_authorization) - .await?; - root_sphere_context.save(None).await?; - - assert!(sphere_context - .verify_authorization(&third_authorization) - .await - .is_err()); - - Ok(()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_catches_revoked_authorizations_when_verifying() -> Result<()> { - let (mut sphere_context, mnemonic) = - simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; - - let other_key = generate_ed25519_key(); - let other_did = Did(other_key.get_did().await?); - - let other_authorization = sphere_context.authorize("other", &other_did).await?; - sphere_context.save(None).await?; - - let mut root_sphere_context = sphere_context.escalate_authority(&mnemonic).await?; - - root_sphere_context - .revoke_authorization(&other_authorization) - .await?; - root_sphere_context.save(None).await?; - - assert!(sphere_context - .verify_authorization(&other_authorization) - .await - .is_err()); - - Ok(()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_perform_access_recovery_given_a_mnemonic() -> Result<()> { - let (mut sphere_context, mnemonic) = - simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; - - let owner = sphere_context.sphere_context().await?.author().clone(); - - let other_key = generate_ed25519_key(); - let other_did = Did(other_key.get_did().await?); - - let other_authorization = sphere_context.authorize("other", &other_did).await?; - sphere_context.save(None).await?; - - let next_owner_key = generate_ed25519_key(); - let next_owner_did = Did(next_owner_key.get_did().await?); - - let mut root_sphere_context = sphere_context.escalate_authority(&mnemonic).await?; - - root_sphere_context - .recover_authority(&next_owner_did) - .await?; - root_sphere_context.save(None).await?; - - assert!(sphere_context - .verify_authorization(&other_authorization) - .await - .is_err()); - - assert!(sphere_context - .verify_authorization(&owner.authorization.unwrap()) - .await - .is_err()); - - sphere_context - .verify_authorization( - &sphere_context - .get_authorization(&next_owner_did) - .await? - .unwrap(), - ) - .await?; - - Ok(()) - } -} diff --git a/rust/noosphere-sphere/src/content/decoder.rs b/rust/noosphere-sphere/src/content/decoder.rs deleted file mode 100644 index 643d8527e..000000000 --- a/rust/noosphere-sphere/src/content/decoder.rs +++ /dev/null @@ -1,29 +0,0 @@ -use async_stream::try_stream; -use bytes::Bytes; -use cid::Cid; -use libipld_cbor::DagCborCodec; -use noosphere_core::data::BodyChunkIpld; -use noosphere_storage::BlockStore; -use tokio_stream::Stream; - -/// Helper to easily decode a linked list of `BodyChunkIpld` as a byte stream -pub struct BodyChunkDecoder<'a, 'b, S: BlockStore>(pub &'a Cid, pub &'b S); - -impl<'a, 'b, S: BlockStore> BodyChunkDecoder<'a, 'b, S> { - /// Consume the [BodyChunkDecoder] and return an async [Stream] of bytes - /// representing the raw body contents - pub fn stream(self) -> impl Stream> + Unpin { - let mut next = Some(*self.0); - let store = self.1.clone(); - Box::pin(try_stream! { - while let Some(cid) = next { - debug!("Unpacking block {}...", cid); - let chunk = store.load::(&cid).await.map_err(|error| { - std::io::Error::new(std::io::ErrorKind::UnexpectedEof, error.to_string()) - })?; - yield Bytes::from(chunk.bytes); - next = chunk.next; - } - }) - } -} diff --git a/rust/noosphere-sphere/src/content/file.rs b/rust/noosphere-sphere/src/content/file.rs deleted file mode 100644 index e0c810b95..000000000 --- a/rust/noosphere-sphere/src/content/file.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::pin::Pin; - -use noosphere_core::data::{Did, Link, MemoIpld}; -use tokio::io::AsyncRead; - -/// A type that may be used as the contents field in a [SphereFile] -#[cfg(not(target_arch = "wasm32"))] -pub trait AsyncFileBody: AsyncRead + Unpin + Send {} - -#[cfg(not(target_arch = "wasm32"))] -impl AsyncFileBody for S where S: AsyncRead + Unpin + Send {} - -#[cfg(target_arch = "wasm32")] -/// A type that may be used as the contents field in a [SphereFile] -pub trait AsyncFileBody: AsyncRead + Unpin {} - -#[cfg(target_arch = "wasm32")] -impl AsyncFileBody for S where S: AsyncRead + Unpin {} - -/// A descriptor for contents that is stored in a sphere. -pub struct SphereFile { - /// The identity of the associated sphere from which the file was read - pub sphere_identity: Did, - /// The version of the associated sphere from which the file was read - pub sphere_version: Link, - /// The version of the memo that wraps the file's body contents - pub memo_version: Link, - /// The memo that wraps the file's body contents - pub memo: MemoIpld, - /// The body contents of the file - pub contents: C, -} - -impl SphereFile -where - C: AsyncFileBody + 'static, -{ - /// Consume the file and return a version of it where its body contents have - /// been boxed and pinned - pub fn boxed(self) -> SphereFile>> { - SphereFile { - sphere_identity: self.sphere_identity, - sphere_version: self.sphere_version, - memo_version: self.memo_version, - memo: self.memo, - contents: Box::pin(self.contents), - } - } -} diff --git a/rust/noosphere-sphere/src/content/mod.rs b/rust/noosphere-sphere/src/content/mod.rs deleted file mode 100644 index ec6444013..000000000 --- a/rust/noosphere-sphere/src/content/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Sphere content is a storage space for any files that the sphere owner wishes to associate -//! with a public "slug", that is addressable by them or others who have replicated the sphere -//! data. - -mod decoder; -mod file; -mod read; -mod write; - -pub use decoder::*; -pub use file::*; -pub use read::*; -pub use write::*; diff --git a/rust/noosphere-sphere/src/content/read.rs b/rust/noosphere-sphere/src/content/read.rs deleted file mode 100644 index 531e6b2eb..000000000 --- a/rust/noosphere-sphere/src/content/read.rs +++ /dev/null @@ -1,49 +0,0 @@ -use anyhow::Result; -use noosphere_storage::Storage; - -use crate::HasSphereContext; -use async_trait::async_trait; - -use crate::{internal::SphereContextInternal, AsyncFileBody, SphereFile}; - -/// Anything that can read content from a sphere should implement [SphereContentRead]. -/// A blanket implementation is provided for anything that implements [HasSphereContext]. -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -pub trait SphereContentRead -where - S: Storage + 'static, -{ - /// Read a file that is associated with a given slug at the revision of the - /// sphere that this view is pointing to. - /// Note that "contents" are `AsyncRead`, and content bytes won't be read - /// until contents is polled. - async fn read(&self, slug: &str) -> Result>>>; - - /// Returns true if the content identitifed by slug exists in the sphere at - /// the current revision. - async fn exists(&self, slug: &str) -> Result { - Ok(self.read(slug).await?.is_some()) - } -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl SphereContentRead for C -where - C: HasSphereContext, - S: Storage + 'static, -{ - async fn read(&self, slug: &str) -> Result>>> { - let revision = self.version().await?; - let sphere = self.to_sphere().await?; - - let links = sphere.get_content().await?; - let hamt = links.get_hamt().await?; - - Ok(match hamt.get(&slug.to_string()).await? { - Some(memo) => Some(self.get_file(&revision, memo.clone()).await?), - None => None, - }) - } -} diff --git a/rust/noosphere-sphere/src/content/write.rs b/rust/noosphere-sphere/src/content/write.rs deleted file mode 100644 index 2904d21e9..000000000 --- a/rust/noosphere-sphere/src/content/write.rs +++ /dev/null @@ -1,183 +0,0 @@ -use anyhow::{anyhow, Result}; -use cid::Cid; -use libipld_cbor::DagCborCodec; -use noosphere_core::data::{BodyChunkIpld, Header, Link, MemoIpld}; -use noosphere_storage::{BlockStore, Storage}; - -use tokio::io::AsyncReadExt; - -use crate::{internal::SphereContextInternal, HasMutableSphereContext, HasSphereContext}; -use async_trait::async_trait; - -use crate::{AsyncFileBody, SphereContentRead}; - -fn validate_slug(slug: &str) -> Result<()> { - if slug.is_empty() { - Err(anyhow!("Slug must not be empty.")) - } else { - Ok(()) - } -} - -/// Anything that can write content to a sphere should implement -/// [SphereContentWrite]. A blanket implementation is provided for anything that -/// implements [HasMutableSphereContext]. -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -pub trait SphereContentWrite: SphereContentRead -where - S: Storage + 'static, -{ - /// Like link, this takes a [Link] that should be associated - /// directly with a slug, but in this case the [Link] is assumed - /// to refer to a memo, so no wrapping memo is created. - async fn link_raw(&mut self, slug: &str, cid: &Link) -> Result<()>; - - /// Similar to write, but instead of generating blocks from some provided - /// bytes, the caller provides a CID of an existing DAG in storage. That - /// CID is used as the body of a Memo that is written to the specified - /// slug, and the CID of the memo is returned. - async fn link( - &mut self, - slug: &str, - content_type: &str, - body_cid: &Cid, - additional_headers: Option>, - ) -> Result>; - - /// Write to a slug in the sphere. In order to commit the change to the - /// sphere, you must call save. You can buffer multiple writes before - /// saving. - /// - /// The returned CID is a link to the memo for the newly added content. - async fn write( - &mut self, - slug: &str, - content_type: &str, - mut value: F, - additional_headers: Option>, - ) -> Result>; - - /// Unlinks a slug from the content space. Note that this does not remove - /// the blocks that were previously associated with the content found at the - /// given slug, because they will still be available at an earlier revision - /// of the sphere. In order to commit the change, you must save. Note that - /// this call is a no-op if there is no matching slug linked in the sphere. - /// - /// The returned value is the CID previously associated with the slug, if - /// any. - async fn remove(&mut self, slug: &str) -> Result>>; -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl SphereContentWrite for C -where - C: HasSphereContext + HasMutableSphereContext, - S: Storage + 'static, -{ - async fn link_raw(&mut self, slug: &str, cid: &Link) -> Result<()> { - self.assert_write_access().await?; - validate_slug(slug)?; - - self.sphere_context_mut() - .await? - .mutation_mut() - .content_mut() - .set(&slug.into(), cid); - - Ok(()) - } - - async fn link( - &mut self, - slug: &str, - content_type: &str, - body_cid: &Cid, - additional_headers: Option>, - ) -> Result> { - self.assert_write_access().await?; - validate_slug(slug)?; - - let memo_cid = { - let current_file = self.read(slug).await?; - let previous_memo_cid = current_file.map(|file| file.memo_version); - - let mut sphere_context = self.sphere_context_mut().await?; - - let mut new_memo = match previous_memo_cid { - Some(cid) => { - let mut memo = MemoIpld::branch_from(&cid, sphere_context.db()).await?; - memo.body = *body_cid; - memo - } - None => MemoIpld { - parent: None, - headers: Vec::new(), - body: *body_cid, - }, - }; - - if let Some(headers) = additional_headers { - new_memo.replace_headers(headers) - } - - new_memo.replace_first_header(&Header::ContentType, content_type); - - // TODO(#43): Configure default/implicit headers here - sphere_context - .db_mut() - .save::(new_memo) - .await? - .into() - }; - - self.link_raw(slug, &memo_cid).await?; - - Ok(memo_cid) - } - - async fn write( - &mut self, - slug: &str, - content_type: &str, - mut value: F, - additional_headers: Option>, - ) -> Result> { - debug!("Writing {}...", slug); - - self.assert_write_access().await?; - validate_slug(slug)?; - - let mut bytes = Vec::new(); - value.read_to_end(&mut bytes).await?; - - // TODO(#38): We imply here that the only content types we care about - // amount to byte streams, but in point of fact we can support anything - // that may be referenced by CID including arbitrary IPLD structures - let body_cid = - BodyChunkIpld::store_bytes(&bytes, self.sphere_context_mut().await?.db_mut()).await?; - - self.link(slug, content_type, &body_cid, additional_headers) - .await - } - - async fn remove(&mut self, slug: &str) -> Result>> { - self.assert_write_access().await?; - - let current_file = self.read(slug).await?; - - Ok(match current_file { - Some(file) => { - self.sphere_context_mut() - .await? - .mutation_mut() - .content_mut() - .remove(&String::from(slug)); - - Some(file.memo_version) - } - None => None, - }) - } -} diff --git a/rust/noosphere-sphere/src/context.rs b/rust/noosphere-sphere/src/context.rs deleted file mode 100644 index 7625c0bf3..000000000 --- a/rust/noosphere-sphere/src/context.rs +++ /dev/null @@ -1,449 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; - -use noosphere_api::client::Client; - -use noosphere_core::{ - authority::{Access, Author, SUPPORTED_KEYS}, - data::{Did, Link, MemoIpld}, - view::{Sphere, SphereMutation}, -}; -use noosphere_storage::{KeyValueStore, SphereDb, Storage}; -use tokio::sync::OnceCell; -use ucan::crypto::{did::DidParser, KeyMaterial}; -use url::Url; - -use crate::metadata::GATEWAY_URL; - -#[cfg(doc)] -use crate::has::HasSphereContext; - -/// The type of any [KeyMaterial] that is used within a [SphereContext] -pub type SphereContextKey = Arc>; - -/// A [SphereContext] is an accessor construct over locally replicated sphere -/// data. It embodies both the storage layer that contains the sphere's data -/// as the information needed to verify a user's intended level of access to -/// it (e.g., local key material and [ucan::Ucan]-based authorization). -/// Additionally, the [SphereContext] maintains a reference to an API [Client] -/// that may be initialized as the network becomes available. -/// -/// All interactions that pertain to a sphere, including reading or writing -/// its contents and syncing with a gateway, flow through the [SphereContext]. -pub struct SphereContext -where - S: Storage, -{ - sphere_identity: Did, - origin_sphere_identity: Did, - author: Author, - access: OnceCell, - db: SphereDb, - did_parser: DidParser, - client: OnceCell>>>, - mutation: SphereMutation, -} - -impl Clone for SphereContext -where - S: Storage, -{ - fn clone(&self) -> Self { - Self { - sphere_identity: self.sphere_identity.clone(), - origin_sphere_identity: self.origin_sphere_identity.clone(), - author: self.author.clone(), - access: OnceCell::new(), - db: self.db.clone(), - did_parser: DidParser::new(SUPPORTED_KEYS), - client: self.client.clone(), - mutation: SphereMutation::new(self.mutation.author()), - } - } -} - -impl SphereContext -where - S: Storage, -{ - /// Instantiate a new [SphereContext] given a sphere [Did], an [Author], a - /// [SphereDb] and an optional origin sphere [Did]. The origin sphere [Did] - /// is intended to signify whether the [SphereContext] is a local sphere, or - /// a global sphere that is being visited by a local author. In most cases, - /// a [SphereContext] with _some_ value set as the origin sphere [Did] will - /// be read-only. - pub async fn new( - sphere_identity: Did, - author: Author, - db: SphereDb, - origin_sphere_identity: Option, - ) -> Result { - let author_did = author.identity().await?; - let origin_sphere_identity = - origin_sphere_identity.unwrap_or_else(|| sphere_identity.clone()); - - Ok(SphereContext { - sphere_identity, - origin_sphere_identity, - access: OnceCell::new(), - author, - db, - did_parser: DidParser::new(SUPPORTED_KEYS), - client: OnceCell::new(), - mutation: SphereMutation::new(&author_did), - }) - } - - /// Clone this [SphereContext], setting the sphere identity to a peer's [Did] - pub async fn to_visitor(&self, peer_identity: &Did) -> Result { - self.db().require_version(peer_identity).await?; - - SphereContext::new( - peer_identity.clone(), - self.author.clone(), - self.db.clone(), - Some(self.origin_sphere_identity.clone()), - ) - .await - } - - /// Clone this [SphereContext], replacing the [Author] with the provided one - pub async fn with_author(&self, author: &Author) -> Result> { - SphereContext::new( - self.sphere_identity.clone(), - author.clone(), - self.db.clone(), - Some(self.origin_sphere_identity.clone()), - ) - .await - } - - /// Given a [Did] of a sphere, produce a [SphereContext] backed by the same credentials and - /// storage primitives as this one, but that accesses the sphere referred to by the provided - /// [Did]. - pub async fn traverse_by_identity(&self, _sphere_identity: &Did) -> Result> { - unimplemented!() - } - - /// The identity of the sphere - pub fn identity(&self) -> &Did { - &self.sphere_identity - } - - /// The identity of the gateway sphere in use during this session, if - /// any; note that this will cause a request to be made to a gateway if no - /// handshake has yet occurred. - pub async fn gateway_identity(&self) -> Result { - Ok(self.client().await?.session.gateway_identity.clone()) - } - - /// The CID of the most recent local version of this sphere - pub async fn version(&self) -> Result> { - Ok(self.db().require_version(self.identity()).await?.into()) - } - - /// The [Author] who is currently accessing the sphere - pub fn author(&self) -> &Author { - &self.author - } - - /// The [Access] level that the configured [Author] has relative to the - /// sphere that this [SphereContext] refers to. - pub async fn access(&self) -> Result { - let access = self - .access - .get_or_try_init(|| async { - self.author.access_to(&self.sphere_identity, &self.db).await - }) - .await?; - Ok(access.clone()) - } - - /// Get a mutable reference to the [DidParser] used in this [SphereContext] - pub fn did_parser_mut(&mut self) -> &mut DidParser { - &mut self.did_parser - } - - /// Sets or unsets the gateway URL that points to the gateway API that the - /// sphere will use when it is syncing. - pub async fn configure_gateway_url(&mut self, url: Option<&Url>) -> Result<()> { - self.client = OnceCell::new(); - - match url { - Some(url) => { - self.db.set_key(GATEWAY_URL, url.to_string()).await?; - } - None => { - self.db.unset_key(GATEWAY_URL).await?; - } - } - - Ok(()) - } - - /// Get the [SphereDb] instance that manages the current sphere's block - /// space and persisted configuration. - pub fn db(&self) -> &SphereDb { - &self.db - } - - /// Get a mutable reference to the [SphereDb] instance that manages the - /// current sphere's block space and persisted configuration. - pub fn db_mut(&mut self) -> &mut SphereDb { - &mut self.db - } - - /// Get a read-only reference to the underlying [SphereMutation] that this - /// [SphereContext] is tracking - pub fn mutation(&self) -> &SphereMutation { - &self.mutation - } - - /// Get a mutable reference to the underlying [SphereMutation] that this - /// [SphereContext] is tracking - pub fn mutation_mut(&mut self) -> &mut SphereMutation { - &mut self.mutation - } - - /// Get a [Sphere] view over the current sphere's latest revision. This view - /// offers lower-level access than [HasSphereContext], but includes affordances to - /// help tranversing and manipulating IPLD structures that are more - /// convenient than working directly with raw data. - pub async fn sphere(&self) -> Result>> { - Ok(Sphere::at( - &self.db.require_version(self.identity()).await?.into(), - self.db(), - )) - } - - /// Get a [Client] that will interact with a configured gateway (if a URL - /// for one has been configured). This will initialize a [Client] if one is - /// not already intialized, and will fail if the [Client] is unable to - /// verify the identity of the gateway or otherwise connect to it. - pub async fn client(&self) -> Result>>> { - let client = self - .client - .get_or_try_init::(|| async { - let gateway_url: Url = self.db.require_key(GATEWAY_URL).await?; - - Ok(Arc::new( - Client::identify( - &self.origin_sphere_identity, - &gateway_url, - &self.author, - // TODO: Kill `DidParser` with fire - &mut DidParser::new(SUPPORTED_KEYS), - self.db.clone(), - ) - .await?, - )) - }) - .await?; - - Ok(client.clone()) - } - - // Reset access so that it is re-evaluated the next time it is measured - // self.access.take(); - pub(crate) fn reset_access(&mut self) { - self.access.take(); - } -} - -#[cfg(test)] -mod tests { - use anyhow::Result; - - use noosphere_core::{ - authority::{generate_capability, generate_ed25519_key, SphereAbility}, - data::{ContentType, LinkRecord, LINK_RECORD_FACT_NAME}, - helpers::make_valid_link_record, - tracing::initialize_tracing, - view::Sphere, - }; - - use noosphere_storage::{MemoryStorage, SphereDb}; - use ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore}; - #[cfg(target_arch = "wasm32")] - use wasm_bindgen_test::wasm_bindgen_test; - - use crate::{ - helpers::{simulated_sphere_context, SimulationAccess}, - HasMutableSphereContext, HasSphereContext, SphereContentWrite, SpherePetnameWrite, - }; - - #[cfg(target_arch = "wasm32")] - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_validates_slug_names_when_writing() -> Result<()> { - initialize_tracing(None); - let valid_names: &[&str] = &["j@__/_大", "/"]; - let invalid_names: &[&str] = &[""]; - - let (mut sphere_context, _) = - simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; - - for invalid_name in invalid_names { - assert!(sphere_context - .write(invalid_name, &ContentType::Text, "hello".as_ref(), None,) - .await - .is_err()); - } - - for valid_name in valid_names { - assert!(sphere_context - .write(valid_name, &ContentType::Text, "hello".as_ref(), None,) - .await - .is_ok()); - } - - Ok(()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_validates_petnames_when_setting() -> Result<()> { - initialize_tracing(None); - let valid_names: &[&str] = &["j@__/_大"]; - let invalid_names: &[&str] = &["", "did:key:foo"]; - - let (mut sphere_context, _) = - simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; - let mut db = sphere_context.sphere_context().await?.db().clone(); - let (other_identity, link_record, _) = make_valid_link_record(&mut db).await?; - - for invalid_name in invalid_names { - assert!(sphere_context - .set_petname_record(invalid_name, &link_record) - .await - .is_err()); - assert!(sphere_context - .set_petname(invalid_name, Some(other_identity.clone())) - .await - .is_err()); - } - - for valid_name in valid_names { - assert!(sphere_context - .set_petname(valid_name, Some(other_identity.clone())) - .await - .is_ok()); - sphere_context.save(None).await?; - assert!(sphere_context - .set_petname_record(valid_name, &link_record) - .await - .is_ok()); - } - - Ok(()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_disallows_adding_self_as_petname() -> Result<()> { - initialize_tracing(None); - - let (mut sphere_context, _) = - simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; - let db = sphere_context.sphere_context().await?.db().clone(); - let sphere_identity = sphere_context.identity().await?; - - let link_record = { - let version = sphere_context.version().await?; - let author = sphere_context.sphere_context().await?.author().clone(); - LinkRecord::from( - UcanBuilder::default() - .issued_by(&author.key) - .for_audience(&sphere_identity) - .witnessed_by( - &author.authorization.as_ref().unwrap().as_ucan(&db).await?, - None, - ) - .claiming_capability(&generate_capability( - &sphere_identity, - SphereAbility::Publish, - )) - .with_lifetime(120) - .with_fact(LINK_RECORD_FACT_NAME, version.to_string()) - .build()? - .sign() - .await?, - ) - }; - - assert!(sphere_context - .set_petname_record("myself", &link_record) - .await - .is_err()); - assert!(sphere_context - .set_petname("myself", Some(sphere_identity.clone())) - .await - .is_err()); - - Ok(()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_disallows_adding_outdated_records() -> Result<()> { - initialize_tracing(None); - - let (mut sphere_context, _) = - simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; - let mut store = sphere_context.sphere_context().await?.db().clone(); - - // Generate two LinkRecords, the first one having a later expiry - // than the second. - let (records, foo_identity) = { - let mut records: Vec = vec![]; - let owner_key = generate_ed25519_key(); - let owner_did = owner_key.get_did().await?; - let mut db = SphereDb::new(&MemoryStorage::default()).await?; - let (sphere, proof, _) = Sphere::generate(&owner_did, &mut db).await?; - let ucan_proof = proof.as_ucan(&db).await?; - let sphere_identity = sphere.get_identity().await?; - store.write_token(&ucan_proof.encode()?).await?; - - for lifetime in [500, 100] { - let link_record = LinkRecord::from( - UcanBuilder::default() - .issued_by(&owner_key) - .for_audience(&sphere_identity) - .witnessed_by(&ucan_proof, None) - .claiming_capability(&generate_capability( - &sphere_identity, - SphereAbility::Publish, - )) - .with_lifetime(lifetime) - .with_fact(LINK_RECORD_FACT_NAME, sphere.cid().to_string()) - .build()? - .sign() - .await?, - ); - - store.write_token(&link_record.encode()?).await?; - records.push(link_record); - } - (records, sphere_identity) - }; - - sphere_context - .set_petname("foo", Some(foo_identity)) - .await?; - sphere_context.save(None).await?; - - assert!(sphere_context - .set_petname_record("foo", records.get(0).unwrap()) - .await - .is_ok()); - sphere_context.save(None).await?; - assert!(sphere_context - .set_petname_record("foo", records.get(1).unwrap()) - .await - .is_err()); - Ok(()) - } -} diff --git a/rust/noosphere-sphere/src/cursor.rs b/rust/noosphere-sphere/src/cursor.rs deleted file mode 100644 index eb2cbbf77..000000000 --- a/rust/noosphere-sphere/src/cursor.rs +++ /dev/null @@ -1,743 +0,0 @@ -use anyhow::Result; -use async_trait::async_trait; -use noosphere_api::data::ReplicateParameters; -use noosphere_core::{ - data::{Link, MemoIpld}, - stream::put_block_stream, - view::{Sphere, Timeline}, -}; -use noosphere_storage::Storage; - -use crate::{HasMutableSphereContext, HasSphereContext, SphereContext, SphereReplicaRead}; -use std::marker::PhantomData; - -/// A [SphereCursor] is a structure that enables reading from and writing to a -/// [SphereContext] at specific versions of the associated sphere's history. -/// There are times when you may wish to be able to use the convenience -/// implementation of traits built on [HasSphereContext], but to always be sure -/// of what version you are using them on (such as when traversing sphere -/// history). That is when you would use a [SphereCursor], which can wrap any -/// implementor of [HasSphereContext] and mount it to a specific version of the -/// sphere. -#[derive(Clone)] -pub struct SphereCursor -where - C: HasSphereContext, - S: Storage + 'static, -{ - has_sphere_context: C, - storage: PhantomData, - sphere_version: Option>, -} - -impl SphereCursor -where - C: HasSphereContext, - S: Storage + 'static, -{ - /// Consume the [SphereCursor] and return its wrapped [HasSphereContext] - pub fn to_inner(self) -> C { - self.has_sphere_context - } - - /// Same as [SphereCursor::mount], but mounts the [SphereCursor] to a known - /// version of the history of the sphere. - pub fn mounted_at(has_sphere_context: C, sphere_version: &Link) -> Self { - SphereCursor { - has_sphere_context, - storage: PhantomData, - sphere_version: Some(sphere_version.clone()), - } - } - - /// Create the [SphereCursor] at the latest local version of the associated - /// sphere, mounted to that version. If the latest version changes due to - /// effects in the distance, the cursor will still point to the same version - /// it referred to when it was created. - pub async fn mounted(has_sphere_context: C) -> Result { - let mut cursor = Self::latest(has_sphere_context); - cursor.mount().await?; - Ok(cursor) - } - - /// "Mount" the [SphereCursor] to the given version of the sphere it refers - /// to. If the [SphereCursor] is already mounted, the version it is mounted - /// to will be overwritten. A mounted [SphereCursor] will remain at the - /// version it is mounted to even when the latest version of the sphere - /// changes. - pub async fn mount_at(&mut self, sphere_version: &Link) -> Result<&Self> { - self.sphere_version = Some(sphere_version.clone()); - - Ok(self) - } - - /// Same as [SphereCursor::mount_at] except that it mounts to the latest - /// local version of the sphere. - pub async fn mount(&mut self) -> Result<&Self> { - let sphere_version = self - .has_sphere_context - .sphere_context() - .await? - .version() - .await?; - - self.mount_at(&sphere_version).await - } - - /// "Unmount" the [SphereCursor] so that it always uses the latest local - /// version of the sphere that it refers to. - pub fn unmount(mut self) -> Result { - self.sphere_version = None; - Ok(self) - } - - /// Create this [SphereCursor] at the latest local version of the associated - /// sphere. The [SphereCursor] will always point to the latest local - /// version, unless subsequently mounted. - pub fn latest(has_sphere_context: C) -> Self { - SphereCursor { - has_sphere_context, - storage: PhantomData, - sphere_version: None, - } - } - - /// Rewind the [SphereCursor] to point to the version of the sphere just - /// prior to this one in the edit chronology. If there was a previous - /// version to rewind to then the returned `Option` has the [Cid] of the - /// revision, otherwise if the current version is the oldest one it is - /// `None`. - pub async fn rewind(&mut self) -> Result>> { - let sphere = self.to_sphere().await?; - - match sphere.get_parent().await? { - Some(parent) => { - self.sphere_version = Some(parent.cid().clone()); - Ok(self.sphere_version.as_ref()) - } - None => Ok(None), - } - } -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl HasMutableSphereContext for SphereCursor -where - C: HasMutableSphereContext, - S: Storage, -{ - type MutableSphereContext = C::MutableSphereContext; - - async fn sphere_context_mut(&mut self) -> Result { - self.has_sphere_context.sphere_context_mut().await - } - - async fn save( - &mut self, - additional_headers: Option>, - ) -> Result> { - let new_version = self.has_sphere_context.save(additional_headers).await?; - - if self.sphere_version.is_some() { - self.sphere_version = Some(new_version.clone()); - } - - Ok(new_version) - } -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl HasSphereContext for SphereCursor -where - C: HasSphereContext, - S: Storage + 'static, -{ - type SphereContext = C::SphereContext; - - async fn sphere_context(&self) -> Result { - self.has_sphere_context.sphere_context().await - } - - async fn version(&self) -> Result> { - match &self.sphere_version { - Some(sphere_version) => Ok(sphere_version.clone()), - None => self.has_sphere_context.version().await, - } - } - - async fn wrap(sphere_context: SphereContext) -> Self { - SphereCursor::latest(C::wrap(sphere_context).await) - } -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl SphereReplicaRead for SphereCursor -where - C: HasSphereContext, - S: Storage + 'static, -{ - #[instrument(level = "debug", skip(self))] - async fn traverse_by_petnames(&self, petname_path: &[String]) -> Result> { - debug!("Traversing by petname..."); - - let replicate = { - let cursor = self.clone(); - - move |version: Link, since: Option>| { - let cursor = cursor.clone(); - - async move { - let replicate_parameters = since.as_ref().map(|since| ReplicateParameters { - since: Some(since.clone()), - }); - let (db, client) = { - let sphere_context = cursor.sphere_context().await?; - (sphere_context.db().clone(), sphere_context.client().await?) - }; - let stream = client - .replicate(&version, replicate_parameters.as_ref()) - .await?; - put_block_stream(db.clone(), stream).await?; - - // If this was incremental replication, we have to hydrate... - if let Some(since) = since { - let since_memo = since.load_from(&db).await?; - let latest_memo = version.load_from(&db).await?; - - // Only hydrate if since is a causal antecedent - if since_memo.lamport_order() < latest_memo.lamport_order() { - let timeline = Timeline::new(&db); - - Sphere::hydrate_timeslice( - &timeline.slice(&version, Some(&since)).exclude_past(), - ) - .await?; - } - } - - Ok(()) as Result<(), anyhow::Error> - } - } - }; - - let sphere = self.to_sphere().await?; - - let peer_sphere = match sphere - .traverse_by_petnames(petname_path, &replicate) - .await? - { - Some(sphere) => sphere, - None => return Ok(None), - }; - - let mut db = sphere.store().clone(); - let peer_identity = peer_sphere.get_identity().await?; - let local_version = db.get_version(&peer_identity).await?.map(|cid| cid.into()); - - let should_update_version = if let Some(since) = local_version { - let since_memo = Sphere::at(&since, &db).to_memo().await?; - let latest_memo = peer_sphere.to_memo().await?; - - since_memo.lamport_order() < latest_memo.lamport_order() - } else { - true - }; - - if should_update_version { - debug!( - "Updating local version of {} to more recent revision {}", - peer_identity, - peer_sphere.cid() - ); - - db.set_version(&peer_identity, peer_sphere.cid()).await?; - } - - let peer_sphere_context = self - .sphere_context() - .await? - .to_visitor(&peer_identity) - .await?; - - Ok(Some(SphereCursor::mounted_at( - C::wrap(peer_sphere_context).await, - peer_sphere.cid(), - ))) - } -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use anyhow::Result; - use noosphere_core::data::{ContentType, Header}; - use noosphere_core::helpers::make_valid_link_record; - use noosphere_core::tracing::initialize_tracing; - use noosphere_storage::UcanStore; - use tokio::io::AsyncReadExt; - - #[cfg(target_arch = "wasm32")] - use wasm_bindgen_test::wasm_bindgen_test; - - #[cfg(target_arch = "wasm32")] - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - - use crate::helpers::{ - make_sphere_context_with_peer_chain, simulated_sphere_context, SimulationAccess, - }; - use crate::{ - HasMutableSphereContext, HasSphereContext, SphereContentRead, SphereContentWrite, - SpherePetnameRead, SpherePetnameWrite, SphereReplicaRead, - }; - - use super::SphereCursor; - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_unlink_slugs_from_the_content_space() { - let (sphere_context, _) = simulated_sphere_context(SimulationAccess::ReadWrite, None) - .await - .unwrap(); - let mut cursor = SphereCursor::latest(sphere_context); - - cursor - .write( - "cats", - &ContentType::Subtext, - b"Cats are great".as_ref(), - None, - ) - .await - .unwrap(); - - cursor.save(None).await.unwrap(); - - assert!(cursor.read("cats").await.unwrap().is_some()); - - cursor.remove("cats").await.unwrap(); - cursor.save(None).await.unwrap(); - - assert!(cursor.read("cats").await.unwrap().is_none()); - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_flushes_on_every_save() { - let (sphere_context, _) = simulated_sphere_context(SimulationAccess::ReadWrite, None) - .await - .unwrap(); - let initial_stats = { - sphere_context - .lock() - .await - .db() - .to_block_store() - .to_stats() - .await - }; - let mut cursor = SphereCursor::latest(sphere_context.clone()); - - cursor - .write( - "cats", - &ContentType::Subtext, - b"Cats are great".as_ref(), - None, - ) - .await - .unwrap(); - - cursor.save(None).await.unwrap(); - - let first_save_stats = { - sphere_context - .lock() - .await - .db() - .to_block_store() - .to_stats() - .await - }; - - assert_eq!(first_save_stats.flushes, initial_stats.flushes + 1); - - cursor.remove("cats").await.unwrap(); - cursor.save(None).await.unwrap(); - - let second_save_stats = { - sphere_context - .lock() - .await - .db() - .to_block_store() - .to_stats() - .await - }; - - assert_eq!(second_save_stats.flushes, first_save_stats.flushes + 1); - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_does_not_allow_writes_when_an_author_has_read_only_access() { - let (sphere_context, _) = simulated_sphere_context(SimulationAccess::Readonly, None) - .await - .unwrap(); - - let mut cursor = SphereCursor::latest(sphere_context); - - let write_result = cursor - .write( - "cats", - &ContentType::Subtext, - b"Cats are great".as_ref(), - None, - ) - .await; - - assert!(write_result.is_err()); - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_write_a_file_and_read_it_back() { - let (sphere_context, _) = simulated_sphere_context(SimulationAccess::ReadWrite, None) - .await - .unwrap(); - let mut cursor = SphereCursor::latest(sphere_context); - - cursor - .write( - "cats", - &ContentType::Subtext, - b"Cats are great".as_ref(), - None, - ) - .await - .unwrap(); - - cursor.save(None).await.unwrap(); - - let mut file = cursor.read("cats").await.unwrap().unwrap(); - - file.memo - .expect_header(&Header::ContentType, &ContentType::Subtext) - .unwrap(); - - let mut value = String::new(); - file.contents.read_to_string(&mut value).await.unwrap(); - - assert_eq!("Cats are great", value.as_str()); - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_overwrite_a_file_with_new_contents_and_preserve_history() { - let (sphere_context, _) = simulated_sphere_context(SimulationAccess::ReadWrite, None) - .await - .unwrap(); - let mut cursor = SphereCursor::latest(sphere_context); - - cursor - .write( - "cats", - &ContentType::Subtext, - b"Cats are great".as_ref(), - None, - ) - .await - .unwrap(); - - cursor.save(None).await.unwrap(); - - cursor - .write( - "cats", - &ContentType::Subtext, - b"Cats are better than dogs".as_ref(), - None, - ) - .await - .unwrap(); - - cursor.save(None).await.unwrap(); - - let mut file = cursor.read("cats").await.unwrap().unwrap(); - - file.memo - .expect_header(&Header::ContentType, &ContentType::Subtext) - .unwrap(); - - let mut value = String::new(); - file.contents.read_to_string(&mut value).await.unwrap(); - - assert_eq!("Cats are better than dogs", value.as_str()); - - assert!(cursor.rewind().await.unwrap().is_some()); - - file = cursor.read("cats").await.unwrap().unwrap(); - - file.memo - .expect_header(&Header::ContentType, &ContentType::Subtext) - .unwrap(); - - value.clear(); - file.contents.read_to_string(&mut value).await.unwrap(); - - assert_eq!("Cats are great", value.as_str()); - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_throws_an_error_when_saving_without_changes() { - let (sphere_context, _) = simulated_sphere_context(SimulationAccess::ReadWrite, None) - .await - .unwrap(); - let mut cursor = SphereCursor::latest(sphere_context); - - let result = cursor.save(None).await; - - assert!(result.is_err()); - assert_eq!(result.unwrap_err().to_string(), "No changes to save"); - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_throws_an_error_when_saving_with_empty_mutation_and_empty_headers() { - let (sphere_context, _) = simulated_sphere_context(SimulationAccess::ReadWrite, None) - .await - .unwrap(); - let mut cursor = SphereCursor::latest(sphere_context); - - let result = cursor.save(Some(vec![])).await; - - assert!(result.is_err()); - assert_eq!(result.unwrap_err().to_string(), "No changes to save"); - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_get_all_petnames_assigned_to_an_identity() -> Result<()> { - let (sphere_context, _) = - simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; - - let mut db = UcanStore(sphere_context.sphere_context().await?.db().clone()); - - let (peer_1, link_record_1, _) = make_valid_link_record(&mut db).await?; - let (peer_2, link_record_2, _) = make_valid_link_record(&mut db).await?; - let (peer_3, link_record_3, _) = make_valid_link_record(&mut db).await?; - - let mut cursor = SphereCursor::latest(sphere_context); - - cursor - .set_petname("foo1", Some(link_record_1.to_sphere_identity())) - .await?; - cursor - .set_petname("bar1", Some(link_record_1.to_sphere_identity())) - .await?; - cursor - .set_petname("baz1", Some(link_record_1.to_sphere_identity())) - .await?; - - cursor - .set_petname("foo2", Some(link_record_2.to_sphere_identity())) - .await?; - cursor.save(None).await?; - - cursor.set_petname_record("foo1", &link_record_1).await?; - cursor.set_petname_record("bar1", &link_record_1).await?; - cursor.set_petname_record("baz1", &link_record_1).await?; - - cursor.set_petname_record("foo2", &link_record_2).await?; - - cursor.save(None).await?; - - assert_eq!( - cursor.get_assigned_petnames(&peer_1).await?, - vec![ - String::from("foo1"), - String::from("bar1"), - String::from("baz1") - ] - ); - - assert_eq!( - cursor.get_assigned_petnames(&peer_2).await?, - vec![String::from("foo2")] - ); - - assert_eq!( - cursor.get_assigned_petnames(&peer_3).await?, - Vec::::new() - ); - - // Check one more time for good measure, since results are cached internally - assert_eq!( - cursor.get_assigned_petnames(&peer_1).await?, - vec![ - String::from("foo1"), - String::from("bar1"), - String::from("baz1") - ] - ); - - cursor - .set_petname("bar2", Some(link_record_2.to_sphere_identity())) - .await?; - cursor - .set_petname("foo3", Some(link_record_3.to_sphere_identity())) - .await?; - cursor.save(None).await?; - - cursor.set_petname_record("bar2", &link_record_2).await?; - cursor.set_petname_record("foo3", &link_record_3).await?; - cursor.save(None).await?; - - assert_eq!( - cursor.get_assigned_petnames(&peer_1).await?, - vec![ - String::from("foo1"), - String::from("bar1"), - String::from("baz1") - ] - ); - - assert_eq!( - cursor.get_assigned_petnames(&peer_2).await?, - vec![String::from("bar2"), String::from("foo2")] - ); - - assert_eq!( - cursor.get_assigned_petnames(&peer_3).await?, - vec![String::from("foo3")] - ); - - Ok(()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_traverse_a_sequence_of_petnames() -> Result<()> { - initialize_tracing(None); - - let name_seqeuence: Vec = vec!["a".into(), "b".into(), "c".into()]; - let (origin_sphere_context, _) = - make_sphere_context_with_peer_chain(&name_seqeuence).await?; - - let cursor = SphereCursor::latest(Arc::new( - origin_sphere_context.sphere_context().await?.clone(), - )); - - let target_sphere_context = cursor - .traverse_by_petnames(&name_seqeuence.into_iter().rev().collect::>()) - .await? - .unwrap(); - - let mut name = String::new(); - let mut file = target_sphere_context.read("my-name").await?.unwrap(); - file.contents.read_to_string(&mut name).await?; - - assert_eq!(name.as_str(), "c"); - Ok(()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_resolves_none_when_a_petname_is_missing_from_the_sequence() -> Result<()> { - initialize_tracing(None); - - let name_sequence: Vec = vec!["b".into(), "c".into()]; - let (origin_sphere_context, _) = - make_sphere_context_with_peer_chain(&name_sequence).await?; - - let cursor = SphereCursor::latest(Arc::new( - origin_sphere_context.sphere_context().await?.clone(), - )); - - let traversed_sequence: Vec = vec!["a".into(), "b".into(), "c".into()]; - - let target_sphere_context = cursor - .traverse_by_petnames( - &traversed_sequence - .into_iter() - .rev() - .collect::>(), - ) - .await - .unwrap(); - - assert!(target_sphere_context.is_none()); - - Ok(()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_correctly_identifies_a_visited_perer() -> Result<()> { - initialize_tracing(None); - - let name_sequence: Vec = vec!["a".into(), "b".into(), "c".into()]; - - let (origin_sphere_context, dids) = - make_sphere_context_with_peer_chain(&name_sequence).await?; - - let cursor = SphereCursor::latest(Arc::new( - origin_sphere_context.sphere_context().await?.clone(), - )); - - let mut target_sphere_context = cursor; - let mut identities = vec![target_sphere_context.identity().await?]; - - for name in name_sequence.iter() { - target_sphere_context = target_sphere_context - .traverse_by_petnames(&[name.clone()]) - .await? - .unwrap(); - identities.push(target_sphere_context.identity().await?); - } - - assert_eq!(identities.into_iter().rev().collect::>(), dids); - - Ok(()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_traverse_a_sequence_of_petnames_one_at_a_time() -> Result<()> { - initialize_tracing(None); - - let name_sequence: Vec = vec!["a".into(), "b".into(), "c".into()]; - - let (origin_sphere_context, _) = - make_sphere_context_with_peer_chain(&name_sequence).await?; - - let cursor = SphereCursor::latest(Arc::new( - origin_sphere_context.sphere_context().await?.clone(), - )); - - let mut target_sphere_context = cursor; - - for name in name_sequence.iter() { - target_sphere_context = target_sphere_context - .traverse_by_petnames(&[name.clone()]) - .await? - .unwrap(); - } - - let mut name = String::new(); - let mut file = target_sphere_context - .read("my-name") - .await - .unwrap() - .unwrap(); - file.contents.read_to_string(&mut name).await.unwrap(); - - assert_eq!(name.as_str(), "c"); - - Ok(()) - } -} diff --git a/rust/noosphere-sphere/src/has.rs b/rust/noosphere-sphere/src/has.rs deleted file mode 100644 index 8e8ce31d0..000000000 --- a/rust/noosphere-sphere/src/has.rs +++ /dev/null @@ -1,223 +0,0 @@ -use anyhow::{anyhow, Result}; -use async_trait::async_trait; -use noosphere_core::{ - authority::Author, - data::{Did, Link, MemoIpld}, - view::Sphere, -}; -use noosphere_storage::{SphereDb, Storage}; -use std::{ - ops::{Deref, DerefMut}, - sync::Arc, -}; -use tokio::sync::{Mutex, OwnedMutexGuard}; - -use crate::SphereContextKey; - -use super::SphereContext; - -#[allow(missing_docs)] -#[cfg(not(target_arch = "wasm32"))] -pub trait HasConditionalSendSync: Send + Sync {} - -#[cfg(not(target_arch = "wasm32"))] -impl HasConditionalSendSync for S where S: Send + Sync {} - -#[allow(missing_docs)] -#[cfg(target_arch = "wasm32")] -pub trait HasConditionalSendSync {} - -#[cfg(target_arch = "wasm32")] -impl HasConditionalSendSync for S {} - -/// Any container that can provide non-mutable access to a [SphereContext] -/// should implement [HasSphereContext]. The most common example of something -/// that may implement this trait is an `Arc>`. Implementors -/// of this trait will automatically implement other traits that provide -/// convience methods for accessing different parts of the sphere, such as -/// content and petnames. -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -pub trait HasSphereContext: Clone + HasConditionalSendSync -where - S: Storage, -{ - /// The type of the internal read-only [SphereContext] - type SphereContext: Deref> + HasConditionalSendSync; - - /// Get the [SphereContext] that is made available by this container. - async fn sphere_context(&self) -> Result; - - /// Get the DID identity of the sphere that this FS view is reading from and - /// writing to - async fn identity(&self) -> Result { - let sphere_context = self.sphere_context().await?; - - Ok(sphere_context.identity().clone()) - } - - /// The CID of the most recent local version of this sphere - async fn version(&self) -> Result> { - self.sphere_context().await?.version().await - } - - /// Get a data view into the sphere at the current revision - async fn to_sphere(&self) -> Result>> { - let version = self.version().await?; - Ok(Sphere::at(&version, self.sphere_context().await?.db())) - } - - /// Create a new [SphereContext] via [SphereContext::with_author] and wrap it in the same - /// [HasSphereContext] implementation, returning the result - async fn with_author(&self, author: &Author) -> Result { - Ok(Self::wrap(self.sphere_context().await?.with_author(author).await?).await) - } - - /// Wrap a given [SphereContext] in this [HasSphereContext] - async fn wrap(sphere_context: SphereContext) -> Self; -} - -/// Any container that can provide mutable access to a [SphereContext] should -/// implement [HasMutableSphereContext]. The most common example of something -/// that may implement this trait is `Arc>>`. -/// Implementors of this trait will automatically implement other traits that -/// provide convenience methods for modifying the contents, petnames and other -/// aspects of a sphere. -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -pub trait HasMutableSphereContext: HasSphereContext + HasConditionalSendSync -where - S: Storage, -{ - /// The type of the internal mutable [SphereContext] - type MutableSphereContext: Deref> - + DerefMut> - + HasConditionalSendSync; - - /// Get a mutable reference to the [SphereContext] that is wrapped by this - /// container. - async fn sphere_context_mut(&mut self) -> Result; - - /// Returns true if any changes have been made to the underlying - /// [SphereContext] that have not been committed to the associated sphere - /// yet (according to local history). - async fn has_unsaved_changes(&self) -> Result { - let context = self.sphere_context().await?; - Ok(!context.mutation().is_empty()) - } - - /// Commits a series of writes to the sphere and signs the new version. The - /// new version [Link] of the sphere is returned. This method must - /// be invoked in order to update the local history of the sphere with any - /// changes that have been made. - async fn save( - &mut self, - additional_headers: Option>, - ) -> Result> { - let sphere = self.to_sphere().await?; - let mut sphere_context = self.sphere_context_mut().await?; - let sphere_identity = sphere_context.identity().clone(); - let mut revision = sphere.apply_mutation(sphere_context.mutation()).await?; - - match additional_headers { - Some(headers) if !headers.is_empty() => revision.memo.replace_headers(headers), - _ if sphere_context.mutation().is_empty() => return Err(anyhow!("No changes to save")), - _ => (), - } - - let new_sphere_version = revision - .sign( - &sphere_context.author().key, - sphere_context.author().authorization.as_ref(), - ) - .await?; - - sphere_context - .db_mut() - .set_version(&sphere_identity, &new_sphere_version) - .await?; - sphere_context.db_mut().flush().await?; - sphere_context.mutation_mut().reset(); - - Ok(new_sphere_version) - } -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl HasSphereContext for Arc>> -where - S: Storage + 'static, -{ - type SphereContext = OwnedMutexGuard>; - - async fn sphere_context(&self) -> Result { - Ok(self.clone().lock_owned().await) - } - - async fn wrap(sphere_context: SphereContext) -> Self { - Arc::new(Mutex::new(sphere_context)) - } -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl HasSphereContext for Box -where - T: HasSphereContext, - S: Storage + 'static, -{ - type SphereContext = T::SphereContext; - - async fn sphere_context(&self) -> Result { - T::sphere_context(self).await - } - - async fn wrap(sphere_context: SphereContext) -> Self { - Box::new(T::wrap(sphere_context).await) - } -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl HasSphereContext for Arc> -where - S: Storage, -{ - type SphereContext = Arc>; - - async fn sphere_context(&self) -> Result { - Ok(self.clone()) - } - - async fn wrap(sphere_context: SphereContext) -> Self { - Arc::new(sphere_context) - } -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl HasMutableSphereContext for Arc>> -where - S: Storage + 'static, -{ - type MutableSphereContext = OwnedMutexGuard>; - - async fn sphere_context_mut(&mut self) -> Result { - self.sphere_context().await - } -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl HasMutableSphereContext for Box -where - T: HasMutableSphereContext, - S: Storage + 'static, -{ - type MutableSphereContext = T::MutableSphereContext; - - async fn sphere_context_mut(&mut self) -> Result { - T::sphere_context_mut(self).await - } -} diff --git a/rust/noosphere-sphere/src/helpers.rs b/rust/noosphere-sphere/src/helpers.rs deleted file mode 100644 index b82a9864f..000000000 --- a/rust/noosphere-sphere/src/helpers.rs +++ /dev/null @@ -1,227 +0,0 @@ -//! These helpers are intended for use in documentation examples and tests only. -//! They are useful for quickly scaffolding common scenarios that would -//! otherwise be verbosely rubber-stamped in a bunch of places. -use std::sync::Arc; - -use anyhow::Result; -use noosphere_core::{ - authority::{generate_capability, generate_ed25519_key, Author, SphereAbility}, - data::{ContentType, Did, LinkRecord, Mnemonic, LINK_RECORD_FACT_NAME}, - view::Sphere, -}; -use noosphere_storage::{BlockStore, MemoryStorage, SphereDb, TrackingStorage, UcanStore}; -use tokio::{io::AsyncReadExt, sync::Mutex}; -use ucan::{builder::UcanBuilder, crypto::KeyMaterial}; - -use crate::{ - walk_versioned_map_elements, walk_versioned_map_elements_and, HasMutableSphereContext, - HasSphereContext, SphereContentRead, SphereContentWrite, SphereContext, SphereContextKey, - SpherePetnameWrite, -}; - -/// Access levels available when simulating a [SphereContext] -pub enum SimulationAccess { - /// Access to the related [SphereContext] is read-only - Readonly, - /// Access to the related [SphereContext] is read+write - ReadWrite, -} - -/// Create a temporary, non-persisted [SphereContext] that tracks usage -/// internally. This is intended for use in docs and tests, and should otherwise -/// be ignored. When creating the simulated [SphereContext], you can pass a -/// [SimulationAccess] to control the kind of access the emphemeral credentials -/// have to the [SphereContext]. -pub async fn simulated_sphere_context( - profile: SimulationAccess, - db: Option>>, -) -> Result<( - Arc>>>, - Mnemonic, -)> { - let mut db = match db { - Some(db) => db, - None => { - let storage_provider = TrackingStorage::wrap(MemoryStorage::default()); - SphereDb::new(&storage_provider).await? - } - }; - - let owner_key: SphereContextKey = Arc::new(Box::new(generate_ed25519_key())); - let owner_did = owner_key.get_did().await?; - - let (sphere, proof, mnemonic) = Sphere::generate(&owner_did, &mut db).await?; - - let sphere_identity = sphere.get_identity().await?; - let author = Author { - key: owner_key, - authorization: match profile { - SimulationAccess::Readonly => None, - SimulationAccess::ReadWrite => Some(proof), - }, - }; - - db.set_version(&sphere_identity, sphere.cid()).await?; - - Ok(( - Arc::new(Mutex::new( - SphereContext::new(sphere_identity, author, db, None).await?, - )), - mnemonic, - )) -} - -#[cfg(docs)] -use noosphere_core::data::MemoIpld; - -/// Attempt to walk an entire sphere, touching every block up to and including -/// any [MemoIpld] nodes, but excluding those memo's body content. This helper -/// is useful for asserting that the blocks expected to be sent during -/// replication have in fact been sent. -pub async fn touch_all_sphere_blocks(sphere: &Sphere) -> Result<()> -where - S: BlockStore + 'static, -{ - trace!("Touching content blocks..."); - let content = sphere.get_content().await?; - let _ = content.load_changelog().await?; - - walk_versioned_map_elements(content).await?; - - trace!("Touching identity blocks..."); - let identities = sphere.get_address_book().await?.get_identities().await?; - let _ = identities.load_changelog().await?; - - walk_versioned_map_elements_and( - identities, - sphere.store().clone(), - |_, identity, store| async move { - let ucan_store = UcanStore(store); - if let Some(record) = identity.link_record(&ucan_store).await { - record.collect_proofs(&ucan_store).await?; - } - Ok(()) - }, - ) - .await?; - - trace!("Touching authority blocks..."); - let authority = sphere.get_authority().await?; - - trace!("Touching delegation blocks..."); - let delegations = authority.get_delegations().await?; - walk_versioned_map_elements(delegations).await?; - - trace!("Touching revocation blocks..."); - let revocations = authority.get_revocations().await?; - walk_versioned_map_elements(revocations).await?; - - Ok(()) -} - -/// A type of [HasMutableSphereContext] that uses [TrackingStorage] internally -pub type TrackedHasMutableSphereContext = Arc>>>; - -/// Create a series of spheres where each sphere has the next as resolved -/// entry in its address book; return a [HasMutableSphereContext] for the -/// first sphere in the sequence. -pub async fn make_sphere_context_with_peer_chain( - peer_chain: &[String], -) -> Result<(TrackedHasMutableSphereContext, Vec)> { - let (origin_sphere_context, _) = simulated_sphere_context(SimulationAccess::ReadWrite, None) - .await - .unwrap(); - - let mut db = origin_sphere_context - .sphere_context() - .await - .unwrap() - .db() - .clone(); - - let mut contexts = vec![origin_sphere_context.clone()]; - - for name in peer_chain.iter() { - let (mut sphere_context, _) = - simulated_sphere_context(SimulationAccess::ReadWrite, Some(db.clone())) - .await - .unwrap(); - - sphere_context - .write("my-name", &ContentType::Subtext, name.as_bytes(), None) - .await - .unwrap(); - sphere_context.save(None).await.unwrap(); - - contexts.push(sphere_context); - } - - let mut next_sphere_context: Option = None; - let mut dids = Vec::new(); - - for mut sphere_context in contexts.into_iter().rev() { - dids.push(sphere_context.identity().await?); - if let Some(next_sphere_context) = next_sphere_context { - let version = next_sphere_context.version().await.unwrap(); - - let next_author = next_sphere_context - .sphere_context() - .await - .unwrap() - .author() - .clone(); - let next_identity = next_sphere_context.identity().await.unwrap(); - - let link_record = LinkRecord::from( - UcanBuilder::default() - .issued_by(&next_author.key) - .for_audience(&next_identity) - .witnessed_by( - &next_author - .authorization - .as_ref() - .unwrap() - .as_ucan(&db) - .await - .unwrap(), - None, - ) - .claiming_capability(&generate_capability( - &next_identity, - SphereAbility::Publish, - )) - .with_lifetime(120) - .with_fact(LINK_RECORD_FACT_NAME, version.to_string()) - .build() - .unwrap() - .sign() - .await - .unwrap(), - ); - - let mut name = String::new(); - let mut file = next_sphere_context.read("my-name").await.unwrap().unwrap(); - file.contents.read_to_string(&mut name).await.unwrap(); - - debug!("Adopting {name}"); - sphere_context - .set_petname(&name, Some(next_identity)) - .await?; - sphere_context.save(None).await?; - - sphere_context - .set_petname_record(&name, &link_record) - .await - .unwrap(); - let identity = sphere_context.identity().await?; - - db.set_version(&identity, &sphere_context.save(None).await.unwrap()) - .await - .unwrap(); - } - - next_sphere_context = Some(sphere_context); - } - - Ok((origin_sphere_context, dids)) -} diff --git a/rust/noosphere-sphere/src/internal.rs b/rust/noosphere-sphere/src/internal.rs deleted file mode 100644 index b736381f5..000000000 --- a/rust/noosphere-sphere/src/internal.rs +++ /dev/null @@ -1,103 +0,0 @@ -use super::{BodyChunkDecoder, SphereFile}; -use crate::{AsyncFileBody, HasSphereContext}; -use anyhow::{anyhow, Result}; -use async_trait::async_trait; -use noosphere_storage::{BlockStore, Storage}; -use std::str::FromStr; -use tokio_util::io::StreamReader; - -use cid::Cid; -use noosphere_core::{ - authority::Access, - data::{ContentType, Header, Link, MemoIpld}, - stream::put_block_stream, -}; - -/// A module-private trait for internal trait methods; this is a workaround for -/// the fact that all trait methods are implicitly public implementation -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -pub(crate) trait SphereContextInternal -where - S: Storage + 'static, -{ - /// Returns an error result if the configured author of the [SphereContext] - /// does not have write access to it (as a matter of cryptographic - /// authorization). - async fn assert_write_access(&self) -> Result<()>; - - async fn get_file( - &self, - sphere_revision: &Cid, - memo_link: Link, - ) -> Result>>; -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl SphereContextInternal for C -where - C: HasSphereContext, - S: Storage + 'static, -{ - async fn assert_write_access(&self) -> Result<()> { - let sphere_context = self.sphere_context().await?; - match sphere_context.access().await? { - Access::ReadOnly => Err(anyhow!( - "Cannot mutate sphere; author only has read access to its contents" - )), - _ => Ok(()), - } - } - - async fn get_file( - &self, - sphere_revision: &Cid, - memo_link: Link, - ) -> Result>> { - let db = self.sphere_context().await?.db().clone(); - let memo = memo_link.load_from(&db).await?; - - // If we have a memo, but not the content it refers to, we should try to - // replicate from the gateway - if db.get_block(&memo.body).await?.is_none() { - let client = self - .sphere_context() - .await? - .client() - .await - .map_err(|error| { - warn!("Unable to initialize API client for replicating missing content"); - error - })?; - - // NOTE: This is kind of a hack, since we may be accessing a - // "read-only" context. Technically this should be acceptable - // because our mutation here is propagating immutable blocks - // into the local DB - let stream = client.replicate(&memo_link, None).await?; - - put_block_stream(db.clone(), stream).await?; - } - - let content_type = match memo.get_first_header(&Header::ContentType) { - Some(content_type) => Some(ContentType::from_str(content_type.as_str())?), - None => None, - }; - - let stream = match content_type { - // TODO(#86): Content-type aware decoding of body bytes - Some(_) => BodyChunkDecoder(&memo.body, &db).stream(), - None => return Err(anyhow!("No content type specified")), - }; - - Ok(SphereFile { - sphere_identity: self.sphere_context().await?.identity().clone(), - sphere_version: sphere_revision.into(), - memo_version: memo_link, - memo, - // NOTE: we have to box here because traits don't support `impl` types in return values - contents: Box::new(StreamReader::new(stream)), - }) - } -} diff --git a/rust/noosphere-sphere/src/lib.rs b/rust/noosphere-sphere/src/lib.rs deleted file mode 100644 index 6a0bf6dd7..000000000 --- a/rust/noosphere-sphere/src/lib.rs +++ /dev/null @@ -1,100 +0,0 @@ -//! This crate implements content, petname and other forms of acccess to -//! spheres. If you have storage and network primitives on your platform, you -//! can initialize a [SphereContext] and use it to work with and synchronize -//! spheres, as well as traverse the broader Noosphere data graph. -//! -//! In order to initialize a [SphereContext], you need a [Did] (like an ID) for -//! a Sphere, a [Storage] primitive and an [Author] (which represents whoever or -//! whatever is trying to access the Sphere inquestion). -//! -//! Once you have a [SphereContext], you can begin reading from, writing to and -//! traversing the Noosphere content graph. -//! -//! ```rust -//! # use anyhow::Result; -//! # #[cfg(feature = "helpers")] -//! # use noosphere_sphere::helpers::{simulated_sphere_context,SimulationAccess}; -//! # -//! # use noosphere_sphere::{SphereCursor, HasMutableSphereContext, SphereContentWrite}; -//! # -//! # #[cfg(feature = "helpers")] -//! # #[tokio::main(flavor = "multi_thread")] -//! # async fn main() -> Result<()> { -//! # let (mut sphere_context, _) = simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; -//! # -//! sphere_context.write("foo", "text/plain", "bar".as_ref(), None).await?; -//! sphere_context.save(None).await?; -//! # -//! # Ok(()) -//! # } -//! # -//! # #[cfg(not(feature = "helpers"))] -//! # fn main() {} -//! ``` -//! -//! You can also use a [SphereContext] to access petnames in the sphere: -//! -//! ```rust -//! # use anyhow::Result; -//! # #[cfg(feature = "helpers")] -//! # use noosphere_sphere::helpers::{simulated_sphere_context,SimulationAccess}; -//! # use noosphere_core::data::Did; -//! # -//! # use noosphere_sphere::{SphereCursor, HasMutableSphereContext, SpherePetnameWrite}; -//! # -//! # #[cfg(feature = "helpers")] -//! # #[tokio::main(flavor = "multi_thread")] -//! # async fn main() -> Result<()> { -//! # let (mut sphere_context, _) = simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; -//! # -//! sphere_context.set_petname("cdata", Some("did:key:example".into())).await?; -//! sphere_context.save(None).await?; -//! # -//! # Ok(()) -//! # } -//! # -//! # #[cfg(not(feature = "helpers"))] -//! # fn main() {} -//! ``` -//! - -#![warn(missing_docs)] - -#[macro_use] -extern crate tracing; - -#[cfg(doc)] -use noosphere_core::data::Did; - -#[cfg(doc)] -use noosphere_core::authority::Author; - -#[cfg(doc)] -use noosphere_storage::Storage; - -mod authority; -mod content; -mod context; -mod cursor; -mod has; -mod replication; -mod walker; - -#[cfg(any(doctest, test, feature = "helpers"))] -pub mod helpers; - -mod internal; -pub mod metadata; -mod petname; -mod sync; - -pub use authority::*; -pub use content::*; -pub use context::*; -pub use cursor::*; -pub use has::*; -pub use metadata::*; -pub use petname::*; -pub use replication::*; -pub use sync::*; -pub use walker::*; diff --git a/rust/noosphere-sphere/src/metadata.rs b/rust/noosphere-sphere/src/metadata.rs deleted file mode 100644 index c3fd017eb..000000000 --- a/rust/noosphere-sphere/src/metadata.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! These constants represent the metadata keys used when a [SphereContext] is -//! is initialized. Since these represent somewhat free-form key/values in the -//! storage layer, we are make a best effort to document them here. - -#[cfg(doc)] -use crate::SphereContext; - -#[cfg(doc)] -use noosphere_core::data::Did; - -#[cfg(doc)] -use cid::Cid; - -#[cfg(doc)] -use url::Url; - -/// A key that corresponds to the sphere's identity, which is represented by a -/// [Did] when it is set. -pub const IDENTITY: &str = "identity"; - -/// A name that corresponds to the locally available key. This name is -/// represented as a string, and should match a credential ID for a key in -/// whatever the supported platform key storage is. -pub const USER_KEY_NAME: &str = "user_key_name"; - -/// The [Cid] of a UCAN JWT that authorizes the configured user key to access -/// the sphere. -pub const AUTHORIZATION: &str = "authorization"; - -/// The base [Url] of a Noosphere Gateway API that will allow this sphere to -/// sync with it. -pub const GATEWAY_URL: &str = "gateway_url"; - -/// The counterpart sphere [Did] that either tracks or is tracked by this -/// sphere. -pub const COUNTERPART: &str = "counterpart"; diff --git a/rust/noosphere-sphere/src/petname/mod.rs b/rust/noosphere-sphere/src/petname/mod.rs deleted file mode 100644 index 0ab1d1c94..000000000 --- a/rust/noosphere-sphere/src/petname/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Sphere petnames are shorthand names that are associated with DIDs. A petname -//! can be any string, and always refers to a DID. The DID, in turn, may be -//! resolved to a CID that represents the tip of history for the sphere that is -//! implicitly identified by the provided DID. Note though that a petname may refer -//! to no CID (`None`) when first added (as no peer may have been resolved yet). - -mod read; -mod write; - -pub use read::*; -pub use write::*; diff --git a/rust/noosphere-sphere/src/petname/read.rs b/rust/noosphere-sphere/src/petname/read.rs deleted file mode 100644 index 54201997e..000000000 --- a/rust/noosphere-sphere/src/petname/read.rs +++ /dev/null @@ -1,138 +0,0 @@ -use std::collections::BTreeMap; - -use anyhow::Result; -use async_trait::async_trait; -use cid::Cid; -use futures_util::TryStreamExt; -use noosphere_core::data::{Did, Link, LinkRecord, MemoIpld}; -use noosphere_storage::{KeyValueStore, Storage}; - -use crate::{HasSphereContext, SphereWalker}; - -/// Anything that provides read access to petnames in a sphere should implement -/// [SpherePetnameRead]. A blanket implementation is provided for any container -/// that implements [HasSphereContext]. -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -pub trait SpherePetnameRead -where - S: Storage + 'static, -{ - /// Get the [Did] that is assigned to a petname, if any - async fn get_petname(&self, name: &str) -> Result>; - - /// Resolve the petname via its assigned [Did] to a [Cid] that refers to a - /// point in history of a sphere - async fn resolve_petname(&self, name: &str) -> Result>>; - - /// Given a [Did], get all the petnames that have been assigned to it - /// in this sphere - async fn get_assigned_petnames(&self, did: &Did) -> Result>; - - /// Given a petname, get the raw last known [LinkRecord] for that peer - async fn get_petname_record(&self, name: &str) -> Result>; -} - -fn assigned_petnames_cache_key(origin: &Did, peer: &Did, origin_version: &Cid) -> String { - format!( - "noosphere:cache:petname:assigned:{}:{}:{}", - origin, peer, origin_version - ) -} - -fn sphere_checkpoint_cache_key(origin: &Did, origin_version: &Cid) -> String { - format!( - "noosphere:cache:petname:checkpoint:{}:{}", - origin, origin_version - ) -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl SpherePetnameRead for C -where - C: HasSphereContext, - S: Storage + 'static, -{ - #[instrument(skip(self))] - async fn get_assigned_petnames(&self, peer: &Did) -> Result> { - let version = self.version().await?; - let origin = self.identity().await?; - - debug!("Getting petnames assigned in {origin} at version {version}"); - - let mut db = self.sphere_context().await?.db().clone(); - let key = assigned_petnames_cache_key(&origin, peer, &version); - - if let Some(names) = db.get_key::<_, Vec>(key).await? { - return Ok(names); - } - - let checkpoint_key = sphere_checkpoint_cache_key(&origin, &version); - - if db.get_key::<_, u8>(&checkpoint_key).await?.is_some() { - warn!("No names were assigned to {peer}",); - return Ok(vec![]); - } - - let walker = SphereWalker::from(self); - let petname_stream = walker.petname_stream(); - let mut did_petnames: BTreeMap> = BTreeMap::new(); - - tokio::pin!(petname_stream); - - while let Some((petname, identity)) = petname_stream.try_next().await? { - match did_petnames.get_mut(&identity.did) { - Some(petnames) => { - petnames.push(petname); - } - None => { - did_petnames.insert(identity.did, vec![petname]); - } - }; - } - - let mut assigned_petnames = None; - - for (did, petnames) in did_petnames { - if &did == peer { - assigned_petnames = Some(petnames.clone()); - } - - let key = assigned_petnames_cache_key(&origin, &did, &version); - db.set_key(key, petnames).await?; - } - - db.set_key(checkpoint_key, 1u8).await?; - - Ok(assigned_petnames.unwrap_or_default()) - } - - async fn get_petname(&self, name: &str) -> Result> { - let sphere = self.to_sphere().await?; - let identities = sphere.get_address_book().await?.get_identities().await?; - let address_ipld = identities.get(&name.to_string()).await?; - - Ok(address_ipld.map(|ipld| ipld.did.clone())) - } - - async fn resolve_petname(&self, name: &str) -> Result>> { - Ok(match self.get_petname_record(name).await? { - Some(link_record) => link_record.get_link(), - None => None, - }) - } - - async fn get_petname_record(&self, name: &str) -> Result> { - let sphere = self.to_sphere().await?; - let identities = sphere.get_address_book().await?.get_identities().await?; - let address_ipld = identities.get(&name.to_string()).await?; - - trace!("Recorded address for {name}: {:?}", address_ipld); - - Ok(match address_ipld { - Some(identity) => identity.link_record(sphere.store()).await, - None => None, - }) - } -} diff --git a/rust/noosphere-sphere/src/petname/write.rs b/rust/noosphere-sphere/src/petname/write.rs deleted file mode 100644 index a496ccb8f..000000000 --- a/rust/noosphere-sphere/src/petname/write.rs +++ /dev/null @@ -1,192 +0,0 @@ -use anyhow::{anyhow, Result}; -use async_trait::async_trait; -use noosphere_core::data::{Did, IdentityIpld, LinkRecord}; -use noosphere_storage::Storage; -use ucan::store::UcanJwtStore; - -use crate::{internal::SphereContextInternal, HasMutableSphereContext, SpherePetnameRead}; - -fn validate_petname(petname: &str) -> Result<()> { - if petname.is_empty() { - Err(anyhow!("Petname must not be empty.")) - } else if petname.len() >= 4 && petname.starts_with("did:") { - Err(anyhow!("Petname must not be a DID.")) - } else { - Ok(()) - } -} - -/// Anything that can write petnames to a sphere should implement -/// [SpherePetnameWrite]. A blanket implementation is provided for anything that -/// implements [HasMutableSphereContext] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -pub trait SpherePetnameWrite: SpherePetnameRead -where - S: Storage + 'static, -{ - /// Configure a petname, by assigning some [Did] to it or none. By assigning - /// none, the petname is implicitly removed from the address space (note: - /// this does not erase the name from historical versions of the sphere). If - /// a name is set that already exists, the previous name shall be - /// overwritten by the new one, and any associated [Jwt] shall be unset. - async fn set_petname(&mut self, name: &str, identity: Option) -> Result<()>; - - /// Set the [LinkRecord] associated with a petname. The [LinkRecord] must - /// resolve a valid UCAN that authorizes the corresponding sphere to be - /// published and grants sufficient authority from the configured [Did] to - /// the publisher. The audience of the UCAN must match the [Did] that was - /// most recently assigned the associated petname. Note that a petname - /// _must_ be assigned to the audience [Did] in order for the record to be - /// set. - async fn set_petname_record(&mut self, name: &str, record: &LinkRecord) -> Result>; - - /// Deprecated; use [SpherePetnameWrite::set_petname_record] instead - #[deprecated(note = "Use set_petname_record instead")] - async fn adopt_petname(&mut self, name: &str, record: &LinkRecord) -> Result>; -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl SpherePetnameWrite for C -where - C: HasMutableSphereContext, - S: Storage + 'static, -{ - async fn set_petname(&mut self, name: &str, identity: Option) -> Result<()> { - self.assert_write_access().await?; - validate_petname(name)?; - - if identity.is_some() - && self.sphere_context().await?.identity() == identity.as_ref().unwrap() - { - return Err(anyhow!("Sphere cannot assign itself to a petname.")); - } - - let current_address = self.get_petname(name).await?; - - if identity != current_address { - let mut context = self.sphere_context_mut().await?; - match identity { - Some(identity) => { - context.mutation_mut().identities_mut().set( - &name.to_string(), - &IdentityIpld { - did: identity, - // TODO: We should backfill this if we have already resolved - // this address by another name - link_record: None, - }, - ); - } - None => context - .mutation_mut() - .identities_mut() - .remove(&name.to_string()), - }; - } - - Ok(()) - } - - async fn adopt_petname(&mut self, name: &str, record: &LinkRecord) -> Result> { - self.set_petname_record(name, record).await - } - - async fn set_petname_record(&mut self, name: &str, record: &LinkRecord) -> Result> { - // NOTE: it is not safe for us to blindly adopt link records that don't - // match up with the petname we are adopting them against. For example, - // consider the following sequence of events: - // - // 1. A petname is assigned to a DID - // 2. During sync, the gateway kicks off a parallel job to resolve the - // petname - // 3. Meanwhile, we unassign the petname - // 4. We sync, the gateway takes no action (no new names to resolve) - // 5. Then, the original resolve job finishes and comes back with a - // record - // - // Record adoption is not able to disambiguate between between a new - // record being added and a race condition like the one described above. - self.assert_write_access().await?; - validate_petname(name)?; - - let identity = record.to_sphere_identity(); - let expected_identity = self.get_petname(name).await?; - - match expected_identity { - Some(expected_identity) => { - if expected_identity != identity { - return Err(anyhow!( - "Cannot adopt petname record for '{}'; expected record for {} but got record for {}", - name, - expected_identity, - identity - )); - } - } - None => { - return Err(anyhow!( - "Cannot adopt petname record for '{}' (not assigned to a sphere identity)", - name - )); - } - }; - - if self.sphere_context().await?.identity() == &identity { - return Err(anyhow!("Sphere cannot assign itself to a petname.")); - } - - if let Some(existing_record) = self.get_petname_record(name).await? { - if !existing_record.superceded_by(record) { - return Err(anyhow!( - "Previously stored record supercedes provided record." - )); - } - } - - let cid = self - .sphere_context_mut() - .await? - .db_mut() - .write_token(&record.encode()?) - .await?; - - // TODO: Validate the record as a UCAN - - debug!( - "Adopting '{}' ({}), resolving to {}...", - name, identity, record - ); - - let new_address = IdentityIpld { - did: identity.clone(), - link_record: Some(cid.into()), - }; - - let identities = self - .sphere_context() - .await? - .sphere() - .await? - .get_address_book() - .await? - .get_identities() - .await?; - let previous_identity = identities.get(&name.into()).await?; - - self.sphere_context_mut() - .await? - .mutation_mut() - .identities_mut() - .set(&name.into(), &new_address); - - if let Some(previous_identity) = previous_identity { - if identity != previous_identity.did { - return Ok(Some(previous_identity.did.to_owned())); - } - } - - Ok(None) - } -} diff --git a/rust/noosphere-sphere/src/replication/car.rs b/rust/noosphere-sphere/src/replication/car.rs deleted file mode 100644 index 5753c4fc0..000000000 --- a/rust/noosphere-sphere/src/replication/car.rs +++ /dev/null @@ -1,79 +0,0 @@ -use anyhow::Result; -use async_stream::try_stream; -use bytes::Bytes; -use cid::Cid; -use futures_util::sink::SinkExt; -use iroh_car::{CarHeader, CarWriter}; -use std::io::{Error as IoError, ErrorKind as IoErrorKind}; -use tokio::sync::mpsc::channel; -use tokio_stream::Stream; -use tokio_util::{ - io::{CopyToBytes, SinkWriter}, - sync::PollSender, -}; - -/// Takes a list of roots and a stream of blocks (pairs of [Cid] and -/// corresponding [Vec]), and produces an async byte stream that yields a -/// valid [CARv1](https://ipld.io/specs/transport/car/carv1/) -pub fn car_stream( - mut roots: Vec, - block_stream: S, -) -> impl Stream> + Send -where - S: Stream)>> + Send, -{ - if roots.is_empty() { - roots = vec![Cid::default()] - } - - try_stream! { - let (tx, mut rx) = channel::(16); - let sink = - PollSender::new(tx).sink_map_err(|error| { - error!("Failed to send CAR frame: {}", error); - IoError::from(IoErrorKind::BrokenPipe) - }); - - let mut car_buffer = SinkWriter::new(CopyToBytes::new(sink)); - let car_header = CarHeader::new_v1(roots); - let mut car_writer = CarWriter::new(car_header, &mut car_buffer); - let mut sent_blocks = false; - - for await item in block_stream { - sent_blocks = true; - let (cid, block) = item.map_err(|error| { - error!("Failed to stream blocks: {}", error); - IoError::from(IoErrorKind::BrokenPipe) - })?; - - car_writer.write(cid, block).await.map_err(|error| { - error!("Failed to write CAR frame: {}", error); - IoError::from(IoErrorKind::BrokenPipe) - })?; - - car_writer.flush().await.map_err(|error| { - error!("Failed to flush CAR frames: {}", error); - IoError::from(IoErrorKind::BrokenPipe) - })?; - - while let Ok(block) = rx.try_recv() { - yield block; - } - } - - if !sent_blocks { - car_writer.write_header().await.map_err(|error| { - error!("Failed to write CAR frame: {}", error); - IoError::from(IoErrorKind::BrokenPipe) - })?; - car_writer.flush().await.map_err(|error| { - error!("Failed to flush CAR frames: {}", error); - IoError::from(IoErrorKind::BrokenPipe) - })?; - - while let Ok(block) = rx.try_recv() { - yield block; - } - } - } -} diff --git a/rust/noosphere-sphere/src/replication/mod.rs b/rust/noosphere-sphere/src/replication/mod.rs deleted file mode 100644 index 09d0dc2db..000000000 --- a/rust/noosphere-sphere/src/replication/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -mod car; -mod read; -mod walk; - -pub use car::*; -pub use read::*; -pub use walk::*; - -#[cfg(not(target_arch = "wasm32"))] -mod stream; -#[cfg(not(target_arch = "wasm32"))] -pub use stream::*; diff --git a/rust/noosphere-sphere/src/replication/read.rs b/rust/noosphere-sphere/src/replication/read.rs deleted file mode 100644 index 3318b9d3c..000000000 --- a/rust/noosphere-sphere/src/replication/read.rs +++ /dev/null @@ -1,18 +0,0 @@ -use anyhow::Result; -use async_trait::async_trait; -use noosphere_storage::Storage; - -/// Implementors are able to traverse from one sphere to the next via -/// the address book entries found in those spheres -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -pub trait SphereReplicaRead: Sized -where - S: Storage + 'static, -{ - /// Accepts a linear sequence of petnames and attempts to recursively - /// traverse through spheres using that sequence. The sequence is traversed - /// from back to front. So, if the sequence is "gold", "cat", "bob", it will - /// traverse to bob, then to bob's cat, then to bob's cat's gold. - async fn traverse_by_petnames(&self, petnames: &[String]) -> Result>; -} diff --git a/rust/noosphere-sphere/src/replication/stream.rs b/rust/noosphere-sphere/src/replication/stream.rs deleted file mode 100644 index c3227e085..000000000 --- a/rust/noosphere-sphere/src/replication/stream.rs +++ /dev/null @@ -1,653 +0,0 @@ -use std::str::FromStr; - -use anyhow::{anyhow, Result}; -use async_stream::try_stream; -use cid::Cid; -use libipld_cbor::DagCborCodec; -use noosphere_core::{ - authority::collect_ucan_proofs, - data::{ContentType, Link, MemoIpld, SphereIpld}, - view::Sphere, -}; -use noosphere_storage::{BlockStore, BlockStoreTap, UcanStore}; -use tokio::sync::mpsc::error::TryRecvError; -use tokio::task::JoinSet; -use tokio_stream::{Stream, StreamExt}; -use ucan::{store::UcanJwtStore, Ucan}; - -use crate::{ - walk_versioned_map_changes_and, walk_versioned_map_elements, walk_versioned_map_elements_and, - BodyChunkDecoder, -}; - -/// Stream all the blocks required to reconstruct the history of a sphere since a -/// given point in time (or else the beginning of the history). -// TODO(tokio-rs/tracing#2503): instrument + impl trait causes clippy warning -#[allow(clippy::let_with_type_underscore)] -#[instrument(level = "trace", skip(store))] -pub fn memo_history_stream( - store: S, - latest: &Link, - since: Option<&Link>, -) -> impl Stream)>> + Send -where - S: BlockStore + 'static, -{ - let latest = latest.clone(); - let since = since.cloned(); - - try_stream! { - - let (store, mut rx) = BlockStoreTap::new(store.clone(), 64); - let memo = store.load::(&latest).await?; - - match memo.content_type() { - Some(ContentType::Sphere) => { - let history_task = tokio::spawn(async move { - let sphere = Sphere::from_memo(&memo, &store)?; - let identity = sphere.get_identity().await?; - let mut tasks = JoinSet::new(); - - let mut previous_sphere_body_version = None; - let mut previous_sphere_body: Option = None; - - let history_stream = sphere.into_history_stream(since.as_ref()); - - tokio::pin!(history_stream); - - while let Some((version, sphere)) = history_stream.try_next().await? { - if let Some(previous_sphere_body_version) = previous_sphere_body_version { - let memo = sphere.to_memo().await?; - if memo.body == previous_sphere_body_version { - warn!("Skipping {version} delta for {identity}, no sphere changes detected..."); - continue; - } - } - - debug!("Replicating {version} delta for {identity}"); - - let sphere_body = sphere.to_body().await?; - let (replicate_authority, replicate_address_book, replicate_content) = { - if let Some(previous_sphere_body) = previous_sphere_body { - (previous_sphere_body.authority != sphere_body.authority, - previous_sphere_body.address_book != sphere_body.address_book, - previous_sphere_body.content != sphere_body.content) - } else { - (true, true, true) - } - }; - - if replicate_authority { - debug!("Replicating authority..."); - let authority = sphere.get_authority().await?; - let store = store.clone(); - - tasks.spawn(async move { - let delegations = authority.get_delegations().await?; - - walk_versioned_map_changes_and(delegations, store, |_, delegation, store| async move { - let ucan_store = UcanStore(store); - - collect_ucan_proofs(&Ucan::from_str(&ucan_store.require_token(&delegation.jwt).await?)?, &ucan_store).await?; - - Ok(()) - }).await?; - - let revocations = authority.get_revocations().await?; - revocations.load_changelog().await?; - - Ok(()) as Result<_, anyhow::Error> - }); - } - - if replicate_address_book { - debug!("Replicating address book..."); - let address_book = sphere.get_address_book().await?; - let identities = address_book.get_identities().await?; - - tasks.spawn(walk_versioned_map_changes_and(identities, store.clone(), |name, identity, store| async move { - let ucan_store = UcanStore(store); - trace!("Replicating proofs for {}", name); - if let Some(link_record) = identity.link_record(&ucan_store).await { - link_record.collect_proofs(&ucan_store).await?; - }; - - Ok(()) - })); - } - - if replicate_content { - debug!("Replicating content..."); - let content = sphere.get_content().await?; - - tasks.spawn(walk_versioned_map_changes_and(content, store.clone(), |_, link, store| async move { - link.load_from(&store).await?; - Ok(()) - })); - } - - previous_sphere_body = Some(sphere_body); - previous_sphere_body_version = Some(sphere.to_memo().await?.body); - - drop(sphere); - } - - drop(store); - - while let Some(result) = tasks.join_next().await { - trace!("Replication branch completed, {} remaining...", tasks.len()); - result??; - } - - trace!("Done replicating!"); - - Ok(()) as Result<(), anyhow::Error> - }); - - let mut yield_count = 0usize; - - while let Some(block) = rx.recv().await { - yield_count += 1; - trace!(cid = ?block.0, "Yielding block {yield_count}..."); - yield block; - } - - trace!("Done yielding {yield_count} blocks!"); - - history_task.await??; - } - _ => { - Err(anyhow!("History streaming is only supported for spheres, but {latest} has content type {:?})", memo.content_type()))?; - } - } - } -} - -/// Stream all the blocks required to read the sphere at a given version (making no -/// assumptions of what historical data may already be available to the reader). -// TODO(tokio-rs/tracing#2503): instrument + impl trait causes clippy warning -#[allow(clippy::let_with_type_underscore)] -#[instrument(level = "trace", skip(store))] -pub fn memo_body_stream( - store: S, - memo_version: &Cid, -) -> impl Stream)>> + Send -where - S: BlockStore + 'static, -{ - let memo_version = *memo_version; - - try_stream! { - let (store, mut rx) = BlockStoreTap::new(store.clone(), 1024); - let memo = store.load::(&memo_version).await?; - - match memo.content_type() { - Some(ContentType::Sphere) => { - let sphere = Sphere::from_memo(&memo, &store)?; - let authority = sphere.get_authority().await?; - let address_book = sphere.get_address_book().await?; - let content = sphere.get_content().await?; - let identities = address_book.get_identities().await?; - let delegations = authority.get_delegations().await?; - let revocations = authority.get_revocations().await?; - - let identities_task = tokio::spawn(walk_versioned_map_elements_and(identities, store.clone(), |_, identity, store| async move { - let ucan_store = UcanStore(store); - if let Some(link_record) = identity.link_record(&ucan_store).await { - link_record.collect_proofs(&ucan_store).await?; - }; - Ok(()) - })); - let content_task = tokio::spawn(walk_versioned_map_elements_and(content, store.clone(), move |_, link, store| async move { - link.load_from(&store).await?; - Ok(()) - })); - let delegations_task = tokio::spawn(walk_versioned_map_elements(delegations)); - let revocations_task = tokio::spawn(walk_versioned_map_elements(revocations)); - - // Drop, so that their internal store is dropped, so that the - // store's internal sender is dropped, so that the receiver doesn't - // think there are outstanding senders after our tasks are finished: - drop(sphere); - drop(authority); - drop(address_book); - drop(store); - - while let Some(block) = rx.recv().await { - trace!("Yielding {}", block.0); - yield block; - } - - let (identities_result, content_result, delegations_result, revocations_result) = tokio::join!( - identities_task, - content_task, - delegations_task, - revocations_task - ); - - identities_result??; - content_result??; - delegations_result??; - revocations_result??; - } - Some(_) => { - let stream = BodyChunkDecoder(&memo.body, &store).stream(); - - drop(store); - - tokio::pin!(stream); - - 'decode: while (stream.try_next().await?).is_some() { - 'flush: loop { - match rx.try_recv() { - Ok(block) => { - yield block - }, - Err(TryRecvError::Empty) => break 'flush, - Err(_) => break 'decode - }; - } - }; - } - None => () - } - } -} - -#[cfg(test)] -mod tests { - use anyhow::Result; - use std::collections::BTreeSet; - use ucan::store::UcanJwtStore; - - use iroh_car::CarReader; - use libipld_cbor::DagCborCodec; - use noosphere_core::{ - data::{BodyChunkIpld, ContentType, LinkRecord, MemoIpld}, - helpers::make_valid_link_record, - tracing::initialize_tracing, - view::Sphere, - }; - use noosphere_storage::{BlockStore, MemoryStore, UcanStore}; - use tokio_stream::StreamExt; - use tokio_util::io::StreamReader; - - use crate::{ - car_stream, - helpers::{simulated_sphere_context, touch_all_sphere_blocks, SimulationAccess}, - memo_body_stream, memo_history_stream, BodyChunkDecoder, HasMutableSphereContext, - HasSphereContext, SphereContentWrite, SpherePetnameWrite, - }; - - #[cfg(target_arch = "wasm32")] - use wasm_bindgen_test::wasm_bindgen_test; - - #[cfg(target_arch = "wasm32")] - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_includes_all_link_records_and_proofs_from_the_address_book() -> Result<()> { - initialize_tracing(None); - - let (mut sphere_context, _) = - simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; - let mut db = sphere_context.sphere_context().await?.db().clone(); - - let (foo_did, foo_link_record, foo_link_record_link) = - make_valid_link_record(&mut db).await?; - - sphere_context.set_petname("foo", Some(foo_did)).await?; - sphere_context.save(None).await?; - sphere_context - .set_petname_record("foo", &foo_link_record) - .await?; - let final_version = sphere_context.save(None).await?; - - let mut other_store = MemoryStore::default(); - - let stream = memo_body_stream( - sphere_context.sphere_context().await?.db().clone(), - &final_version, - ); - - tokio::pin!(stream); - - while let Some((cid, block)) = stream.try_next().await? { - debug!("Received {cid}"); - other_store.put_block(&cid, &block).await?; - } - - let ucan_store = UcanStore(other_store); - - let link_record = - LinkRecord::try_from(ucan_store.require_token(&foo_link_record_link).await?)?; - - assert_eq!(link_record, foo_link_record); - - link_record.collect_proofs(&ucan_store).await?; - - Ok(()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_stream_all_blocks_in_a_sphere_version() -> Result<()> { - initialize_tracing(None); - - let (mut sphere_context, _) = - simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; - - let changes = vec![ - (vec!["dogs", "birds"], vec!["alice", "bob"]), - (vec!["cats", "dogs"], vec!["gordon"]), - (vec!["birds"], vec!["cdata"]), - (vec!["cows", "beetles"], vec!["jordan", "ben"]), - ]; - - for (content_change, petname_change) in changes.iter() { - for slug in content_change { - sphere_context - .write( - slug, - &ContentType::Subtext, - format!("{} are cool", slug).as_bytes(), - None, - ) - .await?; - } - - for petname in petname_change { - sphere_context - .set_petname(petname, Some(format!("did:key:{}", petname).into())) - .await?; - } - - sphere_context.save(None).await?; - } - - let final_version = sphere_context.version().await?; - - let mut other_store = MemoryStore::default(); - - let mut received = BTreeSet::new(); - - let stream = memo_body_stream( - sphere_context.sphere_context().await?.db().clone(), - &final_version, - ); - - tokio::pin!(stream); - - while let Some((cid, block)) = stream.try_next().await? { - debug!("Received {cid}"); - assert!( - !received.contains(&cid), - "Got {cid} but we already received it", - ); - received.insert(cid); - other_store.put_block(&cid, &block).await?; - } - - let sphere = Sphere::at(&final_version, &other_store); - - let content = sphere.get_content().await?; - let identities = sphere.get_address_book().await?.get_identities().await?; - - for (content_change, petname_change) in changes.iter() { - for slug in content_change { - let _ = content.get(&slug.to_string()).await?.cloned().unwrap(); - } - - for petname in petname_change { - let _ = identities.get(&petname.to_string()).await?; - } - } - - touch_all_sphere_blocks(&sphere).await?; - - Ok(()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_stream_all_delta_blocks_for_a_range_of_history() -> Result<()> { - initialize_tracing(None); - - let (mut sphere_context, _) = - simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; - - let changes = vec![ - (vec!["dogs", "birds"], vec!["alice", "bob"]), - (vec!["cats", "dogs"], vec!["gordon"]), - (vec!["birds"], vec!["cdata"]), - (vec!["cows", "beetles"], vec!["jordan", "ben"]), - ]; - - let original_store = sphere_context.sphere_context().await?.db().clone(); - let mut versions = Vec::new(); - - for (content_change, petname_change) in changes.iter() { - for slug in content_change { - sphere_context - .write( - slug, - &ContentType::Subtext, - format!("{} are cool", slug).as_bytes(), - None, - ) - .await?; - } - - for petname in petname_change { - let (id, record, _) = - make_valid_link_record(&mut UcanStore(original_store.clone())).await?; - sphere_context.set_petname(petname, Some(id)).await?; - versions.push(sphere_context.save(None).await?); - sphere_context.set_petname_record(petname, &record).await?; - } - - versions.push(sphere_context.save(None).await?); - } - - let mut other_store = MemoryStore::default(); - - let first_version = versions.first().unwrap(); - let stream = memo_body_stream(original_store.clone(), first_version); - - tokio::pin!(stream); - - while let Some((cid, block)) = stream.try_next().await? { - other_store.put_block(&cid, &block).await?; - } - - let sphere = Sphere::at(first_version, &other_store); - - touch_all_sphere_blocks(&sphere).await?; - - for i in 1..=3 { - let version = versions.get(i).unwrap(); - let sphere = Sphere::at(version, &other_store); - - assert!(touch_all_sphere_blocks(&sphere).await.is_err()); - } - - let stream = memo_history_stream( - original_store, - versions.last().unwrap(), - Some(first_version), - ); - - tokio::pin!(stream); - - while let Some((cid, block)) = stream.try_next().await? { - other_store.put_block(&cid, &block).await?; - } - - for i in 1..=3 { - let version = versions.get(i).unwrap(); - let sphere = Sphere::at(version, &other_store); - sphere.hydrate().await?; - - touch_all_sphere_blocks(&sphere).await?; - } - - Ok(()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_stream_all_blocks_in_some_sphere_content() -> Result<()> { - initialize_tracing(None); - - let (mut sphere_context, _) = - simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; - let mut db = sphere_context.sphere_context().await?.db_mut().clone(); - - let chunks = [b"foo", b"bar", b"baz"]; - - let mut next_chunk_cid = None; - - for bytes in chunks.iter().rev() { - next_chunk_cid = Some( - db.save::(&BodyChunkIpld { - bytes: bytes.to_vec(), - next: next_chunk_cid, - }) - .await?, - ); - } - - let content_cid = sphere_context - .link("foo", &ContentType::Bytes, &next_chunk_cid.unwrap(), None) - .await?; - - let stream = memo_body_stream( - sphere_context.sphere_context().await?.db().clone(), - &content_cid, - ); - - let mut store = MemoryStore::default(); - - tokio::pin!(stream); - - while let Some((cid, block)) = stream.try_next().await? { - store.put_block(&cid, &block).await?; - } - - let memo = store.load::(&content_cid).await?; - - let mut buffer = Vec::new(); - let body_stream = BodyChunkDecoder(&memo.body, &store).stream(); - - tokio::pin!(body_stream); - - while let Some(bytes) = body_stream.try_next().await? { - buffer.append(&mut Vec::from(bytes)); - } - - assert_eq!(buffer.as_slice(), b"foobarbaz"); - - Ok(()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_stream_all_blocks_in_a_sphere_version_as_a_car() -> Result<()> { - initialize_tracing(None); - - let (mut sphere_context, _) = - simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; - - let changes = vec![ - (vec!["dogs", "birds"], vec!["alice", "bob"]), - (vec!["cats", "dogs"], vec!["gordon"]), - (vec!["birds"], vec!["cdata"]), - (vec!["cows", "beetles"], vec!["jordan", "ben"]), - ]; - - for (content_change, petname_change) in changes.iter() { - for slug in content_change { - sphere_context - .write( - slug, - &ContentType::Subtext, - format!("{} are cool", slug).as_bytes(), - None, - ) - .await?; - } - - for petname in petname_change { - sphere_context - .set_petname(petname, Some(format!("did:key:{}", petname).into())) - .await?; - } - - sphere_context.save(None).await?; - } - - let mut db = sphere_context.sphere_context().await?.db().clone(); - let (id, link_record, _) = make_valid_link_record(&mut db).await?; - sphere_context.set_petname("hasrecord", Some(id)).await?; - sphere_context.save(None).await?; - sphere_context - .set_petname_record("hasrecord", &link_record) - .await?; - sphere_context.save(None).await?; - - let final_version = sphere_context.version().await?; - - let mut other_store = MemoryStore::default(); - - let stream = car_stream( - vec![final_version.clone().into()], - memo_body_stream(db.clone(), &final_version), - ); - - tokio::pin!(stream); - - let reader = CarReader::new(StreamReader::new(stream)).await?; - let block_stream = reader.stream(); - - let mut received = BTreeSet::new(); - tokio::pin!(block_stream); - - while let Some((cid, block)) = block_stream.try_next().await? { - debug!("Received {cid}"); - assert!( - !received.contains(&cid), - "Got {cid} but we already received it", - ); - received.insert(cid); - other_store.put_block(&cid, &block).await?; - } - - let sphere = Sphere::at(&final_version, &other_store); - - let content = sphere.get_content().await?; - let identities = sphere.get_address_book().await?.get_identities().await?; - - for (content_change, petname_change) in changes.iter() { - for slug in content_change { - let _ = content.get(&slug.to_string()).await?.cloned().unwrap(); - } - - for petname in petname_change { - let _ = identities.get(&petname.to_string()).await?; - } - } - - let has_record = identities.get(&"hasrecord".into()).await?.unwrap(); - let has_record_version = has_record.link_record(&UcanStore(other_store)).await; - - assert!( - has_record_version.is_some(), - "We got a resolved link record from the stream" - ); - - touch_all_sphere_blocks(&sphere).await?; - - Ok(()) - } -} diff --git a/rust/noosphere-sphere/src/replication/walk.rs b/rust/noosphere-sphere/src/replication/walk.rs deleted file mode 100644 index 1595777d7..000000000 --- a/rust/noosphere-sphere/src/replication/walk.rs +++ /dev/null @@ -1,72 +0,0 @@ -use anyhow::Result; -use noosphere_core::{ - data::{MapOperation, VersionedMapKey, VersionedMapValue}, - view::VersionedMap, -}; -use noosphere_storage::BlockStore; -use std::ops::Fn; -use tokio_stream::StreamExt; - -/// Given a [VersionedMap], visit its changelog and all of its underlying entries -pub async fn walk_versioned_map_elements( - versioned_map: VersionedMap, -) -> Result<()> -where - K: VersionedMapKey + 'static, - V: VersionedMapValue + 'static, - S: BlockStore + 'static, -{ - versioned_map.get_changelog().await?; - let stream = versioned_map.into_stream().await?; - tokio::pin!(stream); - while (stream.try_next().await?).is_some() {} - Ok(()) -} - -/// Given a [VersionedMap] and [BlockStore], visit the [VersionedMap]'s -/// changelog and all of its underlying entries, invoking a callback for each -/// entry -pub async fn walk_versioned_map_elements_and( - versioned_map: VersionedMap, - store: S, - callback: F, -) -> Result<()> -where - K: VersionedMapKey + 'static, - V: VersionedMapValue + 'static, - S: BlockStore + 'static, - Fut: std::future::Future>, - F: 'static + Fn(K, V, S) -> Fut, -{ - versioned_map.get_changelog().await?; - let stream = versioned_map.into_stream().await?; - tokio::pin!(stream); - while let Some((key, value)) = stream.try_next().await? { - callback(key, value, store.clone()).await?; - } - Ok(()) -} - -/// Given a [VersionedMap] and [BlockStore], visit the [VersionedMap]'s -/// changelog; then, invoke the provided callback with each entry associated -/// with an 'add' operation in the changelog -pub async fn walk_versioned_map_changes_and( - versioned_map: VersionedMap, - store: S, - callback: F, -) -> Result<()> -where - K: VersionedMapKey + 'static, - V: VersionedMapValue + 'static, - S: BlockStore + 'static, - Fut: std::future::Future>, - F: 'static + Fn(K, V, S) -> Fut, -{ - let changelog = versioned_map.load_changelog().await?; - for op in changelog.changes { - if let MapOperation::Add { key, value } = op { - callback(key, value, store.clone()).await?; - } - } - Ok(()) -} diff --git a/rust/noosphere-sphere/src/sync/error.rs b/rust/noosphere-sphere/src/sync/error.rs deleted file mode 100644 index fadf93721..000000000 --- a/rust/noosphere-sphere/src/sync/error.rs +++ /dev/null @@ -1,29 +0,0 @@ -use noosphere_api::data::PushError; -use thiserror::Error; - -/// Different classes of error that may occur during synchronization with a -/// gateway -#[derive(Error, Debug)] -pub enum SyncError { - /// The error was a conflict; this is possibly recoverable - #[error("There was a conflict during sync")] - Conflict, - /// The error was some other, non-specific error - #[error("{0}")] - Other(anyhow::Error), -} - -impl From for SyncError { - fn from(value: anyhow::Error) -> Self { - SyncError::Other(value) - } -} - -impl From for SyncError { - fn from(value: PushError) -> Self { - match value { - PushError::Conflict => SyncError::Conflict, - any => SyncError::Other(any.into()), - } - } -} diff --git a/rust/noosphere-sphere/src/sync/mod.rs b/rust/noosphere-sphere/src/sync/mod.rs deleted file mode 100644 index 1ce2773e9..000000000 --- a/rust/noosphere-sphere/src/sync/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod error; -mod recovery; -mod strategy; -mod write; - -pub use error::*; -pub use recovery::*; -pub use strategy::*; -pub use write::*; diff --git a/rust/noosphere-sphere/src/sync/recovery.rs b/rust/noosphere-sphere/src/sync/recovery.rs deleted file mode 100644 index 28bcdfb0d..000000000 --- a/rust/noosphere-sphere/src/sync/recovery.rs +++ /dev/null @@ -1,9 +0,0 @@ -/// Recovery strategies for cases when gateway synchronization fails but may be -/// able to recover gracefully (e.g., when the gateway reports a conflict). -#[derive(Debug)] -pub enum SyncRecovery { - /// Do not attempt to recover - None, - /// Automatically retry the synchronization up to a certain number of times - Retry(u32), -} diff --git a/rust/noosphere-sphere/src/sync/strategy.rs b/rust/noosphere-sphere/src/sync/strategy.rs deleted file mode 100644 index b04ac3f9a..000000000 --- a/rust/noosphere-sphere/src/sync/strategy.rs +++ /dev/null @@ -1,473 +0,0 @@ -use std::{collections::BTreeMap, marker::PhantomData}; - -use anyhow::{anyhow, Result}; -use noosphere_api::data::{FetchParameters, PushBody, PushResponse}; -use noosphere_core::{ - authority::{generate_capability, SphereAbility}, - data::{Did, IdentityIpld, Jwt, Link, MemoIpld, LINK_RECORD_FACT_NAME}, - stream::put_block_stream, - view::{Sphere, Timeline}, -}; -use noosphere_storage::{KeyValueStore, SphereDb, Storage}; -use tokio_stream::StreamExt; -use ucan::builder::UcanBuilder; - -use crate::{ - metadata::COUNTERPART, HasMutableSphereContext, SpherePetnameRead, SpherePetnameWrite, - SyncError, -}; - -type HandshakeResults = (Option>, Did, Option>); -type FetchResults = ( - Link, - Link, - BTreeMap, -); -type CounterpartHistory = Vec, Sphere>)>>; - -/// The default synchronization strategy is a git-like fetch->rebase->push flow. -/// It depends on the corresponding history of a "counterpart" sphere that is -/// owned by a gateway server. As revisions are pushed to the gateway server, it -/// updates its own sphere to point to the tip of the latest lineage of the -/// user's. When a new change needs to be synchronized, the latest history of -/// the counterpart sphere is first fetched, and the local changes are rebased -/// on the counterpart sphere's reckoning of the authoritative lineage of the -/// user's sphere. Finally, after the rebase, the reconciled local lineage is -/// pushed to the gateway. -pub struct GatewaySyncStrategy -where - C: HasMutableSphereContext, - S: Storage + 'static, -{ - has_context_type: PhantomData, - store_type: PhantomData, -} - -impl Default for GatewaySyncStrategy -where - C: HasMutableSphereContext, - S: Storage + 'static, -{ - fn default() -> Self { - Self { - has_context_type: Default::default(), - store_type: Default::default(), - } - } -} - -impl GatewaySyncStrategy -where - C: HasMutableSphereContext, - S: Storage + 'static, -{ - /// Synchronize a local sphere's data with the data in a gateway, and rollback - /// if there is an error. The returned [Link] is the latest version of the local - /// sphere lineage after the sync has completed. - pub async fn sync(&self, context: &mut C) -> Result, SyncError> - where - C: HasMutableSphereContext, - { - let (local_sphere_version, counterpart_sphere_identity, counterpart_sphere_version) = - self.handshake(context).await?; - - let result: Result, anyhow::Error> = { - let (mut local_sphere_version, counterpart_sphere_version, updated_names) = self - .fetch_remote_changes( - context, - local_sphere_version.as_ref(), - &counterpart_sphere_identity, - counterpart_sphere_version.as_ref(), - ) - .await?; - - if let Some(version) = self.adopt_names(context, updated_names).await? { - local_sphere_version = version; - } - - self.push_local_changes( - context, - &local_sphere_version, - &counterpart_sphere_identity, - &counterpart_sphere_version, - ) - .await?; - - Ok(local_sphere_version) - }; - - // Rollback if there is an error while syncing - if result.is_err() { - self.rollback( - context, - local_sphere_version.as_ref(), - &counterpart_sphere_identity, - counterpart_sphere_version.as_ref(), - ) - .await? - } - - Ok(result?) - } - - #[instrument(level = "debug", skip(self, context))] - async fn handshake(&self, context: &mut C) -> Result { - let mut context = context.sphere_context_mut().await?; - let client = context.client().await?; - let counterpart_sphere_identity = client.session.sphere_identity.clone(); - - // TODO(#561): Some kind of due diligence to notify the caller when this - // value changes - context - .db_mut() - .set_key(COUNTERPART, &counterpart_sphere_identity) - .await?; - - let local_sphere_identity = context.identity().clone(); - - let local_sphere_version = context.db().get_version(&local_sphere_identity).await?; - let counterpart_sphere_version = context - .db() - .get_version(&counterpart_sphere_identity) - .await?; - - Ok(( - local_sphere_version.map(|cid| cid.into()), - counterpart_sphere_identity, - counterpart_sphere_version.map(|cid| cid.into()), - )) - } - - /// Fetches the latest changes from a gateway and updates the local lineage - /// using a conflict-free rebase strategy - #[instrument(level = "debug", skip(self, context))] - async fn fetch_remote_changes( - &self, - context: &mut C, - local_sphere_tip: Option<&Link>, - counterpart_sphere_identity: &Did, - counterpart_sphere_base: Option<&Link>, - ) -> Result { - let mut context = context.sphere_context_mut().await?; - let local_sphere_identity = context.identity().clone(); - let client = context.client().await?; - - let fetch_response = client - .fetch(&FetchParameters { - since: counterpart_sphere_base.cloned(), - }) - .await?; - - let mut updated_names = BTreeMap::new(); - - let (counterpart_sphere_tip, block_stream) = match fetch_response { - Some((tip, stream)) => (tip, stream), - None => { - info!("Local history is already up to date..."); - let local_sphere_tip = context - .db() - .require_version(&local_sphere_identity) - .await? - .into(); - return Ok(( - local_sphere_tip, - counterpart_sphere_base - .ok_or_else(|| anyhow!("Counterpart sphere history is missing!"))? - .clone(), - updated_names, - )); - } - }; - - put_block_stream(context.db_mut().clone(), block_stream).await?; - - trace!("Finished putting block stream"); - - let counterpart_history: CounterpartHistory = - Sphere::at(&counterpart_sphere_tip, context.db_mut()) - .into_history_stream(counterpart_sphere_base) - .collect() - .await; - - trace!("Iterating over counterpart history"); - - for item in counterpart_history.into_iter().rev() { - let (_, sphere) = item?; - sphere.hydrate().await?; - updated_names.append( - &mut sphere - .get_address_book() - .await? - .get_identities() - .await? - .get_added() - .await?, - ); - } - - let local_sphere_old_base = match counterpart_sphere_base { - Some(counterpart_sphere_base) => Sphere::at(counterpart_sphere_base, context.db()) - .get_content() - .await? - .get(&local_sphere_identity) - .await? - .cloned(), - None => None, - }; - let local_sphere_new_base = Sphere::at(&counterpart_sphere_tip, context.db()) - .get_content() - .await? - .get(&local_sphere_identity) - .await? - .cloned(); - - let local_sphere_tip = match ( - local_sphere_tip, - local_sphere_old_base, - local_sphere_new_base, - ) { - // History diverged, so rebase our local changes on the newly received branch - (Some(current_tip), Some(old_base), Some(new_base)) if old_base != new_base => { - info!( - ?current_tip, - ?old_base, - ?new_base, - "Syncing received local sphere revisions..." - ); - Sphere::at(current_tip, context.db()) - .rebase( - &old_base, - &new_base, - &context.author().key, - context.author().authorization.as_ref(), - ) - .await? - } - // No diverged history, just new linear history based on our local tip - (None, old_base, Some(new_base)) => { - info!("Hydrating received local sphere revisions..."); - let timeline = Timeline::new(context.db_mut()); - Sphere::hydrate_timeslice( - &timeline.slice(&new_base, old_base.as_ref()).exclude_past(), - ) - .await?; - - new_base.clone() - } - // No new history at all - (Some(current_tip), _, _) => { - info!("Nothing to sync!"); - current_tip.clone() - } - // We should have local history but we don't! - _ => { - return Err(anyhow!("Missing local history for sphere after sync!")); - } - }; - - context - .db_mut() - .set_version(&local_sphere_identity, &local_sphere_tip) - .await?; - - debug!("Setting counterpart sphere version to {counterpart_sphere_tip}"); - - context - .db_mut() - .set_version(counterpart_sphere_identity, &counterpart_sphere_tip) - .await?; - - Ok((local_sphere_tip, counterpart_sphere_tip, updated_names)) - } - - #[instrument(level = "debug", skip(self, context))] - async fn adopt_names( - &self, - context: &mut C, - updated_names: BTreeMap, - ) -> Result>> { - if updated_names.is_empty() { - return Ok(None); - } - info!( - "Considering {} updated link records for adoption...", - updated_names.len() - ); - - let db = context.sphere_context().await?.db().clone(); - - for (name, address) in updated_names.into_iter() { - if let Some(link_record) = address.link_record(&db).await { - if let Some(identity) = context.get_petname(&name).await? { - if identity != address.did { - warn!("Updated link record for {name} referred to unexpected sphere; expected {identity}, but record referred to {}; ignoring...", address.did); - continue; - } - - if context.resolve_petname(&name).await? == link_record.get_link() { - // TODO(#562): Should probably also verify record expiry - // in case we are dealing with a renewed record to the - // same link - debug!("Resolved got new link record for {name} but the link has not changed; skipping..."); - continue; - } - - if let Err(e) = context.set_petname_record(&name, &link_record).await { - warn!("Could not set petname record: {}", e); - continue; - } - } else { - debug!("Not adopting link record for {name}, which is no longer present in the address book") - } - } - } - - Ok(if context.has_unsaved_changes().await? { - Some(context.save(None).await?) - } else { - None - }) - } - - /// Attempts to push the latest local lineage to the gateway, causing the - /// gateway to update its own pointer to the tip of the local sphere's history - #[instrument(level = "debug", skip(self, context))] - async fn push_local_changes( - &self, - context: &mut C, - local_sphere_tip: &Link, - counterpart_sphere_identity: &Did, - counterpart_sphere_tip: &Link, - ) -> Result<(), SyncError> { - let mut context = context.sphere_context_mut().await?; - - let local_sphere_base = Sphere::at(counterpart_sphere_tip, context.db()) - .get_content() - .await? - .get(context.identity()) - .await? - .cloned(); - - if local_sphere_base.as_ref() == Some(local_sphere_tip) { - info!("Gateway is already up to date!"); - return Ok(()); - } - - info!("Collecting blocks from new local history..."); - debug!("Bundling until {:?}", local_sphere_base); - - let bundle = Sphere::at(local_sphere_tip, context.db()) - .bundle_until_ancestor(local_sphere_base.as_ref()) - .await?; - - let mut byte_count = 0; - for bytes in bundle.map().values() { - byte_count += bytes.len(); - } - - trace!("Total bytes in bundle to be pushed: {}", byte_count); - - let client = context.client().await?; - - let local_sphere_identity = context.identity(); - let authorization = context - .author() - .require_authorization()? - .as_ucan(context.db()) - .await?; - - let name_record = Jwt(UcanBuilder::default() - .issued_by(&context.author().key) - .for_audience(local_sphere_identity) - .witnessed_by(&authorization, None) - .claiming_capability(&generate_capability( - local_sphere_identity, - SphereAbility::Publish, - )) - .with_lifetime(120) - .with_fact(LINK_RECORD_FACT_NAME, local_sphere_tip.to_string()) - .build()? - .sign() - .await? - .encode()?); - - info!( - "Pushing new local history to gateway {}...", - client.session.gateway_identity - ); - - let result = client - .push(&PushBody { - sphere: local_sphere_identity.clone(), - local_base: local_sphere_base, - local_tip: local_sphere_tip.clone(), - counterpart_tip: Some(counterpart_sphere_tip.clone()), - blocks: bundle, - name_record: Some(name_record), - }) - .await?; - - let (counterpart_sphere_updated_tip, new_blocks) = match result { - PushResponse::Accepted { new_tip, blocks } => (new_tip, blocks), - PushResponse::NoChange => { - return Err(SyncError::Other(anyhow!("Gateway already up to date!"))); - } - }; - - info!("Saving updated counterpart sphere history..."); - - new_blocks.load_into(context.db_mut()).await?; - - debug!( - "Hydrating updated counterpart sphere history (from {} back to {})...", - counterpart_sphere_tip, counterpart_sphere_updated_tip - ); - - let timeline = Timeline::new(context.db_mut()); - Sphere::hydrate_timeslice( - &timeline - .slice( - &counterpart_sphere_updated_tip, - Some(counterpart_sphere_tip), - ) - .exclude_past(), - ) - .await?; - - context - .db_mut() - .set_version(counterpart_sphere_identity, &counterpart_sphere_updated_tip) - .await?; - - Ok(()) - } - - #[instrument(level = "debug", skip(self, context))] - async fn rollback( - &self, - context: &mut C, - original_sphere_version: Option<&Link>, - counterpart_identity: &Did, - original_counterpart_version: Option<&Link>, - ) -> Result<()> { - debug!("Rolling back!"); - let sphere_identity = context.identity().await?; - let mut context = context.sphere_context_mut().await?; - - if let Some(version) = original_sphere_version { - context - .db_mut() - .set_version(&sphere_identity, version) - .await?; - } - - if let Some(version) = original_counterpart_version { - context - .db_mut() - .set_version(counterpart_identity, version) - .await?; - } - - Ok(()) - } -} diff --git a/rust/noosphere-sphere/src/sync/write.rs b/rust/noosphere-sphere/src/sync/write.rs deleted file mode 100644 index c68da3338..000000000 --- a/rust/noosphere-sphere/src/sync/write.rs +++ /dev/null @@ -1,80 +0,0 @@ -use anyhow::Result; -use async_trait::async_trait; -use noosphere_core::data::{Link, MemoIpld}; -use noosphere_storage::Storage; - -use crate::{HasMutableSphereContext, SyncError, SyncRecovery}; - -use crate::GatewaySyncStrategy; - -/// Implementors of [SphereSync] are able to sychronize with a Noosphere gateway -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -pub trait SphereSync -where - S: Storage + 'static, -{ - /// If a gateway URL has been configured, attempt to synchronize local - /// sphere data with the gateway. Changes on the gateway will first be - /// fetched to local storage. Then, the local changes will be replayed on - /// top of those changes. Finally, the synchronized local history will be - /// pushed up to the gateway. - /// - /// The returned [Link] is the latest version of the local - /// sphere lineage after the sync has completed. - async fn sync(&mut self, recovery: SyncRecovery) -> Result, SyncError>; -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl SphereSync for C -where - C: HasMutableSphereContext, - S: Storage + 'static, -{ - #[instrument(level = "debug", skip(self))] - async fn sync(&mut self, recovery: SyncRecovery) -> Result, SyncError> { - debug!("Attempting to sync..."); - - let sync_strategy = GatewaySyncStrategy::default(); - - let version = match recovery { - SyncRecovery::None => sync_strategy.sync(self).await?, - SyncRecovery::Retry(max_retries) => { - let mut retries = 0; - let version; - - loop { - match sync_strategy.sync(self).await { - Ok(result) => { - debug!("Sync success with {retries} retries"); - version = result; - break; - } - Err(SyncError::Conflict) => { - if retries < max_retries { - warn!( - "Sync conflict; {} retries remaining...", - max_retries - retries - ); - retries += 1; - } else { - warn!("Sync conflict; no retries remaining!"); - return Err(SyncError::Conflict); - } - } - Err(other) => { - return Err(other); - } - } - } - - version - } - }; - - self.sphere_context_mut().await?.reset_access(); - - Ok(version) - } -} diff --git a/rust/noosphere-sphere/src/walker.rs b/rust/noosphere-sphere/src/walker.rs deleted file mode 100644 index 782b39369..000000000 --- a/rust/noosphere-sphere/src/walker.rs +++ /dev/null @@ -1,599 +0,0 @@ -use anyhow::Result; -use noosphere_core::data::{Did, IdentityIpld, Jwt, Link, MapOperation, MemoIpld}; -use std::{collections::BTreeSet, marker::PhantomData}; - -use async_stream::try_stream; -use noosphere_storage::Storage; -use tokio::io::AsyncRead; -use tokio_stream::{Stream, StreamExt}; - -use crate::{ - content::{SphereContentRead, SphereFile}, - internal::SphereContextInternal, - HasSphereContext, SphereAuthorityRead, SpherePetnameRead, -}; - -/// A [SphereWalker] makes it possible to convert anything that implements -/// [HasSphereContext] into an async [Stream] over sphere content, allowing -/// incremental iteration over both the breadth of content at any version, or -/// the depth of changes over a range of history. -pub struct SphereWalker<'a, C, S> -where - C: HasSphereContext, - S: Storage + 'static, -{ - has_sphere_context: &'a C, - storage: PhantomData, -} - -impl<'a, C, S> From<&'a C> for SphereWalker<'a, C, S> -where - C: HasSphereContext, - S: Storage + 'static, -{ - fn from(has_sphere_context: &'a C) -> Self { - SphereWalker { - has_sphere_context, - storage: Default::default(), - } - } -} - -impl<'a, C, S> SphereWalker<'a, C, S> -where - C: SphereAuthorityRead + HasSphereContext, - S: Storage + 'static, -{ - /// Get a stream that yields a link to every authorization to access the - /// sphere along with its corresponding [DelegationIpld]. Note that since a - /// revocation may be issued without necessarily removing its revoked - /// delegation, this will yield all authorizations regardless of revocation - /// status. - pub fn authorization_stream( - &self, - ) -> impl Stream)>> + '_ { - try_stream! { - let sphere = self.has_sphere_context.to_sphere().await?; - let delegations = sphere.get_authority().await?.get_delegations().await?; - let stream = delegations.into_stream().await?; - - for await entry in stream { - let (link, delegation) = entry?; - let ucan = delegation.resolve_ucan(sphere.store()).await?; - yield (delegation.name, Did(ucan.audience().to_string()), link); - } - } - } - - /// Get a [BTreeSet] whose members are all the [Link]s to - /// authorizations that enable sphere access as of this version of the - /// sphere. Note that the full space of authoriztions may be very large; for - /// a more space-efficient approach, use - /// [SphereWalker::authorization_stream] to incrementally access all - /// authorizations in the sphere. - /// - /// This method is forgiving of missing or corrupted data, and will yield an - /// incomplete set of authorizations in the case that some or all names are - /// not able to be accessed. - pub async fn list_authorizations(&self) -> Result>> { - let sphere_identity = self.has_sphere_context.identity().await?; - let authorization_stream = self.authorization_stream(); - - tokio::pin!(authorization_stream); - - Ok(authorization_stream - .fold(BTreeSet::new(), |mut delegations, another_delegation| { - match another_delegation { - Ok((_, _, delegation)) => { - delegations.insert(delegation); - } - Err(error) => { - warn!( - "Could not read a petname from {}: {}", - sphere_identity, error - ) - } - }; - delegations - }) - .await) - } -} - -impl<'a, C, S> SphereWalker<'a, C, S> -where - C: SpherePetnameRead + HasSphereContext, - S: Storage + 'static, -{ - /// Same as [SphereWalker::petname_stream], but consumes the [SphereWalker]. - /// This is useful in cases where it would otherwise be necessary to borrow - /// a reference to [SphereWalker] for a static lifetime. - pub fn into_petname_stream(self) -> impl Stream> + 'a { - try_stream! { - let sphere = self.has_sphere_context.to_sphere().await?; - let petnames = sphere.get_address_book().await?.get_identities().await?; - let stream = petnames.into_stream().await?; - - for await entry in stream { - let (petname, address) = entry?; - yield (petname, address); - } - } - } - - /// Get a stream that yields every petname in the namespace along with its - /// corresponding [AddressIpld]. This is useful for iterating over sphere - /// petnames incrementally without having to load the entire index into - /// memory at once. - pub fn petname_stream(&self) -> impl Stream> + '_ { - try_stream! { - let sphere = self.has_sphere_context.to_sphere().await?; - let petnames = sphere.get_address_book().await?.get_identities().await?; - let stream = petnames.into_stream().await?; - - for await entry in stream { - let (petname, address) = entry?; - yield (petname, address); - } - } - } - - /// Get a stream that yields the set of petnames that changed at each - /// revision of the backing sphere, up to but excluding an optional `since` - /// CID parameter. To stream the entire history, pass `None` as the - /// parameter. - pub fn petname_change_stream<'b>( - &'b self, - since: Option<&'a Link>, - ) -> impl Stream, BTreeSet)>> + 'b { - try_stream! { - let sphere = self.has_sphere_context.to_sphere().await?; - let since = since.cloned(); - let stream = sphere.into_identities_changelog_stream(since.as_ref()); - - for await change in stream { - let (cid, changelog) = change?; - let mut changed_petnames = BTreeSet::new(); - - for operation in changelog.changes { - let petname = match operation { - MapOperation::Add { key, .. } => key, - MapOperation::Remove { key } => key, - }; - changed_petnames.insert(petname); - } - - yield (cid, changed_petnames); - } - } - } - - /// Get a stream that yields the set of petnames that changed at each - /// revision of the backing sphere, up to but excluding an optional `since` - /// CID parameter. To stream the entire history, pass `None` as the - /// parameter. - pub fn into_petname_change_stream( - self, - since: Option<&'a Link>, - ) -> impl Stream, BTreeSet)>> + '_ { - try_stream! { - let sphere = self.has_sphere_context.to_sphere().await?; - let since = since.cloned(); - let stream = sphere.into_identities_changelog_stream(since.as_ref()); - - for await change in stream { - let (cid, changelog) = change?; - let mut changed_petnames = BTreeSet::new(); - - for operation in changelog.changes { - let petname = match operation { - MapOperation::Add { key, .. } => key, - MapOperation::Remove { key } => key, - }; - changed_petnames.insert(petname); - } - - yield (cid, changed_petnames); - } - } - } - - /// Get a [BTreeSet] whose members are all the petnames that have addresses - /// as of this version of the sphere. Note that the full space of names may - /// be very large; for a more space-efficient approach, use - /// [SphereWalker::petname_stream] to incrementally access all petnames in - /// the sphere. - /// - /// This method is forgiving of missing or corrupted data, and will yield an - /// incomplete set of names in the case that some or all names are not able - /// to be accessed. - pub async fn list_petnames(&self) -> Result> { - let sphere_identity = self.has_sphere_context.identity().await?; - let petname_stream = self.petname_stream(); - - tokio::pin!(petname_stream); - - Ok(petname_stream - .fold(BTreeSet::new(), |mut petnames, another_petname| { - match another_petname { - Ok((petname, _)) => { - petnames.insert(petname); - } - Err(error) => { - warn!( - "Could not read a petname from {}: {}", - sphere_identity, error - ) - } - }; - petnames - }) - .await) - } - - /// Get a [BTreeSet] whose members are all the petnames whose values have - /// changed at least once since the provided version of the sphere - /// (exclusive of the provided version; use `None` to get all petnames - /// changed since the beginning of the sphere's history). - /// - /// This method is forgiving of missing or corrupted history, and will yield - /// an incomplete set of changes in the case that some or all changes are - /// not able to be accessed. - /// - /// Note that this operation will scale in memory consumption and duration - /// proportionally to the size of the sphere and the length of its history. - /// For a more efficient method of accessing changes, consider using - /// [SphereWalker::petname_change_stream] instead. - pub async fn petname_changes( - &self, - since: Option<&Link>, - ) -> Result> { - let sphere_identity = self.has_sphere_context.identity().await?; - let change_stream = self.petname_change_stream(since); - - tokio::pin!(change_stream); - - Ok(change_stream - .fold(BTreeSet::new(), |mut all, some| { - match some { - Ok((_, mut changes)) => all.append(&mut changes), - Err(error) => warn!( - "Could not read some changes from {}: {}", - sphere_identity, error - ), - }; - all - }) - .await) - } -} - -impl<'a, C, S> SphereWalker<'a, C, S> -where - C: SphereContentRead + HasSphereContext, - S: Storage + 'static, -{ - /// Same as [SphereWalker::content_stream], but consumes the [SphereWalker]. - /// This is useful in cases where it would otherwise be necessary to borrow - /// a reference to [SphereWalker] for a static lifetime. - pub fn into_content_stream( - self, - ) -> impl Stream)>> + 'a { - try_stream! { - let sphere = self.has_sphere_context.to_sphere().await?; - let content = sphere.get_content().await?; - let stream = content.into_stream().await?; - - for await entry in stream { - let (key, memo_link) = entry?; - let file = self.has_sphere_context.get_file(sphere.cid(), memo_link).await?; - - yield (key.clone(), file); - } - } - } - - /// Get a stream that yields every slug in the namespace along with its - /// corresponding [SphereFile]. This is useful for iterating over sphere - /// content incrementally without having to load the entire index into - /// memory at once. - pub fn content_stream( - &self, - ) -> impl Stream)>> + '_ { - try_stream! { - let sphere = self.has_sphere_context.to_sphere().await?; - let links = sphere.get_content().await?; - let stream = links.into_stream().await?; - - for await entry in stream { - let (key, memo) = entry?; - let file = self.has_sphere_context.get_file(sphere.cid(), memo).await?; - - yield (key.clone(), file); - } - } - } - - /// Get a stream that yields the set of slugs that changed at each revision - /// of the backing sphere, up to but excluding an optional CID. To stream - /// the entire history, pass `None` as the parameter. - pub fn into_content_change_stream( - self, - since: Option<&'a Link>, - ) -> impl Stream, BTreeSet)>> + '_ { - try_stream! { - let sphere = self.has_sphere_context.to_sphere().await?; - let since = since.cloned(); - let stream = sphere.into_content_changelog_stream(since.as_ref()); - - for await change in stream { - let (cid, changelog) = change?; - let mut changed_slugs = BTreeSet::new(); - - for operation in changelog.changes { - let slug = match operation { - MapOperation::Add { key, .. } => key, - MapOperation::Remove { key } => key, - }; - changed_slugs.insert(slug); - } - - yield (cid, changed_slugs); - } - } - } - - /// Get a stream that yields the set of slugs that changed at each revision - /// of the backing sphere, up to but excluding an optional CID. To stream - /// the entire history, pass `None` as the parameter. - pub fn content_change_stream<'b>( - &'b self, - since: Option<&'b Link>, - ) -> impl Stream, BTreeSet)>> + 'b { - try_stream! { - let sphere = self.has_sphere_context.to_sphere().await?; - let since = since.cloned(); - let stream = sphere.into_content_changelog_stream(since.as_ref()); - - for await change in stream { - let (cid, changelog) = change?; - let mut changed_slugs = BTreeSet::new(); - - for operation in changelog.changes { - let slug = match operation { - MapOperation::Add { key, .. } => key, - MapOperation::Remove { key } => key, - }; - changed_slugs.insert(slug); - } - - yield (cid, changed_slugs); - } - } - } - - /// Get a [BTreeSet] whose members are all the slugs that have values as of - /// this version of the sphere. Note that the full space of slugs may be - /// very large; for a more space-efficient approach, use - /// [SphereWalker::content_stream] or [SphereWalker::into_content_stream] to - /// incrementally access all slugs in the sphere. - /// - /// This method is forgiving of missing or corrupted data, and will yield an - /// incomplete set of links in the case that some or all links are not able - /// to be accessed. - pub async fn list_slugs(&self) -> Result> { - let sphere_identity = self.has_sphere_context.identity().await?; - let link_stream = self.content_stream(); - - tokio::pin!(link_stream); - - Ok(link_stream - .fold(BTreeSet::new(), |mut links, another_link| { - match another_link { - Ok((slug, _)) => { - links.insert(slug); - } - Err(error) => { - warn!("Could not read a link from {}: {}", sphere_identity, error) - } - }; - links - }) - .await) - } - - /// Get a [BTreeSet] whose members are all the slugs whose values have - /// changed at least once since the provided version of the sphere - /// (exclusive of the provided version; use `None` to get all slugs changed - /// since the beginning of the sphere's history). - /// - /// This method is forgiving of missing or corrupted history, and will yield - /// an incomplete set of changes in the case that some or all changes are - /// not able to be accessed. - /// - /// Note that this operation will scale in memory consumption and duration - /// proportionally to the size of the sphere and the length of its history. - /// For a more efficient method of accessing changes, consider using - /// [SphereWalker::content_change_stream] instead. - pub async fn content_changes( - &self, - since: Option<&Link>, - ) -> Result> { - let sphere_identity = self.has_sphere_context.identity().await?; - let change_stream = self.content_change_stream(since); - - tokio::pin!(change_stream); - - Ok(change_stream - .fold(BTreeSet::new(), |mut all, some| { - match some { - Ok((_, mut changes)) => all.append(&mut changes), - Err(error) => warn!( - "Could not read some changes from {}: {}", - sphere_identity, error - ), - }; - all - }) - .await) - } -} - -#[cfg(test)] -mod tests { - use anyhow::Result; - use std::collections::BTreeSet; - - use noosphere_core::data::{ContentType, Did}; - use tokio::io::AsyncReadExt; - use tokio_stream::StreamExt; - - #[cfg(target_arch = "wasm32")] - use wasm_bindgen_test::wasm_bindgen_test; - - #[cfg(target_arch = "wasm32")] - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - - use super::SphereWalker; - use crate::helpers::{simulated_sphere_context, SimulationAccess}; - use crate::{HasMutableSphereContext, SphereAuthorityWrite, SphereContentWrite, SphereCursor}; - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_be_initialized_with_a_context_or_a_cursor() { - let (sphere_context, _) = simulated_sphere_context(SimulationAccess::ReadWrite, None) - .await - .unwrap(); - let mut cursor = SphereCursor::latest(sphere_context.clone()); - - let changes = vec![ - vec!["dogs", "birds"], - vec!["cats", "dogs"], - vec!["birds"], - vec!["cows", "beetles"], - ]; - - for change in changes { - for slug in change { - cursor - .write(slug, &ContentType::Subtext, b"are cool".as_ref(), None) - .await - .unwrap(); - } - - cursor.save(None).await.unwrap(); - } - - let walker_cursor = SphereWalker::from(&cursor); - let walker_context = SphereWalker::from(&sphere_context); - - let slugs_cursor = walker_cursor.list_slugs().await.unwrap(); - let slugs_context = walker_context.list_slugs().await.unwrap(); - - assert_eq!(slugs_cursor.len(), 5); - assert_eq!(slugs_cursor, slugs_context); - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_list_all_slugs_currently_in_a_sphere() { - let (sphere_context, _) = simulated_sphere_context(SimulationAccess::ReadWrite, None) - .await - .unwrap(); - let mut cursor = SphereCursor::latest(sphere_context); - - let changes = vec![ - vec!["dogs", "birds"], - vec!["cats", "dogs"], - vec!["birds"], - vec!["cows", "beetles"], - ]; - - for change in changes { - for slug in change { - cursor - .write(slug, &ContentType::Subtext, b"are cool".as_ref(), None) - .await - .unwrap(); - } - - cursor.save(None).await.unwrap(); - } - - let walker_cursor = cursor.clone(); - let walker = SphereWalker::from(&walker_cursor); - let slugs = walker.list_slugs().await.unwrap(); - - assert_eq!(slugs.len(), 5); - - cursor.remove("dogs").await.unwrap(); - cursor.save(None).await.unwrap(); - - let slugs = walker.list_slugs().await.unwrap(); - - assert_eq!(slugs.len(), 4); - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_list_all_authorizations_currently_in_a_sphere() -> Result<()> { - let (sphere_context, _) = - simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; - - let mut cursor = SphereCursor::latest(sphere_context); - let authorizations_to_add = 10; - - for i in 0..authorizations_to_add { - cursor - .authorize(&format!("foo{}", i), &Did(format!("did:key:foo{}", i))) - .await?; - } - - cursor.save(None).await?; - - let authorizations = SphereWalker::from(&cursor).list_authorizations().await?; - - assert_eq!(authorizations.len(), authorizations_to_add + 1); - - Ok(()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_stream_the_whole_index() { - let (sphere_context, _) = simulated_sphere_context(SimulationAccess::ReadWrite, None) - .await - .unwrap(); - let mut cursor = SphereCursor::latest(sphere_context); - - let expected = BTreeSet::<(String, String)>::from([ - ("cats".into(), "Cats are awesome".into()), - ("dogs".into(), "Dogs are pretty cool".into()), - ("birds".into(), "Birds rights".into()), - ("mice".into(), "Mice like cookies".into()), - ]); - - for (slug, content) in &expected { - cursor - .write(slug.as_str(), &ContentType::Subtext, content.as_ref(), None) - .await - .unwrap(); - - cursor.save(None).await.unwrap(); - } - - let mut actual = BTreeSet::new(); - let walker = SphereWalker::from(&cursor); - let stream = walker.content_stream(); - - tokio::pin!(stream); - - while let Some(Ok((slug, mut file))) = stream.next().await { - let mut contents = String::new(); - file.contents.read_to_string(&mut contents).await.unwrap(); - actual.insert((slug, contents)); - } - - assert_eq!(expected, actual); - } -}