From c13bc905c7774b73fe3f735b82a5285c4f62e3fc Mon Sep 17 00:00:00 2001 From: Chris Joel <0xcda7a@gmail.com> Date: Tue, 20 Sep 2022 19:24:24 -0700 Subject: [PATCH 1/2] Partial CLI implementation --- .vscode/settings.json | 3 +- rust/Cargo.lock | 704 ++++++++++++------ rust/Cargo.toml | 3 +- rust/noosphere-api/Cargo.toml | 6 +- rust/noosphere-api/src/authority.rs | 18 +- rust/noosphere-api/src/client.rs | 55 +- rust/noosphere-api/src/data.rs | 6 +- rust/noosphere-api/src/gateway.rs | 4 +- rust/noosphere-api/src/lib.rs | 9 - rust/noosphere-cli/Cargo.toml | 31 + rust/noosphere-cli/src/bin/orb.rs | 14 + rust/noosphere-cli/src/lib.rs | 10 + .../noosphere-cli/src/native/commands/auth.rs | 33 + .../src/native/commands/config.rs | 0 rust/noosphere-cli/src/native/commands/key.rs | 169 +++++ rust/noosphere-cli/src/native/commands/mod.rs | 4 + .../src/native/commands/sphere.rs | 108 +++ rust/noosphere-cli/src/native/mod.rs | 314 ++++++++ rust/noosphere-cli/src/native/workspace.rs | 265 +++++++ rust/noosphere-cli/src/web.rs | 2 + rust/noosphere-collections/Cargo.toml | 3 +- rust/noosphere-collections/src/hamt/hamt.rs | 18 +- .../src/hamt/key_value_pair.rs | 2 +- rust/noosphere-collections/src/hamt/node.rs | 34 +- .../noosphere-collections/src/hamt/pointer.rs | 2 +- rust/noosphere-collections/src/hamt/test.rs | 34 +- rust/noosphere-fs/Cargo.toml | 12 +- rust/noosphere-fs/src/decoder.rs | 11 +- rust/noosphere-fs/src/fs.rs | 135 ++-- rust/noosphere-gateway/Cargo.toml | 15 +- .../src/gateway/authority/keeper.rs | 60 +- .../src/gateway/commands/serve.rs | 90 ++- rust/noosphere-gateway/src/gateway/crypto.rs | 2 +- .../src/gateway/environment/block.rs | 24 +- .../src/gateway/environment/state.rs | 6 +- .../src/gateway/extractors.rs | 2 +- .../src/gateway/handlers/fetch.rs | 6 +- .../src/gateway/handlers/push.rs | 31 +- rust/noosphere-gateway/src/main.rs | 15 +- rust/noosphere-into/Cargo.toml | 13 +- .../examples/notes-to-html/implementation.rs | 28 +- rust/noosphere-into/src/html/into.rs | 107 ++- .../src/html/transform/subtext.rs | 9 +- .../src/html/transform/transclude.rs | 9 +- rust/noosphere-storage/Cargo.toml | 4 + rust/noosphere-storage/src/db.rs | 132 ++++ rust/noosphere-storage/src/helpers.rs | 2 +- rust/noosphere-storage/src/interface.rs | 203 +++-- rust/noosphere-storage/src/lib.rs | 13 +- rust/noosphere-storage/src/memory.rs | 47 +- rust/noosphere-storage/src/native.rs | 5 - rust/noosphere-storage/src/tracking.rs | 8 +- rust/noosphere-storage/src/ucan.rs | 28 + rust/noosphere-storage/src/web.rs | 10 - rust/noosphere/Cargo.toml | 15 +- rust/noosphere/src/authority/capability.rs | 14 +- rust/noosphere/src/authority/verification.rs | 15 +- rust/noosphere/src/data/authorization.rs | 102 +++ rust/noosphere/src/data/body_chunk.rs | 13 +- rust/noosphere/src/data/bundle.rs | 241 +++--- rust/noosphere/src/data/links.rs | 4 - rust/noosphere/src/data/memo.rs | 97 ++- rust/noosphere/src/data/mod.rs | 2 + rust/noosphere/src/data/sphere.rs | 19 +- rust/noosphere/src/data/versioned_map.rs | 75 +- rust/noosphere/src/encoding.rs | 66 ++ rust/noosphere/src/view/authorization.rs | 65 ++ rust/noosphere/src/view/links.rs | 132 ---- rust/noosphere/src/view/mod.rs | 6 +- rust/noosphere/src/view/mutation.rs | 83 ++- rust/noosphere/src/view/sphere.rs | 555 +++++++++++--- rust/noosphere/src/view/timeline.rs | 27 +- rust/noosphere/src/view/versioned_map.rs | 153 ++++ 73 files changed, 3405 insertions(+), 1157 deletions(-) create mode 100644 rust/noosphere-cli/Cargo.toml create mode 100644 rust/noosphere-cli/src/bin/orb.rs create mode 100644 rust/noosphere-cli/src/lib.rs create mode 100644 rust/noosphere-cli/src/native/commands/auth.rs create mode 100644 rust/noosphere-cli/src/native/commands/config.rs create mode 100644 rust/noosphere-cli/src/native/commands/key.rs create mode 100644 rust/noosphere-cli/src/native/commands/mod.rs create mode 100644 rust/noosphere-cli/src/native/commands/sphere.rs create mode 100644 rust/noosphere-cli/src/native/mod.rs create mode 100644 rust/noosphere-cli/src/native/workspace.rs create mode 100644 rust/noosphere-cli/src/web.rs create mode 100644 rust/noosphere-storage/src/db.rs create mode 100644 rust/noosphere-storage/src/ucan.rs create mode 100644 rust/noosphere/src/data/authorization.rs create mode 100644 rust/noosphere/src/view/authorization.rs delete mode 100644 rust/noosphere/src/view/links.rs create mode 100644 rust/noosphere/src/view/versioned_map.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index 0a89a30c5..6f993a78a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "rust-analyzer.cargo.target": null + "rust-analyzer.cargo.target": null, + "lldb.launch.cwd": "${workspaceFolder}" } \ No newline at end of file diff --git a/rust/Cargo.lock b/rust/Cargo.lock index e3b3de7f3..647068752 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" dependencies = [ "memchr", ] @@ -22,9 +22,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.59" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91f1f46651137be86f3a2b9a8359f9ab421d04d941c62b5982e1ca21113adf9" +checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" [[package]] name = "arrayref" @@ -40,9 +40,9 @@ checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] name = "async-channel" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28" dependencies = [ "concurrent-queue", "event-listener", @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "async-global-executor" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5262ed948da60dd8956c6c5aca4d4163593dddb7b32d73267c93dab7b2e98940" +checksum = "0da5b41ee986eed3f524c380e6d64965aea573882a8907682ad100f7859305ca" dependencies = [ "async-channel", "async-executor", @@ -88,16 +88,16 @@ dependencies = [ "async-lock", "blocking", "futures-lite", - "num_cpus", "once_cell", ] [[package]] name = "async-io" -version = "1.7.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07" +checksum = "83e21f3a490c72b3b0cf44962180e60045de2925d8dff97918f7ee43c8f637c7" dependencies = [ + "autocfg", "concurrent-queue", "futures-lite", "libc", @@ -287,9 +287,9 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "277c75e6c814b061ae4947d02335d9659db9771b9950cca670002ae986372f44" +checksum = "69034b3b0fd97923eee2ce8a47540edb21e07f48f87f67d44bb4271cec622bdb" dependencies = [ "axum", "bytes", @@ -322,12 +322,6 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" -[[package]] -name = "base64" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" - [[package]] name = "base64" version = "0.13.0" @@ -336,9 +330,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "base64ct" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdca834647821e0b13d9539a8634eb62d3501b6b6c2cec1722786ee6671b851" +checksum = "ea2b2456fd614d856680dcd9fcc660a51a820fa09daef2e49772b56a193c8474" [[package]] name = "bitflags" @@ -392,9 +386,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" dependencies = [ "generic-array", ] @@ -421,9 +415,9 @@ checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" [[package]] name = "bumpalo" -version = "3.10.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" [[package]] name = "byteorder" @@ -466,9 +460,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cid" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc949bff6704880faf064c42a4854032ab07bfcf3a4fcb82a57470acededb69c" +checksum = "f6ed9c8b2d17acb8110c46f1da5bf4a696d745e1474a16db0cd2b49cd0249bf2" dependencies = [ "core2", "multibase", @@ -480,14 +474,14 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.16" +version = "3.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3dbbb6653e7c55cc8595ad3e1f7be8f32aba4eb7ff7f0fd1163d4f3d137c0a9" +checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" dependencies = [ "atty", "bitflags", - "clap_derive", - "clap_lex", + "clap_derive 3.2.18", + "clap_lex 0.2.4", "indexmap", "once_cell", "strsim", @@ -495,11 +489,39 @@ dependencies = [ "textwrap", ] +[[package]] +name = "clap" +version = "4.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5840cd9093aabeabf7fd932754c435b7674520fc3ddc935c397837050f0f1e4b" +dependencies = [ + "atty", + "bitflags", + "clap_derive 4.0.8", + "clap_lex 0.3.0", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap_derive" +version = "3.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clap_derive" -version = "3.2.15" +version = "4.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba52acd3b0a5c33aeada5cdaa3267cdc7c594a98731d4268cdc1532f4264cb4" +checksum = "92289ffc6fb4a85d85c246ddb874c05a87a2e540fb6ad52f7ca07c8c1e1840b1" dependencies = [ "heck", "proc-macro-error", @@ -517,11 +539,20 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "clap_lex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "combine" -version = "4.6.4" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" dependencies = [ "bytes", "memchr", @@ -585,9 +616,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" dependencies = [ "libc", ] @@ -603,9 +634,9 @@ dependencies = [ [[package]] name = "crdts" -version = "7.1.0" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6be45d6797715089e99bdeda0c45c1bc47326ebf009a08605bf0862e249e2b17" +checksum = "f4c7e621779799b7dfb00cd9bb4542c731de872a7af49c0e550d2a47f46d15c1" dependencies = [ "num", "quickcheck", @@ -615,26 +646,24 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" +checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset", - "once_cell", "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" +checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -665,9 +694,9 @@ dependencies = [ [[package]] name = "cs_serde_bytes" -version = "0.12.0" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebca5e1e931005a5d90cc90831a22619c94fdeb645435c22eae52956dee29675" +checksum = "5fc673ddabf48214550526b068dc28065a75f05e21e452880095247c635b1d91" dependencies = [ "serde", ] @@ -743,15 +772,21 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" dependencies = [ - "block-buffer 0.10.2", + "block-buffer 0.10.3", "crypto-common", "subtle", ] +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + [[package]] name = "ed25519-zebra" version = "3.0.0" @@ -760,7 +795,7 @@ checksum = "403ef3e961ab98f0ba902771d29f842058578bb1ce7e3c59dad5a6a93e784c69" dependencies = [ "curve25519-dalek", "hex", - "rand_core 0.6.3", + "rand_core 0.6.4", "serde", "sha2 0.9.9", "thiserror", @@ -769,9 +804,9 @@ dependencies = [ [[package]] name = "either" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "encoding_rs" @@ -849,11 +884,10 @@ dependencies = [ [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", "percent-encoding", ] @@ -879,9 +913,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" dependencies = [ "futures-channel", "futures-core", @@ -894,9 +928,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" dependencies = [ "futures-core", "futures-sink", @@ -904,15 +938,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" [[package]] name = "futures-executor" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" dependencies = [ "futures-core", "futures-task", @@ -921,9 +955,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" [[package]] name = "futures-lite" @@ -942,9 +976,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" dependencies = [ "proc-macro2", "quote", @@ -953,21 +987,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" +checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" dependencies = [ "futures-channel", "futures-core", @@ -1037,9 +1071,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", @@ -1092,9 +1126,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" dependencies = [ "bytes", "fnv", @@ -1117,18 +1151,18 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "headers" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" dependencies = [ - "base64 0.13.0", + "base64", "bitflags", "bytes", "headers-core", "http", "httpdate", "mime", - "sha-1", + "sha1 0.10.5", ] [[package]] @@ -1167,7 +1201,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.3", + "digest 0.10.5", +] + +[[package]] +name = "home" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654" +dependencies = [ + "winapi", ] [[package]] @@ -1206,9 +1249,9 @@ checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" [[package]] name = "httparse" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" @@ -1264,11 +1307,10 @@ dependencies = [ [[package]] name = "idna" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] @@ -1291,6 +1333,7 @@ checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", "js-sys", + "stdweb", "wasm-bindgen", "web-sys", ] @@ -1303,9 +1346,9 @@ checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" [[package]] name = "itertools" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] @@ -1318,9 +1361,9 @@ checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "js-sys" -version = "0.3.59" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] @@ -1331,7 +1374,7 @@ version = "8.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aa4b4af834c6cfd35d8763d359661b90f2e45d8f750a0849156c7f4671af09c" dependencies = [ - "base64 0.13.0", + "base64", "pem", "ring", "serde", @@ -1365,9 +1408,20 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.126" +version = "0.2.134" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" + +[[package]] +name = "libipld-cbor" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "8dd1ab68c9d26f20c7d0dfea6eecbae8c00359875210001b33ca27d4a02f3d09" +dependencies = [ + "byteorder", + "libipld-core", + "thiserror", +] [[package]] name = "libipld-core" @@ -1384,17 +1438,29 @@ dependencies = [ "thiserror", ] +[[package]] +name = "libipld-json" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18aa481a87f084d98473dd9ece253a9569c762b75f6bbba8217d54e48c9d63b3" +dependencies = [ + "libipld-core", + "multihash", + "serde", + "serde_json", +] + [[package]] name = "libm" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da83a57f3f5ba3680950aa3cbc806fc297bc0b289d42e8942ed528ace71b8145" +checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ "autocfg", "scopeguard", @@ -1425,12 +1491,6 @@ dependencies = [ "regex-automata", ] -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - [[package]] name = "matchit" version = "0.5.0" @@ -1493,19 +1553,19 @@ dependencies = [ [[package]] name = "multihash" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3db354f401db558759dfc1e568d010a5d4146f4d3f637be1275ec4a3cf09689" +checksum = "1c346cf9999c631f002d8f977c4eaeaa0e6386f16007202308d0b3757522c2cc" dependencies = [ "blake2b_simd", "blake2s_simd", "blake3", "core2", - "digest 0.10.3", + "digest 0.10.5", "multihash-derive", "serde", "serde-big-array", - "sha2 0.10.2", + "sha2 0.10.6", "sha3", "unsigned-varint", ] @@ -1557,7 +1617,7 @@ dependencies = [ "async-recursion", "async-std", "async-trait", - "base64 0.13.0", + "base64", "byteorder", "cid", "crdts", @@ -1566,6 +1626,8 @@ dependencies = [ "futures", "fvm_ipld_amt", "getrandom 0.2.7", + "libipld-cbor", + "libipld-core", "log", "noosphere-cbor", "noosphere-collections", @@ -1573,6 +1635,7 @@ dependencies = [ "once_cell", "rand 0.8.5", "serde", + "serde_bytes", "serde_ipld_dagcbor", "tiny-bip39", "tokio", @@ -1611,6 +1674,29 @@ dependencies = [ "serde_ipld_dagcbor", ] +[[package]] +name = "noosphere-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "cid", + "clap 4.0.8", + "home", + "noosphere", + "noosphere-api", + "noosphere-fs", + "noosphere-storage", + "path-absolutize", + "serde_json", + "tokio", + "ucan", + "ucan-key-support", + "url", + "wasm-bindgen", + "whoami", + "witty-phrase-generator", +] + [[package]] name = "noosphere-collections" version = "0.1.0" @@ -1623,6 +1709,7 @@ dependencies = [ "byteorder", "cid", "forest_hash_utils", + "libipld-cbor", "libipld-core", "multihash", "noosphere-cbor", @@ -1630,7 +1717,7 @@ dependencies = [ "serde", "serde_bytes", "serde_ipld_dagcbor", - "sha2 0.10.2", + "sha2 0.10.6", "tokio", "tokio-stream", "unsigned-varint", @@ -1646,6 +1733,8 @@ dependencies = [ "bytes", "cid", "futures-util", + "libipld-cbor", + "libipld-core", "noosphere", "noosphere-storage", "once_cell", @@ -1667,10 +1756,12 @@ dependencies = [ "async-trait", "axum", "cid", - "clap", + "clap 3.2.22", "futures", "hyper", "jsonwebtoken", + "libipld-cbor", + "libipld-core", "mime", "noosphere", "noosphere-api", @@ -1680,8 +1771,9 @@ dependencies = [ "pretty_env_logger", "rand 0.8.5", "serde", + "serde_bytes", "serde_json", - "sha2 0.10.2", + "sha2 0.10.6", "sled", "temp-dir", "tokio", @@ -1736,11 +1828,15 @@ dependencies = [ "async-trait", "cid", "js-sys", + "libipld-cbor", + "libipld-core", "noosphere-cbor", "rexie", + "serde", "sled", "tokio", "tracing", + "ucan", "wasm-bindgen", "wasm-bindgen-test", "web-sys", @@ -1885,9 +1981,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "opaque-debug" @@ -1897,9 +1993,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.41" +version = "0.10.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" +checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13" dependencies = [ "bitflags", "cfg-if", @@ -1929,9 +2025,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.75" +version = "0.9.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" +checksum = "5230151e44c0f05157effb743e8d517472843121cf9243e8b81393edb5acd9ce" dependencies = [ "autocfg", "cc", @@ -1942,9 +2038,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.2.0" +version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" [[package]] name = "parking" @@ -2000,13 +2096,31 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "path-absolutize" +version = "3.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3de4b40bd9736640f14c438304c09538159802388febb02c8abaae0846c1f13" +dependencies = [ + "path-dedot", +] + +[[package]] +name = "path-dedot" +version = "3.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d611d5291372b3738a34ebf0d1f849e58b1dcc1101032f76a346eaa1f8ddbb5b" +dependencies = [ + "once_cell", +] + [[package]] name = "pbkdf2" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest 0.10.3", + "digest 0.10.5", ] [[package]] @@ -2015,7 +2129,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4" dependencies = [ - "base64 0.13.0", + "base64", ] [[package]] @@ -2029,24 +2143,24 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pin-project" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78203e83c48cffbe01e4a2d35d566ca4de445d79a85372fc64e378bfc812a260" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", @@ -2095,10 +2209,11 @@ checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] name = "polling" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +checksum = "899b00b9c8ab553c743b3e11e87c5c7d423b2a2de229ba95b24a756344748011" dependencies = [ + "autocfg", "cfg-if", "libc", "log", @@ -2124,10 +2239,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.1.3" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" dependencies = [ + "once_cell", "thiserror", "toml", ] @@ -2158,9 +2274,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.42" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] @@ -2185,9 +2301,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] @@ -2213,7 +2329,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -2233,7 +2349,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -2247,9 +2363,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.7", ] @@ -2309,11 +2425,11 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.11" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" +checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" dependencies = [ - "base64 0.13.0", + "base64", "bytes", "encoding_rs", "futures-core", @@ -2325,10 +2441,10 @@ dependencies = [ "hyper-tls", "ipnet", "js-sys", - "lazy_static", "log", "mime", "native-tls", + "once_cell", "percent-encoding", "pin-project-lite", "serde", @@ -2380,14 +2496,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cf22754c49613d2b3b119f0e5d46e34a2c628a937e3024b8762de4e7d8c710b" dependencies = [ "byteorder", - "digest 0.10.3", + "digest 0.10.5", "num-bigint-dig", "num-integer", "num-iter", "num-traits", "pkcs1", "pkcs8", - "rand_core 0.6.3", + "rand_core 0.6.4", "smallvec", "subtle", "zeroize", @@ -2399,6 +2515,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" + [[package]] name = "ryu" version = "1.0.11" @@ -2429,9 +2560,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "security-framework" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" dependencies = [ "bitflags", "core-foundation", @@ -2450,11 +2581,26 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "serde" -version = "1.0.140" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc855a42c7967b7c369eb5860f7164ef1f6f81c20c7cc1141f2a604e18723b03" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" dependencies = [ "serde_derive", ] @@ -2470,18 +2616,18 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212e73464ebcde48d723aa02eb270ba62eff38a9b732df31f33f1b4e145f3a54" +checksum = "cfc50e8183eeeb6178dcb167ae34a8051d63535023ae38b5d8d12beae193d37b" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.140" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f2122636b9fe3b81f1cb25099fcf2d3f542cdb1d45940d56c713158884a05da" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" dependencies = [ "proc-macro2", "quote", @@ -2502,9 +2648,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.83" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ "itoa", "ryu", @@ -2556,16 +2702,31 @@ dependencies = [ ] [[package]] -name = "sha-1" -version = "0.10.0" +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.3", + "digest 0.10.5", ] +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "sha2" version = "0.9.9" @@ -2581,22 +2742,22 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.2" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.3", + "digest 0.10.5", ] [[package]] name = "sha3" -version = "0.10.2" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a31480366ec990f395a61b7c08122d99bd40544fdb5abcfc1b06bb29994312c" +checksum = "e2904bea16a1ae962b483322a1c7b81d976029203aea1f461e51cd7705db7ba9" dependencies = [ - "digest 0.10.3", + "digest 0.10.5", "keccak", ] @@ -2657,15 +2818,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" dependencies = [ "libc", "winapi", @@ -2687,12 +2848,82 @@ dependencies = [ "der", ] +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version", + "serde", + "serde_json", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1 0.6.1", + "syn", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtext" version = "0.3.0" @@ -2717,9 +2948,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.98" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" dependencies = [ "proc-macro2", "quote", @@ -2784,36 +3015,26 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "textnonce" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f8d70cd784ed1dc33106a18998d77758d281dc40dc3e6d050cf0f5286683" -dependencies = [ - "base64 0.12.3", - "rand 0.7.3", -] - [[package]] name = "textwrap" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" [[package]] name = "thiserror" -version = "1.0.31" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", @@ -2831,12 +3052,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.12" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74b7cc93fc23ba97fde84f7eea56c55d1ba183f495c6715defdfc7b9cb8c870f" +checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" dependencies = [ "itoa", - "js-sys", "libc", "num_threads", "time-macros", @@ -2860,7 +3080,7 @@ dependencies = [ "pbkdf2", "rand 0.8.5", "rustc-hash", - "sha2 0.10.2", + "sha2 0.10.6", "thiserror", "unicode-normalization", "wasm-bindgen", @@ -2893,9 +3113,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.20.1" +version = "1.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" dependencies = [ "autocfg", "bytes", @@ -2903,7 +3123,6 @@ dependencies = [ "memchr", "mio", "num_cpus", - "once_cell", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", @@ -2935,9 +3154,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" +checksum = "f6edf2d6bc038a43d31353570e27270603f4648d18f5ed10c0e179abe43255af" dependencies = [ "futures-core", "pin-project-lite", @@ -2946,9 +3165,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" dependencies = [ "bytes", "futures-core", @@ -3110,26 +3329,33 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ucan" version = "0.6.0-alpha.1" -source = "git+https://github.com/cdata/rs-ucan.git#5d80833c545a57f3cdd23781cf890854d69a0439" +source = "git+https://github.com/cdata/rs-ucan.git?branch=feature/canonicalization#66fc3ae195997043719ad9663ff5300f719eb850" dependencies = [ "anyhow", "async-recursion", "async-std", "async-trait", - "base64 0.13.0", + "base64", "bs58", + "cid", + "getrandom 0.2.7", "instant", + "libipld-core", + "libipld-json", "log", + "rand 0.8.5", "serde", "serde_json", - "textnonce", + "strum", + "strum_macros", + "unsigned-varint", "url", ] [[package]] name = "ucan-key-support" version = "0.4.0-alpha.1" -source = "git+https://github.com/cdata/rs-ucan.git#5d80833c545a57f3cdd23781cf890854d69a0439" +source = "git+https://github.com/cdata/rs-ucan.git?branch=feature/canonicalization#66fc3ae195997043719ad9663ff5300f719eb850" dependencies = [ "anyhow", "async-trait", @@ -3139,7 +3365,7 @@ dependencies = [ "log", "npm_rs", "rsa", - "sha2 0.10.2", + "sha2 0.10.6", "ucan", "wasm-bindgen", "wasm-bindgen-futures", @@ -3163,30 +3389,30 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" [[package]] name = "unicode-normalization" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "unicode-xid" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "unsigned-varint" @@ -3202,13 +3428,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.2.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", ] @@ -3276,9 +3501,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3286,9 +3511,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", "log", @@ -3301,9 +3526,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" dependencies = [ "cfg-if", "js-sys", @@ -3313,9 +3538,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3323,9 +3548,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", @@ -3336,15 +3561,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] name = "wasm-bindgen-test" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513df541345bb9fcc07417775f3d51bbb677daf307d8035c0afafd87dc2e6599" +checksum = "09d2fff962180c3fadf677438054b1db62bee4aa32af26a45388af07d1287e1d" dependencies = [ "console_error_panic_hook", "js-sys", @@ -3356,9 +3581,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6150d36a03e90a3cf6c12650be10626a9902d70c5270fd47d7a47e5389a10d56" +checksum = "4683da3dfc016f704c9f82cf401520c4f1cb3ee440f7f52b3d6ac29506a49ca7" dependencies = [ "proc-macro2", "quote", @@ -3366,9 +3591,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.59" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" dependencies = [ "js-sys", "wasm-bindgen", @@ -3383,6 +3608,17 @@ dependencies = [ "cc", ] +[[package]] +name = "whoami" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6631b6a2fd59b1841b622e8f1a7ad241ef0a46f2d580464ce8140ac94cbd571" +dependencies = [ + "bumpalo", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 8eaa9d82f..f77b6e4df 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -7,7 +7,8 @@ members = [ "noosphere-api", "noosphere-fs", "noosphere-gateway", - "noosphere-into" + "noosphere-into", + "noosphere-cli" ] # See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352 resolver = "2" diff --git a/rust/noosphere-api/Cargo.toml b/rust/noosphere-api/Cargo.toml index 85bf22a29..a1c0f537b 100644 --- a/rust/noosphere-api/Cargo.toml +++ b/rust/noosphere-api/Cargo.toml @@ -14,16 +14,14 @@ noosphere = { path = "../noosphere" } noosphere-cbor = { path = "../noosphere-cbor" } noosphere-storage = { path = "../noosphere-storage" } reqwest = { version = "~0.11", features = ["json"] } +ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } +ucan-key-support = { version = "0.4.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "^1", features = ["full"] } -ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git" } -ucan-key-support = { version = "0.4.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git" } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = "~0.2" -ucan = { version = "0.6.0-alpha.1", features = ["web"], git = "https://github.com/cdata/rs-ucan.git" } -ucan-key-support = { version = "0.4.0-alpha.1", features = ["web"], git = "https://github.com/cdata/rs-ucan.git" } [dev-dependencies] wasm-bindgen-test = "~0.3" \ No newline at end of file diff --git a/rust/noosphere-api/src/authority.rs b/rust/noosphere-api/src/authority.rs index 2ad6a3634..aeb198f14 100644 --- a/rust/noosphere-api/src/authority.rs +++ b/rust/noosphere-api/src/authority.rs @@ -20,10 +20,10 @@ impl Display for GatewayAction { f, "{}", match self { - GatewayAction::Migrate => "gateway/MIGRATE", - GatewayAction::Publish => "gateway/PUBLISH", - GatewayAction::Push => "gateway/PUSH", - GatewayAction::Fetch => "gateway/FETCH", + GatewayAction::Migrate => "gateway/migrate", + GatewayAction::Publish => "gateway/publish", + GatewayAction::Push => "gateway/push", + GatewayAction::Fetch => "gateway/fetch", } ) } @@ -34,16 +34,16 @@ impl TryFrom for GatewayAction { fn try_from(value: String) -> Result { Ok(match value.as_str() { - "gateway/MIGRATE" => GatewayAction::Migrate, - "gateway/PUBLISH" => GatewayAction::Publish, - "gateway/PUSH" => GatewayAction::Push, - "gateway/FETCH" => GatewayAction::Fetch, + "gateway/migrate" => GatewayAction::Migrate, + "gateway/publish" => GatewayAction::Publish, + "gateway/push" => GatewayAction::Push, + "gateway/fetch" => GatewayAction::Fetch, _ => return Err(anyhow!("Unrecognized action: {:?}", value)), }) } } -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Eq)] pub struct GatewayIdentity { pub did: String, } diff --git a/rust/noosphere-api/src/client.rs b/rust/noosphere-api/src/client.rs index c21678923..e4486e750 100644 --- a/rust/noosphere-api/src/client.rs +++ b/rust/noosphere-api/src/client.rs @@ -1,4 +1,4 @@ -use std::{ops::Deref, sync::Arc}; +use std::{str::FromStr}; use crate::{ authority::{GatewayAction, GatewayIdentity}, @@ -7,36 +7,41 @@ use crate::{ }; use anyhow::Result; +use cid::Cid; use noosphere_cbor::TryDagCbor; -use noosphere_storage::interface::{StorageProvider, Store}; -use reqwest::Body; +use reqwest::{header::HeaderMap, Body}; use ucan::{ builder::UcanBuilder, capability::{Capability, Resource, With}, crypto::KeyMaterial, + store::{UcanJwtStore, UcanStore}, ucan::Ucan, }; use url::Url; -pub struct Client<'a, K> +pub struct Client<'a, K, S> where K: KeyMaterial, + S: UcanStore, { pub gateway: GatewayReference, pub credential: &'a K, pub authorization: Vec, + pub store: S, client: reqwest::Client, } -impl<'a, K> Client<'a, K> +impl<'a, K, S> Client<'a, K, S> where K: KeyMaterial, + S: UcanStore, { pub async fn identify( gateway: &GatewayReference, credential: &'a K, authorization: Option>, - ) -> Result> { + store: S, + ) -> Result> { let mut url = Url::try_from(gateway)?; url.set_path(&Route::Identify.to_string()); @@ -50,7 +55,8 @@ where Ok(Client { gateway, credential, - authorization: authorization.unwrap_or_else(|| Vec::new()), + authorization: authorization.unwrap_or_default(), + store, client: reqwest::Client::new(), }) } @@ -58,7 +64,7 @@ where async fn make_bearer_token( &self, capability: &Capability, - ) -> Result { + ) -> Result<(String, HeaderMap)> { let mut builder = UcanBuilder::default() .issued_by(self.credential) .for_audience(&self.gateway.require_identity()?.did) @@ -70,7 +76,30 @@ where builder = builder.witnessed_by(proof); } - Ok(builder.build()?.sign().await?.encode()?) + let final_ucan = builder.build()?.sign().await?; + + // TODO: We should integrate a helper for this kind of stuff into rs-ucan + let mut proofs_to_search: Vec = final_ucan.proofs().clone(); + let mut ucan_headers = HeaderMap::new(); + + println!("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 = self.store.require_token(&cid).await?; + let ucan = Ucan::try_from_token_string(&jwt)?; + + println!("Adding UCAN header for {}", cid); + + proofs_to_search.extend(ucan.proofs().clone().into_iter()); + ucan_headers.append("ucan", format!("{} {}", cid, jwt).parse()?); + } + + // 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((final_ucan.encode()?, ucan_headers)) } async fn fetch(&self, params: &FetchParameters) -> Result { @@ -82,18 +111,19 @@ where can: GatewayAction::Fetch, }; - let token = self.make_bearer_token(&capability).await?; + let (token, ucan_headers) = self.make_bearer_token(&capability).await?; let bytes = self .client .get(url) .bearer_auth(token) + .headers(ucan_headers) .send() .await? .bytes() .await?; - Ok(FetchResponse::try_from_dag_cbor(&bytes)?) + FetchResponse::try_from_dag_cbor(&bytes) } pub async fn push(&self, push_body: &PushBody) -> Result { @@ -106,12 +136,13 @@ where can: GatewayAction::Push, }; - let token = self.make_bearer_token(&capability).await?; + let (token, ucan_headers) = self.make_bearer_token(&capability).await?; Ok(self .client .put(url) .bearer_auth(token) + .headers(ucan_headers) .header("Content-Type", "application/octet-stream") .body(Body::from(push_body.try_into_dag_cbor()?)) .send() diff --git a/rust/noosphere-api/src/data.rs b/rust/noosphere-api/src/data.rs index d1f960b9a..fa6c3694e 100644 --- a/rust/noosphere-api/src/data.rs +++ b/rust/noosphere-api/src/data.rs @@ -31,14 +31,14 @@ pub struct FetchResponse { pub blocks: Bundle, } -#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct OutOfDateResponse { pub sphere: String, pub presumed_base: Option, pub actual_tip: Cid, } -#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct MissingRevisionsResponse { pub sphere: String, pub presumed_base: Cid, @@ -54,7 +54,7 @@ pub struct PushBody { pub blocks: Bundle, } -#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum PushResponse { Ok, OutOfDate(OutOfDateResponse), diff --git a/rust/noosphere-api/src/gateway.rs b/rust/noosphere-api/src/gateway.rs index d999fad33..66e6b2aae 100644 --- a/rust/noosphere-api/src/gateway.rs +++ b/rust/noosphere-api/src/gateway.rs @@ -58,10 +58,10 @@ impl GatewayReference { } pub fn require_identity(&self) -> Result<&GatewayIdentity> { - Ok(self + self .identity .as_ref() - .ok_or_else(|| anyhow!("No DID configured for gateway identity"))?) + .ok_or_else(|| anyhow!("No DID configured for gateway identity")) } pub fn ensure_identity(&mut self, claimed_identity: &GatewayIdentity) -> Result<()> { diff --git a/rust/noosphere-api/src/lib.rs b/rust/noosphere-api/src/lib.rs index 8b35ac39b..b98694a2a 100644 --- a/rust/noosphere-api/src/lib.rs +++ b/rust/noosphere-api/src/lib.rs @@ -2,12 +2,3 @@ pub mod authority; pub mod client; pub mod data; pub mod gateway; - -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); - } -} diff --git a/rust/noosphere-cli/Cargo.toml b/rust/noosphere-cli/Cargo.toml new file mode 100644 index 000000000..0226016df --- /dev/null +++ b/rust/noosphere-cli/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "noosphere-cli" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "^1", features = ["full"] } +home = "~0.5" +clap = { version = "^4", features = ["derive", "cargo"] } +path-absolutize = "^3" +noosphere = { path = "../noosphere" } +noosphere-fs = { path = "../noosphere-fs" } +noosphere-storage = { path = "../noosphere-storage" } +noosphere-api = { path = "../noosphere-api" } +ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } +ucan-key-support = { version = "0.4.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } +witty-phrase-generator = "~0.2" +anyhow = "^1" +url = "^2" +whoami = "^1" +cid = "~0.8" +serde_json = "^1" + + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "~0.2" diff --git a/rust/noosphere-cli/src/bin/orb.rs b/rust/noosphere-cli/src/bin/orb.rs new file mode 100644 index 000000000..d271c161d --- /dev/null +++ b/rust/noosphere-cli/src/bin/orb.rs @@ -0,0 +1,14 @@ +// pub use noosphere_cli::native::main; + +#[cfg(not(target_arch = "wasm32"))] +#[tokio::main] +pub async fn main() -> anyhow::Result<()> { + // Call out to an external module for platform-specific compilation purposes + noosphere_cli::native::main().await?; + Ok(()) +} + +#[cfg(target_arch = "wasm32")] +pub fn main() { + noosphere_cli::web::main(); +} diff --git a/rust/noosphere-cli/src/lib.rs b/rust/noosphere-cli/src/lib.rs new file mode 100644 index 000000000..5ce894b44 --- /dev/null +++ b/rust/noosphere-cli/src/lib.rs @@ -0,0 +1,10 @@ +// pub mod commands; +// pub mod native; + +// pub mod env; + +#[cfg(not(target_arch = "wasm32"))] +pub mod native; + +#[cfg(target_arch = "wasm32")] +pub mod web; diff --git a/rust/noosphere-cli/src/native/commands/auth.rs b/rust/noosphere-cli/src/native/commands/auth.rs new file mode 100644 index 000000000..8e18706d9 --- /dev/null +++ b/rust/noosphere-cli/src/native/commands/auth.rs @@ -0,0 +1,33 @@ +use anyhow::Result; + + +use crate::native::workspace::Workspace; + +pub async fn auth_add(_did: &str, name: Option, workspace: &Workspace) -> Result<()> { + workspace.expect_local_directories()?; + + let _name = match name { + Some(name) => name, + None => { + let random_name = witty_phrase_generator::WPGen::new() + .with_words(3) + .unwrap_or_else(|| vec!["Unnamed"]) + .into_iter() + .map(String::from) + .collect::>() + .join("-"); + println!( + "Note: since no name was specified, the authorization will be saved with the pet name \"{}\"", + random_name + ); + random_name + } + }; + + // let key_material = workspace.get_local_key().await?; + // let sphere = + + // let ucan = UcanBuilder::default().issued_by(&key_material).for_audience(did).claiming_capability(capability) + + Ok(()) +} diff --git a/rust/noosphere-cli/src/native/commands/config.rs b/rust/noosphere-cli/src/native/commands/config.rs new file mode 100644 index 000000000..e69de29bb diff --git a/rust/noosphere-cli/src/native/commands/key.rs b/rust/noosphere-cli/src/native/commands/key.rs new file mode 100644 index 000000000..ec8e47095 --- /dev/null +++ b/rust/noosphere-cli/src/native/commands/key.rs @@ -0,0 +1,169 @@ +use anyhow::{anyhow, Result}; +use serde_json::json; +use tokio::fs; +use ucan::crypto::KeyMaterial; + +use noosphere::authority::{ed25519_key_to_mnemonic, generate_ed25519_key}; + +use crate::native::workspace::Workspace; + +pub static SERVICE_NAME: &str = "noosphere"; + +pub async fn create_key(name: String, working_paths: &Workspace) -> Result<()> { + working_paths.initialize_global_directories().await?; + + let key_base_path = working_paths.keys_path().join(&name); + let private_key_path = key_base_path.with_extension("private"); + + if private_key_path.exists() { + return Err(anyhow!("A key called {:?} already exists!", name)); + } + + let did_path = key_base_path.with_extension("public"); + + let key_pair = generate_ed25519_key(); + let did = key_pair.get_did().await?; + + let mnemonic = ed25519_key_to_mnemonic(&key_pair)?; + + tokio::try_join!( + fs::write(private_key_path, mnemonic), + fs::write(did_path, &did), + )?; + + println!("Created key {:?} in {:?}", name, working_paths.keys_path()); + println!("Public identity {}", did); + + Ok(()) +} + +pub async fn list_keys(as_json: bool, working_paths: &Workspace) -> Result<()> { + if let Err(error) = working_paths.expect_global_directories() { + return Err(anyhow!( + "{:?}\nTip: you may need to create a key first", + error + )); + } + + let keys = working_paths.get_all_keys().await?; + let max_name_length = keys + .iter() + .fold(7, |length, (key_name, _)| key_name.len().max(length)); + + if as_json { + println!("{}", serde_json::to_string_pretty(&json!(keys))?); + } else { + println!("{:1$} IDENTITY", "NAME", max_name_length); + for (name, did) in keys { + println!("{:1$} {did}", name, max_name_length); + } + } + + Ok(()) +} + +// use webauthn_rs::prelude::*; +// use webauthn_rs::WebauthnBuilder; +// use webauthn_rs_proto::COSEAlgorithm; +// use webauthn_rs_proto::PubKeyCredParams; + +// use webauthn_authenticator_rs::u2fhid::U2FHid; +// use webauthn_authenticator_rs::WebauthnAuthenticator; + +// let webauthn = WebauthnBuilder::new("noosphere", &Url::parse("https://cli.noosphere")?)? +// // .allow_subdomains(true) +// // .rp_name("Noosphere") +// .build()?; +// let id = Uuid::new_v4(); +// println!("Using ID: {:?}", id); + +// let (mut creation_challenge_response, registration) = +// webauthn.start_passkey_registration(id.clone(), &name, &name, None)?; + +// creation_challenge_response.public_key.pub_key_cred_params = vec![PubKeyCredParams { +// type_: "public-key".into(), +// alg: COSEAlgorithm::EDDSA as i64, +// }]; + +// // println!("{:#?}", creation_challenge_response); +// // println!("{:#?}", registration); + +// let mut authenticator = WebauthnAuthenticator::new(U2FHid::default()); + +// let result = match authenticator.do_registration( +// Url::parse("https://cli.noosphere")?, +// creation_challenge_response, +// ) { +// Ok(result) => Ok(result), +// Err(error) => Err(anyhow!("{:?}", error)), +// }?; + +// let passkey = webauthn.finish_passkey_registration(&result, ®istration)?; + +// println!("{:?}", passkey); +// // println!("RESULT: {:#?}", result); + +// Ok(()) + +// use ctap_hid_fido2::FidoKeyHid; +// use ctap_hid_fido2::fidokey; +// use ctap_hid_fido2::public_key_credential_user_entity::PublicKeyCredentialUserEntity; +// use ctap_hid_fido2::verifier; +// use ctap_hid_fido2::Cfg; +// use ctap_hid_fido2::FidoKeyHidFactory; + +// println!("Enumerate HID devices."); +// let devs = ctap_hid_fido2::get_fidokey_devices(); +// for info in devs { +// println!( +// "- vid=0x{:04x} , pid=0x{:04x} , info={:?}", +// info.vid, info.pid, info.info +// ); +// } + +// // let key = fidokey::CredentialSupportedKeyType:: +// // let user_entity = PublicKeyCredentialUserEntity::new(None, Some(name.as_str()), None); +// let challenge = verifier::create_challenge(); +// // challenge.fill(8u8); +// println!("Challenge: {:?}", challenge); + +// let fido_args = fidokey::MakeCredentialArgsBuilder::new("noosphere.com", &challenge) +// // .key_type(fidokey::CredentialSupportedKeyType::default()) +// .key_type(fidokey::CredentialSupportedKeyType::Ed25519) +// .user_entity(&PublicKeyCredentialUserEntity::new( +// Some(name.as_bytes()), +// Some(&name), +// Some(&name), +// )) +// .resident_key() +// .build(); + +// println!("FIDO args: {:#?}", fido_args); + +// // println!("Config... {:?}", Cfg::init()); +// let mut cfg = Cfg::init(); +// cfg.enable_log = true; + +// let device = FidoKeyHidFactory::create(&cfg)?; + +// println!("Device..."); +// let attestation = device.make_credential_with_args(&fido_args)?; + +// println!("Attestation: {:#?}", attestation); + +// let key_material = generate_ed25519_key(); +// let did = key_material.get_did().await?; + +// let private_key = key_material +// .1 +// .ok_or_else(|| anyhow!("Unable to access generated private key"))?; + +// let entry = keyring::Entry::new_with_target(&name, SERVICE_NAME, &username()); + +// entry.set_password(&base64_encode(private_key.as_ref())?)?; + +// println!( +// "New key '{}' generated and stored in a platform keyring", +// name +// ); +// println!("{}", did); diff --git a/rust/noosphere-cli/src/native/commands/mod.rs b/rust/noosphere-cli/src/native/commands/mod.rs new file mode 100644 index 000000000..d49c24c9b --- /dev/null +++ b/rust/noosphere-cli/src/native/commands/mod.rs @@ -0,0 +1,4 @@ +pub mod auth; +pub mod config; +pub mod key; +pub mod sphere; diff --git a/rust/noosphere-cli/src/native/commands/sphere.rs b/rust/noosphere-cli/src/native/commands/sphere.rs new file mode 100644 index 000000000..01269721e --- /dev/null +++ b/rust/noosphere-cli/src/native/commands/sphere.rs @@ -0,0 +1,108 @@ +use crate::native::workspace::Workspace; +use anyhow::{anyhow, Result}; +use noosphere::view::Sphere; +use noosphere_storage::{ + interface::StorageProvider, + native::{NativeStorageInit, NativeStorageProvider}, + BLOCK_STORE, +}; +use tokio::fs; + +pub async fn initialize_sphere(owner_key: &str, working_paths: &Workspace) -> Result<()> { + if working_paths.expect_local_directories().is_ok() { + return Err(anyhow!( + "A sphere is already initialized in {:?}", + working_paths.root_path() + )); + } + + working_paths.initialize_local_directories().await?; + + let owner_did = working_paths + .get_key_did(owner_key) + .await + .map_err(|error| anyhow!("Could not look up the key {:?}:\n{:?}", owner_key, error))?; + + let storage_provider = + NativeStorageProvider::new(NativeStorageInit::Path(working_paths.blocks_path().clone()))?; + let mut block_store = storage_provider.get_store(BLOCK_STORE).await?; + + let (sphere, ucan, mnemonic) = Sphere::try_generate(&owner_did, &mut block_store).await?; + + fs::write(working_paths.authorization_path(), ucan.encode()?).await?; + fs::write(working_paths.key_path(), owner_did).await?; + + println!( + r#"A new sphere has been created in {:?} +Its identity is {} +Your key {:?} is considered its owner +The owner of a sphere can authorize other keys to write to it + +IMPORTANT: Write down the following sequence of words... + +{} + +...and keep it somewhere safe! +You will be asked to enter them if you ever need to transfer ownership of the sphere to a different key."#, + working_paths.root_path(), + sphere.try_get_identity().await?, + owner_key, + mnemonic + ); + + Ok(()) +} + +pub async fn join_sphere( + local_key: &str, + token: Option, + sphere_did: &str, + working_paths: &Workspace, +) -> Result<()> { + if working_paths.expect_local_directories().is_ok() { + return Err(anyhow!( + "A sphere is already initialized in {:?}", + working_paths.root_path() + )); + } + + println!("Joining sphere {}...", sphere_did); + + let did = working_paths.get_key_did(local_key).await?; + + let _token = match token { + None => { + println!( + r#"In order to join the sphere, its owner must first authorize your key +This is your key's ID; share it with the owner of the sphere: + + {0} + +Hint: if the owner is using the Noosphere CLI, they can use this command from the sphere root directory to authorize your key: + + orb auth add {0} + +Once authorized, the owner will give you a code. +Type or paste the code here and press enter:"#, + did + ); + + let mut token = String::new(); + + std::io::stdin().read_line(&mut token)?; + + token + } + Some(token) => token, + }; + // working_paths.initialize_local_directories().await?; + + Ok(()) +} + +pub async fn authorize(_key_did: &str, working_paths: &Workspace) -> Result<()> { + working_paths.expect_local_directories()?; + + Ok(()) + // TODO: Authorize... +} diff --git a/rust/noosphere-cli/src/native/mod.rs b/rust/noosphere-cli/src/native/mod.rs new file mode 100644 index 000000000..3124e08b6 --- /dev/null +++ b/rust/noosphere-cli/src/native/mod.rs @@ -0,0 +1,314 @@ +pub mod commands; +pub mod workspace; + +use anyhow::Result; +use std::ffi::OsString; + +use std::path::PathBuf; + +use cid::Cid; + +use clap::Parser; +use clap::Subcommand; +use url::Url; + +use commands::key::create_key; +use commands::key::list_keys; +use commands::sphere::initialize_sphere; +use commands::sphere::join_sphere; +use workspace::Workspace; + +use self::commands::auth::auth_add; + +// orb config set -> Set local configuration +// orb config get -> Read local configuration +// orb config import [--replace] -> Import settings from toml +// orb config export -> Export settings as toml + +// orb sphere initialize +// orb sphere join + +// orb auth add [--as ] +// orb auth list +// orb auth revoke + +// orb history -> Show the history of changes to the sphere (like git log) +// orb history -> Show the history of a file in the working tree + +// orb status -> Show the list of things that have changed since last save (like git status) +// orb diff [] -> If a difftool is configured, show a text diff (as appropriate) between the latest sphere revision and files in the working tree +// orb save -> Save all changes to the working tree as a revision to the sphere +// orb sync -> Bi-directionally sync with a configured gateway (if any); like get fetch + rebase + push +// orb publish [] -> Instruct the gateway to publish a version of the sphere to the DHT + +#[derive(Debug, Parser)] +#[clap(name = "orb")] +#[clap(about = "A CLI tool for saving, syncing and sharing content to the Noosphere", long_about = Some( +r#"The orb CLI tool is a utility for saving, syncing and sharing content to the +Noosphere. In practical terms, this means it helps you with tasks such as key +management, creating and updating spheres, managing acccess to said spheres and +publishing the contents of those spheres to public networks."#))] +pub struct Cli { + #[clap(subcommand)] + pub command: OrbCommand, +} + +#[derive(Debug, Subcommand)] +pub enum OrbCommand { + Config { + #[clap(subcommand)] + command: ConfigCommand, + }, + + Key { + #[clap(subcommand)] + command: KeyCommand, + }, + + Sphere { + #[clap(subcommand)] + command: SphereCommand, + }, + + Auth { + #[clap(subcommand)] + command: AuthCommand, + }, + + /// Show details about files in the sphere directory that have changed since + /// the last time the sphere was saved + Status, + + /// If a difftool is configured, show a diff between files on disk and saved versions in the sphere + Diff { + /// The specific file or files to show a diff of + paths: Vec, + + /// The base revision of the sphere to diff files against + #[clap(short, long, value_name = "CID")] + base: Option, + }, + + /// Saves changed files to a sphere, creating and signing a new revision in + /// the process; does nothing if there have been no changes to the files + /// since the last revision + Save, + + /// Synchronizes the local sphere with the copy in a configured gateway; + /// note that this is a "conflict-free" sync that may cause local changes + /// to be overwritten in cases where two or more clients have made changes + /// to the same files + Sync, + + /// Tell a configured gateway to update the published version of the sphere + /// in the Noosphere name system + Publish { + /// The version of the sphere to publish; if none is specified, the + /// latest saved version will be used + #[clap(value_name = "CID")] + version: Option, + }, +} + +/// Read and manage configuration values for a local sphere +#[derive(Debug, Subcommand)] +pub enum ConfigCommand { + /// Set a configuration value for the local sphere + Set { + #[clap(subcommand)] + command: ConfigSetCommand, + }, + /// Retrieve a configuration value if one is set + Get { + #[clap(subcommand)] + command: ConfigGetCommand, + }, + /// Import configuration settings from a TOML file + Import { + /// A TOML file containing key-value configuration entries to set + file: PathBuf, + + /// Replace all configurations with the values in the specified file + #[clap(short, long)] + replace: bool, + }, + + /// Print all current configuration values and exit + Export, +} + +#[derive(Debug, Subcommand)] +pub enum ConfigSetCommand { + /// Configure the Noosphere gateway to use for publishing and sync + Gateway { + /// The URL for a gateway API host that the owner key of this sphere is authorized to use + url: Url, + }, + + /// Configure a command to be used when diffing files + Difftool { + /// A command that can be used when diffing files + command: OsString, + }, +} + +#[derive(Debug, Subcommand)] +pub enum ConfigGetCommand { + /// Read the configured gateway URL + Gateway, + + /// Read the configured difftool command + Difftool, +} + +/// Create and securely manage personal keys +#[derive(Debug, Subcommand)] +pub enum KeyCommand { + /// Generate and securely store a new named key; this key is the analog of + /// a user account in the Noosphere. + Create { + /// The pet name for the newly created key; you will refer to it by this + /// name when using it in other commands + name: String, + }, + + /// Print the pet name and DID for all available keys + List { + /// Output the list of available keys as formatted JSON + #[clap(short = 'j', long)] + as_json: bool, + }, +} + +/// Create, join or share access to a sphere +#[derive(Debug, Subcommand)] +pub enum SphereCommand { + /// Initialize a new sphere and assign a key as its owner + Initialize { + /// The pet name of a key to assign as the owner of the sphere + #[clap(short = 'k', long)] + owner_key: Option, + + /// An optional path to a directory where the sphere should be + /// initialized; by default, the current working directory will + /// be used + path: Option, + }, + + /// Join an existing sphere by its ID and set up a local working copy + Join { + /// The pet name of a key to use when requesting access to the sphere + #[clap(short = 'k', long)] + local_key: Option, + + /// An authorization token allowing the specified key to join the + /// sphere (if already known) + #[clap(short = 't', long)] + token: Option, + + /// The ID (specifically: a DID) of an existing sphere to join + id: String, + + /// An optional path to a directory where the sphere should be + /// initialized; by default, the current working directory will + /// be used + path: Option, + }, + + /// Transfer ownership of the sphere in the current directory to another key + Transfer { + /// The pet name of the key to transfer ownership to + new_owner_key: String, + }, +} + +/// Manage access to a sphere by holders of other keys +#[derive(Debug, Subcommand)] +pub enum AuthCommand { + /// Authorize a key to work on the sphere in the current directory + Add { + /// The DID of the key to authorize + did: String, + + /// An optional name to give the key; if one is not specified, a random + /// one will be assigned + #[clap(short = 'n', long)] + name: Option, + }, + + /// Print the name and DID for all keys that the owner has authorized + /// to work on this sphere + List { + /// Output the list of available keys as formatted JSON + #[clap(short = 'j', long)] + as_json: bool, + }, + + /// Revoke authorization to work on the sphere from a specified key + Remove { + /// The name of a key to revoke authorization for + name: String, + }, +} + +pub async fn main() -> Result<()> { + // println!("Hello, Orb!"); + let args = Cli::parse(); + + let mut workspace = Workspace::new(&std::env::current_dir()?)?; + + // println!("{:#?}", args); + // println!("{:#?}", working_paths); + + match args.command { + OrbCommand::Config { command: _ } => todo!(), + OrbCommand::Key { command } => match command { + KeyCommand::Create { name } => create_key(name, &workspace).await?, + KeyCommand::List { as_json } => list_keys(as_json, &workspace).await?, + }, + OrbCommand::Sphere { command } => match command { + SphereCommand::Initialize { owner_key, path } => { + if let Some(path) = path { + workspace = Workspace::new(&workspace.root_path().join(path))?; + } + + let owner_key = match owner_key { + Some(owner_key) => owner_key, + None => workspace.unambiguous_default_key_name().await?, + }; + + initialize_sphere(&owner_key, &workspace).await?; + } + SphereCommand::Join { + local_key, + token, + id, + path, + } => { + if let Some(path) = path { + workspace = Workspace::new(&workspace.root_path().join(path))?; + } + + let local_key = match local_key { + Some(local_key) => local_key, + None => workspace.unambiguous_default_key_name().await?, + }; + + join_sphere(&local_key, token, &id, &workspace).await?; + } + SphereCommand::Transfer { new_owner_key: _ } => todo!(), + }, + OrbCommand::Status => todo!(), + OrbCommand::Diff { paths: _, base: _ } => todo!(), + OrbCommand::Save => todo!(), + OrbCommand::Sync => todo!(), + OrbCommand::Publish { version: _ } => todo!(), + OrbCommand::Auth { command } => match command { + AuthCommand::Add { did, name } => auth_add(&did, name, &workspace).await?, + AuthCommand::List { as_json: _ } => todo!(), + AuthCommand::Remove { name: _ } => todo!(), + }, + }; + + Ok(()) +} diff --git a/rust/noosphere-cli/src/native/workspace.rs b/rust/noosphere-cli/src/native/workspace.rs new file mode 100644 index 000000000..51dc49cd8 --- /dev/null +++ b/rust/noosphere-cli/src/native/workspace.rs @@ -0,0 +1,265 @@ +use anyhow::{anyhow, Result}; +use noosphere::authority::{restore_ed25519_key}; +use path_absolutize::Absolutize; +use std::{collections::BTreeMap, path::PathBuf}; +use tokio::fs; +use ucan::{Ucan}; +use ucan_key_support::ed25519::Ed25519KeyMaterial; + +const NOOSPHERE_DIRECTORY: &str = ".noosphere"; +const SPHERE_DIRECTORY: &str = ".sphere"; +const BLOCKS_DIRECTORY: &str = "blocks"; +const KEYS_DIRECTORY: &str = "keys"; +const AUTHORIZATION_FILE: &str = "AUTHORIZATION"; +const KEY_FILE: &str = "KEY"; + +/// A utility for discovering and initializing the well-known paths for a +/// working copy of a sphere and relevant global Noosphere configuration +#[derive(Clone, Debug)] +pub struct Workspace { + root: PathBuf, + sphere: PathBuf, + blocks: PathBuf, + noosphere: PathBuf, + keys: PathBuf, + authorization: PathBuf, + key: PathBuf, +} + +impl Workspace { + /// The root directory containing the working copy of sphere files on + /// disk, as well as the local sphere data + pub fn root_path(&self) -> &PathBuf { + &self.root + } + + /// The path to the sphere data folder within the working file tree + pub fn sphere_path(&self) -> &PathBuf { + &self.sphere + } + + /// The path to the block storage database within the working file tree + pub fn blocks_path(&self) -> &PathBuf { + &self.blocks + } + + /// The path to the folder that contains global Noosphere configuration + /// and keys generated by the user + pub fn noosphere_path(&self) -> &PathBuf { + &self.noosphere + } + + /// The path to the folder containing user-generated keys when there is + /// no secure option for generating them available + pub fn keys_path(&self) -> &PathBuf { + &self.keys + } + + /// Path to the local authorization (the granted UCAN) for the key that + /// is authorized to work on the sphere + pub fn authorization_path(&self) -> &PathBuf { + &self.authorization + } + + pub fn key_path(&self) -> &PathBuf { + &self.key + } + + /// Attempts to read the locally stored authorization that enables the key + /// to operate on this sphere, and returns it as a UCAN + pub async fn get_local_authorization(&self) -> Result { + self.expect_local_directories()?; + + let authorization_jwt = fs::read_to_string(&self.authorization).await?; + let ucan = Ucan::try_from_token_string(&authorization_jwt)?; + + Ok(ucan) + } + + pub async fn get_local_key(&self) -> Result { + self.expect_global_directories()?; + self.expect_local_directories()?; + + let local_key_did = fs::read_to_string(&self.key).await?; + let keys = self.get_all_keys().await?; + + for (key, did) in keys { + if did == local_key_did { + let private_key_mnemonic = self.get_key_mnemonic(&key).await?; + return restore_ed25519_key(&private_key_mnemonic); + } + } + + Err(anyhow!( + "Could not resolve private key material for {:?}", + local_key_did + )) + } + + /// Look up the DID for the key by its name + pub async fn get_key_did(&self, name: &str) -> Result { + Ok(fs::read_to_string(self.keys.join(name).with_extension("public")).await?) + } + + pub async fn get_key_mnemonic(&self, name: &str) -> Result { + Ok(fs::read_to_string(self.keys.join(name).with_extension("private")).await?) + } + + /// Returns true if there are no files in the configured root path + pub async fn is_root_empty(&self) -> Result { + let mut directory = fs::read_dir(&self.root).await?; + + Ok(if let Some(_) = directory.next_entry().await? { + false + } else { + true + }) + } + + /// Reads all the available keys and returns a map of their names to their + /// DIDs + pub async fn get_all_keys(&self) -> Result> { + self.expect_global_directories()?; + + let mut key_names = BTreeMap::::new(); + let mut directory = fs::read_dir(&self.keys).await?; + + while let Some(entry) = directory.next_entry().await? { + let key_path = entry.path(); + let key_name = key_path.file_stem().map(|stem| stem.to_str()); + let extension = key_path.extension().map(|extension| extension.to_str()); + + match (key_name, extension) { + (Some(Some(key_name)), Some(Some("public"))) => { + let did = self.get_key_did(key_name).await?; + key_names.insert(key_name.to_string(), did); + } + _ => continue, + }; + } + + Ok(key_names) + } + + /// If there is only one key to choose from, returns its name. Otherwise + /// returns an error result. + pub async fn unambiguous_default_key_name(&self) -> Result { + if self.expect_global_directories().is_ok() { + let keys = self.get_all_keys().await?; + + if keys.len() > 1 { + let key_names = keys + .into_iter() + .map(|(name, _)| name) + .collect::>() + .join("\n"); + return Err(anyhow!( + r#"There is more than one key; you should specify a key to use by name +The available keys are: + +{}"#, + key_names + )); + } else if let Some((key_name, _)) = keys.iter().next() { + return Ok(key_name.clone()); + } + } + + Err(anyhow!("No keys found; have you created any yet?")) + } + + /// Asserts that all related directories for the suggested working file + /// tree root are present + pub fn expect_local_directories(&self) -> Result<()> { + if !self.root.is_dir() { + return Err(anyhow!( + "Configured sphere root {:?} is not a directory!", + self.root + )); + } + + if !self.sphere.is_dir() { + return Err(anyhow!( + "The {:?} folder within {:?} is missing or corrupted", + SPHERE_DIRECTORY, + self.root + )); + } + + Ok(()) + } + + /// Asserts that the global Noosphere directories are present + pub fn expect_global_directories(&self) -> Result<()> { + if !self.noosphere.is_dir() || !self.keys.is_dir() { + return Err(anyhow!( + "The Noosphere config directory ({:?}) is missing or corrupted", + self.noosphere + )); + } + + Ok(()) + } + + /// Creates all the directories needed to start rendering a sphere in the + /// configured working file tree root + pub async fn initialize_local_directories(&self) -> Result<()> { + let mut root = self.root.clone(); + + while let Some(parent) = root.clone().parent() { + root = parent.to_path_buf(); + let working_paths = Workspace::new(&root)?; + if let Ok(_) = working_paths.expect_local_directories() { + return Err(anyhow!( + r#"Tried to initialize sphere directories in {:?} +...but a sphere is already initialized in {:?} +Unexpected things will happen if you try to nest spheres this way!"#, + self.root, + parent + ))?; + } + } + + fs::create_dir_all(&self.sphere).await?; + + Ok(()) + } + + /// Creates the global Noosphere config and keys directories + pub async fn initialize_global_directories(&self) -> Result<()> { + fs::create_dir_all(&self.keys).await?; + + Ok(()) + } + + pub fn new(root: &PathBuf) -> Result { + if !root.is_absolute() { + return Err(anyhow!("Ambiguous path to sphere root: {:?}", root)); + } + + let root = root.absolutize()?.to_path_buf(); + let sphere = root.join(SPHERE_DIRECTORY); + let blocks = sphere.join(BLOCKS_DIRECTORY); + let authorization = sphere.join(AUTHORIZATION_FILE); + let key = sphere.join(KEY_FILE); + let noosphere = home::home_dir() + .ok_or_else(|| { + anyhow!( + "Could not discover home directory for {}", + whoami::username() + ) + })? + .join(NOOSPHERE_DIRECTORY); + let keys = noosphere.join(KEYS_DIRECTORY); + + Ok(Workspace { + root, + sphere, + blocks, + authorization, + key, + noosphere, + keys, + }) + } +} diff --git a/rust/noosphere-cli/src/web.rs b/rust/noosphere-cli/src/web.rs new file mode 100644 index 000000000..041752089 --- /dev/null +++ b/rust/noosphere-cli/src/web.rs @@ -0,0 +1,2 @@ +// TODO: Future entrypoint for orb.js? +pub async fn main() {} diff --git a/rust/noosphere-collections/Cargo.toml b/rust/noosphere-collections/Cargo.toml index f58db4912..772edd3cd 100644 --- a/rust/noosphere-collections/Cargo.toml +++ b/rust/noosphere-collections/Cargo.toml @@ -18,7 +18,8 @@ multihash = "0.16" # NOTE: async-once-cell 0.4.0 shipped unstable feature usage async-once-cell = "0.3" async-recursion = "^1" -libipld-core = { version = "0.14", features = ["serde-codec"] } +libipld-core = "~0.14" +libipld-cbor = "~0.14" noosphere-storage = { path = "../noosphere-storage" } noosphere-cbor = { path = "../noosphere-cbor" } diff --git a/rust/noosphere-collections/src/hamt/hamt.rs b/rust/noosphere-collections/src/hamt/hamt.rs index e6dc26f5a..7b7fd780f 100644 --- a/rust/noosphere-collections/src/hamt/hamt.rs +++ b/rust/noosphere-collections/src/hamt/hamt.rs @@ -4,8 +4,9 @@ // SPDX-License-Identifier: Apache-2.0, MIT use anyhow::Result; -use noosphere_cbor::TryDagCbor; -use noosphere_storage::interface::{DagCborStore, Store}; +use libipld_cbor::DagCborCodec; + +use noosphere_storage::interface::BlockStore; use std::borrow::Borrow; use std::marker::PhantomData; use std::pin::Pin; @@ -85,7 +86,7 @@ where impl< K: PartialEq + TargetConditionalSendSync, V: PartialEq + TargetConditionalSendSync, - S: Store, + S: BlockStore, H: HashAlgorithm, > PartialEq for Hamt { @@ -98,7 +99,7 @@ impl Hamt where K: Hash + Eq + PartialOrd + Serialize + DeserializeOwned + TargetConditionalSendSync, V: Serialize + DeserializeOwned + TargetConditionalSendSync + PartialEq, - BS: Store, + BS: BlockStore, H: HashAlgorithm, { pub fn new(store: BS) -> Self { @@ -122,7 +123,7 @@ where /// Lazily instantiate a hamt from this root Cid with a specified bit width. pub async fn load_with_bit_width(cid: &Cid, store: BS, bit_width: u32) -> Result { - let root: Node = store.load(cid).await?; + let root: Node = store.load::(cid).await?; Ok(Self { root, store, @@ -133,7 +134,7 @@ where /// Sets the root based on the Cid of the root node using the Hamt store pub async fn set_root(&mut self, cid: &Cid) -> Result<()> { - self.root = self.store.load(cid).await?; + self.root = self.store.load::(cid).await?; Ok(()) } @@ -319,8 +320,7 @@ where /// Flush root and return Cid for hamt pub async fn flush(&mut self) -> Result { self.root.flush(&mut self.store).await?; - let bytes = self.root.try_into_dag_cbor()?; - Ok(self.store.write_cbor(&bytes).await?) + self.store.save::(&self.root).await } /// Returns true if the HAMT has no entries @@ -359,7 +359,6 @@ where V: DeserializeOwned, F: FnMut(&K, &V) -> anyhow::Result<()>, { - // self.root.for_each(self.store.borrow(), &mut f).await let mut stream = self.stream(); while let Some(Ok((key, value))) = stream.next().await { @@ -367,7 +366,6 @@ where } Ok(()) - // for item } pub fn stream<'a>(&'a self) -> Pin> + 'a>> { diff --git a/rust/noosphere-collections/src/hamt/key_value_pair.rs b/rust/noosphere-collections/src/hamt/key_value_pair.rs index 2f9b2a526..b893ec833 100644 --- a/rust/noosphere-collections/src/hamt/key_value_pair.rs +++ b/rust/noosphere-collections/src/hamt/key_value_pair.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use super::TargetConditionalSendSync; -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] pub struct KeyValuePair(K, V); impl KeyValuePair { diff --git a/rust/noosphere-collections/src/hamt/node.rs b/rust/noosphere-collections/src/hamt/node.rs index 10c96f3db..bfcb4b343 100644 --- a/rust/noosphere-collections/src/hamt/node.rs +++ b/rust/noosphere-collections/src/hamt/node.rs @@ -6,8 +6,9 @@ use anyhow::Result; use async_recursion::async_recursion; use async_stream::try_stream; -use noosphere_cbor::TryDagCbor; -use noosphere_storage::interface::{DagCborStore, Store}; +use libipld_cbor::DagCborCodec; + +use noosphere_storage::interface::BlockStore; use std::borrow::Borrow; use std::fmt::Debug; use std::marker::PhantomData; @@ -92,7 +93,7 @@ where H: HashAlgorithm + TargetConditionalSendSync, V: Serialize + DeserializeOwned + TargetConditionalSendSync, { - pub async fn set( + pub async fn set( &mut self, key: K, value: V, @@ -116,7 +117,7 @@ where } #[inline] - pub async fn get( + pub async fn get( &self, k: &Q, store: &S, @@ -139,7 +140,7 @@ where where K: Borrow, Q: Eq + Hash + TargetConditionalSendSync, - S: Store, + S: BlockStore, { self.rm_value(HashBits::new(H::hash(k)), bit_width, 0, k, store) .await @@ -154,7 +155,7 @@ where store: &'a S, ) -> Pin> + 'a>> where - S: Store, + S: BlockStore, { Box::pin(try_stream! { for p in &self.pointers { @@ -167,7 +168,7 @@ where yield item?; } } else { - let node = match store.load(cid).await { + let node = match store.load::(cid).await { Ok(node) => Ok(node), Err(error) => { #[cfg(feature = "ignore-dead-links")] @@ -204,7 +205,7 @@ where } /// Search for a key. - async fn search( + async fn search( &self, q: &Q, store: &S, @@ -220,7 +221,7 @@ where #[cfg_attr(target_arch="wasm32", async_recursion(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_recursion)] - async fn get_value( + async fn get_value( &self, mut hashed_key: HashBits, bit_width: u32, @@ -248,7 +249,7 @@ where .get_value(hashed_key, bit_width, depth + 1, key, store) .await } else { - let node = match store.load(cid).await { + let node = match store.load::(cid).await { Ok(node) => node, Err(error) => { #[cfg(not(feature = "ignore-dead-links"))] @@ -278,7 +279,7 @@ where #[allow(clippy::too_many_arguments)] #[cfg_attr(target_arch="wasm32", async_recursion(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_recursion)] - async fn modify_value( + async fn modify_value( &mut self, mut hashed_key: HashBits, bit_width: u32, @@ -305,7 +306,7 @@ where match child { Pointer::Link { cid, cache } => { cache - .get_or_try_init(async { store.load(cid).await }) + .get_or_try_init(async { store.load::(cid).await }) .await?; let child_node = cache.get_mut().expect("filled line above"); @@ -409,7 +410,7 @@ where /// Internal method to delete entries. #[cfg_attr(target_arch="wasm32", async_recursion(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_recursion)] - async fn rm_value( + async fn rm_value( &mut self, mut hashed_key: HashBits, bit_width: u32, @@ -434,7 +435,7 @@ where match child { Pointer::Link { cid, cache } => { cache - .get_or_try_init(async { store.load(cid).await }) + .get_or_try_init(async { store.load::(cid).await }) .await?; let child_node = cache.get_mut().expect("filled line above"); @@ -484,15 +485,14 @@ where #[cfg_attr(target_arch="wasm32", async_recursion(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_recursion)] - pub async fn flush(&mut self, store: &mut S) -> Result<()> { + pub async fn flush(&mut self, store: &mut S) -> Result<()> { for pointer in &mut self.pointers { if let Pointer::Dirty(node) = pointer { // Flush cached sub node to clear it's cache node.flush(store).await?; // Put node in blockstore and retrieve Cid - let node_bytes = node.try_into_dag_cbor()?; - let cid = store.write_cbor(&node_bytes).await?; + let cid = store.save::(&node).await?; // Can keep the flushed node in link cache let cache = OnceCell::new_with(Some(std::mem::take(node))); diff --git a/rust/noosphere-collections/src/hamt/pointer.rs b/rust/noosphere-collections/src/hamt/pointer.rs index 4fad16f48..63506c9d9 100644 --- a/rust/noosphere-collections/src/hamt/pointer.rs +++ b/rust/noosphere-collections/src/hamt/pointer.rs @@ -40,7 +40,7 @@ impl Self::Values(arg0.clone()), Self::Link { cid, .. } => Self::Link { - cid: cid.clone(), + cid: *cid, cache: OnceCell::new(), }, Self::Dirty(arg0) => Self::Dirty(arg0.clone()), diff --git a/rust/noosphere-collections/src/hamt/test.rs b/rust/noosphere-collections/src/hamt/test.rs index 3ac788f7e..329aa1d3d 100644 --- a/rust/noosphere-collections/src/hamt/test.rs +++ b/rust/noosphere-collections/src/hamt/test.rs @@ -115,7 +115,7 @@ async fn test_set_if_absent() { assert_eq!( stats, StoreStats { - reads: 2, + reads: 1, writes: 1, removes: 0, bytes_read: 63, @@ -159,7 +159,7 @@ async fn set_with_no_effect_does_not_put() { assert_eq!( stats, StoreStats { - reads: 18, + reads: 0, writes: 18, removes: 0, bytes_read: 0, @@ -186,11 +186,11 @@ async fn set_with_no_effect_does_not_put() { assert_eq!( stats, StoreStats { - reads: 19, - writes: 18, + reads: 0, + writes: 19, removes: 0, bytes_read: 0, - bytes_written: 1282, + bytes_written: 1372, bytes_removed: 0 } ); @@ -234,7 +234,7 @@ async fn delete() { assert_eq!( stats, StoreStats { - reads: 3, + reads: 1, writes: 2, removes: 0, bytes_read: 79, @@ -275,7 +275,7 @@ async fn delete_case() { assert_eq!( stats, StoreStats { - reads: 3, + reads: 1, writes: 2, removes: 0, bytes_read: 31, @@ -310,11 +310,11 @@ async fn reload_empty() { assert_eq!( stats, StoreStats { - reads: 3, - writes: 1, + reads: 1, + writes: 2, removes: 0, bytes_read: 3, - bytes_written: 3, + bytes_written: 6, bytes_removed: 0 } ); @@ -367,11 +367,11 @@ async fn set_delete_many() { assert_eq!( stats, StoreStats { - reads: 91, - writes: 62, + reads: 0, + writes: 91, removes: 0, bytes_read: 0, - bytes_written: 6715, + bytes_written: 9251, bytes_removed: 0 } ); @@ -443,11 +443,11 @@ async fn for_each() { assert_eq!( stats, StoreStats { - reads: 59, - writes: 29, + reads: 29, + writes: 30, removes: 0, bytes_read: 2536, - bytes_written: 2536, + bytes_written: 3803, bytes_removed: 0 } ); @@ -687,7 +687,7 @@ async fn clean_child_ordering() { assert_eq!( stats, StoreStats { - reads: 14, + reads: 3, writes: 11, removes: 0, bytes_read: 1449, diff --git a/rust/noosphere-fs/Cargo.toml b/rust/noosphere-fs/Cargo.toml index fb3ce4b96..16c9eb05d 100644 --- a/rust/noosphere-fs/Cargo.toml +++ b/rust/noosphere-fs/Cargo.toml @@ -17,16 +17,12 @@ bytes = "^1" once_cell = "^1" cid = "~0.8" anyhow = "^1" +libipld-core = "~0.14" +libipld-cbor = "~0.14" futures-util = "~0.3" +ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } +ucan-key-support = { version = "0.4.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } [dev-dependencies] wasm-bindgen-test = "~0.3" - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git" } -ucan-key-support = { version = "0.4.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git" } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -ucan = { version = "0.6.0-alpha.1", features = ["web"], git = "https://github.com/cdata/rs-ucan.git" } -ucan-key-support = { version = "0.4.0-alpha.1", features = ["web"], git = "https://github.com/cdata/rs-ucan.git" } \ No newline at end of file diff --git a/rust/noosphere-fs/src/decoder.rs b/rust/noosphere-fs/src/decoder.rs index b0fdc44c1..ba96cf6d0 100644 --- a/rust/noosphere-fs/src/decoder.rs +++ b/rust/noosphere-fs/src/decoder.rs @@ -1,20 +1,21 @@ use async_stream::try_stream; use bytes::Bytes; use cid::Cid; +use libipld_cbor::DagCborCodec; use noosphere::data::BodyChunkIpld; -use noosphere_storage::interface::{DagCborStore, Store}; +use noosphere_storage::interface::BlockStore; use tokio_stream::Stream; /// Helper to easily decode a linked list of `BodyChunkIpld` as a byte stream -pub struct BodyChunkDecoder<'a, 'b, S: Store>(pub &'a Cid, pub &'b S); +pub struct BodyChunkDecoder<'a, 'b, S: BlockStore>(pub &'a Cid, pub &'b S); -impl<'a, 'b, S: Store> BodyChunkDecoder<'a, 'b, S> { +impl<'a, 'b, S: BlockStore> BodyChunkDecoder<'a, 'b, S> { pub fn stream(self) -> impl Stream> + Unpin { - let mut next = Some(self.0.clone()); + let mut next = Some(*self.0); let store = self.1.clone(); Box::pin(try_stream! { while let Some(cid) = next { - let chunk: BodyChunkIpld = store.load(&cid).await.map_err(|error| { + 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); diff --git a/rust/noosphere-fs/src/fs.rs b/rust/noosphere-fs/src/fs.rs index 593093b53..77f7493ee 100644 --- a/rust/noosphere-fs/src/fs.rs +++ b/rust/noosphere-fs/src/fs.rs @@ -1,9 +1,13 @@ use anyhow::{anyhow, Result}; +use libipld_cbor::DagCborCodec; use noosphere::{ - data::{BodyChunkIpld, ContentType, Header, MemoIpld, ReferenceIpld}, + data::{BodyChunkIpld, ContentType, Header, MemoIpld}, view::{Sphere, SphereMutation}, }; -use noosphere_storage::interface::{DagCborStore, KeyValueStore, Store}; +use noosphere_storage::{ + db::SphereDb, + interface::{BlockStore, Store}, +}; use std::str::FromStr; use tokio_util::io::StreamReader; use ucan::{crypto::KeyMaterial, ucan::Ucan}; @@ -26,8 +30,7 @@ where { sphere_identity: String, sphere_revision: Cid, - block_store: S, - sphere_store: S, + db: SphereDb, } impl SphereFs @@ -43,20 +46,20 @@ where } pub fn to_sphere(&self) -> Sphere { - Sphere::at(self.revision(), &self.block_store) + Sphere::at(self.revision(), &self.db.to_block_store()) } - async fn require_sphere_revision(sphere_identity: &str, sphere_store: &S) -> Result { - let reference: ReferenceIpld = sphere_store - .get(sphere_identity) + async fn require_sphere_version(sphere_identity: &str, db: &SphereDb) -> Result { + db.get_version(sphere_identity) .await? - .ok_or_else(|| anyhow!("No reference to sphere {} found", sphere_identity))?; - - Ok(reference.link) + .ok_or_else(|| anyhow!("No reference to sphere {} found", sphere_identity)) } async fn get_file(&self, memo_revision: &Cid) -> Result> { - let memo: MemoIpld = self.block_store.load(memo_revision).await?; + let memo = self + .db + .load::(memo_revision) + .await?; let content_type = match memo.get_first_header(&Header::ContentType.to_string()) { Some(content_type) => Some(ContentType::from_str(content_type.as_str())?), None => None, @@ -64,7 +67,7 @@ where let stream = match content_type { Some(ContentType::Subtext) | Some(ContentType::Bytes) => { - BodyChunkDecoder(&memo.body, &self.block_store).stream() + BodyChunkDecoder(&memo.body, &self.db).stream() } Some(content_type) => { return Err(anyhow!("Unsupported content type: {}", content_type)) @@ -73,8 +76,8 @@ where }; Ok(SphereFile { - sphere_revision: self.sphere_revision.clone(), - memo_revision: memo_revision.clone(), + sphere_revision: self.sphere_revision, + memo_revision: *memo_revision, memo, contents: StreamReader::new(stream), }) @@ -82,18 +85,13 @@ where /// Create an FS view into the latest revision found in the provided sphere /// reference storage - pub async fn latest( - sphere_identity: &str, - block_store: &S, - sphere_store: &S, - ) -> Result> { - let sphere_revision = Self::require_sphere_revision(sphere_identity, sphere_store).await?; + pub async fn latest(sphere_identity: &str, db: &SphereDb) -> Result> { + let sphere_revision = Self::require_sphere_version(sphere_identity, db).await?; Ok(SphereFs { sphere_identity: sphere_identity.into(), sphere_revision, - block_store: block_store.clone(), - sphere_store: sphere_store.clone(), + db: db.clone(), }) } @@ -103,14 +101,12 @@ where pub fn at( sphere_identity: &str, sphere_revision: &Cid, - block_store: &S, - sphere_store: &S, + db: &SphereDb, ) -> Result>> { Ok(Some(SphereFs { sphere_identity: sphere_identity.into(), - sphere_revision: sphere_revision.clone(), - block_store: block_store.clone(), - sphere_store: sphere_store.clone(), + sphere_revision: *sphere_revision, + db: db.clone(), })) } @@ -119,12 +115,12 @@ where /// 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 = Sphere::at(&self.sphere_revision, &self.block_store); + let sphere = Sphere::at(&self.sphere_revision, &self.db); match sphere.try_get_parent().await? { Some(parent) => { - self.sphere_revision = parent.cid().clone(); - Ok(Some(self.sphere_revision.clone())) + self.sphere_revision = *parent.cid(); + Ok(Some(self.sphere_revision)) } None => Ok(None), } @@ -141,12 +137,12 @@ where /// Note that "contents" are `AsyncRead`, and content bytes won't be read /// until contents is polled. pub async fn read(&self, slug: &str) -> Result>> { - let sphere = Sphere::at(&self.sphere_revision, &self.block_store); + let sphere = Sphere::at(&self.sphere_revision, &self.db); let links = sphere.try_get_links().await?; let hamt = links.try_get_hamt().await?; Ok(match hamt.get(&slug.to_string()).await? { - Some(content_cid) => Some(self.get_file(&content_cid).await?), + Some(content_cid) => Some(self.get_file(content_cid).await?), None => None, }) } @@ -169,12 +165,10 @@ where additional_headers: Option>, ) -> Result { let current_file = self.read(slug).await?; - let previous_memo_cid = current_file.map_or(None, |file| Some(file.memo_revision)); + let previous_memo_cid = current_file.map(|file| file.memo_revision); - // let sphere_cid = self.require_sphere_revision().await?; - let sphere_cid = - Self::require_sphere_revision(&self.sphere_identity, &self.sphere_store).await?; - let sphere = Sphere::at(&sphere_cid, &self.block_store); + let sphere_cid = Self::require_sphere_version(&self.sphere_identity, &self.db).await?; + let sphere = Sphere::at(&sphere_cid, &self.db); let mut bytes = Vec::new(); value.read_to_end(&mut bytes).await?; @@ -182,11 +176,11 @@ where // 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, &mut self.block_store).await?; + let body_cid = BodyChunkIpld::store_bytes(&bytes, &mut self.db).await?; let mut new_memo = match previous_memo_cid { Some(cid) => { - let mut memo = MemoIpld::branch_from(&cid, &self.block_store).await?; + let mut memo = MemoIpld::branch_from(&cid, &self.db).await?; memo.body = body_cid; memo } @@ -206,23 +200,18 @@ where new_memo.replace_header(&Header::ContentType.to_string(), content_type); // TODO(#43): Configure default/implicit headers here - let memo_cid = self.block_store.save(&new_memo).await?; + let memo_cid = self.db.save::(new_memo).await?; let author_did = credential.get_did().await?; let mut mutation = SphereMutation::new(&author_did); - mutation.links_mut().set(slug, &memo_cid); + mutation.links_mut().set(&slug.into(), &memo_cid); - let mut revision = sphere.try_apply(&mutation).await?; + let mut revision = sphere.try_apply_mutation(&mutation).await?; let next_sphere_cid = revision.try_sign(credential, proof).await?; - self.sphere_store - .set( - &self.sphere_identity, - &ReferenceIpld { - link: next_sphere_cid.clone(), - }, - ) + self.db + .set_version(&self.sphere_identity, &next_sphere_cid) .await?; self.sphere_revision = next_sphere_cid; @@ -235,11 +224,11 @@ where pub mod tests { use noosphere::{ authority::generate_ed25519_key, - data::{ContentType, Header, ReferenceIpld}, + data::{ContentType, Header}, view::Sphere, }; - use noosphere_storage::interface::KeyValueStore; - use noosphere_storage::memory::MemoryStore; + use noosphere_storage::{db::SphereDb}; + use noosphere_storage::{memory::MemoryStorageProvider}; use tokio::io::AsyncReadExt; use ucan::crypto::KeyMaterial; @@ -253,31 +242,21 @@ pub mod tests { #[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 mut sphere_store = MemoryStore::default(); - let mut block_store = MemoryStore::default(); + let storage_provider = MemoryStorageProvider::default(); + let mut db = SphereDb::new(&storage_provider).await.unwrap(); let owner_key = generate_ed25519_key(); let owner_did = owner_key.get_did().await.unwrap(); - let (sphere, proof, _) = Sphere::try_generate(&owner_did, &mut block_store) - .await - .unwrap(); + let (sphere, proof, _) = Sphere::try_generate(&owner_did, &mut db).await.unwrap(); let sphere_identity = sphere.try_get_identity().await.unwrap(); - sphere_store - .set( - &sphere_identity, - &ReferenceIpld { - link: sphere.cid().clone(), - }, - ) + db.set_version(&sphere_identity, sphere.cid()) .await .unwrap(); - let mut fs = SphereFs::latest(&sphere_identity, &block_store, &sphere_store) - .await - .unwrap(); + let mut fs = SphereFs::latest(&sphere_identity, &db).await.unwrap(); fs.write( "cats", @@ -308,31 +287,21 @@ pub mod tests { #[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 mut sphere_store = MemoryStore::default(); - let mut block_store = MemoryStore::default(); + let storage_provider = MemoryStorageProvider::default(); + let mut db = SphereDb::new(&storage_provider).await.unwrap(); let owner_key = generate_ed25519_key(); let owner_did = owner_key.get_did().await.unwrap(); - let (sphere, proof, _) = Sphere::try_generate(&owner_did, &mut block_store) - .await - .unwrap(); + let (sphere, proof, _) = Sphere::try_generate(&owner_did, &mut db).await.unwrap(); let sphere_identity = sphere.try_get_identity().await.unwrap(); - sphere_store - .set( - &sphere_identity, - &ReferenceIpld { - link: sphere.cid().clone(), - }, - ) + db.set_version(&sphere_identity, sphere.cid()) .await .unwrap(); - let mut fs = SphereFs::latest(&sphere_identity, &block_store, &sphere_store) - .await - .unwrap(); + let mut fs = SphereFs::latest(&sphere_identity, &db).await.unwrap(); fs.write( "cats", diff --git a/rust/noosphere-gateway/Cargo.toml b/rust/noosphere-gateway/Cargo.toml index 1e652fbaa..93201560b 100644 --- a/rust/noosphere-gateway/Cargo.toml +++ b/rust/noosphere-gateway/Cargo.toml @@ -14,13 +14,9 @@ anyhow = "^1" [dev-dependencies] temp-dir = "0.1" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[target.'cfg(target_arch = "wasm32")'.dependencies] -tokio = { version = "^1", features = ["rt", "macros", "rt-multi-thread"] } - [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git" } -ucan-key-support = { version = "0.4.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git" } +ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } +ucan-key-support = { version = "0.4.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } cid = "0.8" @@ -43,6 +39,7 @@ tokio = { version = "^1", features = ["full"] } tower = "~0.4" tower-http = { version = "~0.3", features = ["cors", "trace"] } tracing-subscriber = { version = "~0.3", features = ["env-filter"] } +# axum = { version = "~0.5.16", features = ["headers", "macros"] } hyper = { version = "~0.14", features = ["client"] } sled = "~0.34" @@ -55,4 +52,8 @@ futures = "~0.3" clap = { version = "^3", features = ["derive"] } pretty_env_logger = "~0.4" serde_json = "^1" -serde = "^1" \ No newline at end of file +serde = "^1" +serde_bytes = "~0.11" + +libipld-core = "~0.14" +libipld-cbor = "~0.14" \ No newline at end of file diff --git a/rust/noosphere-gateway/src/gateway/authority/keeper.rs b/rust/noosphere-gateway/src/gateway/authority/keeper.rs index 200620903..d82285e3e 100644 --- a/rust/noosphere-gateway/src/gateway/authority/keeper.rs +++ b/rust/noosphere-gateway/src/gateway/authority/keeper.rs @@ -6,6 +6,11 @@ use axum::{ headers::{authorization::Bearer, Authorization}, TypedHeader, }; +use cid::Cid; +use noosphere_storage::ucan::UcanStore; +use noosphere_storage::{interface::BlockStore, native::NativeStore}; +use serde_bytes::Bytes; +use std::ops::{Deref}; use std::sync::Arc; use ucan::{ capability::{Capability, Resource, With}, @@ -13,7 +18,10 @@ use ucan::{ crypto::did::DidParser, }; -use crate::gateway::{environment::GatewayConfig, AuthzError}; +use crate::gateway::{ + environment::{Blocks, GatewayConfig}, + AuthzError, +}; use noosphere_api::authority::{GatewayAction, GatewayIdentity, GATEWAY_SEMANTICS}; @@ -26,10 +34,16 @@ impl GatewayAuthority { pub async fn for_bearer( config: Arc, did_parser: Arc>, + blocks: Blocks, auth_token: &str, ) -> Result { let mut did_parser = did_parser.lock().await; - let proof_chain = ProofChain::try_from_token_string(auth_token, &mut did_parser).await?; + let proof_chain = ProofChain::try_from_token_string( + auth_token, + &mut did_parser, + &UcanStore(blocks.deref().clone()), + ) + .await?; Ok(GatewayAuthority { config, proof_chain, @@ -88,6 +102,14 @@ where ))? .clone(); + let mut blocks = req + .extensions() + .get::>() + .ok_or(AuthzError::Internal( + "No gateway configuration found".into(), + ))? + .clone(); + let TypedHeader(Authorization(bearer)) = TypedHeader::>::from_request(req) .await @@ -96,13 +118,35 @@ where AuthzError::MalformedToken })?; - Ok( - GatewayAuthority::for_bearer(config.clone(), did_parser.clone(), bearer.token()) + // TODO: We should write a typed header thing for this: + let ucan_headers = req.headers().get_all("ucan"); + for header in ucan_headers.iter() { + let value = header.to_str().map_err(|_| AuthzError::MalformedToken)?; + let mut parts: Vec<&str> = value.split_ascii_whitespace().take(2).collect(); + + let jwt = parts.pop().ok_or(AuthzError::MalformedToken)?; + let cid_string = parts.pop().ok_or(AuthzError::MalformedToken)?; + + let cid = Cid::try_from(cid_string).map_err(|_| AuthzError::MalformedToken)?; + + println!("SAVING {} -> {}", cid, jwt); + + blocks + .put_block(&cid, Bytes::new(jwt.as_bytes())) .await - .map_err(|error| { - error!("{:?}", error); - AuthzError::InvalidCredentials - })?, + .map_err(|_| AuthzError::Internal("Failed to save UCAN JWT".into()))?; + } + + Ok(GatewayAuthority::for_bearer( + config.clone(), + did_parser.clone(), + blocks.clone(), + bearer.token(), ) + .await + .map_err(|error| { + error!("{:?}", error); + AuthzError::InvalidCredentials + })?) } } diff --git a/rust/noosphere-gateway/src/gateway/commands/serve.rs b/rust/noosphere-gateway/src/gateway/commands/serve.rs index 583d1c706..1d98e71f6 100644 --- a/rust/noosphere-gateway/src/gateway/commands/serve.rs +++ b/rust/noosphere-gateway/src/gateway/commands/serve.rs @@ -23,7 +23,6 @@ use crate::gateway::{ }; pub const GATEWAY_STATE_STORE: &str = "gateway_state"; -pub const BLOCK_STORE: &str = "blocks"; pub async fn serve( listener: TcpListener, @@ -75,7 +74,7 @@ where .route("/api/v0alpha1/identify", get(identify_handler)) .layer(cors) .layer(Extension(Arc::new(Mutex::new(state)))) - .layer(Extension(Arc::new(Mutex::new(block_store)))) + .layer(Extension(block_store)) .layer(Extension(Arc::new(storage_provider))) .layer(Extension(Arc::new(config))) .layer(Extension(Arc::new(Mutex::new(did_parser)))) @@ -92,7 +91,7 @@ where #[cfg(test)] mod tests { - use std::net::TcpListener; + use std::{net::TcpListener, ops::Deref}; use noosphere::{ authority::generate_ed25519_key, @@ -104,14 +103,17 @@ mod tests { gateway::GatewayReference, }; - use noosphere_storage::{interface::DagCborStore, memory::MemoryStore}; + use noosphere_storage::{ + memory::{MemoryStore}, + ucan::UcanStore, + }; use temp_dir::TempDir; - use ucan::crypto::KeyMaterial; + + use ucan::{crypto::KeyMaterial, store::UcanJwtStore}; use crate::gateway::{ commands::{initialize, serve}, environment::{Blocks, GatewayRoot}, - // tracing::initialize_tracing, }; #[tokio::test] @@ -138,13 +140,18 @@ mod tests { }); let client_task = tokio::spawn(async move { + let ucan_store = UcanStore(MemoryStore::default()); + // ucan_store + // .write_token(&sphere_proof.encode().unwrap()) + // .await + // .unwrap(); let gateway = GatewayReference::try_from_uri(&format!( "http://{}:{}", gateway_address.ip(), gateway_address.port() )) .unwrap(); - let client = Client::identify(&gateway, &owner_key_material, None) + let client = Client::identify(&gateway, &owner_key_material, None, ucan_store) .await .unwrap(); @@ -158,8 +165,8 @@ mod tests { } #[tokio::test] - async fn it_can_receive_a_newly_initialized_subspace_from_the_client() { - //initialize_tracing(); + async fn it_can_receive_a_newly_initialized_sphere_from_the_client() { + // initialize_tracing(); let owner_key_material = generate_ed25519_key(); let owner_did = owner_key_material.get_did().await.unwrap(); @@ -191,6 +198,12 @@ mod tests { .await .unwrap(); + let mut ucan_store = UcanStore(memory_store.clone()); + ucan_store + .write_token(&sphere_proof.encode().unwrap()) + .await + .unwrap(); + let gateway = GatewayReference::try_from_uri(&format!( "http://{}:{}", gateway_address.ip(), @@ -198,18 +211,23 @@ mod tests { )) .unwrap(); - let client = Client::identify(&gateway, &owner_key_material, Some(vec![sphere_proof])) - .await - .unwrap(); + let client = Client::identify( + &gateway, + &owner_key_material, + Some(vec![sphere_proof]), + ucan_store, + ) + .await + .unwrap(); let sphere_did = sphere.try_get_identity().await.unwrap(); - let bundle = sphere.try_as_bundle().await.unwrap(); + let bundle = sphere.try_bundle_until_ancestor(None).await.unwrap(); let push_result = client .push(&PushBody { sphere: sphere_did, base: None, - tip: sphere.cid().clone(), + tip: *sphere.cid(), blocks: bundle.clone(), }) .await @@ -224,17 +242,17 @@ mod tests { .await .unwrap(); - for cid in memory_store.get_stored_cids().await { - debug!("Checking for {}", cid); - assert!(&block_store.contains_cbor(&cid).await.unwrap()); - } + memory_store + .expect_replica_in(block_store.deref()) + .await + .unwrap(); }); client_task.await.unwrap(); } #[tokio::test] - async fn it_can_update_an_existing_subspace_with_changes_from_the_client() { + async fn it_can_update_an_existing_sphere_with_changes_from_the_client() { // initialize_tracing(); let owner_key_material = generate_ed25519_key(); @@ -266,6 +284,11 @@ mod tests { let (sphere, sphere_proof, _) = Sphere::try_generate(&owner_did, &mut memory_store) .await .unwrap(); + let mut ucan_store = UcanStore(memory_store.clone()); + ucan_store + .write_token(&sphere_proof.encode().unwrap()) + .await + .unwrap(); let gateway = GatewayReference::try_from_uri(&format!( "http://{}:{}", @@ -278,19 +301,20 @@ mod tests { &gateway, &owner_key_material, Some(vec![sphere_proof.clone()]), + ucan_store, ) .await .unwrap(); let sphere_did = sphere.try_get_identity().await.unwrap(); - let bundle = sphere.try_as_bundle().await.unwrap(); - let original_cid = sphere.cid().clone(); + let bundle = sphere.try_bundle_until_ancestor(None).await.unwrap(); + let original_cid = *sphere.cid(); let push_result = client .push(&PushBody { sphere: sphere_did.clone(), base: None, - tip: original_cid.clone(), + tip: original_cid, blocks: bundle.clone(), }) .await @@ -299,9 +323,9 @@ mod tests { assert_eq!(push_result, PushResponse::Ok); let mut mutation = SphereMutation::new(&owner_did); - mutation.links_mut().set("zero", sphere.cid()); + mutation.links_mut().set(&"zero".into(), sphere.cid()); - let mut revision = sphere.try_apply(&mutation).await.unwrap(); + let mut revision = sphere.try_apply_mutation(&mutation).await.unwrap(); let first_revision_cid = revision .try_sign(&owner_key_material, Some(&sphere_proof)) .await @@ -309,9 +333,9 @@ mod tests { let sphere = Sphere::at(&first_revision_cid, &memory_store); let mut mutation = SphereMutation::new(&owner_did); - mutation.links_mut().set("one", &first_revision_cid); + mutation.links_mut().set(&"one".into(), &first_revision_cid); - let mut revision = sphere.try_apply(&mutation).await.unwrap(); + let mut revision = sphere.try_apply_mutation(&mutation).await.unwrap(); let second_revision_cid = revision .try_sign(&owner_key_material, Some(&sphere_proof)) .await @@ -319,9 +343,11 @@ mod tests { let sphere = Sphere::at(&second_revision_cid, &memory_store); let mut mutation = SphereMutation::new(&owner_did); - mutation.links_mut().set("two", &second_revision_cid); + mutation + .links_mut() + .set(&"two".into(), &second_revision_cid); - let mut revision = sphere.try_apply(&mutation).await.unwrap(); + let mut revision = sphere.try_apply_mutation(&mutation).await.unwrap(); let final_revision_cid = revision .try_sign(&owner_key_material, Some(&sphere_proof)) .await @@ -349,10 +375,12 @@ mod tests { let block_store = Blocks::from_storage_provider(&storage_provider) .await - .unwrap() - .into_store(); + .unwrap(); - memory_store.expect_replica_in(&block_store).await.unwrap(); + memory_store + .expect_replica_in(block_store.deref()) + .await + .unwrap(); }); client_task.await.unwrap(); diff --git a/rust/noosphere-gateway/src/gateway/crypto.rs b/rust/noosphere-gateway/src/gateway/crypto.rs index f00b2ec38..075d97881 100644 --- a/rust/noosphere-gateway/src/gateway/crypto.rs +++ b/rust/noosphere-gateway/src/gateway/crypto.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::{Result}; use async_trait::async_trait; use noosphere::authority::{ed25519_key_to_mnemonic, generate_ed25519_key, restore_ed25519_key}; use ucan::crypto::KeyMaterial; diff --git a/rust/noosphere-gateway/src/gateway/environment/block.rs b/rust/noosphere-gateway/src/gateway/environment/block.rs index 360cbed94..75b278abb 100644 --- a/rust/noosphere-gateway/src/gateway/environment/block.rs +++ b/rust/noosphere-gateway/src/gateway/environment/block.rs @@ -1,34 +1,36 @@ use std::ops::{Deref, DerefMut}; use anyhow::Result; -use noosphere_storage::interface::{StorageProvider, Store}; +use noosphere_storage::{ + interface::{BlockStore, StorageProvider, Store}, + BLOCK_STORE, +}; -use crate::gateway::commands::BLOCK_STORE; +#[derive(Clone)] +pub struct Blocks(pub S); -pub struct Blocks(pub Storage); - -impl Blocks { - pub async fn from_storage_provider(provider: &Provider) -> Result> +impl Blocks { + pub async fn from_storage_provider(provider: &Provider) -> Result> where - Provider: StorageProvider, + Provider: StorageProvider, { Ok(Blocks(provider.get_store(BLOCK_STORE).await?)) } - pub fn into_store(self) -> Storage { + pub fn into_store(self) -> impl BlockStore { self.0 } } -impl Deref for Blocks { - type Target = Storage; +impl Deref for Blocks { + type Target = S; fn deref(&self) -> &Self::Target { &self.0 } } -impl DerefMut for Blocks { +impl DerefMut for Blocks { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } diff --git a/rust/noosphere-gateway/src/gateway/environment/state.rs b/rust/noosphere-gateway/src/gateway/environment/state.rs index 20b186f24..31c5ec784 100644 --- a/rust/noosphere-gateway/src/gateway/environment/state.rs +++ b/rust/noosphere-gateway/src/gateway/environment/state.rs @@ -2,11 +2,11 @@ use std::ops::{Deref, DerefMut}; use anyhow::Result; use cid::Cid; -use noosphere_cbor::TryDagCbor; + use noosphere_storage::interface::{KeyValueStore, StorageProvider, Store}; use serde::{Deserialize, Serialize}; -use crate::gateway::{commands::GATEWAY_STATE_STORE, schema::PublishedSphere}; +use crate::gateway::{commands::GATEWAY_STATE_STORE}; #[derive(Serialize, Deserialize)] pub struct SphereTracker { @@ -18,7 +18,7 @@ pub struct GatewayState(pub Storage); impl GatewayState { pub async fn get_or_initialize_tracker(&self, sphere: &str) -> Result { - match self.get(sphere).await? { + match self.get_key(sphere).await? { Some(sphere @ SphereTracker { .. }) => Ok(sphere), None => Ok(SphereTracker { latest: None, diff --git a/rust/noosphere-gateway/src/gateway/extractors.rs b/rust/noosphere-gateway/src/gateway/extractors.rs index 8fcad19d0..a1c65d3c4 100644 --- a/rust/noosphere-gateway/src/gateway/extractors.rs +++ b/rust/noosphere-gateway/src/gateway/extractors.rs @@ -33,7 +33,7 @@ where .map_err(|error| GatewayError::Internal(anyhow!(error)))?; Ok(DagCbor( - T::try_from_dag_cbor(&bytes).map_err(|error| GatewayError::Internal(error))?, + T::try_from_dag_cbor(&bytes).map_err(GatewayError::Internal)?, )) } } diff --git a/rust/noosphere-gateway/src/gateway/handlers/fetch.rs b/rust/noosphere-gateway/src/gateway/handlers/fetch.rs index 0131b1790..426fae7ec 100644 --- a/rust/noosphere-gateway/src/gateway/handlers/fetch.rs +++ b/rust/noosphere-gateway/src/gateway/handlers/fetch.rs @@ -3,11 +3,11 @@ use std::sync::Arc; use axum::{extract::Query, response::IntoResponse, Extension}; use noosphere_api::data::FetchParameters; use noosphere_storage::native::NativeStore; -use serde::Deserialize; + pub async fn fetch_handler( - Query(FetchParameters { sphere, since }): Query, - Extension(store): Extension>, + Query(FetchParameters { sphere: _, since: _ }): Query, + Extension(_store): Extension>, ) -> impl IntoResponse { todo!() } diff --git a/rust/noosphere-gateway/src/gateway/handlers/push.rs b/rust/noosphere-gateway/src/gateway/handlers/push.rs index c6bf44e15..8de56af77 100644 --- a/rust/noosphere-gateway/src/gateway/handlers/push.rs +++ b/rust/noosphere-gateway/src/gateway/handlers/push.rs @@ -1,9 +1,11 @@ -use anyhow::{anyhow, Result}; +use anyhow::Result; use async_std::sync::Mutex; use hyper::StatusCode; +use libipld_cbor::DagCborCodec; use noosphere::view::{Sphere, Timeline}; use noosphere_api::data::PushResponse; use noosphere_storage::interface::KeyValueStore; +use std::ops::Deref; use std::sync::Arc; use axum::debug_handler; @@ -12,7 +14,7 @@ use noosphere_api::{ authority::GatewayAction, data::{MissingRevisionsResponse, OutOfDateResponse, PushBody}, }; -use noosphere_storage::{interface::DagCborStore, native::NativeStore}; +use noosphere_storage::{interface::BlockStore, native::NativeStore}; use crate::gateway::environment::{Blocks, SphereTracker}; use crate::gateway::extractors::DagCbor; @@ -20,34 +22,33 @@ use crate::gateway::{authority::GatewayAuthority, environment::GatewayState, Gat async fn incorporate_lineage( state: Arc>>, - store: Arc>>, + mut store: Blocks, push_body: &PushBody, ) -> Result<()> { - let mut store = store.lock().await.clone(); - - for (expected_cid, block) in push_body.blocks.map() { - let actual_cid = store.write_cbor(block).await?; - if expected_cid != &actual_cid { - return Err(anyhow!("Invalid block")); - } + for (cid, block) in push_body.blocks.map() { + debug!("Saving pushed block {}", cid); + // TODO: Use SphereDb instead + store.put_block(cid, block).await?; + store.put_links::(cid, block).await?; } let PushBody { base, tip, .. } = push_body; - let timeline = Timeline::new(&store); + let timeline = Timeline::new(store.deref()); let timeslice = timeline.slice(tip, base.as_ref()); let steps = timeslice.try_to_chronological().await?; for (cid, _) in steps { - Sphere::at(&cid, &store).try_hydrate().await?; + debug!("Hydrating {}", cid); + Sphere::at(&cid, store.deref()).try_hydrate().await?; } let mut state = state.lock().await; let mut tracker = state.get_or_initialize_tracker(&push_body.sphere).await?; - tracker.latest = Some(push_body.tip.clone()); + tracker.latest = Some(push_body.tip); - state.set(&push_body.sphere, &tracker).await?; + state.set_key(&push_body.sphere, &tracker).await?; Ok(()) } @@ -57,7 +58,7 @@ pub async fn push_handler( authority: GatewayAuthority, ContentLengthLimit(DagCbor(push_body)): ContentLengthLimit, { 1024 * 5000 }>, Extension(state): Extension>>>, - Extension(store): Extension>>>, + Extension(store): Extension>, ) -> Result<(StatusCode, Json), GatewayError> { authority.try_authorize(GatewayAction::Push).await?; diff --git a/rust/noosphere-gateway/src/main.rs b/rust/noosphere-gateway/src/main.rs index ea0c51e5b..88c82eda2 100644 --- a/rust/noosphere-gateway/src/main.rs +++ b/rust/noosphere-gateway/src/main.rs @@ -2,10 +2,8 @@ extern crate tracing; #[cfg(target_arch = "wasm32")] -mod inner { - pub async fn main() -> anyhow::Result<()> { - Ok(()) - } +pub fn main() -> anyhow::Result<()> { + Ok(()) } #[cfg(not(target_arch = "wasm32"))] @@ -13,17 +11,13 @@ mod gateway; #[cfg(not(target_arch = "wasm32"))] mod inner { - use std::net::{SocketAddr, TcpListener}; + use std::net::TcpListener; use crate::gateway::{ - commands, - environment::{GatewayConfig, GatewayRoot}, - tracing::initialize_tracing, - Cli, Command, + commands, environment::GatewayRoot, tracing::initialize_tracing, Cli, Command, }; use anyhow::Result; use clap::Parser; - use noosphere_storage::native::{NativeStorageInit, NativeStorageProvider}; pub async fn main() -> Result<()> { initialize_tracing(); @@ -53,6 +47,7 @@ mod inner { } } +#[cfg(not(target_arch = "wasm32"))] #[tokio::main] async fn main() -> anyhow::Result<()> { inner::main().await diff --git a/rust/noosphere-into/Cargo.toml b/rust/noosphere-into/Cargo.toml index c945a40ac..2fcf97dca 100644 --- a/rust/noosphere-into/Cargo.toml +++ b/rust/noosphere-into/Cargo.toml @@ -27,6 +27,11 @@ futures = { version = "~0.3" } async-compat = { version = "~0.2" } async-utf8-decoder = { version = "~0.3" } +ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } +ucan-key-support = { version = "0.4.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } + + + [dev-dependencies] wasm-bindgen-test = "~0.3" @@ -39,11 +44,3 @@ axum-extra = { version = "~0.3", features = ["spa"] } tower-http = { version = "~0.3", features = ["fs", "trace"] } tracing = "~0.1" tracing-subscriber = { version = "~0.3", features = ["env-filter"] } - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git" } -ucan-key-support = { version = "0.4.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git" } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -ucan = { version = "0.6.0-alpha.1", features = ["web"], git = "https://github.com/cdata/rs-ucan.git" } -ucan-key-support = { version = "0.4.0-alpha.1", features = ["web"], git = "https://github.com/cdata/rs-ucan.git" } \ No newline at end of file diff --git a/rust/noosphere-into/examples/notes-to-html/implementation.rs b/rust/noosphere-into/examples/notes-to-html/implementation.rs index 365217817..09c33d0d0 100644 --- a/rust/noosphere-into/examples/notes-to-html/implementation.rs +++ b/rust/noosphere-into/examples/notes-to-html/implementation.rs @@ -9,31 +9,27 @@ use tower_http::services::ServeDir; use noosphere::{ authority::generate_ed25519_key, - data::{ContentType, Header, ReferenceIpld}, + data::{ContentType, Header}, view::Sphere, }; -use noosphere_storage::{interface::KeyValueStore, memory::MemoryStore}; +use noosphere_storage::{ + db::SphereDb, + memory::{MemoryStorageProvider}, +}; use ucan::crypto::KeyMaterial; pub async fn main() -> Result<()> { - let mut sphere_store = MemoryStore::default(); - let mut block_store = MemoryStore::default(); + let storage_provider = MemoryStorageProvider::default(); + let mut db = SphereDb::new(&storage_provider).await.unwrap(); let owner_key = generate_ed25519_key(); let owner_did = owner_key.get_did().await?; - let (sphere, proof, _) = Sphere::try_generate(&owner_did, &mut block_store).await?; + let (sphere, proof, _) = Sphere::try_generate(&owner_did, &mut db).await?; let sphere_identity = sphere.try_get_identity().await?; - sphere_store - .set( - &sphere_identity, - &ReferenceIpld { - link: sphere.cid().clone(), - }, - ) - .await?; + db.set_version(&sphere_identity, sphere.cid()).await?; let content_root = std::env::current_dir()?.join(Path::new("examples/notes-to-html/content")); let html_root = TempDir::new()?; @@ -41,7 +37,7 @@ pub async fn main() -> Result<()> { println!("Content root: {:?}", content_root); println!("HTML root: {:?}", html_root.path()); - let mut sphere_fs = SphereFs::latest(&sphere_identity, &block_store, &sphere_store).await?; + let mut sphere_fs = SphereFs::latest(&sphere_identity, &db).await?; let mut read_dir = fs::read_dir(content_root).await?; @@ -61,7 +57,7 @@ pub async fn main() -> Result<()> { .as_bytes(), )?; let file = File::open(&file_path).await?; - let title = capitalize(&slug); + let title = capitalize(slug); sphere_fs .write( @@ -79,7 +75,7 @@ pub async fn main() -> Result<()> { root: html_root.path().to_path_buf(), }; - sphere_into_html(&sphere_identity, &sphere_store, &block_store, &native_fs).await?; + sphere_into_html(&sphere_identity, &db, &native_fs).await?; let app = Router::new() .fallback(get_service(ServeDir::new(html_root.path())).handle_error(handle_error)); diff --git a/rust/noosphere-into/src/html/into.rs b/rust/noosphere-into/src/html/into.rs index 59b51dfe0..6455fd6b2 100644 --- a/rust/noosphere-into/src/html/into.rs +++ b/rust/noosphere-into/src/html/into.rs @@ -2,9 +2,12 @@ use std::{collections::BTreeSet, io::Cursor, path::PathBuf, sync::Arc}; use anyhow::{anyhow, Result}; use cid::Cid; -use noosphere::{data::ReferenceIpld, view::Sphere}; +use noosphere::{view::Sphere}; use noosphere_fs::SphereFs; -use noosphere_storage::interface::{KeyValueStore, Store}; +use noosphere_storage::{ + db::SphereDb, + interface::{Store}, +}; use tokio::sync::Mutex; use tokio_stream::StreamExt; @@ -19,16 +22,15 @@ static DEFAULT_STYLES: &[u8] = include_bytes!("./static/styles.css"); /// of the slug-named content of the sphere. pub async fn sphere_into_html( sphere_identity: &str, - sphere_store: &S, - block_store: &S, + db: &SphereDb, write_target: &W, ) -> Result<()> where S: Store + 'static, W: WriteTarget + 'static, { - let mut next_sphere_cid = match sphere_store.get(sphere_identity).await? { - Some(ReferenceIpld { link }) => Some(link), + let mut next_sphere_cid = match db.get_version(sphere_identity).await? { + Some(link) => Some(link), _ => { return Err(anyhow!( "Could not resolve CID for sphere {}", @@ -41,8 +43,7 @@ where let mut latest_revision = true; while let Some(sphere_cid) = next_sphere_cid { - let sphere_index: PathBuf = - format!("permalink/{}/index.html", sphere_cid.to_string()).into(); + let sphere_index: PathBuf = format!("permalink/{}/index.html", sphere_cid).into(); // We write the sphere index last, so if we already have it we can // assume this revision has been written in the past @@ -53,14 +54,14 @@ where } let write_actions = Arc::new(Mutex::new(BTreeSet::::new())); - let sphere = Sphere::at(&sphere_cid, block_store); + let sphere = Sphere::at(&sphere_cid, db); let links = sphere.try_get_links().await?; let mut link_stream = links.stream().await?; let mut tasks = Vec::new(); while let Some(Ok((slug, cid))) = link_stream.next().await { - let file_name: PathBuf = format!("permalink/{}/index.html", cid.to_string()).into(); + let file_name: PathBuf = format!("permalink/{}/index.html", cid).into(); // Skip this write entirely if the content has been written // TODO(#55): This may not hold in a world where there are multiple @@ -73,12 +74,13 @@ where tasks.push(W::spawn({ let slug = slug.clone(); - let cid = cid.clone(); + let cid = *cid; let write_actions = write_actions.clone(); let write_target = write_target.clone(); let sphere_identity = sphere_identity.to_string(); - let block_store = block_store.clone(); - let sphere_store = sphere_store.clone(); + let db = db.clone(); + // let block_store = block_store.clone(); + // let sphere_store = sphere_store.clone(); let latest_revision = latest_revision; async move { @@ -91,13 +93,12 @@ where if write_actions.contains(&cid) { return Ok(()); } else { - write_actions.insert(cid.clone()); + write_actions.insert(cid); } } let fs = - SphereFs::at(&sphere_identity, &sphere_cid, &block_store, &sphere_store)? - .ok_or_else(|| { + SphereFs::at(&sphere_identity, &sphere_cid, &db)?.ok_or_else(|| { anyhow!( "Unable to find revision for sphere {}: {}", sphere_identity, @@ -132,14 +133,13 @@ where // cases where writing content may fail futures::future::try_join_all(tasks).await?; - let fs = SphereFs::at(&sphere_identity, &sphere_cid, block_store, sphere_store)? - .ok_or_else(|| { - anyhow!( - "Unable to find revision for sphere {}: {}", - sphere_identity, - sphere_cid - ) - })?; + let fs = SphereFs::at(sphere_identity, &sphere_cid, db)?.ok_or_else(|| { + anyhow!( + "Unable to find revision for sphere {}: {}", + sphere_identity, + sphere_cid + ) + })?; let sphere_transformer = SphereToHtmlTransformer::new(&fs); if let Some(read) = sphere_transformer.transform().await? { @@ -183,11 +183,14 @@ pub mod tests { use noosphere::{ authority::generate_ed25519_key, - data::{ContentType, Header, ReferenceIpld}, + data::{ContentType, Header}, view::Sphere, }; use noosphere_fs::SphereFs; - use noosphere_storage::{interface::KeyValueStore, memory::MemoryStore}; + use noosphere_storage::{ + db::SphereDb, + memory::{MemoryStorageProvider}, + }; use ucan::crypto::KeyMaterial; #[cfg(target_arch = "wasm32")] @@ -202,31 +205,24 @@ pub mod tests { #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] async fn it_writes_a_file_from_the_sphere_to_the_target_as_html() { - let mut sphere_store = MemoryStore::default(); - let mut block_store = MemoryStore::default(); + // let mut sphere_store = MemoryStore::default(); + // let mut block_store = MemoryStore::default(); + + let storage_provider = MemoryStorageProvider::default(); + let mut db = SphereDb::new(&storage_provider).await.unwrap(); let owner_key = generate_ed25519_key(); let owner_did = owner_key.get_did().await.unwrap(); - let (sphere, proof, _) = Sphere::try_generate(&owner_did, &mut block_store) - .await - .unwrap(); + let (sphere, proof, _) = Sphere::try_generate(&owner_did, &mut db).await.unwrap(); let sphere_identity = sphere.try_get_identity().await.unwrap(); - sphere_store - .set( - &sphere_identity, - &ReferenceIpld { - link: sphere.cid().clone(), - }, - ) + db.set_version(&sphere_identity, sphere.cid()) .await .unwrap(); - let mut fs = SphereFs::latest(&sphere_identity, &block_store, &sphere_store) - .await - .unwrap(); + let mut fs = SphereFs::latest(&sphere_identity, &db).await.unwrap(); let cats_cid = fs .write( @@ -254,15 +250,12 @@ pub mod tests { let write_target = MemoryWriteTarget::default(); - sphere_into_html(&sphere_identity, &sphere_store, &block_store, &write_target) + sphere_into_html(&sphere_identity, &db, &write_target) .await .unwrap(); let bytes = write_target - .read(&PathBuf::from(format!( - "permalink/{}/index.html", - cats_cid.to_string() - ))) + .read(&PathBuf::from(format!("permalink/{}/index.html", cats_cid))) .await .unwrap(); @@ -296,31 +289,21 @@ pub mod tests { #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] async fn it_symlinks_a_file_slug_to_the_latest_file_version() { - let mut sphere_store = MemoryStore::default(); - let mut block_store = MemoryStore::default(); + let storage_provider = MemoryStorageProvider::default(); + let mut db = SphereDb::new(&storage_provider).await.unwrap(); let owner_key = generate_ed25519_key(); let owner_did = owner_key.get_did().await.unwrap(); - let (sphere, proof, _) = Sphere::try_generate(&owner_did, &mut block_store) - .await - .unwrap(); + let (sphere, proof, _) = Sphere::try_generate(&owner_did, &mut db).await.unwrap(); let sphere_identity = sphere.try_get_identity().await.unwrap(); - sphere_store - .set( - &sphere_identity, - &ReferenceIpld { - link: sphere.cid().clone(), - }, - ) + db.set_version(&sphere_identity, sphere.cid()) .await .unwrap(); - let mut fs = SphereFs::latest(&sphere_identity, &block_store, &sphere_store) - .await - .unwrap(); + let mut fs = SphereFs::latest(&sphere_identity, &db).await.unwrap(); let _cats_cid = fs .write( @@ -348,7 +331,7 @@ pub mod tests { let write_target = MemoryWriteTarget::default(); - sphere_into_html(&sphere_identity, &sphere_store, &block_store, &write_target) + sphere_into_html(&sphere_identity, &db, &write_target) .await .unwrap(); diff --git a/rust/noosphere-into/src/html/transform/subtext.rs b/rust/noosphere-into/src/html/transform/subtext.rs index 4bdb8447e..2505cdf44 100644 --- a/rust/noosphere-into/src/html/transform/subtext.rs +++ b/rust/noosphere-into/src/html/transform/subtext.rs @@ -80,8 +80,7 @@ where let mut transclude_html_strings = Vec::new(); let content_entities: Vec = block .to_content_entities() - .into_iter() - .map(|entity| entity.clone()) + .into_iter().cloned() .collect(); let is_solo_slashlink = if let (Some(&Entity::SlashLink(_)), 1) = (content_entities.first(), content_entities.len()) @@ -131,12 +130,12 @@ where let transclude_html = transclude_html_strings.join("\n"); Ok(html! { - @if content_html.len() > 0 || transclude_html.len() > 0 { + @if !content_html.is_empty() || !transclude_html.is_empty() { li(class="block") { - @if content_html.len() > 0 { + @if !content_html.is_empty() { section(class="block-content") : Raw(&content_html); } - @if transclude_html.len() > 0 { + @if !transclude_html.is_empty() { ul(class="block-transcludes") : Raw(&transclude_html); } } diff --git a/rust/noosphere-into/src/html/transform/transclude.rs b/rust/noosphere-into/src/html/transform/transclude.rs index 5d12540ff..32fa73398 100644 --- a/rust/noosphere-into/src/html/transform/transclude.rs +++ b/rust/noosphere-into/src/html/transform/transclude.rs @@ -26,9 +26,7 @@ where pub async fn transform(&'a self, slug: &str) -> Result> { let transclude = self.transcluder.transclude(slug).await?; - Ok(match transclude { - Some(Transclude::Text(text_transclude)) => Some( - html! { + Ok(transclude.map(|Transclude::Text(text_transclude)| html! { li(class="transclude-item") { a(class="transclude-format-text", href=&text_transclude.href) { @ if let Some(title) = &text_transclude.title { @@ -43,9 +41,6 @@ where } } } - .to_string(), - ), - None => None, - }) + .to_string())) } } diff --git a/rust/noosphere-storage/Cargo.toml b/rust/noosphere-storage/Cargo.toml index a94649063..61017aa3b 100644 --- a/rust/noosphere-storage/Cargo.toml +++ b/rust/noosphere-storage/Cargo.toml @@ -12,6 +12,10 @@ async-trait = "~0.1" cid = "~0.8" noosphere-cbor = { path = "../noosphere-cbor" } tracing = "~0.1" +ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } +libipld-core = "~0.14" +libipld-cbor = "~0.14" +serde = "^1" [dev-dependencies] witty-phrase-generator = "~0.2" diff --git a/rust/noosphere-storage/src/db.rs b/rust/noosphere-storage/src/db.rs new file mode 100644 index 000000000..bf976a660 --- /dev/null +++ b/rust/noosphere-storage/src/db.rs @@ -0,0 +1,132 @@ +use anyhow::Result; +use async_trait::async_trait; +use cid::Cid; +use libipld_core::{ + codec::{Codec, References}, + ipld::Ipld, +}; + +use crate::interface::{BlockStore, KeyValueStore, StorageProvider, Store}; + +#[cfg(not(target_arch = "wasm32"))] +pub trait SphereDbSendSync: Send + Sync {} + +#[cfg(not(target_arch = "wasm32"))] +impl SphereDbSendSync for T where T: Send + Sync {} + +#[cfg(target_arch = "wasm32")] +pub trait SphereDbSendSync {} + +#[cfg(target_arch = "wasm32")] +impl SphereDbSendSync for T {} + +pub const BLOCK_STORE: &str = "blocks"; +pub const LINK_STORE: &str = "links"; +pub const VERSION_STORE: &str = "versions"; + +#[derive(Clone)] +pub struct SphereDb +where + S: Store, +{ + block_store: S, + link_store: S, + version_store: S, +} + +impl SphereDb +where + S: Store, +{ + pub async fn new>(storage_provider: &P) -> Result> { + Ok(SphereDb { + block_store: storage_provider.get_store(BLOCK_STORE).await?, + link_store: storage_provider.get_store(LINK_STORE).await?, + version_store: storage_provider.get_store(VERSION_STORE).await?, + }) + } + + pub async fn set_version(&mut self, identity: &str, version: &Cid) -> Result<()> { + self.version_store + .set_key(identity.to_string(), version) + .await + } + + pub async fn get_version(&self, identity: &str) -> Result> { + self.version_store.get_key(identity).await + } + + pub async fn get_links(&self, cid: &Cid) -> Result>> { + self.link_store.get_key(&cid.to_string()).await + } + + pub fn to_block_store(&self) -> S { + self.block_store.clone() + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl BlockStore for SphereDb +where + S: Store, +{ + async fn put_links(&mut self, cid: &Cid, block: &[u8]) -> Result<()> + where + C: Codec + Default, + Ipld: References, + { + let codec = C::default(); + let mut links = Vec::new(); + + codec.references::(block, &mut links)?; + + self.link_store.set_key(&cid.to_string(), links).await?; + + Ok(()) + } + + async fn put_block(&mut self, cid: &cid::Cid, block: &[u8]) -> Result<()> { + self.block_store.put_block(cid, block).await + } + + async fn get_block(&self, cid: &cid::Cid) -> Result>> { + self.block_store.get_block(cid).await + } +} + +#[cfg(test)] +mod tests { + + use libipld_cbor::DagCborCodec; + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; + + use crate::{interface::BlockStore, memory::MemoryStorageProvider}; + + use super::SphereDb; + + #[cfg(target_arch = "wasm32")] + wasm_bindgen_test_configure!(run_in_browser); + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + pub async fn it_stores_links_when_a_block_is_saved() { + let storage_provider = MemoryStorageProvider::default(); + let mut db = SphereDb::new(&storage_provider).await.unwrap(); + + let list1 = vec!["cats", "dogs", "pigeons"]; + let list2 = vec!["apples", "oranges", "starfruit"]; + + let cid1 = db.save::(&list1).await.unwrap(); + let cid2 = db.save::(&list2).await.unwrap(); + + let list3 = vec![cid1, cid2]; + + let cid3 = db.save::(&list3).await.unwrap(); + + let links = db.get_links(&cid3).await.unwrap(); + + assert_eq!(Some(list3), links); + } +} diff --git a/rust/noosphere-storage/src/helpers.rs b/rust/noosphere-storage/src/helpers.rs index 9c9243b60..c02f22134 100644 --- a/rust/noosphere-storage/src/helpers.rs +++ b/rust/noosphere-storage/src/helpers.rs @@ -11,7 +11,7 @@ pub async fn make_disposable_store() -> Result { .with_words(3) .unwrap() .into_iter() - .map(|word| String::from(word)) + .map(String::from) .collect(); let db = sled::open(temp_dir.join(temp_name)).unwrap(); let provider = NativeStorageProvider::new(NativeStorageInit::Db(db))?; diff --git a/rust/noosphere-storage/src/interface.rs b/rust/noosphere-storage/src/interface.rs index 064585306..56bb38b3a 100644 --- a/rust/noosphere-storage/src/interface.rs +++ b/rust/noosphere-storage/src/interface.rs @@ -1,12 +1,21 @@ +use std::io::Cursor; + use anyhow::{anyhow, Result}; use async_trait::async_trait; use cid::{ multihash::{Code, MultihashDigest}, Cid, }; -use noosphere_cbor::{TryDagCbor, TryDagCborSendSync}; - -pub const DAG_CBOR_CODEC: u64 = 0x71; +use libipld_cbor::DagCborCodec; +use libipld_core::{ + codec::References, + serde::{from_ipld, to_ipld}, +}; +use libipld_core::{ + codec::{Codec, Decode, Encode}, + ipld::Ipld, +}; +use serde::{de::DeserializeOwned, Serialize}; #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -17,9 +26,15 @@ pub trait StorageProvider { #[cfg(not(target_arch = "wasm32"))] pub trait StoreConditionalSendSync: Send + Sync {} +#[cfg(not(target_arch = "wasm32"))] +impl StoreConditionalSendSync for S where S: Send + Sync {} + #[cfg(target_arch = "wasm32")] pub trait StoreConditionalSendSync {} +#[cfg(target_arch = "wasm32")] +impl StoreConditionalSendSync for S {} + #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] pub trait Store: Clone + StoreConditionalSendSync { @@ -30,9 +45,6 @@ pub trait Store: Clone + StoreConditionalSendSync { /// value stored against that key if any async fn write(&mut self, key: &[u8], bytes: &[u8]) -> Result>>; - /// Returns true if the given key can be read from local storage - async fn contains(&self, key: &[u8]) -> Result; - /// Remove a value given a key, returning the removed value if any async fn remove(&mut self, key: &[u8]) -> Result>>; @@ -42,91 +54,162 @@ pub trait Store: Clone + StoreConditionalSendSync { } } +#[cfg(not(target_arch = "wasm32"))] +pub trait BlockStoreSendSync: Send + Sync {} + +#[cfg(not(target_arch = "wasm32"))] +impl BlockStoreSendSync for T where T: Send + Sync {} + +#[cfg(target_arch = "wasm32")] +pub trait BlockStoreSendSync {} + +#[cfg(target_arch = "wasm32")] +impl BlockStoreSendSync for T {} + +#[cfg(not(target_arch = "wasm32"))] +pub trait BlockStoreSend: Send {} + +#[cfg(not(target_arch = "wasm32"))] +impl BlockStoreSend for T where T: Send {} + +#[cfg(target_arch = "wasm32")] +pub trait BlockStoreSend {} + +#[cfg(target_arch = "wasm32")] +impl BlockStoreSend for T {} + #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -pub trait DagCborStore: Store { - /// Saves a serializable type and returns the CID of the DAG-CBOR bytes - async fn save(&mut self, data: &Type) -> Result { - let cbor = data.try_into_dag_cbor()?; - self.write_cbor(&cbor).await +pub trait BlockStore: Clone + BlockStoreSendSync { + #[allow(unused_variables)] + async fn put_links(&mut self, cid: &Cid, block: &[u8]) -> Result<()> + where + C: Codec + Default, + Ipld: References, + { + Ok(()) } - /// Loads a deserializable type from storage - async fn load(&self, cid: &Cid) -> Result { - let bytes = self.require_cbor(cid).await?; - Type::try_from_dag_cbor(&bytes) + async fn put_block(&mut self, cid: &Cid, block: &[u8]) -> Result<()>; + + async fn get_block(&self, cid: &Cid) -> Result>>; + + async fn put(&mut self, data: T) -> Result + where + C: Codec + Default, + T: Encode + BlockStoreSend + core::fmt::Debug, + Ipld: References, + { + let codec = C::default(); + let block = codec.encode(&data)?; + let cid = Cid::new_v1(codec.into(), Code::Blake2b256.digest(&block)); + + self.put_block(&cid, &block).await?; + self.put_links::(&cid, &block).await?; + + Ok(cid) } - /// Helper to conditionally load an Option - async fn load_some( - &self, - maybe_cid: Option<&Cid>, - ) -> Result> { - Ok(match maybe_cid { - Some(cid) => Some(self.load(cid).await?), + async fn get(&self, cid: &Cid) -> Result> + where + C: Codec + Default, + T: Decode, + { + let codec = C::default(); + let block = self.get_block(cid).await?; + + Ok(match block { + Some(bytes) => Some(T::decode(codec, &mut Cursor::new(bytes))?), None => None, }) } - /// Read optionally available DAG-CBOR bytes from the store given a CID - async fn read_cbor(&self, cid: &Cid) -> Result>> { - Ok(self.read(&cid.to_bytes()).await?) + async fn save(&mut self, data: T) -> Result + where + C: Codec + Default, + T: Serialize + BlockStoreSend, + Ipld: Encode + References, + { + self.put::(to_ipld(data)?).await } - /// Read DAG-CBOR bytes, but return an error if none are found - async fn require_cbor(&self, cid: &Cid) -> Result> { - match self.read_cbor(cid).await? { - Some(bytes) => Ok(bytes), - None => Err(anyhow!("No bytes found for {:?}", cid)), + async fn load(&self, cid: &Cid) -> Result + where + C: Codec + Default, + T: DeserializeOwned + BlockStoreSend, + u64: From, + Ipld: Decode, + { + let codec = u64::from(C::default()); + + if cid.codec() != codec { + return Err(anyhow!( + "Incorrect codec; expected {}, but CID refers to {}", + codec, + cid.codec() + )); } - } - /// Writes DAG-CBOR bytes to storage and returns the CID of those bytes - async fn write_cbor(&mut self, bytes: &[u8]) -> Result { - let cid = Self::make_cid(bytes); - trace!("Writing CBOR for {}", cid); - if !self.contains_cbor(&cid).await? { - self.write(&cid.to_bytes(), bytes).await?; - } - Ok(cid) + Ok(match self.get::(cid).await? { + Some(ipld) => from_ipld(ipld)?, + None => return Err(anyhow!("No block found for {}", cid)), + }) } - /// Returns true if bytes are stored against the given CID in local storage - async fn contains_cbor(&self, cid: &Cid) -> Result { - self.contains(&cid.to_bytes()).await + async fn require_block(&self, cid: &Cid) -> Result> { + match self.get_block(cid).await? { + Some(block) => Ok(block), + None => Err(anyhow!("Block {cid} was required but not found")), + } } +} - /// Removes a value from storage by CID and returns the removed value - async fn remove_dag(&mut self, cid: &Cid) -> Result>> { - self.remove(&cid.to_bytes()).await +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl BlockStore for S +where + S: Store, +{ + async fn put_block(&mut self, cid: &Cid, block: &[u8]) -> Result<()> { + self.write(&cid.to_bytes(), block).await?; + Ok(()) } - /// Helper to derive a CID from a byte slice (assumed to be encoded DAG-CBOR) - fn make_cid(bytes: &[u8]) -> Cid { - Cid::new_v1(DAG_CBOR_CODEC, Code::Blake2b256.digest(bytes)) + async fn get_block(&self, cid: &Cid) -> Result>> { + self.read(&cid.to_bytes()).await } } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] pub trait KeyValueStore: Store { - async fn set(&mut self, key: &str, value: &V) -> Result<()> { - let bytes = value.try_into_dag_cbor()?; - self.write(key.as_ref(), &bytes).await?; + async fn set_key(&mut self, key: K, value: V) -> Result<()> + where + K: AsRef<[u8]> + BlockStoreSend, + V: Serialize + BlockStoreSend, + { + let ipld = to_ipld(value)?; + let codec = DagCborCodec; + let cbor = codec.encode(&ipld)?; + let key_bytes = K::as_ref(&key); + self.write(key_bytes, &cbor).await?; Ok(()) } - async fn get(&self, key: &str) -> Result> { - Ok(match self.read(key.as_ref()).await? { - Some(bytes) => Some(V::try_from_dag_cbor(&bytes)?), + async fn get_key(&self, key: K) -> Result> + where + K: AsRef<[u8]> + BlockStoreSend, + V: DeserializeOwned + BlockStoreSend, + { + let key_bytes = K::as_ref(&key); + Ok(match self.read(key_bytes).await? { + Some(bytes) => Some(from_ipld(Ipld::decode( + DagCborCodec, + &mut Cursor::new(bytes), + )?)?), None => None, }) } } -/// Blanket implementations for anything that implements Store -impl KeyValueStore for S {} - -impl DagCborStore for S {} - -impl StoreConditionalSendSync for S {} +impl KeyValueStore for S where S: Store {} diff --git a/rust/noosphere-storage/src/lib.rs b/rust/noosphere-storage/src/lib.rs index 0c570615c..a49fb46e7 100644 --- a/rust/noosphere-storage/src/lib.rs +++ b/rust/noosphere-storage/src/lib.rs @@ -1,9 +1,13 @@ #[macro_use] extern crate tracing; +pub mod db; pub mod interface; pub mod memory; pub mod tracking; +pub mod ucan; + +pub const BLOCK_STORE: &str = "blocks"; #[cfg(not(target_arch = "wasm32"))] pub mod native; @@ -15,8 +19,9 @@ pub mod helpers; #[cfg(test)] mod tests { - use crate::{helpers::make_disposable_store, interface::DagCborStore}; + use crate::{helpers::make_disposable_store, interface::BlockStore}; + use libipld_cbor::DagCborCodec; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::wasm_bindgen_test; @@ -28,9 +33,9 @@ mod tests { let mut storage = make_disposable_store().await.unwrap(); let bytes = b"I love every kind of cat"; - let cid = storage.write_cbor(bytes).await.unwrap(); - let retrieved = storage.read_cbor(&cid).await.unwrap(); + let cid = storage.save::(bytes).await.unwrap(); + let retrieved = storage.load::>(&cid).await.unwrap(); - assert_eq!(retrieved, Some(bytes.to_vec())); + assert_eq!(retrieved, bytes); } } diff --git a/rust/noosphere-storage/src/memory.rs b/rust/noosphere-storage/src/memory.rs index 5eefc776e..6ec98ec7b 100644 --- a/rust/noosphere-storage/src/memory.rs +++ b/rust/noosphere-storage/src/memory.rs @@ -4,7 +4,43 @@ use async_trait::async_trait; use cid::Cid; use std::{collections::HashMap, sync::Arc}; -use crate::interface::{DagCborStore, Store}; +use crate::interface::{StorageProvider, Store}; + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +pub trait StoreContainsCid { + async fn contains_cid(&self, cid: &Cid) -> Result; +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl StoreContainsCid for S { + async fn contains_cid(&self, cid: &Cid) -> Result { + Ok(self.read(&cid.to_bytes()).await?.is_some()) + } +} + +#[derive(Default)] +pub struct MemoryStorageProvider { + stores: Arc>>, +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl StorageProvider for MemoryStorageProvider { + async fn get_store(&self, name: &str) -> Result { + let mut stores = self.stores.lock().await; + + if !stores.contains_key(name) { + stores.insert(name.to_string(), Default::default()); + } + + stores + .get(name) + .cloned() + .ok_or_else(|| anyhow!("Failed to initialize {} store", name)) + } +} #[derive(Clone, Default, Debug)] pub struct MemoryStore { @@ -31,13 +67,13 @@ impl MemoryStore { for cid in cids { trace!("Checking for {}", cid); - if !other.contains_cbor(&cid).await? { + if !other.contains_cid(&cid).await? { trace!("Not found!"); missing.push(cid); } } - if missing.len() > 0 { + if !missing.is_empty() { return Err(anyhow!( "Expected replica, but the following CIDs are missing: {:#?}", missing @@ -74,11 +110,6 @@ impl Store for MemoryStore { Ok(old_value) } - async fn contains(&self, key: &[u8]) -> Result { - let dags = self.dags.lock().await; - Ok(dags.contains_key(key)) - } - async fn remove(&mut self, key: &[u8]) -> Result>> { let mut dags = self.dags.lock().await; Ok(dags.remove(key)) diff --git a/rust/noosphere-storage/src/native.rs b/rust/noosphere-storage/src/native.rs index 3e73a9513..933d075b0 100644 --- a/rust/noosphere-storage/src/native.rs +++ b/rust/noosphere-storage/src/native.rs @@ -46,7 +46,6 @@ impl NativeStore { #[async_trait] impl Store for NativeStore { - // impl Store for NativeStore { async fn read(&self, key: &[u8]) -> Result>> { Ok(self.db.get(key)?.map(|entry| entry.to_vec())) } @@ -59,10 +58,6 @@ impl Store for NativeStore { Ok(old_bytes) } - async fn contains(&self, key: &[u8]) -> Result { - Ok(self.db.contains_key(key)?) - } - /// Remove a value given a CID async fn remove(&mut self, key: &[u8]) -> Result>> { Ok(self diff --git a/rust/noosphere-storage/src/tracking.rs b/rust/noosphere-storage/src/tracking.rs index 4c65dec2a..3e5ccb045 100644 --- a/rust/noosphere-storage/src/tracking.rs +++ b/rust/noosphere-storage/src/tracking.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use crate::interface::Store; -#[derive(Debug, Clone, Default, PartialEq)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct StoreStats { pub reads: usize, pub writes: usize, @@ -57,12 +57,6 @@ impl Store for TrackingStore { self.store.write(key, bytes).await } - async fn contains(&self, key: &[u8]) -> Result { - let mut stats = self.stats.lock().await; - stats.reads += 1; - self.store.contains(key).await - } - async fn remove(&mut self, key: &[u8]) -> Result>> { let mut stats = self.stats.lock().await; stats.removes += 1; diff --git a/rust/noosphere-storage/src/ucan.rs b/rust/noosphere-storage/src/ucan.rs new file mode 100644 index 000000000..35ad1c5aa --- /dev/null +++ b/rust/noosphere-storage/src/ucan.rs @@ -0,0 +1,28 @@ +use anyhow::Result; +use async_trait::async_trait; +use cid::Cid; +use core::fmt::Debug; +use libipld_core::{ + codec::{Decode, Encode}, + raw::RawCodec, +}; +use ucan::store::{UcanStore as UcanStoreTrait, UcanStoreConditionalSend}; + +use crate::interface::BlockStore; + +pub struct UcanStore(pub S); + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl UcanStoreTrait for UcanStore { + async fn read>(&self, cid: &Cid) -> Result> { + self.0.get::(cid).await + } + + async fn write + UcanStoreConditionalSend + Debug>( + &mut self, + token: T, + ) -> Result { + self.0.put::(token).await + } +} diff --git a/rust/noosphere-storage/src/web.rs b/rust/noosphere-storage/src/web.rs index 53f7dbb67..00d08e487 100644 --- a/rust/noosphere-storage/src/web.rs +++ b/rust/noosphere-storage/src/web.rs @@ -136,16 +136,6 @@ impl Store for WebStore { Ok(old_bytes) } - async fn contains(&self, key: &[u8]) -> Result { - let (store, tx) = self.start_transaction(TransactionMode::ReadOnly)?; - - let contains = WebStore::contains(&WebStore::bytes_to_typed_array(key)?, &store).await?; - - WebStore::finish_transaction(tx).await?; - - Ok(contains) - } - async fn remove(&mut self, key: &[u8]) -> Result>> { let (store, tx) = self.start_transaction(TransactionMode::ReadWrite)?; diff --git a/rust/noosphere/Cargo.toml b/rust/noosphere/Cargo.toml index 80962429d..c3a62dfef 100644 --- a/rust/noosphere/Cargo.toml +++ b/rust/noosphere/Cargo.toml @@ -28,21 +28,26 @@ serde_ipld_dagcbor = "~0.2" tiny-bip39 = "^1" crdts = "^7" tokio-stream = "~0.1" +libipld-core = "~0.14" +libipld-cbor = "~0.14" noosphere-storage = { path = "../noosphere-storage" } noosphere-cbor = { path = "../noosphere-cbor" } noosphere-collections = { path = "../noosphere-collections" } +ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } +ucan-key-support = { version = "0.4.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } + +# ucan = { version = "0.6.0-alpha.1", path = "../../../../ucan-wg/rs-ucan/ucan" } +# ucan-key-support = { version = "0.4.0-alpha.1", path = "../../../../ucan-wg/rs-ucan/ucan-key-support" } + [dev-dependencies] wasm-bindgen-test = "~0.3" +serde_bytes = "~0.11" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git" } -ucan-key-support = { version = "0.4.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git" } tokio = { version = "^1", features = ["full"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -ucan = { version = "0.6.0-alpha.1", features = ["web"], git = "https://github.com/cdata/rs-ucan.git" } -ucan-key-support = { version = "0.4.0-alpha.1", features = ["web"], git = "https://github.com/cdata/rs-ucan.git" } # NOTE: This is needed so that rand can be included in WASM builds -getrandom = { version = "0.2.5", features = ["js"] } +getrandom = { version = "~0.2", features = ["js"] } diff --git a/rust/noosphere/src/authority/capability.rs b/rust/noosphere/src/authority/capability.rs index 5ebde3b29..7eb5134b2 100644 --- a/rust/noosphere/src/authority/capability.rs +++ b/rust/noosphere/src/authority/capability.rs @@ -14,9 +14,9 @@ impl Action for SphereAction {} impl ToString for SphereAction { fn to_string(&self) -> String { match self { - SphereAction::Authorize => "sphere/AUTHORIZE", - SphereAction::Publish => "sphere/PUBLISH", - SphereAction::Sign => "sphere/SIGN", + SphereAction::Authorize => "sphere/authorize", + SphereAction::Publish => "sphere/publish", + SphereAction::Sign => "sphere/sign", } .into() } @@ -27,15 +27,15 @@ impl TryFrom for SphereAction { fn try_from(value: String) -> Result { Ok(match value.as_str() { - "sphere/AUTHORIZE" => SphereAction::Authorize, - "sphere/PUBLISH" => SphereAction::Publish, - "sphere/SIGN" => SphereAction::Sign, + "sphere/authorize" => SphereAction::Authorize, + "sphere/publish" => SphereAction::Publish, + "sphere/sign" => SphereAction::Sign, _ => return Err(anyhow!("Unrecognized action: {:?}", value)), }) } } -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Eq)] pub struct SphereReference { pub did: String, } diff --git a/rust/noosphere/src/authority/verification.rs b/rust/noosphere/src/authority/verification.rs index b8a0fddd3..ec78d29b5 100644 --- a/rust/noosphere/src/authority/verification.rs +++ b/rust/noosphere/src/authority/verification.rs @@ -1,5 +1,6 @@ use anyhow::{anyhow, Result}; use cid::Cid; +use libipld_cbor::DagCborCodec; use ucan::{ capability::{Capability, Resource, With}, chain::ProofChain, @@ -12,18 +13,18 @@ use crate::{ encoding::base64_decode, }; -use noosphere_storage::interface::{DagCborStore, Store}; +use noosphere_storage::{interface::BlockStore, ucan::UcanStore}; use crate::authority::SPHERE_SEMANTICS; use super::{SphereAction, SphereReference}; -pub async fn verify_sphere_cid( +pub async fn verify_sphere_cid( cid: &Cid, - store: &Storage, + store: &S, did_parser: &mut DidParser, ) -> Result<()> { - let memo: MemoIpld = store.load(cid).await?; + let memo = store.load::(cid).await?; // Ensure that we have the correct content type memo.expect_header( @@ -41,10 +42,12 @@ pub async fn verify_sphere_cid( let signature = base64_decode(&signature_header)?; // Load up the sphere being verified - let sphere: SphereIpld = store.load(&memo.body).await?; + let sphere = store.load::(&memo.body).await?; // If we have an authorizing proof... if let Some(proof_header) = memo.get_header(&Header::Proof.to_string()).first() { + let ucan_store = UcanStore(store.clone()); + // Extract a UCAN from the proof header, or... let ucan = Ucan::try_from_token_string(proof_header)?; @@ -55,7 +58,7 @@ pub async fn verify_sphere_cid( credential.verify(&memo.body.to_bytes(), &signature).await?; // Check the proof's provenance and that it enables the signer to sign - let proof = ProofChain::from_ucan(ucan, did_parser).await?; + let proof = ProofChain::from_ucan(ucan, did_parser, &ucan_store).await?; let desired_capability = Capability { with: With::Resource { diff --git a/rust/noosphere/src/data/authorization.rs b/rust/noosphere/src/data/authorization.rs new file mode 100644 index 000000000..94f4a6f46 --- /dev/null +++ b/rust/noosphere/src/data/authorization.rs @@ -0,0 +1,102 @@ +use anyhow::Result; +use cid::Cid; +use libipld_cbor::DagCborCodec; +use std::hash::Hash; +use ucan::{ + crypto::KeyMaterial, + store::{UcanJwtStore}, +}; + +use noosphere_storage::{interface::BlockStore, ucan::UcanStore}; +use serde::{Deserialize, Serialize}; + +use crate::{ + data::{CidKey, VersionedMapIpld}, + encoding::{base64_decode, base64_encode}, +}; + +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +pub struct AuthorizationIpld { + pub allowed: Cid, + pub revoked: Cid, +} + +impl AuthorizationIpld { + pub async fn try_empty(store: &mut S) -> Result { + let allowed_ipld = AllowedIpld::try_empty(store).await?; + let allowed = store.save::(allowed_ipld).await?; + let revoked_ipld = RevokedIpld::try_empty(store).await?; + let revoked = store.save::(revoked_ipld).await?; + + Ok(AuthorizationIpld { allowed, revoked }) + } +} + +/// This delegation represents the sharing of access to resources within a +/// sphere. The name of the delegation is for display purposes only, and helps +/// the user identify the client device or application that the delegation is +/// intended for. +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, Hash)] +pub struct DelegationIpld { + pub name: String, + pub jwt: Cid, +} + +impl DelegationIpld { + pub async fn try_register(name: &str, jwt: &str, store: &S) -> Result { + let mut store = UcanStore(store.clone()); + let cid = store.write_token(jwt).await?; + + Ok(DelegationIpld { + name: name.to_string(), + jwt: cid, + }) + } +} + +/// See https://github.com/ucan-wg/spec#66-revocation +/// TODO(ucan-wg/spec#112): Verify the form of this +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, Hash)] +pub struct RevocationIpld { + /// Issuer's DID + pub iss: String, + /// JWT CID of the revoked UCAN (provisionally encoded as base64 URL-safe + /// string) + pub revoke: String, + /// Issuer's signature of "REVOKE:{jwt_cid}", provisionally encoded + /// as unpadded base64 URL-safe string + pub challenge: String, +} + +impl RevocationIpld { + pub async fn try_revoke(cid: &Cid, issuer: &K) -> Result { + Ok(RevocationIpld { + iss: issuer.get_did().await?, + revoke: cid.to_string(), + challenge: base64_encode(&issuer.sign(&Self::make_challenge_payload(cid)).await?)?, + }) + } + + pub async fn try_verify(&self, claimed_issuer: &K) -> Result<()> { + let cid = Cid::try_from(self.revoke.as_str())?; + let challenge_payload = Self::make_challenge_payload(&cid); + let signature = base64_decode(&self.challenge)?; + + claimed_issuer + .verify(&challenge_payload, &signature) + .await?; + + Ok(()) + } + + fn make_challenge_payload(cid: &Cid) -> Vec { + format!("REVOKE:{cid}").as_bytes().to_vec() + } +} + +/// The key is the CID of a UCAN JWT, and the value is the JWT itself +pub type AllowedIpld = VersionedMapIpld; + +/// The key is the CID of the original UCAN JWT, and the value is the revocation +/// order by the UCAN issuer +pub type RevokedIpld = VersionedMapIpld; diff --git a/rust/noosphere/src/data/body_chunk.rs b/rust/noosphere/src/data/body_chunk.rs index 748d8593d..22aa5eb81 100644 --- a/rust/noosphere/src/data/body_chunk.rs +++ b/rust/noosphere/src/data/body_chunk.rs @@ -1,9 +1,10 @@ use anyhow::{anyhow, Result}; use cid::Cid; use fastcdc::FastCDC; +use libipld_cbor::DagCborCodec; use serde::{Deserialize, Serialize}; -use noosphere_storage::interface::{DagCborStore, Store}; +use noosphere_storage::interface::BlockStore; pub const BODY_CHUNK_MAX_SIZE: usize = 1024 * 64 * 8; // ~.5mb/chunk worst case @@ -20,7 +21,7 @@ pub struct BodyChunkIpld { } impl BodyChunkIpld { - pub async fn store_bytes(bytes: &[u8], store: &mut Storage) -> Result { + pub async fn store_bytes(bytes: &[u8], store: &mut S) -> Result { let chunks = FastCDC::new( bytes, fastcdc::MINIMUM_MIN, @@ -43,7 +44,7 @@ impl BodyChunkIpld { for byte_chunk in byte_chunks.into_iter().rev() { next_chunk_cid = Some( store - .save(&BodyChunkIpld { + .save::(&BodyChunkIpld { bytes: Vec::from(byte_chunk), next: next_chunk_cid, }) @@ -51,15 +52,15 @@ impl BodyChunkIpld { ); } - Ok(next_chunk_cid.ok_or(anyhow!("No CID; did you try to store zero bytes?"))?) + next_chunk_cid.ok_or(anyhow!("No CID; did you try to store zero bytes?")) } - pub async fn load_all_bytes(&self, store: Storage) -> Result> { + pub async fn load_all_bytes(&self, store: &S) -> Result> { let mut all_bytes = self.bytes.clone(); let mut next_cid = self.next; while let Some(cid) = next_cid { - let BodyChunkIpld { mut bytes, next } = store.load(&cid).await?; + let BodyChunkIpld { mut bytes, next } = store.load::(&cid).await?; all_bytes.append(&mut bytes); next_cid = next; diff --git a/rust/noosphere/src/data/bundle.rs b/rust/noosphere/src/data/bundle.rs index 83eeb9d92..75204376a 100644 --- a/rust/noosphere/src/data/bundle.rs +++ b/rust/noosphere/src/data/bundle.rs @@ -1,12 +1,14 @@ use std::{collections::BTreeMap, str::FromStr}; -use anyhow::{anyhow, Result}; +use anyhow::Result; use async_trait::async_trait; use cid::Cid; use futures::{pin_mut, StreamExt}; +use libipld_cbor::DagCborCodec; +use libipld_core::{serde::to_ipld}; use noosphere_cbor::{TryDagCbor, TryDagCborSendSync}; -use noosphere_storage::interface::{DagCborStore, Store}; +use noosphere_storage::interface::BlockStore; use serde::{Deserialize, Serialize}; use crate::{ @@ -14,34 +16,32 @@ use crate::{ BodyChunkIpld, ChangelogIpld, ContentType, Header, LinksIpld, MapOperation, MemoIpld, SphereIpld, VersionedMapIpld, }, + encoding::{block_deserialize, block_serialize}, view::Timeslice, }; +use super::{AllowedIpld, AuthorizationIpld, RevokedIpld, VersionedMapKey, VersionedMapValue}; + // TODO: This should maybe only collect CIDs, and then streaming-serialize to // a CAR (https://ipld.io/specs/transport/car/carv2/) #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Bundle(BTreeMap>); impl Bundle { - pub async fn load_into(&self, store: &mut Storage) -> Result<()> { + pub async fn load_into(&self, store: &mut S) -> Result<()> { // TODO: Parrallelize this for (cid, cbor_bytes) in self.0.iter() { - let stored_cid = store.write_cbor(cbor_bytes).await?; - if cid != &stored_cid { - return Err(anyhow!( - "CID in bundle ({:?}) did not match CID as stored ({:?})", - cid, - stored_cid - )); - } + // TODO: Verify CID is correct + store.put_block(cid, cbor_bytes).await?; + store.put_links::(cid, cbor_bytes).await?; } Ok(()) } - pub async fn try_from_timeslice<'a, Storage: Store>( - timeslice: &Timeslice<'a, Storage>, - store: &Storage, + pub async fn try_from_timeslice<'a, S: BlockStore>( + timeslice: &Timeslice<'a, S>, + store: &S, ) -> Result { let stream = timeslice.try_stream(); let mut bundle = Bundle::default(); @@ -74,10 +74,10 @@ impl Bundle { &self.0 } - pub async fn extend( + pub async fn extend( &mut self, cid: &Cid, - store: &Storage, + store: &S, ) -> Result<()> { CanBundle::try_extend_bundle_with_cid(cid, self, store).await?; Ok(()) @@ -87,32 +87,34 @@ impl Bundle { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] pub trait TryBundle: TryDagCborSendSync { - async fn try_extend_bundle( + async fn try_extend_bundle( &self, bundle: &mut Bundle, - store: &Storage, + _store: &S, ) -> Result<()> { - let self_bytes = self.try_into_dag_cbor()?; - let self_cid = Storage::make_cid(&self_bytes); - Self::try_extend_bundle_with_cid(&self_cid, bundle, store).await + let (self_cid, self_bytes) = block_serialize::(self)?; + bundle.add(self_cid, self_bytes); + Ok(()) } - async fn try_extend_bundle_with_cid( + async fn try_extend_bundle_with_cid( cid: &Cid, bundle: &mut Bundle, - store: &Storage, + store: &S, ) -> Result<()> { - bundle.add(cid.clone(), store.require_cbor(cid).await?); + let item = store.load::(cid).await?; + item.try_extend_bundle(bundle, store).await?; + Ok(()) } - async fn try_bundle(&self, store: &Storage) -> Result { + async fn try_bundle(&self, store: &S) -> Result { let mut bundle = Bundle::default(); self.try_extend_bundle(&mut bundle, store).await?; Ok(bundle) } - async fn try_bundle_with_cid(cid: &Cid, store: &Storage) -> Result { + async fn try_bundle_with_cid(cid: &Cid, store: &S) -> Result { let mut bundle = Bundle::default(); Self::try_extend_bundle_with_cid(cid, &mut bundle, store).await?; Ok(bundle) @@ -122,15 +124,15 @@ pub trait TryBundle: TryDagCborSendSync { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] impl TryBundle for BodyChunkIpld { - async fn try_extend_bundle_with_cid( + async fn try_extend_bundle_with_cid( cid: &Cid, bundle: &mut Bundle, - store: &Storage, + store: &S, ) -> Result<()> { - let mut next_cid = Some(cid.clone()); + let mut next_cid = Some(*cid); while let Some(cid) = next_cid { - let bytes = store.require_cbor(&cid).await?; + let bytes = store.require_block(&cid).await?; let chunk = BodyChunkIpld::try_from_dag_cbor(&bytes)?; bundle.add(cid, bytes); next_cid = chunk.next; @@ -140,32 +142,65 @@ impl TryBundle for BodyChunkIpld { } } +// #[cfg_attr(not(target_arch = "wasm32"), async_trait)] +// #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +// impl TryBundle for ChangelogIpld> +// where +// K: VersionedMapKey, +// { +// async fn try_extend_bundle_with_cid( +// cid: &Cid, +// bundle: &mut Bundle, +// store: &S, +// ) -> Result<()> { +// let bytes = store.require_block(cid).await?; +// let changelog = Self::try_from_dag_cbor(&bytes)?; + +// bundle.add(*cid, bytes); + +// for op in changelog.changes { +// match op { +// MapOperation::<_, Cid>::Add { value, .. } => { +// let bytes = store.require_block(&cid).await?; +// bundle.add(cid, bytes); +// } +// _ => (), +// }; +// } + +// Ok(()) +// } +// } + #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl TryBundle for ChangelogIpld> +impl TryBundle for ChangelogIpld> where - Self: TryDagCborSendSync, + K: VersionedMapKey, + V: VersionedMapValue, { - async fn try_extend_bundle_with_cid( + async fn try_extend_bundle_with_cid( cid: &Cid, bundle: &mut Bundle, - store: &Storage, + store: &S, ) -> Result<()> { - let bytes = store.require_cbor(cid).await?; + let bytes = store.require_block(cid).await?; + let mut cids = Vec::new(); let changelog = Self::try_from_dag_cbor(&bytes)?; - bundle.add(cid.clone(), bytes); + bundle.add(*cid, bytes); for op in changelog.changes { match op { - MapOperation::Add { value: cid, .. } => { - let bytes = store.require_cbor(&cid).await?; - bundle.add(cid, bytes); - } + MapOperation::Add { .. } => to_ipld(&op)?.references(&mut cids), _ => (), }; } + for cid in cids { + bundle.add(cid, store.require_block(&cid).await?); + } + Ok(()) } } @@ -173,13 +208,8 @@ where #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] impl TryBundle for MemoIpld { - async fn try_extend_bundle( - &self, - bundle: &mut Bundle, - store: &Storage, - ) -> Result<()> { - let self_bytes = self.try_into_dag_cbor()?; - let self_cid = Storage::make_cid(&self_bytes); + async fn try_extend_bundle(&self, bundle: &mut Bundle, store: &S) -> Result<()> { + let (self_cid, self_bytes) = block_serialize::(self)?; bundle.add(self_cid, self_bytes); @@ -199,13 +229,15 @@ impl TryBundle for MemoIpld { Ok(()) } - async fn try_extend_bundle_with_cid( + async fn try_extend_bundle_with_cid( cid: &Cid, bundle: &mut Bundle, - store: &Storage, + store: &S, ) -> Result<()> { + let memo = store.load::(cid).await?; + println!("BUNDLING MEMO {} {:?}", cid, memo); store - .load::(cid) + .load::(cid) .await? .try_extend_bundle(bundle, store) .await?; @@ -215,19 +247,15 @@ impl TryBundle for MemoIpld { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl TryBundle for VersionedMapIpld +impl TryBundle for VersionedMapIpld where - Self: TryDagCborSendSync, + K: VersionedMapKey, + V: VersionedMapValue, { - async fn try_extend_bundle( - &self, - bundle: &mut Bundle, - store: &Storage, - ) -> Result<()> { - let self_bytes = self.try_into_dag_cbor()?; - let self_cid = Storage::make_cid(&self_bytes); + async fn try_extend_bundle(&self, bundle: &mut Bundle, store: &S) -> Result<()> { + let (self_cid, self_bytes) = block_serialize::(self)?; - ChangelogIpld::>::try_extend_bundle_with_cid( + ChangelogIpld::>::try_extend_bundle_with_cid( &self.changelog, bundle, store, @@ -239,12 +267,12 @@ where Ok(()) } - async fn try_extend_bundle_with_cid( + async fn try_extend_bundle_with_cid( cid: &Cid, bundle: &mut Bundle, - store: &Storage, + store: &S, ) -> Result<()> { - let map: Self = store.load(cid).await?; + let map: Self = store.load::(cid).await?; map.try_extend_bundle(bundle, store).await?; Ok(()) } @@ -253,15 +281,15 @@ where #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] impl TryBundle for SphereIpld { - async fn try_extend_bundle_with_cid( + async fn try_extend_bundle_with_cid( cid: &Cid, bundle: &mut Bundle, - store: &Storage, + store: &S, ) -> Result<()> { - let self_bytes = store.require_cbor(cid).await?; + let self_bytes = store.require_block(cid).await?; let sphere = SphereIpld::try_from_dag_cbor(&self_bytes)?; - bundle.add(cid.clone(), self_bytes); + bundle.add(*cid, self_bytes); match sphere.links { Some(cid) => { @@ -270,9 +298,9 @@ impl TryBundle for SphereIpld { _ => (), } - match sphere.revocations { - Some(_cid) => { - todo!(); + match sphere.authorization { + Some(cid) => { + AuthorizationIpld::try_extend_bundle_with_cid(&cid, bundle, store).await?; } _ => (), } @@ -288,9 +316,32 @@ impl TryBundle for SphereIpld { } } +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl TryBundle for AuthorizationIpld { + async fn try_extend_bundle_with_cid( + cid: &Cid, + bundle: &mut Bundle, + store: &S, + ) -> Result<()> { + let self_bytes = store.require_block(cid).await?; + let authorization_ipld = block_deserialize::(&self_bytes)?; + + AllowedIpld::try_extend_bundle_with_cid(&authorization_ipld.allowed, bundle, store).await?; + RevokedIpld::try_extend_bundle_with_cid(&authorization_ipld.revoked, bundle, store).await?; + + bundle.add(*cid, self_bytes); + + Ok(()) + } +} + #[cfg(test)] mod tests { - use noosphere_storage::{interface::DagCborStore, memory::MemoryStore}; + use libipld_cbor::DagCborCodec; + use libipld_core::raw::RawCodec; + use noosphere_storage::{interface::BlockStore, memory::MemoryStore}; + use serde_bytes::Bytes; use ucan::crypto::KeyMaterial; #[cfg(target_arch = "wasm32")] @@ -330,18 +381,19 @@ mod tests { let (sphere, ucan, _) = Sphere::try_generate(&owner_did, &mut store).await.unwrap(); - let foo_cid = store.write_cbor(b"foo").await.unwrap(); + let foo_key = String::from("foo"); + let foo_cid = store.save::(Bytes::new(b"foo")).await.unwrap(); let mut mutation = SphereMutation::new(&owner_did); - mutation.links_mut().set("foo", &foo_cid); + mutation.links_mut().set(&foo_key, &foo_cid); - let mut revision = sphere.try_apply(&mutation).await.unwrap(); + let mut revision = sphere.try_apply_mutation(&mutation).await.unwrap(); let new_cid = revision.try_sign(&owner_key, Some(&ucan)).await.unwrap(); let bundle = MemoIpld::try_bundle_with_cid(&new_cid, &store) .await .unwrap(); - assert_eq!(bundle.map().keys().len(), 5); + assert_eq!(bundle.map().keys().len(), 11); let sphere = Sphere::at(&new_cid, &store); @@ -356,7 +408,10 @@ mod tests { assert!(bundle.map().contains_key(&links_cid)); - let links_ipld = store.load::(&links_cid).await.unwrap(); + let links_ipld = store + .load::(&links_cid) + .await + .unwrap(); assert!(bundle.map().contains_key(&links_ipld.changelog)); assert!(bundle.map().contains_key(&foo_cid)); @@ -371,27 +426,29 @@ mod tests { let (sphere, ucan, _) = Sphere::try_generate(&owner_did, &mut store).await.unwrap(); - let foo_cid = store.write_cbor(b"foo").await.unwrap(); + let foo_key = String::from("foo"); + let foo_cid = store.save::(Bytes::new(b"foo")).await.unwrap(); let mut first_mutation = SphereMutation::new(&owner_did); - first_mutation.links_mut().set("foo", &foo_cid); + first_mutation.links_mut().set(&foo_key, &foo_cid); - let mut revision = sphere.try_apply(&first_mutation).await.unwrap(); + let mut revision = sphere.try_apply_mutation(&first_mutation).await.unwrap(); let new_cid = revision.try_sign(&owner_key, Some(&ucan)).await.unwrap(); let sphere = Sphere::at(&new_cid, &store); - let bar_cid = store.write_cbor(b"bar").await.unwrap(); + let bar_key = String::from("bar"); + let bar_cid = store.save::(Bytes::new(b"bar")).await.unwrap(); let mut second_mutation = SphereMutation::new(&owner_did); - second_mutation.links_mut().set("bar", &bar_cid); + second_mutation.links_mut().set(&bar_key, &bar_cid); - let mut revision = sphere.try_apply(&second_mutation).await.unwrap(); + let mut revision = sphere.try_apply_mutation(&second_mutation).await.unwrap(); let new_cid = revision.try_sign(&owner_key, Some(&ucan)).await.unwrap(); let bundle = MemoIpld::try_bundle_with_cid(&new_cid, &store) .await .unwrap(); - assert_eq!(bundle.map().keys().len(), 5); + assert_eq!(bundle.map().keys().len(), 11); assert!(!bundle.map().contains_key(&foo_cid)); assert!(bundle.map().contains_key(&bar_cid)); } @@ -405,22 +462,24 @@ mod tests { let (sphere, ucan, _) = Sphere::try_generate(&owner_did, &mut store).await.unwrap(); - let original_cid = sphere.cid().clone(); + let original_cid = *sphere.cid(); - let foo_cid = store.write_cbor(b"foo").await.unwrap(); + let foo_key = String::from("foo"); + let foo_cid = store.save::(Bytes::new(b"foo")).await.unwrap(); let mut first_mutation = SphereMutation::new(&owner_did); - first_mutation.links_mut().set("foo", &foo_cid); + first_mutation.links_mut().set(&foo_key, &foo_cid); - let mut revision = sphere.try_apply(&first_mutation).await.unwrap(); + let mut revision = sphere.try_apply_mutation(&first_mutation).await.unwrap(); let second_cid = revision.try_sign(&owner_key, Some(&ucan)).await.unwrap(); let sphere = Sphere::at(&second_cid, &store); - let bar_cid = store.write_cbor(b"bar").await.unwrap(); + let bar_key = String::from("bar"); + let bar_cid = store.save::(Bytes::new(b"bar")).await.unwrap(); let mut second_mutation = SphereMutation::new(&owner_did); - second_mutation.links_mut().set("bar", &bar_cid); + second_mutation.links_mut().set(&bar_key, &bar_cid); - let mut revision = sphere.try_apply(&second_mutation).await.unwrap(); + let mut revision = sphere.try_apply_mutation(&second_mutation).await.unwrap(); let final_cid = revision.try_sign(&owner_key, Some(&ucan)).await.unwrap(); let timeline = Timeline::new(&store); @@ -430,7 +489,7 @@ mod tests { .await .unwrap(); - assert_eq!(bundle.map().keys().len(), 10); + assert_eq!(bundle.map().keys().len(), 16); assert!(bundle.map().contains_key(&foo_cid)); assert!(bundle.map().contains_key(&bar_cid)); diff --git a/rust/noosphere/src/data/links.rs b/rust/noosphere/src/data/links.rs index 310b90a9b..25ae8b2fe 100644 --- a/rust/noosphere/src/data/links.rs +++ b/rust/noosphere/src/data/links.rs @@ -2,8 +2,4 @@ use cid::Cid; use crate::data::VersionedMapIpld; -use super::{ChangelogIpld, MapOperation}; - -pub type LinksOperation = MapOperation; -pub type LinksChangelogIpld = ChangelogIpld; pub type LinksIpld = VersionedMapIpld; diff --git a/rust/noosphere/src/data/memo.rs b/rust/noosphere/src/data/memo.rs index 99ba45dfe..0524d7a9a 100644 --- a/rust/noosphere/src/data/memo.rs +++ b/rust/noosphere/src/data/memo.rs @@ -2,13 +2,13 @@ use std::{collections::BTreeMap, str::FromStr}; use anyhow::{anyhow, Result}; use cid::Cid; +use libipld_cbor::DagCborCodec; use serde::{Deserialize, Serialize}; use ucan::{crypto::KeyMaterial, ucan::Ucan}; use crate::{data::Header, encoding::base64_encode}; -use noosphere_cbor::TryDagCbor; -use noosphere_storage::interface::{DagCborStore, Store}; +use noosphere_storage::interface::{BlockStore, BlockStoreSend}; use super::ContentType; @@ -25,14 +25,18 @@ pub struct MemoIpld { } impl MemoIpld { - pub async fn try_compare_body(&self, store: &Storage) -> Result> { - Ok(match store.load_some(self.parent.as_ref()).await? { - Some(MemoIpld { body, .. }) => match self.body != body { - true => Some(self.body), - false => None, - }, - None => Some(self.body), - }) + /// If the body of this memo is different from it's parent, returns true. + pub async fn try_compare_body(&self, store: &S) -> Result { + let parent_cid = match self.parent { + Some(cid) => cid, + None => return Ok(true), + }; + + let MemoIpld { + body: parent_body, .. + } = store.load::(&parent_cid).await?; + + Ok(self.body != parent_body) } /// Get the list of headers that either do not appear in other, or @@ -56,11 +60,14 @@ impl MemoIpld { Ok(diff) } - pub async fn for_body( - store: &mut Storage, - body: &Body, + /// Initializes a memo for the provided body, persisting the body to storage + /// and returning the memo. Note that only the body is persisted, not the + /// memo that wraps it. + pub async fn for_body( + store: &mut S, + body: Body, ) -> Result { - let body_cid = store.write_cbor(&body.try_into_dag_cbor()?).await?; + let body_cid = store.save::(body).await?; Ok(MemoIpld { parent: None, headers: Vec::new(), @@ -68,10 +75,13 @@ impl MemoIpld { }) } - pub async fn branch_from(cid: &Cid, store: &Storage) -> Result { - match store.load(cid).await { - Ok(mut memo @ MemoIpld { .. }) => { - memo.parent = Some(cid.clone()); + /// Loads a memo from the provided CID, initializes a copy of it, sets + /// the copy's parent to the provided CID and cleans signature information + /// from the copy's headers; the new memo is returned. + pub async fn branch_from(cid: &Cid, store: &S) -> Result { + match store.load::(cid).await { + Ok(mut memo) => { + memo.parent = Some(*cid); memo.remove_header(&Header::Signature.to_string()); memo.remove_header(&Header::Proof.to_string()); @@ -81,6 +91,8 @@ impl MemoIpld { } } + /// Sign the memo's body CID, adding the signature and proof as headers in + /// the memo pub async fn sign( &mut self, credential: &Credential, @@ -103,6 +115,7 @@ impl MemoIpld { Ok(()) } + /// Retreive the set of headers that matches the given string name pub fn get_header(&self, name: &str) -> Vec { let lower_name = name.to_lowercase(); @@ -118,6 +131,7 @@ impl MemoIpld { .collect() } + /// Retrieve the first header (if any) that matches the given string name pub fn get_first_header(&self, name: &str) -> Option { let lower_name = name.to_lowercase(); @@ -129,6 +143,7 @@ impl MemoIpld { None } + /// Asserts that a header with the given name and value exists in the memo pub fn expect_header(&self, name: &str, value: &str) -> Result<()> { let lower_name = name.to_lowercase(); @@ -145,6 +160,8 @@ impl MemoIpld { )) } + /// Replaces the value of the first header that matches name with provided + /// value pub fn replace_header(&mut self, name: &str, value: &str) { let mut found = 0usize; @@ -173,6 +190,7 @@ impl MemoIpld { } } + /// Removes all headers with the given name from the memo pub fn remove_header(&mut self, name: &str) { let lower_name = name.to_lowercase(); @@ -184,6 +202,7 @@ impl MemoIpld { .collect(); } + /// Helper to quickly deserialize a content-type (if any) from the memo pub fn content_type(&self) -> Option { if let Some(content_type) = self.get_first_header(&Header::ContentType.to_string()) { if let Ok(content_type) = ContentType::from_str(&content_type) { @@ -199,6 +218,7 @@ impl MemoIpld { #[cfg(test)] mod test { + use libipld_cbor::DagCborCodec; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::wasm_bindgen_test; @@ -209,27 +229,35 @@ mod test { use serde::{Deserialize, Serialize}; pub fn make_raw_cid>(bytes: B) -> Cid { - Cid::new_v1(0x55, Code::Sha2_256.digest(bytes.as_ref())) + Cid::new_v1(0x55, Code::Blake2b256.digest(bytes.as_ref())) } - use crate::data::MemoIpld; - use noosphere_cbor::TryDagCbor; - use noosphere_storage::{interface::DagCborStore, memory::MemoryStore}; + use crate::{ + data::MemoIpld, + encoding::{block_deserialize, block_serialize}, + }; + + use noosphere_storage::{interface::BlockStore, memory::MemoryStore}; - #[test] - fn it_round_trips_as_cbor() { + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_round_trips_as_cbor() { let body_cid = make_raw_cid(b"foobar"); + let mut store = MemoryStore::default(); let memo = MemoIpld { parent: None, headers: Vec::new(), - body: body_cid.clone(), + body: body_cid, }; - let memo_bytes = memo.try_into_dag_cbor().unwrap(); - let decoded_memo = MemoIpld::try_from_dag_cbor(&memo_bytes).unwrap(); + let memo_cid = store.save::(&memo).await.unwrap(); + let loaded_memo = store + .load::(&memo_cid) + .await + .unwrap(); - assert_eq!(memo, decoded_memo); + assert_eq!(memo, loaded_memo); } #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] @@ -245,15 +273,18 @@ mod test { let structured = Structured { foo: String::from("bar"), }; - let body_cid = store.save(&structured).await.unwrap(); + let body_cid = store.save::(&structured).await.unwrap(); let memo = MemoIpld { parent: None, headers: Vec::new(), - body: body_cid.clone(), + body: body_cid, }; - let memo_bytes = memo.try_into_dag_cbor().unwrap(); - let decoded_memo = MemoIpld::try_from_dag_cbor(&memo_bytes).unwrap(); - let decoded_body: Structured = store.load(&decoded_memo.body).await.unwrap(); + let (_, memo_bytes) = block_serialize::(&memo).unwrap(); + let decoded_memo = block_deserialize::(&memo_bytes).unwrap(); + let decoded_body: Structured = store + .load::(&decoded_memo.body) + .await + .unwrap(); assert_eq!(decoded_body.foo, String::from("bar")); } diff --git a/rust/noosphere/src/data/mod.rs b/rust/noosphere/src/data/mod.rs index b52cbecbe..de01de293 100644 --- a/rust/noosphere/src/data/mod.rs +++ b/rust/noosphere/src/data/mod.rs @@ -1,3 +1,4 @@ +mod authorization; mod body_chunk; mod bundle; mod changelog; @@ -8,6 +9,7 @@ mod reference; mod sphere; mod versioned_map; +pub use authorization::*; pub use body_chunk::*; pub use bundle::*; pub use changelog::*; diff --git a/rust/noosphere/src/data/sphere.rs b/rust/noosphere/src/data/sphere.rs index 0f5543911..288e269ca 100644 --- a/rust/noosphere/src/data/sphere.rs +++ b/rust/noosphere/src/data/sphere.rs @@ -16,8 +16,8 @@ pub struct SphereIpld { /// The non-public content of the sphere (SealedIpld) pub sealed: Option, - /// TODO: UCAN revocations (RevocationsIpld) - pub revocations: Option, + /// Authorization and revocation state for non-owner keys (AuthorizationIpld) + pub authorization: Option, } impl SphereIpld {} @@ -25,6 +25,7 @@ impl SphereIpld {} #[cfg(test)] mod tests { use ed25519_zebra::{SigningKey as Ed25519PrivateKey, VerificationKey as Ed25519PublicKey}; + use libipld_cbor::DagCborCodec; use ucan::{ builder::UcanBuilder, capability::{Capability, Resource, With}, @@ -39,7 +40,7 @@ mod tests { data::{ContentType, Header, MemoIpld, SphereIpld}, }; - use noosphere_storage::{interface::DagCborStore, memory::MemoryStore}; + use noosphere_storage::{interface::BlockStore, memory::MemoryStore}; fn generate_credential() -> Ed25519KeyMaterial { let private_key = Ed25519PrivateKey::new(rand::thread_rng()); @@ -59,10 +60,10 @@ mod tests { identity: identity_did.clone(), links: None, sealed: None, - revocations: None, + authorization: None, }; - let sphere_cid = store.save(&sphere).await.unwrap(); + let sphere_cid = store.save::(&sphere).await.unwrap(); let mut memo = MemoIpld { parent: None, @@ -95,7 +96,7 @@ mod tests { memo.sign(&identity_credential, Some(&proof)).await.unwrap(); - let memo_cid = store.save(&memo).await.unwrap(); + let memo_cid = store.save::(&memo).await.unwrap(); let mut did_parser = DidParser::new(SUPPORTED_KEYS); @@ -119,10 +120,10 @@ mod tests { identity: identity_did.clone(), links: None, sealed: None, - revocations: None, + authorization: None, }; - let sphere_cid = store.save(&sphere).await.unwrap(); + let sphere_cid = store.save::(&sphere).await.unwrap(); let mut memo = MemoIpld { parent: None, @@ -157,7 +158,7 @@ mod tests { .await .unwrap(); - let memo_cid = store.save(&memo).await.unwrap(); + let memo_cid = store.save::(&memo).await.unwrap(); let mut did_parser = DidParser::new(SUPPORTED_KEYS); diff --git a/rust/noosphere/src/data/versioned_map.rs b/rust/noosphere/src/data/versioned_map.rs index 8293d767e..f3c8bd738 100644 --- a/rust/noosphere/src/data/versioned_map.rs +++ b/rust/noosphere/src/data/versioned_map.rs @@ -1,16 +1,56 @@ -use anyhow::{anyhow, Result}; +use anyhow::Result; use cid::Cid; pub use crdts::{map, Orswot}; +use libipld_cbor::DagCborCodec; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{hash::Hash, marker::PhantomData}; -use noosphere_cbor::{TryDagCbor, TryDagCborConditionalSendSync}; use noosphere_collections::hamt::{Hamt, Hash as HamtHash, Sha256}; -use noosphere_storage::interface::{DagCborStore, Store}; +use noosphere_storage::interface::BlockStore; use super::ChangelogIpld; -// pub type Changelog = Vec, String>>; +#[cfg(not(target_arch = "wasm32"))] +pub trait VersionedMapSendSync: Send + Sync {} + +#[cfg(not(target_arch = "wasm32"))] +impl VersionedMapSendSync for T where T: Send + Sync {} + +#[cfg(target_arch = "wasm32")] +pub trait VersionedMapSendSync {} + +#[cfg(target_arch = "wasm32")] +impl VersionedMapSendSync for T {} + +#[repr(transparent)] +#[derive(Ord, Eq, PartialEq, PartialOrd, Debug, Clone, Serialize, Deserialize)] +pub struct CidKey(pub Cid); + +impl HamtHash for CidKey { + fn hash(&self, state: &mut H) { + self.0.hash().hash(state); + } +} + +pub trait VersionedMapKey: + Serialize + DeserializeOwned + HamtHash + Clone + Eq + Ord + VersionedMapSendSync +{ +} + +impl VersionedMapKey for T where + T: Serialize + DeserializeOwned + HamtHash + Clone + Eq + Ord + VersionedMapSendSync +{ +} + +pub trait VersionedMapValue: + Serialize + DeserializeOwned + Clone + Eq + Hash + VersionedMapSendSync +{ +} + +impl VersionedMapValue for T where + T: Serialize + DeserializeOwned + Clone + Eq + Hash + VersionedMapSendSync +{ +} #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] pub enum MapOperation { @@ -21,8 +61,8 @@ pub enum MapOperation { #[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize)] pub struct VersionedMapIpld where - Key: HamtHash + Eq + Ord + TryDagCborConditionalSendSync, - Value: Clone + Eq + Hash + TryDagCborConditionalSendSync, + Key: VersionedMapKey, + Value: VersionedMapValue, { /// A pointer to a HAMT pub hamt: Cid, @@ -33,36 +73,37 @@ where // pub changelog: ChangelogIpld>, pub changelog: Cid, + #[serde(skip)] pub signature: PhantomData<(Key, Value)>, } impl VersionedMapIpld where - Key: DeserializeOwned + Serialize + HamtHash + Eq + Ord + TryDagCborConditionalSendSync, - Value: DeserializeOwned + Serialize + Clone + Eq + Hash + TryDagCborConditionalSendSync, + Key: VersionedMapKey, + Value: VersionedMapValue, { - pub async fn try_load_hamt( + pub async fn try_load_hamt( &self, - store: &Storage, - ) -> Result> { + store: &S, + ) -> Result> { Hamt::load(&self.hamt, store.clone()).await } - pub async fn try_load_changelog( + pub async fn try_load_changelog( &self, - store: &Storage, + store: &S, ) -> Result>> { - store.load(&self.changelog).await + store.load::(&self.changelog).await } // NOTE: We currently don't have a mechanism to prepuplate the store with // "empty" DAGs like a HAMT. So, we do it lazily by requiring async // initialization of this struct even when it is empty. - pub async fn try_empty(store: &mut Storage) -> Result { - let mut hamt = Hamt::::new(store.clone()); + pub async fn try_empty(store: &mut S) -> Result { + let mut hamt = Hamt::::new(store.clone()); let changelog = ChangelogIpld::>::default(); - let changelog_cid = store.write_cbor(&changelog.try_into_dag_cbor()?).await?; + let changelog_cid = store.save::(&changelog).await?; let hamt_cid = hamt.flush().await?; Ok(VersionedMapIpld { diff --git a/rust/noosphere/src/encoding.rs b/rust/noosphere/src/encoding.rs index 28b577670..5b5e37f3a 100644 --- a/rust/noosphere/src/encoding.rs +++ b/rust/noosphere/src/encoding.rs @@ -1,9 +1,75 @@ use anyhow::Result; +use cid::{ + multihash::{Code, MultihashDigest}, + Cid, +}; +use libipld_core::{ + codec::{Codec, Decode, Encode}, + ipld::Ipld, + serde::{from_ipld, to_ipld}, +}; +use serde::{de::DeserializeOwned, Serialize}; +/// Encode some bytes as an unpadded URL-safe base64 string pub fn base64_encode(data: &[u8]) -> Result { Ok(base64::encode_config(&data, base64::URL_SAFE_NO_PAD)) } +/// Decode some bytes from an unpadded URL-safe base64 string pub fn base64_decode(encoded: &str) -> Result> { Ok(base64::decode_config(encoded, base64::URL_SAFE_NO_PAD)?) } + +/// Produces a CID for a block with a Blake2b hash; note that the bytes are +/// presumed to be encoded with the specified codec (honor system; this is +/// not validated in any way). +pub fn derive_cid(block: &[u8]) -> Cid +where + C: Codec + Default, + u64: From, +{ + Cid::new_v1(u64::from(C::default()), Code::Blake2b256.digest(block)) +} + +/// Encode any encodable type as a block using the specified codec +pub fn block_encode(encodable: &T) -> Result<(Cid, Vec)> +where + C: Codec + Default, + T: Encode, + u64: From, +{ + let codec = C::default(); + let block = codec.encode(encodable)?; + + Ok((derive_cid(&block), block)) +} + +/// Decode any block as IPLD using the specified codec +pub fn block_decode(block: &[u8]) -> Result +where + C: Codec + Default, + Ipld: Decode, +{ + C::default().decode::(block) +} + +/// Encode any serializable type as a block using the specified codec +pub fn block_serialize(any: T) -> Result<(Cid, Vec)> +where + C: Codec + Default, + T: Serialize, + Ipld: Encode, + u64: From, +{ + block_encode(&to_ipld(any)?) +} + +/// Decode any block as a deserializable type using the specified codec +pub fn block_deserialize(block: &[u8]) -> Result +where + C: Codec + Default, + T: DeserializeOwned, + Ipld: Decode, +{ + Ok(from_ipld(block_decode(block)?)?) +} diff --git a/rust/noosphere/src/view/authorization.rs b/rust/noosphere/src/view/authorization.rs new file mode 100644 index 000000000..90af23981 --- /dev/null +++ b/rust/noosphere/src/view/authorization.rs @@ -0,0 +1,65 @@ +use anyhow::Result; +use cid::Cid; +use libipld_cbor::DagCborCodec; +use noosphere_storage::interface::BlockStore; + +use crate::{ + data::AuthorizationIpld, + view::{AllowedUcans, RevokedUcans}, +}; + +pub struct Authorization { + cid: Cid, + store: S, +} + +impl Authorization +where + S: BlockStore, +{ + pub fn cid(&self) -> &Cid { + &self.cid + } + + pub fn at(cid: &Cid, store: &S) -> Self { + Authorization { + cid: *cid, + store: store.clone(), + } + } + + pub async fn try_at_or_empty(cid: Option<&Cid>, store: &mut S) -> Result> { + Ok(match cid { + Some(cid) => Self::at(cid, store), + None => Self::try_empty(store).await?, + }) + } + + pub async fn try_empty(store: &mut S) -> Result { + let ipld = AuthorizationIpld::try_empty(store).await?; + let cid = store.save::(ipld).await?; + + Ok(Authorization { + cid, + store: store.clone(), + }) + } + + pub async fn try_get_allowed_ucans(&self) -> Result> { + let ipld = self + .store + .load::(&self.cid) + .await?; + + AllowedUcans::try_at_or_empty(Some(&ipld.allowed), &mut self.store.clone()).await + } + + pub async fn try_get_revoked_ucans(&self) -> Result> { + let ipld = self + .store + .load::(&self.cid) + .await?; + + RevokedUcans::try_at_or_empty(Some(&ipld.revoked), &mut self.store.clone()).await + } +} diff --git a/rust/noosphere/src/view/links.rs b/rust/noosphere/src/view/links.rs deleted file mode 100644 index 35df73f3c..000000000 --- a/rust/noosphere/src/view/links.rs +++ /dev/null @@ -1,132 +0,0 @@ -use std::pin::Pin; - -use anyhow::Result; -use async_once_cell::OnceCell; -use cid::Cid; -use futures::Stream; - -use crate::{ - data::{LinksChangelogIpld, LinksIpld, MapOperation}, - view::LinksMutation, -}; - -use noosphere_collections::hamt::Hamt; -use noosphere_storage::interface::{DagCborStore, Store}; - -#[derive(Debug)] -pub struct Links { - cid: Cid, - store: Storage, - // NOTE: OnceCell used here for the caching benefits; it may not be necessary for changelog - hamt: OnceCell>, - changelog: OnceCell, -} - -impl Links { - pub async fn try_get_changelog(&self) -> Result<&LinksChangelogIpld> { - self.changelog - .get_or_try_init(async { - let ipld: LinksIpld = self.store.load(&self.cid).await?; - self.store.load(&ipld.changelog).await - }) - .await - } - - pub async fn try_get_hamt(&self) -> Result<&Hamt> { - self.hamt - .get_or_try_init(async { self.try_load_hamt().await }) - .await - } - - async fn try_load_hamt(&self) -> Result> { - let ipld: LinksIpld = self.store.load(&self.cid).await?; - ipld.try_load_hamt(&self.store).await - } - - pub async fn try_at_or_empty(cid: Option<&Cid>, store: &mut Storage) -> Result> { - Ok(match cid { - Some(cid) => Links::at(&cid, store), - None => Links::try_empty(store).await?, - }) - } - - pub fn cid(&self) -> &Cid { - &self.cid - } - - pub fn at(cid: &Cid, store: &Storage) -> Links { - Links { - cid: cid.clone(), - store: store.clone(), - hamt: OnceCell::new(), - changelog: OnceCell::new(), - } - } - - pub async fn try_empty(store: &mut Storage) -> Result> { - let ipld = LinksIpld::try_empty(store).await?; - let cid = store.save(&ipld).await?; - - Ok(Links { - cid, - hamt: OnceCell::new(), - changelog: OnceCell::new(), - store: store.clone(), - }) - } - - pub async fn get(&self, slug: &str) -> Result> { - let links = self.try_get_hamt().await?; - - Ok(match links.get(&slug.to_string()).await? { - Some(cid) => Some(cid.clone()), - None => None, - }) - } - - pub async fn try_apply_with_cid( - cid: Option<&Cid>, - mutation: &LinksMutation, - store: &mut Storage, - ) -> Result { - let links = Self::try_at_or_empty(cid, store).await?; - let mut changelog = links.try_get_changelog().await?.mark(mutation.did()); - let mut hamt = links.try_load_hamt().await?; - - for change in mutation.changes() { - match change { - MapOperation::Add { key, value } => { - hamt.set(key.clone(), value.clone()).await?; - } - MapOperation::Remove { key } => { - hamt.delete(key).await?; - } - }; - - changelog.push(change.clone())?; - } - - let changelog_cid = store.save(&changelog).await?; - let hamt_cid = hamt.flush().await?; - let links_ipld = LinksIpld { - hamt: hamt_cid, - changelog: changelog_cid, - ..Default::default() - }; - - Ok(store.save(&links_ipld).await?) - } - - pub async fn for_each(&self, for_each: ForEach) -> Result<()> - where - ForEach: FnMut(&String, &Cid) -> Result<()>, - { - self.try_get_hamt().await?.for_each(for_each).await - } - - pub async fn stream<'a>( - &'a self, - ) -> Result> + 'a>>> { - Ok(self.try_get_hamt().await?.stream()) - } -} diff --git a/rust/noosphere/src/view/mod.rs b/rust/noosphere/src/view/mod.rs index 570cbc0f4..09f34e9a8 100644 --- a/rust/noosphere/src/view/mod.rs +++ b/rust/noosphere/src/view/mod.rs @@ -1,9 +1,11 @@ -mod links; +mod authorization; mod mutation; mod sphere; mod timeline; +mod versioned_map; -pub use links::*; +pub use authorization::*; pub use mutation::*; pub use sphere::*; pub use timeline::*; +pub use versioned_map::*; diff --git a/rust/noosphere/src/view/mutation.rs b/rust/noosphere/src/view/mutation.rs index 4193125b7..3ca95653a 100644 --- a/rust/noosphere/src/view/mutation.rs +++ b/rust/noosphere/src/view/mutation.rs @@ -1,26 +1,29 @@ use anyhow::{anyhow, Result}; use cid::Cid; +use libipld_cbor::DagCborCodec; use ucan::{crypto::KeyMaterial, ucan::Ucan}; -use crate::data::{LinksChangelogIpld, LinksOperation, MemoIpld}; +use crate::data::{ + ChangelogIpld, CidKey, DelegationIpld, MapOperation, MemoIpld, RevocationIpld, VersionedMapKey, + VersionedMapValue, +}; -use noosphere_storage::interface::{DagCborStore, Store}; +use noosphere_storage::interface::BlockStore; #[derive(Debug)] -pub struct SphereRevision { - pub store: Storage, +pub struct SphereRevision { + pub store: S, pub memo: MemoIpld, } -impl SphereRevision { - // TODO: It would be nice if this was internally mutable +impl SphereRevision { pub async fn try_sign( &mut self, credential: &Credential, proof: Option<&Ucan>, ) -> Result { self.memo.sign(credential, proof).await?; - self.store.save(&self.memo).await + self.store.save::(&self.memo).await } } @@ -28,6 +31,8 @@ impl SphereRevision { pub struct SphereMutation { did: String, links: LinksMutation, + allowed_ucans: AllowedUcansMutation, + revoked_ucans: RevokedUcansMutation, } impl<'a> SphereMutation { @@ -35,6 +40,8 @@ impl<'a> SphereMutation { SphereMutation { did: did.into(), links: LinksMutation::new(did), + allowed_ucans: AllowedUcansMutation::new(did), + revoked_ucans: RevokedUcansMutation::new(did), } } @@ -49,23 +56,60 @@ impl<'a> SphereMutation { pub fn links(&self) -> &LinksMutation { &self.links } + + pub fn allowed_ucans_mut(&mut self) -> &mut AllowedUcansMutation { + &mut self.allowed_ucans + } + + pub fn allowed_ucans(&self) -> &AllowedUcansMutation { + &self.allowed_ucans + } + + pub fn revoked_ucans_mut(&mut self) -> &mut RevokedUcansMutation { + &mut self.revoked_ucans + } + + pub fn revoked_ucans(&self) -> &RevokedUcansMutation { + &self.revoked_ucans + } } +pub type LinksMutation = VersionedMapMutation; +pub type AllowedUcansMutation = VersionedMapMutation; +pub type RevokedUcansMutation = VersionedMapMutation; + #[derive(Default, Debug)] -pub struct LinksMutation { +pub struct VersionedMapMutation +where + K: VersionedMapKey, + V: VersionedMapValue, +{ did: String, - changes: Vec, + changes: Vec>, } -impl LinksMutation { - pub fn try_apply_changelog(&mut self, changelog: &LinksChangelogIpld) -> Result<()> { +impl VersionedMapMutation +where + K: VersionedMapKey, + V: VersionedMapValue, +{ + pub fn try_apply_changelog( + &mut self, + changelog: &ChangelogIpld>, + ) -> Result<()> { let did = changelog .did .as_ref() .ok_or_else(|| anyhow!("Changelog did not have an author DID"))?; + // println!("CHANGELOG INSIDE MUTATION: {:?}", changelog); + if did != &self.did { - return Err(anyhow!("Changelog has unexpected author")); + return Err(anyhow!( + "Changelog has unexpected author (was {}, expected {})", + did, + self.did + )); } self.changes = changelog.changes.clone(); @@ -74,7 +118,7 @@ impl LinksMutation { } pub fn new(did: &str) -> Self { - LinksMutation { + VersionedMapMutation { did: did.into(), changes: Default::default(), } @@ -83,19 +127,18 @@ impl LinksMutation { &self.did } - pub fn changes(&self) -> &[LinksOperation] { + pub fn changes(&self) -> &[MapOperation] { &self.changes } - pub fn set(&mut self, slug: &str, value: &Cid) { - self.changes.push(LinksOperation::Add { - key: slug.into(), + pub fn set(&mut self, key: &K, value: &V) { + self.changes.push(MapOperation::Add { + key: key.clone(), value: value.clone(), }); } - pub fn remove(&mut self, slug: &str) { - self.changes - .push(LinksOperation::Remove { key: slug.into() }); + pub fn remove(&mut self, key: &K) { + self.changes.push(MapOperation::Remove { key: key.clone() }); } } diff --git a/rust/noosphere/src/view/sphere.rs b/rust/noosphere/src/view/sphere.rs index 78b191da0..d0926da8f 100644 --- a/rust/noosphere/src/view/sphere.rs +++ b/rust/noosphere/src/view/sphere.rs @@ -1,5 +1,9 @@ use anyhow::{anyhow, Result}; -use cid::Cid; +use cid::{ + Cid, +}; +use libipld_cbor::DagCborCodec; +use libipld_core::{codec::Codec, ipld::Ipld, raw::RawCodec}; use ucan::{ builder::UcanBuilder, capability::{Capability, Resource, With}, @@ -13,25 +17,31 @@ use crate::{ ed25519_key_to_mnemonic, generate_ed25519_key, restore_ed25519_key, SphereAction, SphereReference, SPHERE_SEMANTICS, }, - data::{Bundle, ContentType, Header, MemoIpld, SphereIpld, TryBundle}, + data::{ + AuthorizationIpld, Bundle, CidKey, ContentType, DelegationIpld, Header, MemoIpld, + RevocationIpld, SphereIpld, TryBundle, + }, + encoding::block_encode, view::{Links, SphereMutation, SphereRevision, Timeline}, }; -use noosphere_storage::interface::{DagCborStore, Store}; +use noosphere_storage::{interface::BlockStore, ucan::UcanStore}; + +use super::{AllowedUcans, Authorization, RevokedUcans}; pub const SPHERE_LIFETIME: u64 = 315360000000; // 10,000 years (arbitrarily high) /// High-level Sphere I/O -pub struct Sphere { - store: Storage, +pub struct Sphere { + store: S, cid: Cid, } -impl Sphere { - pub fn at(cid: &Cid, store: &Storage) -> Sphere { +impl Sphere { + pub fn at(cid: &Cid, store: &S) -> Sphere { Sphere { store: store.clone(), - cid: cid.clone(), + cid: *cid, } } @@ -40,11 +50,13 @@ impl Sphere { } pub async fn try_as_memo(&self) -> Result { - self.store.load(&self.cid).await + self.store.load::(&self.cid).await } pub async fn try_as_body(&self) -> Result { - self.store.load(&self.try_as_memo().await?.body).await + self.store + .load::(&self.try_as_memo().await?.body) + .await } pub async fn try_as_bundle(&self) -> Result { @@ -59,17 +71,23 @@ impl Sphere { .await } - pub async fn try_get_parent(&self) -> Result>> { + pub async fn try_get_parent(&self) -> Result>> { match self.try_as_memo().await?.parent { Some(cid) => Ok(Some(Sphere::at(&cid, &self.store))), None => Ok(None), } } - pub async fn try_get_links(&self) -> Result> { + pub async fn try_get_links(&self) -> Result> { let sphere = self.try_as_body().await?; - Ok(Links::try_at_or_empty(sphere.links.as_ref(), &mut self.store.clone()).await?) + Links::try_at_or_empty(sphere.links.as_ref(), &mut self.store.clone()).await + } + + pub async fn try_get_authorization(&self) -> Result> { + let sphere = self.try_as_body().await?; + + Authorization::try_at_or_empty(sphere.authorization.as_ref(), &mut self.store.clone()).await } pub async fn try_get_identity(&self) -> Result { @@ -78,44 +96,131 @@ impl Sphere { Ok(sphere.identity) } - pub async fn try_to_mutation(&self) -> Result { - let memo: MemoIpld = self.store.load(&self.cid).await?; + /// Derive the mutation that would be required to produce the current + /// sphere revision given its immediate ancestral parent. Note that + /// this only considers changes that are internal to the sphere, and is + /// not inclusive of the headers of the sphere's memo. + pub async fn try_derive_mutation(&self) -> Result { + let memo = self.try_as_memo().await?; let author = memo .get_first_header(&Header::Author.to_string()) .ok_or_else(|| anyhow!("No author header found"))?; + let mut mutation = SphereMutation::new(&author); + let parent = match self.try_get_parent().await? { + Some(parent) => parent, + None => return Ok(mutation), + }; + + let parent_links = parent.try_get_links().await?; let links = self.try_get_links().await?; - let changelog = links.try_get_changelog().await?; - if !changelog.is_empty() { - mutation.links_mut().try_apply_changelog(&changelog)?; + if links.cid() != parent_links.cid() { + let changelog = links.try_get_changelog().await?; + + if changelog.is_empty() { + return Err(anyhow!("Links has changed but the changelog is empty")); + } + + mutation.links_mut().try_apply_changelog(changelog)?; + } + + let parent_authorization = parent.try_get_authorization().await?; + let authorization = self.try_get_authorization().await?; + + if authorization.cid() != parent_authorization.cid() { + let parent_allowed_ucans = parent_authorization.try_get_allowed_ucans().await?; + let allowed_ucans = authorization.try_get_allowed_ucans().await?; + + if allowed_ucans.cid() != parent_allowed_ucans.cid() { + let changelog = allowed_ucans.try_get_changelog().await?; + + if changelog.is_empty() { + return Err(anyhow!("Allowed UCANs changed but the changelog is empty")); + } + + mutation + .allowed_ucans_mut() + .try_apply_changelog(changelog)?; + } + + let parent_revoked_ucans = parent_authorization.try_get_revoked_ucans().await?; + let revoked_ucans = authorization.try_get_revoked_ucans().await?; + + if revoked_ucans.cid() != parent_revoked_ucans.cid() { + let changelog = revoked_ucans.try_get_changelog().await?; + + if changelog.is_empty() { + return Err(anyhow!("Revoked UCANs changed but the changelog is empty")); + } + + mutation + .revoked_ucans_mut() + .try_apply_changelog(changelog)?; + } } Ok(mutation) } - pub async fn try_apply(&self, mutation: &SphereMutation) -> Result> { - Sphere::try_apply_with_cid(self.cid(), mutation, &mut self.store.clone()).await + /// Apply a mutation to the sphere, producing a new sphere revision that + /// must then be signed as an additional step. + pub async fn try_apply_mutation(&self, mutation: &SphereMutation) -> Result> { + Sphere::try_apply_mutation_with_cid(self.cid(), mutation, &mut self.store.clone()).await } - pub async fn try_apply_with_cid( + /// Apply a mutation to the sphere given a revision CID, producing a new + /// sphere revision that must then be signed as an additional step + pub async fn try_apply_mutation_with_cid( cid: &Cid, mutation: &SphereMutation, - store: &mut Storage, - ) -> Result> { + store: &mut S, + ) -> Result> { let links_mutation = mutation.links(); - let mut memo: MemoIpld = MemoIpld::branch_from(cid, store).await?; - let mut sphere: SphereIpld = store.load(&memo.body).await?; + let mut memo = MemoIpld::branch_from(cid, store).await?; + let mut sphere = store.load::(&memo.body).await?; - sphere.links = match links_mutation.changes().len() > 0 { + sphere.links = match !links_mutation.changes().is_empty() { true => { Some(Links::try_apply_with_cid(sphere.links.as_ref(), links_mutation, store).await?) } false => sphere.links, }; - memo.body = store.save(&sphere).await?; + let allowed_ucans_mutation = mutation.allowed_ucans(); + let revoked_ucans_mutation = mutation.revoked_ucans(); + + if !allowed_ucans_mutation.changes().is_empty() + || !revoked_ucans_mutation.changes().is_empty() + { + let mut authorization = match sphere.authorization { + Some(cid) => store.load::(&cid).await?, + None => AuthorizationIpld::try_empty(store).await?, + }; + + if !allowed_ucans_mutation.changes().is_empty() { + authorization.allowed = AllowedUcans::try_apply_with_cid( + Some(&authorization.allowed), + allowed_ucans_mutation, + store, + ) + .await?; + } + + if !revoked_ucans_mutation.changes().is_empty() { + authorization.revoked = RevokedUcans::try_apply_with_cid( + Some(&authorization.revoked), + revoked_ucans_mutation, + store, + ) + .await?; + } + + sphere.authorization = Some(store.save::(&authorization).await?); + } + + memo.body = store.save::(&sphere).await?; Ok(SphereRevision { memo, @@ -123,18 +228,18 @@ impl Sphere { }) } - pub async fn try_rebase(&self, onto: &Cid) -> Result> { + pub async fn try_rebase(&self, onto: &Cid) -> Result> { Sphere::try_rebase_with_cid(self.cid(), onto, &mut self.store.clone()).await } pub async fn try_rebase_with_cid( cid: &Cid, onto: &Cid, - store: &mut Storage, - ) -> Result> { - Sphere::try_apply_with_cid( + store: &mut S, + ) -> Result> { + Sphere::try_apply_mutation_with_cid( onto, - &Sphere::at(cid, store).try_to_mutation().await?, + &Sphere::at(cid, store).try_derive_mutation().await?, store, ) .await @@ -144,7 +249,7 @@ impl Sphere { Sphere::try_hydrate_with_cid(self.cid(), &mut self.store.clone()).await } - pub async fn try_hydrate_with_cid(cid: &Cid, store: &mut Storage) -> Result<()> { + pub async fn try_hydrate_with_cid(cid: &Cid, store: &mut S) -> Result<()> { let sphere = Sphere::at(cid, store); let memo = sphere.try_as_memo().await?; let base_cid = match memo.parent { @@ -153,12 +258,16 @@ impl Sphere { let mut base_sphere = SphereIpld::default(); base_sphere.identity = sphere.try_get_identity().await?; let empty_dag = MemoIpld::for_body(store, &base_sphere).await?; - store.save(&empty_dag).await? + store.save::(&empty_dag).await? } }; - let hydrated_revision = - Sphere::try_apply_with_cid(&base_cid, &sphere.try_to_mutation().await?, store).await?; + let hydrated_revision = Sphere::try_apply_mutation_with_cid( + &base_cid, + &sphere.try_derive_mutation().await?, + store, + ) + .await?; if hydrated_revision.memo.body != memo.body { return Err(anyhow!( @@ -171,6 +280,8 @@ impl Sphere { Ok(()) } + /// Attempt to linearize the canonical history of the sphere by re-basing + /// the history onto a branch with an implicitly common lineage. pub async fn try_sync( &self, old_base: &Cid, @@ -190,7 +301,7 @@ impl Sphere { let timeslice = timeline.slice(self.cid(), Some(old_base)); let rebase_revisions = timeslice.try_to_chronological().await?; - let mut next_base = new_base.clone(); + let mut next_base = *new_base; for (cid, _) in rebase_revisions.iter().skip(1) { let mut revision = Sphere::try_rebase_with_cid(cid, &next_base, &mut store).await?; @@ -200,20 +311,22 @@ impl Sphere { Ok(next_base) } - pub async fn try_generate( - owner_did: &str, - store: &mut Storage, - ) -> Result<(Sphere, Ucan, String)> { - let key_material = generate_ed25519_key(); - let mnemonic = ed25519_key_to_mnemonic(&key_material)?; - let did = key_material.get_did().await?; + /// Generate a new sphere and assign a DID as its owner. The returned tuple + /// includes the UCAN authorization that enables the owner to manage the + /// the sphere, as well as a mnemonic string that should be stored side-band + /// by the owner for the case that they wish to transfer ownership (e.g., if + /// key rotation is called for). + pub async fn try_generate(owner_did: &str, store: &mut S) -> Result<(Sphere, Ucan, String)> { + let sphere_key = generate_ed25519_key(); + let mnemonic = ed25519_key_to_mnemonic(&sphere_key)?; + let sphere_did = sphere_key.get_did().await?; let mut memo = MemoIpld::for_body( store, &SphereIpld { - identity: did.clone(), + identity: sphere_did.clone(), links: None, sealed: None, - revocations: None, + authorization: None, }, ) .await?; @@ -225,13 +338,15 @@ impl Sphere { let capability = Capability { with: With::Resource { - kind: Resource::Scoped(SphereReference { did }), + kind: Resource::Scoped(SphereReference { + did: sphere_did.clone(), + }), }, can: SphereAction::Authorize, }; let ucan = UcanBuilder::default() - .issued_by(&key_material) + .issued_by(&sphere_key) .for_audience(owner_did) .with_lifetime(SPHERE_LIFETIME) .claiming_capability(&capability) @@ -239,35 +354,57 @@ impl Sphere { .sign() .await?; - memo.sign(&key_material, None).await?; + memo.sign(&sphere_key, None).await?; + + let sphere_cid = store.save::(&memo).await?; + + let jwt = ucan.encode()?; + let delegation = DelegationIpld::try_register("(OWNER)", &jwt, store).await?; + + let sphere = Sphere::at(&sphere_cid, store); + let mut mutation = SphereMutation::new(&sphere_did); + mutation + .allowed_ucans_mut() + .set(&CidKey(delegation.jwt), &delegation); - let sphere_cid = store.save(&memo).await?; + let mut revision = sphere.try_apply_mutation(&mutation).await?; + let sphere_cid = revision.try_sign(&sphere_key, None).await?; - Ok((Sphere::at(&sphere_cid, &store), ucan, mnemonic)) + Ok((Sphere::at(&sphere_cid, store), ucan, mnemonic)) } - pub async fn try_authorize( + /// Change ownership of the sphere, producing a new UCAN authorization for + /// the new owner and registering a revocation of the previous owner's + /// authorization within the sphere. + pub async fn try_change_owner( &self, mnemonic: &str, next_owner_did: &str, current_proof: &Ucan, did_parser: &mut DidParser, - ) -> Result { - let memo: MemoIpld = self.store.load(&self.cid).await?; - let sphere: SphereIpld = self.store.load(&memo.body).await?; - let did = sphere.identity; + ) -> Result<(Sphere, Ucan)> { + let memo = self.store.load::(&self.cid).await?; + let sphere = self + .store + .load::(&memo.body) + .await?; + let sphere_did = sphere.identity; let restored_key = restore_ed25519_key(mnemonic)?; let restored_did = restored_key.get_did().await?; - if did != restored_did { + if sphere_did != restored_did { return Err(anyhow!("Incorrect mnemonic provided")); } - let proof_chain = ProofChain::from_ucan(current_proof.clone(), did_parser).await?; + let ucan_store = UcanStore(self.store.clone()); + let proof_chain = + ProofChain::from_ucan(current_proof.clone(), did_parser, &ucan_store).await?; let authorize_capability = Capability { with: With::Resource { - kind: Resource::Scoped(SphereReference { did: did.clone() }), + kind: Resource::Scoped(SphereReference { + did: sphere_did.clone(), + }), }, can: SphereAction::Authorize, }; @@ -275,7 +412,9 @@ impl Sphere { let mut proof_is_valid = false; for info in proof_chain.reduce_capabilities(&SPHERE_SEMANTICS) { - if info.capability.enables(&authorize_capability) && info.originators.contains(&did) { + if info.capability.enables(&authorize_capability) + && info.originators.contains(&sphere_did) + { proof_is_valid = true; break; } @@ -287,7 +426,9 @@ impl Sphere { )); } - // TODO(#21): Revoke old proof + let (current_jwt_cid, _) = + block_encode::(&Ipld::Bytes(current_proof.encode()?.as_bytes().to_vec()))?; + let revocation = RevocationIpld::try_revoke(¤t_jwt_cid, &restored_key).await?; let ucan = UcanBuilder::default() .issued_by(&restored_key) @@ -298,14 +439,39 @@ impl Sphere { .sign() .await?; - Ok(ucan) + let jwt = ucan.encode()?; + let delegation = DelegationIpld::try_register("(OWNER)", &jwt, &self.store).await?; + + let mut mutation = SphereMutation::new(&sphere_did); + mutation + .allowed_ucans_mut() + .set(&CidKey(delegation.jwt), &delegation); + mutation + .revoked_ucans_mut() + .set(&CidKey(current_jwt_cid), &revocation); + + let mut revision = self.try_apply_mutation(&mutation).await?; + let sphere_cid = revision.try_sign(&restored_key, None).await?; + + Ok((Sphere::at(&sphere_cid, &self.store), ucan)) } + + // pub async fn try_add_author( + // &self, + // new_author_did: &str, + // authorizing_proof: &Ucan, + // ) -> Result<(Sphere, Ucan)> { + // // Turn the proof into a JWT + // // Make a new revision of the sphere + // // let mutation = SphereMutation::new + // } } #[cfg(test)] mod tests { use cid::Cid; - use futures::{pin_mut, StreamExt}; + use libipld_core::{ipld::Ipld, raw::RawCodec}; + use serde_bytes::Bytes; use ucan::{ builder::UcanBuilder, capability::{Capability, Resource, With}, @@ -321,12 +487,13 @@ mod tests { ed25519_key_to_mnemonic, generate_ed25519_key, SphereAction, SphereReference, SUPPORTED_KEYS, }, - data::Bundle, + data::{Bundle, CidKey, DelegationIpld, RevocationIpld}, + encoding::block_encode, view::{Sphere, SphereMutation, Timeline, SPHERE_LIFETIME}, }; use noosphere_storage::{ - interface::{DagCborStore, Store}, + interface::{BlockStore, Store}, memory::MemoryStore, }; @@ -340,10 +507,7 @@ mod tests { let owner_did = owner_key.get_did().await.unwrap(); let (sphere, _, _) = Sphere::try_generate(&owner_did, &mut store).await.unwrap(); - ( - sphere.cid().clone(), - sphere.try_get_identity().await.unwrap(), - ) + (*sphere.cid(), sphere.try_get_identity().await.unwrap()) }; let restored_sphere = Sphere::at(&sphere_cid, &store); @@ -352,6 +516,36 @@ mod tests { assert_eq!(sphere_identity, restored_identity); } + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_includes_the_owner_in_the_list_of_authorizations() { + let mut store = MemoryStore::default(); + + let owner_key = generate_ed25519_key(); + let owner_did = owner_key.get_did().await.unwrap(); + let (sphere, ucan, _) = Sphere::try_generate(&owner_did, &mut store).await.unwrap(); + + // let ucan_jwt_bytes = RawCodec + // .encode(&Ipld::Bytes(ucan.encode().unwrap().as_bytes().to_vec())) + // .unwrap(); + // let ucan_jwt_cid = Cid::new_v1(RawCodec.into(), Code::Blake2b256.digest(&ucan_jwt_bytes)); + let (ucan_jwt_cid, _) = + block_encode::(&Ipld::Bytes(ucan.encode().unwrap().as_bytes().to_vec())) + .unwrap(); + + let authorization = sphere.try_get_authorization().await.unwrap(); + let allowed_ucans = authorization.try_get_allowed_ucans().await.unwrap(); + let authorization = allowed_ucans.get(&CidKey(ucan_jwt_cid)).await.unwrap(); + + assert_eq!( + authorization, + Some(&DelegationIpld { + name: String::from("(OWNER)"), + jwt: ucan_jwt_cid + }) + ); + } + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] async fn it_may_authorize_a_different_key_after_being_created() { @@ -367,14 +561,73 @@ mod tests { let next_owner_did = next_owner_key.get_did().await.unwrap(); let mut did_parser = DidParser::new(SUPPORTED_KEYS); - let new_ucan = sphere - .try_authorize(&mnemonic, &next_owner_did, &ucan, &mut did_parser) + let (_, new_ucan) = sphere + .try_change_owner(&mnemonic, &next_owner_did, &ucan, &mut did_parser) .await .unwrap(); assert_ne!(ucan.audience(), new_ucan.audience()); } + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_delegates_to_a_new_owner_and_revokes_the_old_delegation() { + let mut store = MemoryStore::default(); + let owner_key = generate_ed25519_key(); + let owner_did = owner_key.get_did().await.unwrap(); + + let (sphere, original_ucan, mnemonic) = + { Sphere::try_generate(&owner_did, &mut store).await.unwrap() }; + + let sphere_identity = sphere.try_get_identity().await.unwrap(); + + let next_owner_key = generate_ed25519_key(); + let next_owner_did = next_owner_key.get_did().await.unwrap(); + + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + let (sphere, new_ucan) = sphere + .try_change_owner(&mnemonic, &next_owner_did, &original_ucan, &mut did_parser) + .await + .unwrap(); + + let (original_jwt_cid, _) = block_encode::(&Ipld::Bytes( + original_ucan.encode().unwrap().as_bytes().to_vec(), + )) + .unwrap(); + + let (new_jwt_cid, _) = block_encode::(&Ipld::Bytes( + new_ucan.encode().unwrap().as_bytes().to_vec(), + )) + .unwrap(); + + let authorization = sphere.try_get_authorization().await.unwrap(); + + let allowed_ucans = authorization.try_get_allowed_ucans().await.unwrap(); + let revoked_ucans = authorization.try_get_revoked_ucans().await.unwrap(); + + let new_delegation = allowed_ucans.get(&CidKey(new_jwt_cid)).await.unwrap(); + let new_revocation = revoked_ucans.get(&CidKey(original_jwt_cid)).await.unwrap(); + + assert_eq!( + new_delegation, + Some(&DelegationIpld { + name: "(OWNER)".into(), + jwt: new_jwt_cid + }) + ); + + assert!(new_revocation.is_some()); + + let new_revocation = new_revocation.unwrap(); + + assert_eq!(new_revocation.iss, sphere.try_get_identity().await.unwrap()); + assert_eq!(new_revocation.revoke, original_jwt_cid.to_string()); + + let sphere_key = did_parser.parse(&sphere_identity).unwrap(); + + new_revocation.try_verify(&sphere_key).await.unwrap(); + } + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] async fn it_wont_authorize_a_different_key_if_the_mnemonic_is_wrong() { @@ -392,7 +645,7 @@ mod tests { let mut did_parser = DidParser::new(SUPPORTED_KEYS); let authorize_result = sphere - .try_authorize(&incorrect_mnemonic, &next_owner_did, &ucan, &mut did_parser) + .try_change_owner(&incorrect_mnemonic, &next_owner_did, &ucan, &mut did_parser) .await; assert!(authorize_result.is_err()); @@ -431,7 +684,7 @@ mod tests { let mut did_parser = DidParser::new(SUPPORTED_KEYS); let authorize_result = sphere - .try_authorize( + .try_change_owner( &mnemonic, &next_owner_did, &insufficient_ucan, @@ -446,7 +699,8 @@ mod tests { #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] async fn it_can_assign_a_link_and_later_read_the_link() { let mut store = MemoryStore::default(); - let foo_cid = store.write_cbor(b"foo").await.unwrap(); + let foo_cid = store.save::(Bytes::new(b"foo")).await.unwrap(); + let foo_key = String::from("foo"); let sphere_cid = { let owner_key = generate_ed25519_key(); @@ -454,17 +708,17 @@ mod tests { let (sphere, ucan, _) = Sphere::try_generate(&owner_did, &mut store).await.unwrap(); let mut mutation = SphereMutation::new(&owner_did); - mutation.links_mut().set("foo", &foo_cid); + mutation.links_mut().set(&foo_key, &foo_cid); - let mut revision = sphere.try_apply(&mutation).await.unwrap(); + let mut revision = sphere.try_apply_mutation(&mutation).await.unwrap(); revision.try_sign(&owner_key, Some(&ucan)).await.unwrap() }; let restored_sphere = Sphere::at(&sphere_cid, &store); let restored_links = restored_sphere.try_get_links().await.unwrap(); - let restored_foo_cid = restored_links.get("foo").await.unwrap().unwrap(); + let restored_foo_cid = restored_links.get(&foo_key).await.unwrap().unwrap(); - assert_eq!(foo_cid, restored_foo_cid); + assert_eq!(&foo_cid, restored_foo_cid); } #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] @@ -475,14 +729,16 @@ mod tests { let owner_did = owner_key.get_did().await.unwrap(); let (mut sphere, ucan, _) = Sphere::try_generate(&owner_did, &mut store).await.unwrap(); - let mut lineage = vec![sphere.cid().clone()]; + let mut lineage = vec![*sphere.cid()]; + let foo_key = String::from("foo"); for i in 0..2u8 { let mut mutation = SphereMutation::new(&owner_did); - mutation - .links_mut() - .set("foo", &store.write_cbor(&[i]).await.unwrap()); - let mut revision = sphere.try_apply(&mutation).await.unwrap(); + mutation.links_mut().set( + &foo_key, + &store.save::(Bytes::new(&[i])).await.unwrap(), + ); + let mut revision = sphere.try_apply_mutation(&mutation).await.unwrap(); let next_cid = revision.try_sign(&owner_key, Some(&ucan)).await.unwrap(); sphere = Sphere::at(&next_cid, &store); @@ -508,15 +764,25 @@ mod tests { let (sphere, ucan, _) = Sphere::try_generate(&owner_did, &mut store).await.unwrap(); - let bar_cid = store.write_cbor(b"bar").await.unwrap(); - let baz_cid = store.write_cbor(b"baz").await.unwrap(); - let foobar_cid = store.write_cbor(b"foobar").await.unwrap(); - let flurb_cid = store.write_cbor(b"flurb").await.unwrap(); + let foo_key = String::from("foo"); + let bar_key = String::from("bar"); + let baz_key = String::from("baz"); + + let bar_cid = store.save::(Bytes::new(b"bar")).await.unwrap(); + let baz_cid = store.save::(Bytes::new(b"baz")).await.unwrap(); + let foobar_cid = store + .save::(Bytes::new(b"foobar")) + .await + .unwrap(); + let flurb_cid = store + .save::(Bytes::new(b"flurb")) + .await + .unwrap(); let mut base_mutation = SphereMutation::new(&owner_did); - base_mutation.links_mut().set("foo", &bar_cid); + base_mutation.links_mut().set(&foo_key, &bar_cid); - let mut base_revision = sphere.try_apply(&base_mutation).await.unwrap(); + let mut base_revision = sphere.try_apply_mutation(&base_mutation).await.unwrap(); let base_cid = base_revision .try_sign(&owner_key, Some(&ucan)) @@ -524,10 +790,10 @@ mod tests { .unwrap(); let mut lineage_a_mutation = SphereMutation::new(&owner_did); - lineage_a_mutation.links_mut().set("bar", &baz_cid); + lineage_a_mutation.links_mut().set(&bar_key, &baz_cid); let mut lineage_a_revision = - Sphere::try_apply_with_cid(&base_cid, &lineage_a_mutation, &mut store) + Sphere::try_apply_mutation_with_cid(&base_cid, &lineage_a_mutation, &mut store) .await .unwrap(); let lineage_a_cid = lineage_a_revision @@ -536,11 +802,11 @@ mod tests { .unwrap(); let mut lineage_b_mutation = SphereMutation::new(&owner_did); - lineage_b_mutation.links_mut().set("foo", &foobar_cid); - lineage_b_mutation.links_mut().set("baz", &flurb_cid); + lineage_b_mutation.links_mut().set(&foo_key, &foobar_cid); + lineage_b_mutation.links_mut().set(&baz_key, &flurb_cid); let mut lineage_b_revision = - Sphere::try_apply_with_cid(&base_cid, &lineage_b_mutation, &mut store) + Sphere::try_apply_mutation_with_cid(&base_cid, &lineage_b_mutation, &mut store) .await .unwrap(); let lineage_b_cid = lineage_b_revision @@ -566,9 +832,12 @@ mod tests { let grandparent_sphere = parent_sphere.try_get_parent().await.unwrap().unwrap(); assert_eq!(grandparent_sphere.cid(), &base_cid); - assert_eq!(rebased_links.get("foo").await.unwrap(), Some(foobar_cid)); - assert_eq!(rebased_links.get("bar").await.unwrap(), Some(baz_cid)); - assert_eq!(rebased_links.get("baz").await.unwrap(), Some(flurb_cid)); + assert_eq!( + rebased_links.get(&foo_key).await.unwrap(), + Some(&foobar_cid) + ); + assert_eq!(rebased_links.get(&bar_key).await.unwrap(), Some(&baz_cid)); + assert_eq!(rebased_links.get(&baz_key).await.unwrap(), Some(&flurb_cid)); } #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] @@ -584,10 +853,11 @@ mod tests { let mut mutation = SphereMutation::new(&owner_did); let key = format!("key{}", i); - mutation - .links_mut() - .set(&key, &store.write_cbor(&[i]).await.unwrap()); - let mut revision = sphere.try_apply(&mutation).await.unwrap(); + mutation.links_mut().set( + &key, + &store.save::(Bytes::new(&[i])).await.unwrap(), + ); + let mut revision = sphere.try_apply_mutation(&mutation).await.unwrap(); let next_cid = revision.try_sign(&owner_key, Some(&ucan)).await.unwrap(); sphere = Sphere::at(&next_cid, &store); } @@ -595,8 +865,64 @@ mod tests { let bundle = sphere.try_bundle_until_ancestor(None).await.unwrap(); let mut other_store = MemoryStore::default(); - for (_, block) in bundle.map() { - other_store.write_cbor(block).await.unwrap(); + for (cid, block) in bundle.map() { + other_store.put_block(cid, block).await.unwrap(); + } + + let timeline = Timeline::new(&other_store); + let timeslice = timeline.slice(sphere.cid(), None); + let items = timeslice.try_to_chronological().await.unwrap(); + + for (cid, _) in items { + Sphere::at(&cid, &other_store).try_hydrate().await.unwrap(); + } + + store.expect_replica_in(&other_store).await.unwrap(); + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_can_hydrate_revisions_of_authorization_changes() { + let mut store = MemoryStore::default(); + let owner_key = generate_ed25519_key(); + let owner_did = owner_key.get_did().await.unwrap(); + + let (mut sphere, ucan, _) = Sphere::try_generate(&owner_did, &mut store).await.unwrap(); + + let delegation = DelegationIpld::try_register("Test", &ucan.encode().unwrap(), &store) + .await + .unwrap(); + + let mut mutation = SphereMutation::new(&owner_did); + + mutation + .allowed_ucans_mut() + .set(&CidKey(delegation.jwt), &delegation); + + let mut revision = sphere.try_apply_mutation(&mutation).await.unwrap(); + let next_cid = revision.try_sign(&owner_key, Some(&ucan)).await.unwrap(); + + sphere = Sphere::at(&next_cid, &store); + + let mut mutation = SphereMutation::new(&owner_did); + + mutation.revoked_ucans_mut().set( + &CidKey(delegation.jwt), + &RevocationIpld::try_revoke(&delegation.jwt, &owner_key) + .await + .unwrap(), + ); + + let mut revision = sphere.try_apply_mutation(&mutation).await.unwrap(); + let next_cid = revision.try_sign(&owner_key, Some(&ucan)).await.unwrap(); + + sphere = Sphere::at(&next_cid, &store); + + let bundle = sphere.try_bundle_until_ancestor(None).await.unwrap(); + let mut other_store = MemoryStore::default(); + + for (cid, block) in bundle.map() { + other_store.put_block(cid, block).await.unwrap(); } let timeline = Timeline::new(&other_store); @@ -626,18 +952,25 @@ mod tests { (change_key, change_cid): (&str, &Cid), ) -> anyhow::Result { let mut mutation = SphereMutation::new(author_did); - mutation.links_mut().set(change_key, change_cid); + mutation.links_mut().set(&change_key.into(), change_cid); - let mut base_revision = Sphere::try_apply_with_cid(base_cid, &mutation, store).await?; + let mut base_revision = + Sphere::try_apply_mutation_with_cid(base_cid, &mutation, store).await?; - Ok(base_revision.try_sign(credential, Some(&proof)).await?) + base_revision.try_sign(credential, Some(proof)).await } - let foo_cid = store.write_cbor(b"foo").await.unwrap(); - let bar_cid = store.write_cbor(b"bar").await.unwrap(); - let baz_cid = store.write_cbor(b"baz").await.unwrap(); - let foobar_cid = store.write_cbor(b"foobar").await.unwrap(); - let flurb_cid = store.write_cbor(b"flurb").await.unwrap(); + let foo_cid = store.save::(Bytes::new(b"foo")).await.unwrap(); + let bar_cid = store.save::(Bytes::new(b"bar")).await.unwrap(); + let baz_cid = store.save::(Bytes::new(b"baz")).await.unwrap(); + let foobar_cid = store + .save::(Bytes::new(b"foobar")) + .await + .unwrap(); + let flurb_cid = store + .save::(Bytes::new(b"flurb")) + .await + .unwrap(); let (sphere, ucan, _) = Sphere::try_generate(&owner_did, &mut store).await.unwrap(); diff --git a/rust/noosphere/src/view/timeline.rs b/rust/noosphere/src/view/timeline.rs index f7063cb56..2e711ad02 100644 --- a/rust/noosphere/src/view/timeline.rs +++ b/rust/noosphere/src/view/timeline.rs @@ -2,26 +2,27 @@ use std::{collections::VecDeque, fmt::Display}; use anyhow::Result; use cid::Cid; -use futures::{stream, Stream, StreamExt, TryStream}; +use futures::{stream, StreamExt, TryStream}; +use libipld_cbor::DagCborCodec; use crate::data::MemoIpld; -use noosphere_storage::interface::{DagCborStore, Store}; +use noosphere_storage::interface::BlockStore; // Assumptions: // - network operations are _always_ mediated by a "remote" agent (no client-to-client syncing) // - the "remote" always has the authoritative state (we always rebase merge onto remote's tip) -pub struct Timeline<'a, Storage: Store> { - pub store: &'a Storage, +pub struct Timeline<'a, S: BlockStore> { + pub store: &'a S, } -impl<'a, Storage: Store> Timeline<'a, Storage> { - pub fn new(store: &'a Storage) -> Self { +impl<'a, S: BlockStore> Timeline<'a, S> { + pub fn new(store: &'a S) -> Self { Timeline { store } } - pub fn slice(&'a self, future: &'a Cid, past: Option<&'a Cid>) -> Timeslice<'a, Storage> { + pub fn slice(&'a self, future: &'a Cid, past: Option<&'a Cid>) -> Timeslice<'a, S> { Timeslice { timeline: self, past, @@ -36,12 +37,12 @@ impl<'a, Storage: Store> Timeline<'a, Storage> { past: Option<&Cid>, ) -> impl TryStream> { stream::try_unfold( - (Some(future.clone()), past.cloned(), self.store.clone()), + (Some(*future), past.cloned(), self.store.clone()), |(from, to, storage)| async move { match from { Some(from) => { let cid = from; - let next_dag: MemoIpld = storage.load(&cid).await?; + let next_dag = storage.load::(&cid).await?; let next_from = match to { Some(to) if from == to => None, @@ -57,13 +58,13 @@ impl<'a, Storage: Store> Timeline<'a, Storage> { } } -pub struct Timeslice<'a, Storage: Store> { - pub timeline: &'a Timeline<'a, Storage>, +pub struct Timeslice<'a, S: BlockStore> { + pub timeline: &'a Timeline<'a, S>, pub past: Option<&'a Cid>, pub future: &'a Cid, } -impl<'a, Storage: Store> Timeslice<'a, Storage> { +impl<'a, S: BlockStore> Timeslice<'a, S> { pub fn try_stream(&self) -> impl TryStream> { self.timeline.try_stream(self.future, self.past) } @@ -80,7 +81,7 @@ impl<'a, Storage: Store> Timeslice<'a, Storage> { } } -impl<'a, Storage: Store> Display for Timeslice<'a, Storage> { +impl<'a, S: BlockStore> Display for Timeslice<'a, S> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, diff --git a/rust/noosphere/src/view/versioned_map.rs b/rust/noosphere/src/view/versioned_map.rs new file mode 100644 index 000000000..a52e2186f --- /dev/null +++ b/rust/noosphere/src/view/versioned_map.rs @@ -0,0 +1,153 @@ +use std::pin::Pin; + +use anyhow::Result; +use async_once_cell::OnceCell; +use cid::Cid; +use futures::Stream; +use libipld_cbor::DagCborCodec; + +use crate::data::{ + ChangelogIpld, CidKey, DelegationIpld, LinksIpld, MapOperation, RevocationIpld, + VersionedMapIpld, VersionedMapKey, VersionedMapValue, +}; + +use noosphere_collections::hamt::Hamt; +use noosphere_storage::interface::BlockStore; + +use super::VersionedMapMutation; + +pub type Links = VersionedMap; +pub type AllowedUcans = VersionedMap; +pub type RevokedUcans = VersionedMap; + +#[derive(Debug)] +pub struct VersionedMap +where + K: VersionedMapKey, + V: VersionedMapValue, + S: BlockStore, +{ + cid: Cid, + store: S, + // NOTE: OnceCell used here for the caching benefits; it may not be necessary for changelog + hamt: OnceCell>, + changelog: OnceCell>>, +} + +impl VersionedMap { + pub async fn try_get_changelog(&self) -> Result<&ChangelogIpld>> { + self.changelog + .get_or_try_init(async { + let ipld = self + .store + .load::>(&self.cid) + .await?; + self.store + .load::>>(&ipld.changelog) + .await + }) + .await + } + + pub async fn try_get_hamt(&self) -> Result<&Hamt> { + self.hamt + .get_or_try_init(async { self.try_load_hamt().await }) + .await + } + + async fn try_load_hamt(&self) -> Result> { + let ipld = self + .store + .load::>(&self.cid) + .await?; + + ipld.try_load_hamt(&self.store).await + } + + pub async fn try_at_or_empty( + cid: Option<&Cid>, + store: &mut S, + ) -> Result> { + Ok(match cid { + Some(cid) => VersionedMap::::at(cid, store), + None => VersionedMap::::try_empty(store).await?, + }) + } + + pub fn cid(&self) -> &Cid { + &self.cid + } + + pub fn at(cid: &Cid, store: &S) -> VersionedMap { + VersionedMap { + cid: *cid, + store: store.clone(), + hamt: OnceCell::new(), + changelog: OnceCell::new(), + } + } + + pub async fn try_empty(store: &mut S) -> Result> { + let ipld = LinksIpld::try_empty(store).await?; + let cid = store.save::(ipld).await?; + + Ok(VersionedMap { + cid, + hamt: OnceCell::new(), + changelog: OnceCell::new(), + store: store.clone(), + }) + } + + pub async fn get(&self, key: &K) -> Result> { + let hamt = self.try_get_hamt().await?; + + hamt.get(key).await + } + + pub async fn try_apply_with_cid( + cid: Option<&Cid>, + mutation: &VersionedMapMutation, + store: &mut S, + ) -> Result { + let map = Self::try_at_or_empty(cid, store).await?; + let mut changelog = map.try_get_changelog().await?.mark(mutation.did()); + let mut hamt = map.try_load_hamt().await?; + + for change in mutation.changes() { + match change { + MapOperation::Add { key, value } => { + hamt.set(key.clone(), value.clone()).await?; + } + MapOperation::Remove { key } => { + hamt.delete(key).await?; + } + }; + + changelog.push(change.clone())?; + } + + let changelog_cid = store.save::(&changelog).await?; + let hamt_cid = hamt.flush().await?; + let links_ipld = LinksIpld { + hamt: hamt_cid, + changelog: changelog_cid, + ..Default::default() + }; + + store.save::(&links_ipld).await + } + + pub async fn for_each(&self, for_each: ForEach) -> Result<()> + where + ForEach: FnMut(&K, &V) -> Result<()>, + { + self.try_get_hamt().await?.for_each(for_each).await + } + + pub async fn stream<'a>( + &'a self, + ) -> Result> + 'a>>> { + Ok(self.try_get_hamt().await?.stream()) + } +} From 9b9ce7d61dffff31ae8484bffa05398259f67968 Mon Sep 17 00:00:00 2001 From: Chris Joel <0xcda7a@gmail.com> Date: Tue, 4 Oct 2022 12:23:37 -0700 Subject: [PATCH 2/2] Update UCAN dependency --- rust/Cargo.lock | 26 ++++++++++++++------------ rust/noosphere-api/Cargo.toml | 5 +++-- rust/noosphere-cli/Cargo.toml | 4 ++-- rust/noosphere-fs/Cargo.toml | 4 ++-- rust/noosphere-gateway/Cargo.toml | 5 ++--- rust/noosphere-into/Cargo.toml | 4 ++-- rust/noosphere-storage/Cargo.toml | 2 +- rust/noosphere/Cargo.toml | 7 ++----- 8 files changed, 28 insertions(+), 29 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 647068752..1c69b1097 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -491,13 +491,13 @@ dependencies = [ [[package]] name = "clap" -version = "4.0.8" +version = "4.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5840cd9093aabeabf7fd932754c435b7674520fc3ddc935c397837050f0f1e4b" +checksum = "30607dd93c420c6f1f80b544be522a0238a7db35e6a12968d28910983fee0df0" dependencies = [ "atty", "bitflags", - "clap_derive 4.0.8", + "clap_derive 4.0.9", "clap_lex 0.3.0", "once_cell", "strsim", @@ -519,9 +519,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.0.8" +version = "4.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92289ffc6fb4a85d85c246ddb874c05a87a2e540fb6ad52f7ca07c8c1e1840b1" +checksum = "a4a307492e1a34939f79d3b6b9650bd2b971513cd775436bf2b78defeb5af00b" dependencies = [ "heck", "proc-macro-error", @@ -1680,7 +1680,7 @@ version = "0.1.0" dependencies = [ "anyhow", "cid", - "clap 4.0.8", + "clap 4.0.9", "home", "noosphere", "noosphere-api", @@ -3052,9 +3052,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" +checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c" dependencies = [ "itoa", "libc", @@ -3328,8 +3328,9 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ucan" -version = "0.6.0-alpha.1" -source = "git+https://github.com/cdata/rs-ucan.git?branch=feature/canonicalization#66fc3ae195997043719ad9663ff5300f719eb850" +version = "0.7.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f04c190573eadd24c3888253029aed61ba319261c67b1ef7f7edd16d47ef70" dependencies = [ "anyhow", "async-recursion", @@ -3354,8 +3355,9 @@ dependencies = [ [[package]] name = "ucan-key-support" -version = "0.4.0-alpha.1" -source = "git+https://github.com/cdata/rs-ucan.git?branch=feature/canonicalization#66fc3ae195997043719ad9663ff5300f719eb850" +version = "0.7.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc1d5cf7db286f46fdbed194a8562329affea51a153fd02c370de15b37488f76" dependencies = [ "anyhow", "async-trait", diff --git a/rust/noosphere-api/Cargo.toml b/rust/noosphere-api/Cargo.toml index a1c0f537b..13322d6e7 100644 --- a/rust/noosphere-api/Cargo.toml +++ b/rust/noosphere-api/Cargo.toml @@ -14,8 +14,9 @@ noosphere = { path = "../noosphere" } noosphere-cbor = { path = "../noosphere-cbor" } noosphere-storage = { path = "../noosphere-storage" } reqwest = { version = "~0.11", features = ["json"] } -ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } -ucan-key-support = { version = "0.4.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } + +ucan = { version = "0.7.0-alpha.1" } +ucan-key-support = { version = "0.7.0-alpha.1" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "^1", features = ["full"] } diff --git a/rust/noosphere-cli/Cargo.toml b/rust/noosphere-cli/Cargo.toml index 0226016df..29ed204ca 100644 --- a/rust/noosphere-cli/Cargo.toml +++ b/rust/noosphere-cli/Cargo.toml @@ -17,8 +17,8 @@ noosphere = { path = "../noosphere" } noosphere-fs = { path = "../noosphere-fs" } noosphere-storage = { path = "../noosphere-storage" } noosphere-api = { path = "../noosphere-api" } -ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } -ucan-key-support = { version = "0.4.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } +ucan = { version = "0.7.0-alpha.1" } +ucan-key-support = { version = "0.7.0-alpha.1" } witty-phrase-generator = "~0.2" anyhow = "^1" url = "^2" diff --git a/rust/noosphere-fs/Cargo.toml b/rust/noosphere-fs/Cargo.toml index 16c9eb05d..062aa8580 100644 --- a/rust/noosphere-fs/Cargo.toml +++ b/rust/noosphere-fs/Cargo.toml @@ -21,8 +21,8 @@ libipld-core = "~0.14" libipld-cbor = "~0.14" futures-util = "~0.3" -ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } -ucan-key-support = { version = "0.4.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } +ucan = { version = "0.7.0-alpha.1" } +ucan-key-support = { version = "0.7.0-alpha.1" } [dev-dependencies] wasm-bindgen-test = "~0.3" diff --git a/rust/noosphere-gateway/Cargo.toml b/rust/noosphere-gateway/Cargo.toml index 93201560b..1dadd9e30 100644 --- a/rust/noosphere-gateway/Cargo.toml +++ b/rust/noosphere-gateway/Cargo.toml @@ -15,9 +15,8 @@ anyhow = "^1" temp-dir = "0.1" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } -ucan-key-support = { version = "0.4.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } - +ucan = { version = "0.7.0-alpha.1" } +ucan-key-support = { version = "0.7.0-alpha.1" } cid = "0.8" noosphere = { path = "../noosphere" } diff --git a/rust/noosphere-into/Cargo.toml b/rust/noosphere-into/Cargo.toml index 2fcf97dca..3a08344e3 100644 --- a/rust/noosphere-into/Cargo.toml +++ b/rust/noosphere-into/Cargo.toml @@ -27,8 +27,8 @@ futures = { version = "~0.3" } async-compat = { version = "~0.2" } async-utf8-decoder = { version = "~0.3" } -ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } -ucan-key-support = { version = "0.4.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } +ucan = { version = "0.7.0-alpha.1" } +ucan-key-support = { version = "0.7.0-alpha.1" } diff --git a/rust/noosphere-storage/Cargo.toml b/rust/noosphere-storage/Cargo.toml index 61017aa3b..f7f64a18f 100644 --- a/rust/noosphere-storage/Cargo.toml +++ b/rust/noosphere-storage/Cargo.toml @@ -12,7 +12,7 @@ async-trait = "~0.1" cid = "~0.8" noosphere-cbor = { path = "../noosphere-cbor" } tracing = "~0.1" -ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } +ucan = { version = "0.7.0-alpha.1" } libipld-core = "~0.14" libipld-cbor = "~0.14" serde = "^1" diff --git a/rust/noosphere/Cargo.toml b/rust/noosphere/Cargo.toml index c3a62dfef..ae9dd9239 100644 --- a/rust/noosphere/Cargo.toml +++ b/rust/noosphere/Cargo.toml @@ -35,11 +35,8 @@ noosphere-storage = { path = "../noosphere-storage" } noosphere-cbor = { path = "../noosphere-cbor" } noosphere-collections = { path = "../noosphere-collections" } -ucan = { version = "0.6.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } -ucan-key-support = { version = "0.4.0-alpha.1", git = "https://github.com/cdata/rs-ucan.git", branch = "feature/canonicalization" } - -# ucan = { version = "0.6.0-alpha.1", path = "../../../../ucan-wg/rs-ucan/ucan" } -# ucan-key-support = { version = "0.4.0-alpha.1", path = "../../../../ucan-wg/rs-ucan/ucan-key-support" } +ucan = { version = "0.7.0-alpha.1" } +ucan-key-support = { version = "0.7.0-alpha.1" } [dev-dependencies] wasm-bindgen-test = "~0.3"