From 3e313d6856404d3670cefb160c293f235d4dbc75 Mon Sep 17 00:00:00 2001 From: Trezy Date: Fri, 20 Mar 2026 02:16:57 +0000 Subject: [PATCH] wasm infra --- Cargo.lock | 1411 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------- Cargo.toml | 5 +++++ migrations/postgres/20260319000001_plugin_tables.sql | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ migrations/sqlite/20260319000001_plugin_tables.sql | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 9 +++++++++ src/external_auth/mod.rs | 4 ++++ src/external_auth/routes.rs | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/external_auth/sync.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 +++ src/lua/atproto_api.rs | 2 ++ src/lua/db_api.rs | 2 ++ src/lua/execute.rs | 2 ++ src/lua/http_api.rs | 2 ++ src/main.rs | 40 ++++++++++++++++++++++++++++++++++++++++ src/plugin/encryption.rs | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/plugin/host/http.rs | 133 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/plugin/host/kv.rs | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/plugin/host/logging.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/plugin/host/lookup.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/plugin/host/mod.rs | 41 +++++++++++++++++++++++++++++++++++++++++ src/plugin/host/secrets.rs | 33 +++++++++++++++++++++++++++++++++ src/plugin/loader.rs | 201 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/plugin/mod.rs | 41 +++++++++++++++++++++++++++++++++++++++++ src/plugin/runtime.rs | 27 +++++++++++++++++++++++++++ src/plugin/types.rs | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/server.rs | 1 + tests/common/app.rs | 2 ++ tests/lua_atproto_api.rs | 2 ++ tests/lua_db_api.rs | 2 ++ tests/plugin_integration.rs | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 file(s) changed, 2753 insertion(s)(+), 22 deletion(s)(-) diff --git a/Cargo.lock b/Cargo.lock --- a/Cargo.lock +++ b/Cargo.lock @@ -3,12 +3,68 @@ # It is not intended for manual editing. version = 4 [[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -24,6 +80,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + +[[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -39,6 +101,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object 0.37.3", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] name = "arc-swap" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -168,7 +245,7 @@ "atrium-api", "atrium-common", "atrium-identity", "atrium-xrpc", - "base64", + "base64 0.22.1", "chrono", "dashmap", "ecdsa", @@ -332,6 +409,12 @@ ] [[package]] name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" @@ -375,6 +458,9 @@ name = "bumpalo" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +dependencies = [ + "allocator-api2", +] [[package]] name = "byteorder" @@ -389,6 +475,84 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] +name = "cap-fs-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes", + "windows-sys 0.52.0", +] + +[[package]] +name = "cap-net-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7" +dependencies = [ + "cap-primitives", + "cap-std", + "rustix 1.1.3", + "smallvec", +] + +[[package]] +name = "cap-primitives" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix 1.1.3", + "rustix-linux-procfs", + "windows-sys 0.52.0", + "winx", +] + +[[package]] +name = "cap-rand" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d" +dependencies = [ + "ambient-authority", + "rand 0.8.5", +] + +[[package]] +name = "cap-std" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes", + "rustix 1.1.3", +] + +[[package]] +name = "cap-time-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80" +dependencies = [ + "ambient-authority", + "cap-primitives", + "iana-time-zone", + "once_cell", + "rustix 1.1.3", + "winx", +] + +[[package]] name = "cc" version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -462,6 +626,16 @@ "unsigned-varint", ] [[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] name = "cmake" version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -471,6 +645,15 @@ "cc", ] [[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] name = "compression-codecs" version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -514,7 +697,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ - "base64", + "base64 0.22.1", "hkdf", "hmac", "percent-encoding", @@ -551,6 +734,15 @@ "memchr", ] [[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + +[[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -560,6 +752,113 @@ "libc", ] [[package]] +name = "cranelift-bforest" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e15d04a0ce86cb36ead88ad68cf693ffd6cda47052b9e0ac114bc47fd9cd23c4" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c6e3969a7ce267259ce244b7867c5d3bc9e65b0a87e81039588dfdeaede9f34" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-codegen" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c22032c4cb42558371cf516bb47f26cdad1819d3475c133e93c49f50ebf304e" +dependencies = [ + "bumpalo", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.14.5", + "log", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c904bc71c61b27fc57827f4a1379f29de64fe95653b620a3db77d59655eee0b8" +dependencies = [ + "cranelift-codegen-shared", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40180f5497572f644ce88c255480981ae2ec1d7bb4d8e0c0136a13b87a2f2ceb" + +[[package]] +name = "cranelift-control" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d132c6d0bd8a489563472afc171759da0707804a65ece7ceb15a8c6d7dd5ef" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d0d9618275474fbf679dd018ac6e009acbd6ae6850f6a67be33fb3b00b323" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-frontend" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fac41e16729107393174b0c9e3730fb072866100e1e64e80a1a963b2e484d57" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca20d576e5070044d0a72a9effc2deacf4d6aa650403189d8ea50126483944d" + +[[package]] +name = "cranelift-native" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dee82f3f1f2c4cba9177f1cc5e350fe98764379bcd29340caa7b01f85076c7" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] name = "crc" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -599,6 +898,16 @@ "crossbeam-utils", ] [[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -647,10 +956,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] [[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] name = "dashmap" version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -709,6 +1028,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" [[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] name = "der" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -741,6 +1069,47 @@ "subtle", ] [[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -807,6 +1176,18 @@ "zeroize", ] [[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -893,12 +1274,29 @@ "pin-project-lite", ] [[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix 1.1.3", + "windows-sys 0.52.0", +] + +[[package]] name = "ff" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -969,6 +1367,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", +] + +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes", + "rustix 1.1.3", + "windows-sys 0.52.0", ] [[package]] @@ -1078,6 +1487,28 @@ "slab", ] [[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "fxprof-processed-profile" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd" +dependencies = [ + "bitflags", + "debugid", + "fxhash", + "serde", + "serde_json", +] + +[[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1127,6 +1558,27 @@ "wasip3", ] [[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + +[[package]] name = "group" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1171,6 +1623,8 @@ [[package]] name = "happyview" version = "0.1.0" dependencies = [ + "aes-gcm", + "anyhow", "arc-swap", "atrium-api", "atrium-common", @@ -1179,7 +1633,7 @@ "atrium-oauth", "atrium-xrpc", "axum", "axum-extra", - "base64", + "base64 0.22.1", "bytes", "chrono", "ciborium", @@ -1205,6 +1659,7 @@ "serde_json", "serial_test", "sha2", "sqlx", + "thiserror 2.0.18", "tokio", "tokio-rustls", "tokio-tungstenite", @@ -1214,6 +1669,8 @@ "tracing", "tracing-subscriber", "urlencoding", "uuid", + "wasmtime", + "wasmtime-wasi", "webpki-roots 0.26.11", "wiremock", ] @@ -1223,6 +1680,9 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] [[package]] name = "hashbrown" @@ -1233,6 +1693,7 @@ dependencies = [ "allocator-api2", "equivalent", "foldhash", + "serde", ] [[package]] @@ -1453,7 +1914,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -1617,6 +2078,31 @@ "serde_core", ] [[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "io-extras" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" +dependencies = [ + "io-lifetimes", + "windows-sys 0.52.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] name = "ipconfig" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1656,12 +2142,41 @@ "serde", ] [[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + +[[package]] name = "jobserver" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1721,7 +2236,7 @@ version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ - "base64", + "base64 0.22.1", "js-sys", "pem", "ring", @@ -1763,6 +2278,12 @@ "spin", ] [[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] name = "leb128fmt" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1801,6 +2322,12 @@ "cc", "pkg-config", "vcpkg", ] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" @@ -1855,6 +2382,15 @@ checksum = "a86cc925d4053d0526ae7f5bc765dbd0d7a5d1a63d43974f4966cb349ca63295" dependencies = [ "cc", "which", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", ] [[package]] @@ -1884,6 +2420,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + +[[package]] name = "md-5" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1898,6 +2440,15 @@ name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix 1.1.3", +] [[package]] name = "mime" @@ -2129,6 +2680,27 @@ "libc", ] [[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap", + "memchr", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2137,6 +2709,12 @@ dependencies = [ "critical-section", "portable-atomic", ] + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" @@ -2233,12 +2811,18 @@ "windows-link", ] [[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] name = "pem" version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64", + "base64 0.22.1", "serde_core", ] @@ -2297,12 +2881,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] name = "potential_utf" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2355,6 +2963,28 @@ "unicode-ident", ] [[package]] +name = "psm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "pulley-interpreter" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62d95f8575df49a2708398182f49a888cf9dc30210fb1fd2df87c889edcee75d" +dependencies = [ + "cranelift-bitset", + "log", + "sptr", + "wasmtime-math", +] + +[[package]] name = "quote" version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2426,6 +3056,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", ] [[package]] @@ -2447,6 +3097,31 @@ "bitflags", ] [[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regalloc2" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc06e6b318142614e4a48bc725abbf08ff166694835c43c9dae5a9009704639a" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash", + "smallvec", +] + +[[package]] name = "regex" version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2481,7 +3156,7 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -2566,6 +3241,12 @@ "zeroize", ] [[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2573,6 +3254,19 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" @@ -2580,8 +3274,18 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.11.0", "windows-sys 0.61.2", +] + +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix 1.1.3", ] [[package]] @@ -2705,6 +3409,10 @@ name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -2794,6 +3502,15 @@ "serde_core", ] [[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2863,6 +3580,15 @@ "lazy_static", ] [[package]] +name = "shellexpand" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +dependencies = [ + "dirs", +] + +[[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2961,6 +3687,12 @@ "der", ] [[package]] +name = "sptr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" + +[[package]] name = "sqlx" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2979,7 +3711,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "chrono", "crc", @@ -3055,7 +3787,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "bytes", @@ -3098,7 +3830,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "chrono", @@ -3230,12 +3962,34 @@ "libc", ] [[package]] +name = "system-interface" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" +dependencies = [ + "bitflags", + "cap-fs-ext", + "cap-std", + "fd-lock", + "io-lifetimes", + "rustix 0.38.44", + "windows-sys 0.52.0", + "winx", +] + +[[package]] name = "tagptr" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] name = "tempfile" version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3244,8 +3998,17 @@ dependencies = [ "fastrand", "getrandom 0.4.1", "once_cell", - "rustix", + "rustix 1.1.3", "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", ] [[package]] @@ -3442,6 +4205,47 @@ "tokio", ] [[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] name = "tower" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3655,12 +4459,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] name = "unsigned-varint" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3831,12 +4651,32 @@ ] [[package]] name = "wasm-encoder" +version = "0.221.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc8444fe4920de80a4fe5ab564fff2ae58b6b73166b89751f8c6c93509da32e5" +dependencies = [ + "leb128", + "wasmparser 0.221.3", +] + +[[package]] +name = "wasm-encoder" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ "leb128fmt", - "wasmparser", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" +dependencies = [ + "leb128fmt", + "wasmparser 0.245.1", ] [[package]] @@ -3847,8 +4687,21 @@ checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", "indexmap", - "wasm-encoder", - "wasmparser", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasmparser" +version = "0.221.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", + "serde", ] [[package]] @@ -3864,6 +4717,343 @@ "semver", ] [[package]] +name = "wasmparser" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" +dependencies = [ + "bitflags", + "indexmap", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.221.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7343c42a97f2926c7819ff81b64012092ae954c5d83ddd30c9fcdefd97d0b283" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.221.3", +] + +[[package]] +name = "wasmtime" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11976a250672556d1c4c04c6d5d7656ac9192ac9edc42a4587d6c21460010e69" +dependencies = [ + "addr2line", + "anyhow", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "fxprof-processed-profile", + "gimli", + "hashbrown 0.14.5", + "indexmap", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object 0.36.7", + "once_cell", + "paste", + "postcard", + "psm", + "pulley-interpreter", + "rayon", + "rustix 0.38.44", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "sptr", + "target-lexicon", + "trait-variant", + "wasm-encoder 0.221.3", + "wasmparser 0.221.3", + "wasmtime-asm-macros", + "wasmtime-cache", + "wasmtime-component-macro", + "wasmtime-component-util", + "wasmtime-cranelift", + "wasmtime-environ", + "wasmtime-fiber", + "wasmtime-jit-debug", + "wasmtime-jit-icache-coherence", + "wasmtime-math", + "wasmtime-slab", + "wasmtime-versioned-export-macros", + "wasmtime-winch", + "wat", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-asm-macros" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f178b0d125201fbe9f75beaf849bd3e511891f9e45ba216a5b620802ccf64f2" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-cache" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1161c8f62880deea07358bc40cceddc019f1c81d46007bc390710b2fe24ffc" +dependencies = [ + "anyhow", + "base64 0.21.7", + "directories-next", + "log", + "postcard", + "rustix 0.38.44", + "serde", + "serde_derive", + "sha2", + "toml", + "windows-sys 0.59.0", + "zstd", +] + +[[package]] +name = "wasmtime-component-macro" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d74de6592ed945d0a602f71243982a304d5d02f1e501b638addf57f42d57dfaf" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn", + "wasmtime-component-util", + "wasmtime-wit-bindgen", + "wit-parser 0.221.3", +] + +[[package]] +name = "wasmtime-component-util" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707dc7b3c112ab5a366b30cfe2fb5b2f8e6a0f682f16df96a5ec582bfe6f056e" + +[[package]] +name = "wasmtime-cranelift" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366be722674d4bf153290fbcbc4d7d16895cc82fb3e869f8d550ff768f9e9e87" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object 0.36.7", + "smallvec", + "target-lexicon", + "thiserror 1.0.69", + "wasmparser 0.221.3", + "wasmtime-environ", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "wasmtime-environ" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdadc1af7097347aa276a4f008929810f726b5b46946971c660b6d421e9994ad" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "indexmap", + "log", + "object 0.36.7", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder 0.221.3", + "wasmparser 0.221.3", + "wasmprinter", + "wasmtime-component-util", +] + +[[package]] +name = "wasmtime-fiber" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccba90d4119f081bca91190485650730a617be1fff5228f8c4757ce133d21117" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "rustix 0.38.44", + "wasmtime-asm-macros", + "wasmtime-versioned-export-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-jit-debug" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e7b61488a5ee00c35c8c22de707c36c0aecacf419a3be803a6a2ba5e860f56a" +dependencies = [ + "object 0.36.7", + "rustix 0.38.44", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec5e8552e01692e6c2e5293171704fed8abdec79d1a6995a0870ab190e5747d1" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-math" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29210ec2aa25e00f4d54605cedaf080f39ec01a872c5bd520ad04c67af1dde17" +dependencies = [ + "libm", +] + +[[package]] +name = "wasmtime-slab" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb5821a96fa04ac14bc7b158bb3d5cd7729a053db5a74dad396cd513a5e5ccf" + +[[package]] +name = "wasmtime-versioned-export-macros" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ff86db216dc0240462de40c8290887a613dddf9685508eb39479037ba97b5b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasmtime-wasi" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d1be69bfcab1bdac74daa7a1f9695ab992b9c8e21b9b061e7d66434097e0ca4" +dependencies = [ + "anyhow", + "async-trait", + "bitflags", + "bytes", + "cap-fs-ext", + "cap-net-ext", + "cap-rand", + "cap-std", + "cap-time-ext", + "fs-set-times", + "futures", + "io-extras", + "io-lifetimes", + "rustix 0.38.44", + "system-interface", + "thiserror 1.0.69", + "tokio", + "tracing", + "trait-variant", + "url", + "wasmtime", + "wiggle", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-winch" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdbabfb8f20502d5e1d81092b9ead3682ae59988487aafcd7567387b7a43cf8f" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli", + "object 0.36.7", + "target-lexicon", + "wasmparser 0.221.3", + "wasmtime-cranelift", + "wasmtime-environ", + "winch-codegen", +] + +[[package]] +name = "wasmtime-wit-bindgen" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8358319c2dd1e4db79e3c1c5d3a5af84956615343f9f89f4e4996a36816e06e6" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "wit-parser 0.221.3", +] + +[[package]] +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", +] + +[[package]] +name = "wast" +version = "245.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cf1149285569120b8ce39db8b465e8a2b55c34cbb586bd977e43e2bc7300bf" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.245.1", +] + +[[package]] +name = "wat" +version = "1.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd48d1679b6858988cb96b154dda0ec5bbb09275b71db46057be37332d5477be" +dependencies = [ + "wast 245.0.1", +] + +[[package]] name = "web-sys" version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3908,7 +5098,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" dependencies = [ "env_home", - "rustix", + "rustix 1.1.3", "winsafe", ] @@ -3929,6 +5119,97 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" [[package]] +name = "wiggle" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9af35bc9629c52c261465320a9a07959164928b4241980ba1cf923b9e6751d" +dependencies = [ + "anyhow", + "async-trait", + "bitflags", + "thiserror 1.0.69", + "tracing", + "wasmtime", + "wiggle-macro", +] + +[[package]] +name = "wiggle-generate" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cf267dd05673912c8138f4b54acabe6bd53407d9d1536f0fadb6520dd16e101" +dependencies = [ + "anyhow", + "heck", + "proc-macro2", + "quote", + "shellexpand", + "syn", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c5c473d4198e6c2d377f3809f713ff0c110cab88a0805ae099a82119ee250c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wiggle-generate", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winch-codegen" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f849ef2c5f46cb0a20af4b4487aaa239846e52e2c03f13fa3c784684552859c" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli", + "regalloc2", + "smallvec", + "target-lexicon", + "thiserror 1.0.69", + "wasmparser 0.221.3", + "wasmtime-cranelift", + "wasmtime-environ", +] + +[[package]] name = "windows-core" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4012,6 +5293,15 @@ name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] @@ -4221,6 +5511,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] name = "winreg" version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4237,13 +5536,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags", + "windows-sys 0.52.0", +] + +[[package]] name = "wiremock" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" dependencies = [ "assert-json-diff", - "base64", + "base64 0.22.1", "deadpool", "futures", "http", @@ -4276,7 +5585,7 @@ checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", "heck", - "wit-parser", + "wit-parser 0.244.0", ] [[package]] @@ -4323,10 +5632,28 @@ "log", "serde", "serde_derive", "serde_json", - "wasm-encoder", + "wasm-encoder 0.244.0", "wasm-metadata", - "wasmparser", - "wit-parser", + "wasmparser 0.244.0", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-parser" +version = "0.221.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "896112579ed56b4a538b07a3d16e562d101ff6265c46b515ce0c701eef16b2ac" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.221.3", ] [[package]] @@ -4344,7 +5671,19 @@ "serde", "serde_derive", "serde_json", "unicode-xid", - "wasmparser", + "wasmparser 0.244.0", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast 35.0.2", ] [[package]] @@ -4464,3 +5803,31 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,8 @@ edition = "2024" default-run = "happyview" [dependencies] +aes-gcm = "0.10" +anyhow = "1" arc-swap = "1" atrium-oauth = { version = "0.1", features = ["default-client"] } atrium-identity = "0.1" @@ -37,6 +39,7 @@ sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "sqlite", "any", "json", "chrono", "migrate"] } tokio = { version = "1", features = ["full"] } tokio-rustls = "0.26" tokio-tungstenite = { version = "0.26", features = ["rustls-tls-webpki-roots"] } +thiserror = "2" tower = { version = "0.5", features = ["util"] } tower-http = { version = "0.6", features = ["cors", "fs", "trace"] } http-body-util = "0.1" @@ -47,6 +50,8 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } urlencoding = "2.1.3" webpki-roots = "0.26" +wasmtime = { version = "29", features = ["async"] } +wasmtime-wasi = "29" regex = "1.12.3" [[bin]] diff --git a/migrations/postgres/20260319000001_plugin_tables.sql b/migrations/postgres/20260319000001_plugin_tables.sql new file mode 100644 --- /dev/null +++ b/migrations/postgres/20260319000001_plugin_tables.sql @@ -0,0 +1,60 @@ +-- Plugin registry +CREATE TABLE plugins ( + id TEXT PRIMARY KEY, + source TEXT NOT NULL CHECK (source IN ('file', 'url')), + url TEXT, + sha256 TEXT, + enabled BOOLEAN NOT NULL DEFAULT true, + loaded_at TIMESTAMPTZ, + api_version TEXT NOT NULL +); + +-- Plugin configuration +CREATE TABLE plugin_configs ( + plugin_id TEXT PRIMARY KEY REFERENCES plugins(id) ON DELETE CASCADE, + config JSONB NOT NULL DEFAULT '{}', + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- External account tokens (encrypted) +CREATE TABLE external_account_tokens ( + id TEXT PRIMARY KEY, + did TEXT NOT NULL, + plugin_id TEXT NOT NULL REFERENCES plugins(id) ON DELETE CASCADE, + account_id TEXT NOT NULL, + access_token BYTEA NOT NULL, + refresh_token BYTEA, + token_type TEXT, + scope TEXT, + expires_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(did, plugin_id) +); + +-- Deduplication keys for sync records +CREATE TABLE plugin_dedup_keys ( + plugin_id TEXT NOT NULL REFERENCES plugins(id) ON DELETE CASCADE, + did TEXT NOT NULL, + dedup_key TEXT NOT NULL, + record_uri TEXT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (plugin_id, did, dedup_key) +); + +-- KV storage for plugins (scoped per plugin + context) +CREATE TABLE plugin_kv ( + plugin_id TEXT NOT NULL REFERENCES plugins(id) ON DELETE CASCADE, + scope TEXT NOT NULL, + key TEXT NOT NULL, + value BYTEA NOT NULL, + expires_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (plugin_id, scope, key) +); + +-- Index for KV expiration cleanup +CREATE INDEX idx_plugin_kv_expires ON plugin_kv(expires_at) WHERE expires_at IS NOT NULL; + +-- Index for token lookup by DID +CREATE INDEX idx_external_tokens_did ON external_account_tokens(did); diff --git a/migrations/sqlite/20260319000001_plugin_tables.sql b/migrations/sqlite/20260319000001_plugin_tables.sql new file mode 100644 --- /dev/null +++ b/migrations/sqlite/20260319000001_plugin_tables.sql @@ -0,0 +1,60 @@ +-- Plugin registry +CREATE TABLE plugins ( + id TEXT PRIMARY KEY, + source TEXT NOT NULL CHECK (source IN ('file', 'url')), + url TEXT, + sha256 TEXT, + enabled INTEGER NOT NULL DEFAULT 1, + loaded_at TEXT, + api_version TEXT NOT NULL +); + +-- Plugin configuration +CREATE TABLE plugin_configs ( + plugin_id TEXT PRIMARY KEY REFERENCES plugins(id) ON DELETE CASCADE, + config TEXT NOT NULL DEFAULT '{}', + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- External account tokens (encrypted) +CREATE TABLE external_account_tokens ( + id TEXT PRIMARY KEY, + did TEXT NOT NULL, + plugin_id TEXT NOT NULL REFERENCES plugins(id) ON DELETE CASCADE, + account_id TEXT NOT NULL, + access_token BLOB NOT NULL, + refresh_token BLOB, + token_type TEXT, + scope TEXT, + expires_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + UNIQUE(did, plugin_id) +); + +-- Deduplication keys for sync records +CREATE TABLE plugin_dedup_keys ( + plugin_id TEXT NOT NULL REFERENCES plugins(id) ON DELETE CASCADE, + did TEXT NOT NULL, + dedup_key TEXT NOT NULL, + record_uri TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + PRIMARY KEY (plugin_id, did, dedup_key) +); + +-- KV storage for plugins (scoped per plugin + context) +CREATE TABLE plugin_kv ( + plugin_id TEXT NOT NULL REFERENCES plugins(id) ON DELETE CASCADE, + scope TEXT NOT NULL, + key TEXT NOT NULL, + value BLOB NOT NULL, + expires_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + PRIMARY KEY (plugin_id, scope, key) +); + +-- Index for KV expiration cleanup +CREATE INDEX idx_plugin_kv_expires ON plugin_kv(expires_at) WHERE expires_at IS NOT NULL; + +-- Index for token lookup by DID +CREATE INDEX idx_external_tokens_did ON external_account_tokens(did); diff --git a/src/config.rs b/src/config.rs --- a/src/config.rs +++ b/src/config.rs @@ -21,6 +21,7 @@ pub app_name: Option, pub logo_uri: Option, pub tos_uri: Option, pub policy_uri: Option, + pub token_encryption_key: Option<[u8; 32]>, } impl Config { @@ -55,6 +56,13 @@ app_name: env::var("APP_NAME").ok(), logo_uri: env::var("LOGO_URI").ok(), tos_uri: env::var("TOS_URI").ok(), policy_uri: env::var("POLICY_URI").ok(), + token_encryption_key: env::var("TOKEN_ENCRYPTION_KEY").ok().and_then(|s| { + use base64::Engine; + base64::engine::general_purpose::STANDARD + .decode(&s) + .ok() + .and_then(|bytes| bytes.try_into().ok()) + }), } } @@ -120,6 +128,7 @@ app_name: None, logo_uri: None, tos_uri: None, policy_uri: None, + token_encryption_key: None, }; assert_eq!( config.listen_addr(), diff --git a/src/external_auth/mod.rs b/src/external_auth/mod.rs new file mode 100644 --- /dev/null +++ b/src/external_auth/mod.rs @@ -0,0 +1,4 @@ +mod routes; +mod sync; + +pub use routes::routes; diff --git a/src/external_auth/routes.rs b/src/external_auth/routes.rs new file mode 100644 --- /dev/null +++ b/src/external_auth/routes.rs @@ -0,0 +1,114 @@ +use axum::{ + Json, Router, + extract::{Path, Query, State}, + response::Redirect, + routing::{get, post}, +}; +use serde::{Deserialize, Serialize}; + +use crate::AppState; +use crate::error::AppError; + +pub fn routes() -> Router { + Router::new() + .route("/providers", get(list_providers)) + .route("/{plugin_id}/authorize", get(authorize)) + .route("/{plugin_id}/callback", get(callback)) + .route("/{plugin_id}/sync", post(sync)) + .route("/{plugin_id}/unlink", post(unlink)) +} + +#[derive(Serialize)] +struct ProviderInfo { + id: String, + name: String, + icon_url: Option, +} + +async fn list_providers( + State(state): State, +) -> Result>, AppError> { + let plugins = state.plugin_registry.list().await; + + let providers: Vec = plugins + .into_iter() + .map(|p| ProviderInfo { + id: p.info.id.clone(), + name: p.info.name.clone(), + icon_url: p.info.icon_url.clone(), + }) + .collect(); + + Ok(Json(providers)) +} + +#[derive(Deserialize)] +struct AuthorizeQuery { + redirect_uri: String, +} + +async fn authorize( + State(state): State, + Path(plugin_id): Path, + Query(query): Query, +) -> Result, AppError> { + let _plugin = state + .plugin_registry + .get(&plugin_id) + .await + .ok_or_else(|| AppError::NotFound(format!("Plugin not found: {}", plugin_id)))?; + + // Generate state parameter for CSRF protection + let state_param = uuid::Uuid::new_v4().to_string(); + + // TODO: Store state in KV, call plugin's get_authorize_url() + // For now, return placeholder + let _ = query.redirect_uri; + + Ok(Json(serde_json::json!({ + "authorize_url": format!("https://example.com/oauth?state={}", state_param), + "state": state_param + }))) +} + +#[derive(Deserialize)] +#[allow(dead_code)] // Fields used when full OAuth flow is implemented +struct CallbackQuery { + code: Option, + state: Option, + error: Option, +} + +async fn callback( + State(_state): State, + Path(_plugin_id): Path, + Query(_query): Query, +) -> Result { + // TODO: Validate state, call plugin's handle_callback(), store tokens + + // For now, redirect to a placeholder + Ok(Redirect::to("/")) +} + +async fn sync( + State(_state): State, + Path(_plugin_id): Path, +) -> Result, AppError> { + // TODO: Call plugin's sync_account(), process SyncRecords + + Ok(Json(serde_json::json!({ + "status": "ok", + "synced": 0 + }))) +} + +async fn unlink( + State(_state): State, + Path(_plugin_id): Path, +) -> Result, AppError> { + // TODO: Delete tokens, delete accountLink record + + Ok(Json(serde_json::json!({ + "status": "ok" + }))) +} diff --git a/src/external_auth/sync.rs b/src/external_auth/sync.rs new file mode 100644 --- /dev/null +++ b/src/external_auth/sync.rs @@ -0,0 +1,55 @@ +use crate::db::adapt_sql; +use crate::plugin::SyncRecord; + +#[allow(dead_code)] // Used when full sync flow is implemented +#[derive(Debug, thiserror::Error)] +pub enum SyncError { + #[error("Database error: {0}")] + Database(#[from] sqlx::Error), + #[error("Validation error: {0}")] + Validation(String), + #[error("PDS write error: {0}")] + PdsWrite(String), +} + +/// Process sync records from a plugin +#[allow(dead_code)] // Used when full sync flow is implemented +pub async fn process_sync_records( + db: &sqlx::AnyPool, + db_backend: crate::db::DatabaseBackend, + plugin_id: &str, + user_did: &str, + records: Vec, +) -> Result { + let mut processed = 0; + + for record in records { + // TODO: Validate against lexicon schema + // TODO: Check dedup_key + // TODO: Sign attestation + // TODO: Write to PDS + + // For now, just track dedup key + if let Some(dedup_key) = &record.dedup_key { + let sql = adapt_sql( + "INSERT INTO plugin_dedup_keys (plugin_id, did, dedup_key, record_uri, updated_at) + VALUES (?, ?, ?, ?, datetime('now')) + ON CONFLICT (plugin_id, did, dedup_key) + DO UPDATE SET record_uri = excluded.record_uri, updated_at = excluded.updated_at", + db_backend, + ); + + sqlx::query(&sql) + .bind(plugin_id) + .bind(user_did) + .bind(dedup_key) + .bind("at://placeholder") // TODO: Real URI after PDS write + .execute(db) + .await?; + } + + processed += 1; + } + + Ok(processed) +} diff --git a/src/lib.rs b/src/lib.rs --- a/src/lib.rs +++ b/src/lib.rs @@ -5,9 +5,11 @@ pub mod db; pub mod dns; pub mod error; pub mod event_log; +pub mod external_auth; pub mod labeler; pub mod lexicon; pub mod lua; +pub mod plugin; pub mod profile; pub mod rate_limit; pub mod record_refs; @@ -56,6 +58,7 @@ pub labeler_subscriptions_tx: watch::Sender<()>, pub rate_limiter: Arc, pub oauth: Arc, pub cookie_key: axum_extra::extract::cookie::Key, + pub plugin_registry: Arc, } impl axum::extract::FromRef for axum_extra::extract::cookie::Key { diff --git a/src/lua/atproto_api.rs b/src/lua/atproto_api.rs --- a/src/lua/atproto_api.rs +++ b/src/lua/atproto_api.rs @@ -222,6 +222,7 @@ app_name: None, logo_uri: None, tos_uri: None, policy_uri: None, + token_encryption_key: None, }; let (tx, _) = watch::channel(vec![]); let (labeler_tx, _) = watch::channel(()); @@ -287,6 +288,7 @@ oauth: std::sync::Arc::new(oauth), cookie_key: axum_extra::extract::cookie::Key::derive_from( b"test-secret-for-tests-only-not-production", ), + plugin_registry: std::sync::Arc::new(crate::plugin::PluginRegistry::new()), } } diff --git a/src/lua/db_api.rs b/src/lua/db_api.rs --- a/src/lua/db_api.rs +++ b/src/lua/db_api.rs @@ -630,6 +630,7 @@ app_name: None, logo_uri: None, tos_uri: None, policy_uri: None, + token_encryption_key: None, }; let (tx, _) = watch::channel(vec![]); let (labeler_tx, _) = watch::channel(()); @@ -695,6 +696,7 @@ oauth: std::sync::Arc::new(oauth), cookie_key: axum_extra::extract::cookie::Key::derive_from( b"test-secret-for-tests-only-not-production", ), + plugin_registry: std::sync::Arc::new(crate::plugin::PluginRegistry::new()), } } diff --git a/src/lua/execute.rs b/src/lua/execute.rs --- a/src/lua/execute.rs +++ b/src/lua/execute.rs @@ -964,6 +964,7 @@ app_name: None, logo_uri: None, tos_uri: None, policy_uri: None, + token_encryption_key: None, }; let (tx, _) = watch::channel(vec![]); let (labeler_tx, _) = watch::channel(()); @@ -1029,6 +1030,7 @@ oauth: std::sync::Arc::new(oauth), cookie_key: axum_extra::extract::cookie::Key::derive_from( b"test-secret-for-tests-only-not-production", ), + plugin_registry: std::sync::Arc::new(crate::plugin::PluginRegistry::new()), } } diff --git a/src/lua/http_api.rs b/src/lua/http_api.rs --- a/src/lua/http_api.rs +++ b/src/lua/http_api.rs @@ -104,6 +104,7 @@ app_name: None, logo_uri: None, tos_uri: None, policy_uri: None, + token_encryption_key: None, }; let (tx, _) = watch::channel(vec![]); let (labeler_tx, _) = watch::channel(()); @@ -169,6 +170,7 @@ oauth: std::sync::Arc::new(oauth), cookie_key: axum_extra::extract::cookie::Key::derive_from( b"test-secret-for-tests-only-not-production", ), + plugin_registry: std::sync::Arc::new(crate::plugin::PluginRegistry::new()), } } diff --git a/src/main.rs b/src/main.rs --- a/src/main.rs +++ b/src/main.rs @@ -173,6 +173,45 @@ "processed network lexicons on startup" ); } + // Initialize plugin registry + let plugin_registry = Arc::new(happyview::plugin::PluginRegistry::new()); + + // Load plugins from PLUGIN_URLS env var + if let Ok(urls) = std::env::var("PLUGIN_URLS") { + for (id, url, sha256) in happyview::plugin::loader::parse_plugin_urls(&urls) { + match happyview::plugin::loader::load_from_url(&http, &url, sha256.as_deref()).await { + Ok(plugin) => { + tracing::info!(id = %id, "Loaded plugin from URL"); + plugin_registry.register(plugin).await; + } + Err(e) => { + tracing::error!(id = %id, error = %e, "Failed to load plugin"); + } + } + } + } + + // Load plugins from directory + let plugin_dir = std::path::Path::new("./plugins"); + if plugin_dir.exists() + && let Ok(entries) = std::fs::read_dir(plugin_dir) + { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + match happyview::plugin::loader::load_from_file(&path).await { + Ok(plugin) => { + tracing::info!(id = %plugin.info.id, "Loaded plugin from file"); + plugin_registry.register(plugin).await; + } + Err(e) => { + tracing::error!(path = %path.display(), error = %e, "Failed to load plugin"); + } + } + } + } + } + // Initialize rate limiter from DB. let rl_state = RateLimiter::load_from_db(&db_pool).await; let rate_limiter = RateLimiter::new(rl_state.enabled, rl_state.global, rl_state.allowlist); @@ -279,6 +318,7 @@ labeler_subscriptions_tx, rate_limiter, oauth: Arc::new(oauth_client), cookie_key, + plugin_registry, }; // Sync initial collections to Tap on startup. diff --git a/src/plugin/encryption.rs b/src/plugin/encryption.rs new file mode 100644 --- /dev/null +++ b/src/plugin/encryption.rs @@ -0,0 +1,111 @@ +use aes_gcm::{ + Aes256Gcm, Nonce, + aead::{Aead, KeyInit}, +}; +use rand::RngCore; + +#[derive(Debug, thiserror::Error)] +pub enum EncryptionError { + #[error("Encryption key not configured")] + KeyNotConfigured, + #[error("Encryption failed")] + EncryptionFailed, + #[error("Decryption failed")] + DecryptionFailed, + #[error("Invalid ciphertext format")] + InvalidFormat, +} + +const NONCE_SIZE: usize = 12; + +/// Encrypt data using AES-256-GCM +/// Returns: nonce || ciphertext || tag (concatenated) +pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result, EncryptionError> { + let cipher = Aes256Gcm::new_from_slice(key).map_err(|_| EncryptionError::EncryptionFailed)?; + + // Generate random nonce + let mut nonce_bytes = [0u8; NONCE_SIZE]; + rand::rng().fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + + // Encrypt + let ciphertext = cipher + .encrypt(nonce, plaintext) + .map_err(|_| EncryptionError::EncryptionFailed)?; + + // Concatenate: nonce || ciphertext + let mut result = Vec::with_capacity(NONCE_SIZE + ciphertext.len()); + result.extend_from_slice(&nonce_bytes); + result.extend_from_slice(&ciphertext); + + Ok(result) +} + +/// Decrypt data encrypted with encrypt() +pub fn decrypt(key: &[u8; 32], ciphertext: &[u8]) -> Result, EncryptionError> { + if ciphertext.len() < NONCE_SIZE + 16 { + // Minimum: nonce + auth tag + return Err(EncryptionError::InvalidFormat); + } + + let cipher = Aes256Gcm::new_from_slice(key).map_err(|_| EncryptionError::DecryptionFailed)?; + + let nonce = Nonce::from_slice(&ciphertext[..NONCE_SIZE]); + let encrypted = &ciphertext[NONCE_SIZE..]; + + cipher + .decrypt(nonce, encrypted) + .map_err(|_| EncryptionError::DecryptionFailed) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encrypt_decrypt_roundtrip() { + let key = [0x42u8; 32]; + let plaintext = b"hello world"; + + let ciphertext = encrypt(&key, plaintext).unwrap(); + assert_ne!(&ciphertext[NONCE_SIZE..], plaintext); + + let decrypted = decrypt(&key, &ciphertext).unwrap(); + assert_eq!(decrypted, plaintext); + } + + #[test] + fn test_different_nonces() { + let key = [0x42u8; 32]; + let plaintext = b"hello world"; + + let ct1 = encrypt(&key, plaintext).unwrap(); + let ct2 = encrypt(&key, plaintext).unwrap(); + + // Same plaintext should produce different ciphertext (different nonces) + assert_ne!(ct1, ct2); + + // Both should decrypt correctly + assert_eq!(decrypt(&key, &ct1).unwrap(), plaintext); + assert_eq!(decrypt(&key, &ct2).unwrap(), plaintext); + } + + #[test] + fn test_invalid_ciphertext() { + let key = [0x42u8; 32]; + + // Too short + assert!(matches!( + decrypt(&key, &[0u8; 10]), + Err(EncryptionError::InvalidFormat) + )); + + // Corrupted + let mut ciphertext = encrypt(&key, b"hello").unwrap(); + ciphertext[NONCE_SIZE] ^= 0xFF; + assert!(matches!( + decrypt(&key, &ciphertext), + Err(EncryptionError::DecryptionFailed) + )); + } +} diff --git a/src/plugin/host/http.rs b/src/plugin/host/http.rs new file mode 100644 --- /dev/null +++ b/src/plugin/host/http.rs @@ -0,0 +1,133 @@ +use super::{ + HostContext, MAX_HTTP_REQUESTS, MAX_HTTP_RESPONSE_SIZE, MAX_HTTP_TOTAL_TRANSFER, ResourceUsage, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HttpRequest { + pub method: String, + pub url: String, + pub headers: Vec<(String, String)>, + pub body: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HttpResponse { + pub status: u16, + pub headers: Vec<(String, String)>, + pub body: Vec, +} + +#[derive(Debug, thiserror::Error)] +pub enum HttpError { + #[error("Too many requests: {0} > {MAX_HTTP_REQUESTS}")] + TooManyRequests(u32), + #[error("Response too large: {0} > {MAX_HTTP_RESPONSE_SIZE}")] + ResponseTooLarge(u64), + #[error("Transfer limit exceeded: {0} > {MAX_HTTP_TOTAL_TRANSFER}")] + TransferLimitExceeded(u64), + #[error("Request failed: {0}")] + RequestFailed(#[from] reqwest::Error), +} + +pub async fn http_request( + ctx: &HostContext, + usage: &mut ResourceUsage, + req: HttpRequest, +) -> Result { + // Check request count limit + usage.http_requests += 1; + if usage.http_requests > MAX_HTTP_REQUESTS { + return Err(HttpError::TooManyRequests(usage.http_requests)); + } + + // Build request + let method = req.method.parse().unwrap_or(reqwest::Method::GET); + let mut builder = ctx.http_client.request(method, &req.url); + + for (name, value) in &req.headers { + builder = builder.header(name, value); + } + + if let Some(body) = req.body { + usage.http_bytes_transferred += body.len() as u64; + builder = builder.body(body); + } + + // Check transfer limit before sending + if usage.http_bytes_transferred > MAX_HTTP_TOTAL_TRANSFER { + return Err(HttpError::TransferLimitExceeded( + usage.http_bytes_transferred, + )); + } + + // Execute request + let response = builder.send().await?; + let status = response.status().as_u16(); + + let headers: Vec<(String, String)> = response + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) + .collect(); + + let body = response.bytes().await?; + + // Check response size + if body.len() as u64 > MAX_HTTP_RESPONSE_SIZE { + return Err(HttpError::ResponseTooLarge(body.len() as u64)); + } + + usage.http_bytes_transferred += body.len() as u64; + if usage.http_bytes_transferred > MAX_HTTP_TOTAL_TRANSFER { + return Err(HttpError::TransferLimitExceeded( + usage.http_bytes_transferred, + )); + } + + Ok(HttpResponse { + status, + headers, + body: body.to_vec(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_http_request_limit_check() { + let mut usage = ResourceUsage { + http_requests: MAX_HTTP_REQUESTS, + ..Default::default() + }; + + // Verify the limit check would fail + usage.http_requests += 1; + assert!(usage.http_requests > MAX_HTTP_REQUESTS); + } + + #[test] + fn test_http_transfer_limit_check() { + let mut usage = ResourceUsage { + http_bytes_transferred: MAX_HTTP_TOTAL_TRANSFER, + ..Default::default() + }; + + // Adding more would exceed limit + usage.http_bytes_transferred += 1; + assert!(usage.http_bytes_transferred > MAX_HTTP_TOTAL_TRANSFER); + } + + #[test] + fn test_http_response_struct() { + let response = HttpResponse { + status: 200, + headers: vec![("content-type".into(), "application/json".into())], + body: b"{}".to_vec(), + }; + assert_eq!(response.status, 200); + assert_eq!(response.headers.len(), 1); + } +} diff --git a/src/plugin/host/kv.rs b/src/plugin/host/kv.rs new file mode 100644 --- /dev/null +++ b/src/plugin/host/kv.rs @@ -0,0 +1,104 @@ +use super::{HostContext, MAX_KV_SIZE_PER_USER, ResourceUsage}; +use crate::db::adapt_sql; + +#[derive(Debug, thiserror::Error)] +pub enum KvError { + #[error("Storage quota exceeded: {0} > {MAX_KV_SIZE_PER_USER}")] + QuotaExceeded(u64), + #[error("Database error: {0}")] + Database(#[from] sqlx::Error), +} + +pub async fn kv_get(ctx: &HostContext, key: &str) -> Result>, KvError> { + let sql = adapt_sql( + "SELECT value FROM plugin_kv + WHERE plugin_id = ? AND scope = ? AND key = ? + AND (expires_at IS NULL OR expires_at > datetime('now'))", + ctx.db_backend, + ); + + let result: Option<(Vec,)> = sqlx::query_as(&sql) + .bind(&ctx.plugin_id) + .bind(&ctx.scope) + .bind(key) + .fetch_optional(&ctx.db) + .await?; + + Ok(result.map(|(v,)| v)) +} + +pub async fn kv_set( + ctx: &HostContext, + usage: &mut ResourceUsage, + key: &str, + value: Vec, + ttl_secs: Option, +) -> Result<(), KvError> { + // Check quota (simple check - full implementation would sum all keys) + usage.kv_bytes_used += value.len() as u64; + if usage.kv_bytes_used > MAX_KV_SIZE_PER_USER { + return Err(KvError::QuotaExceeded(usage.kv_bytes_used)); + } + + let expires_at = ttl_secs + .map(|secs| (chrono::Utc::now() + chrono::Duration::seconds(secs as i64)).to_rfc3339()); + + // Upsert + let sql = adapt_sql( + "INSERT INTO plugin_kv (plugin_id, scope, key, value, expires_at, created_at) + VALUES (?, ?, ?, ?, ?, datetime('now')) + ON CONFLICT (plugin_id, scope, key) + DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at", + ctx.db_backend, + ); + + sqlx::query(&sql) + .bind(&ctx.plugin_id) + .bind(&ctx.scope) + .bind(key) + .bind(&value) + .bind(expires_at) + .execute(&ctx.db) + .await?; + + Ok(()) +} + +pub async fn kv_delete(ctx: &HostContext, key: &str) -> Result<(), KvError> { + let sql = adapt_sql( + "DELETE FROM plugin_kv WHERE plugin_id = ? AND scope = ? AND key = ?", + ctx.db_backend, + ); + + sqlx::query(&sql) + .bind(&ctx.plugin_id) + .bind(&ctx.scope) + .bind(key) + .execute(&ctx.db) + .await?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_quota_exceeded_check() { + let mut usage = ResourceUsage { + kv_bytes_used: MAX_KV_SIZE_PER_USER, + ..Default::default() + }; + + // Adding more would exceed quota + usage.kv_bytes_used += 1; + assert!(usage.kv_bytes_used > MAX_KV_SIZE_PER_USER); + } + + #[test] + fn test_kv_error_display() { + let err = KvError::QuotaExceeded(2_000_000); + assert!(err.to_string().contains("exceeded")); + } +} diff --git a/src/plugin/host/logging.rs b/src/plugin/host/logging.rs new file mode 100644 --- /dev/null +++ b/src/plugin/host/logging.rs @@ -0,0 +1,70 @@ +use std::str::FromStr; +use tracing::{debug, error, info, warn}; + +/// Log level for plugin logging +#[derive(Debug, Clone, Copy, Default)] +pub enum LogLevel { + Debug, + #[default] + Info, + Warn, + Error, +} + +impl FromStr for LogLevel { + type Err = std::convert::Infallible; + + fn from_str(s: &str) -> Result { + Ok(match s.to_lowercase().as_str() { + "debug" => Self::Debug, + "info" => Self::Info, + "warn" | "warning" => Self::Warn, + "error" => Self::Error, + _ => Self::Info, + }) + } +} + +/// Log a message from a plugin +pub fn log(plugin_id: &str, level: LogLevel, message: &str) { + match level { + LogLevel::Debug => debug!(plugin = %plugin_id, "{}", message), + LogLevel::Info => info!(plugin = %plugin_id, "{}", message), + LogLevel::Warn => warn!(plugin = %plugin_id, "{}", message), + LogLevel::Error => error!(plugin = %plugin_id, "{}", message), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_log_level_from_str_known_values() { + assert!(matches!("debug".parse::(), Ok(LogLevel::Debug))); + assert!(matches!("DEBUG".parse::(), Ok(LogLevel::Debug))); + assert!(matches!("info".parse::(), Ok(LogLevel::Info))); + assert!(matches!("INFO".parse::(), Ok(LogLevel::Info))); + assert!(matches!("warn".parse::(), Ok(LogLevel::Warn))); + assert!(matches!("warning".parse::(), Ok(LogLevel::Warn))); + assert!(matches!("WARN".parse::(), Ok(LogLevel::Warn))); + assert!(matches!("error".parse::(), Ok(LogLevel::Error))); + assert!(matches!("ERROR".parse::(), Ok(LogLevel::Error))); + } + + #[test] + fn test_log_level_from_str_unknown_defaults_to_info() { + assert!(matches!("trace".parse::(), Ok(LogLevel::Info))); + assert!(matches!("".parse::(), Ok(LogLevel::Info))); + assert!(matches!("unknown".parse::(), Ok(LogLevel::Info))); + } + + #[test] + fn test_log_does_not_panic() { + // Verify log() runs without panicking for each level + log("test-plugin", LogLevel::Debug, "debug message"); + log("test-plugin", LogLevel::Info, "info message"); + log("test-plugin", LogLevel::Warn, "warn message"); + log("test-plugin", LogLevel::Error, "error message"); + } +} diff --git a/src/plugin/host/lookup.rs b/src/plugin/host/lookup.rs new file mode 100644 --- /dev/null +++ b/src/plugin/host/lookup.rs @@ -0,0 +1,67 @@ +use super::HostContext; +use crate::db::adapt_sql; +use crate::plugin::StrongRef; + +#[derive(Debug, thiserror::Error)] +pub enum LookupError { + #[error("Database error: {0}")] + Database(#[from] sqlx::Error), + #[error("Invalid external ID field path")] + InvalidFieldPath, +} + +/// Look up a record by external ID +/// +/// # Arguments +/// * `collection` - Lexicon collection ID (e.g., "games.gamesgamesgamesgames.game") +/// * `external_id_field` - JSON path to external ID field (e.g., "externalIds.steam") +/// * `external_id_value` - Value to match +pub async fn lookup_record( + ctx: &HostContext, + collection: &str, + external_id_field: &str, + external_id_value: &str, +) -> Result, LookupError> { + // Validate field path (basic check) + if external_id_field.is_empty() || external_id_field.contains("..") { + return Err(LookupError::InvalidFieldPath); + } + + // Build JSON path for query + let json_path = format!("$.{}", external_id_field); + + let sql = adapt_sql( + "SELECT uri, cid FROM records + WHERE collection = ? + AND json_extract(record, ?) = ? + LIMIT 1", + ctx.db_backend, + ); + + let result: Option<(String, String)> = sqlx::query_as(&sql) + .bind(collection) + .bind(&json_path) + .bind(external_id_value) + .fetch_optional(&ctx.db) + .await?; + + Ok(result.map(|(uri, cid)| StrongRef { uri, cid })) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_invalid_field_path_empty() { + // Can't test async without runtime, but we can verify error types exist + let err = LookupError::InvalidFieldPath; + assert!(err.to_string().contains("Invalid")); + } + + #[test] + fn test_lookup_error_display() { + let err = LookupError::InvalidFieldPath; + assert_eq!(err.to_string(), "Invalid external ID field path"); + } +} diff --git a/src/plugin/host/mod.rs b/src/plugin/host/mod.rs new file mode 100644 --- /dev/null +++ b/src/plugin/host/mod.rs @@ -0,0 +1,41 @@ +mod http; +mod kv; +mod logging; +mod lookup; +mod secrets; + +pub use http::*; +pub use kv::*; +pub use logging::*; +pub use lookup::*; +pub use secrets::*; + +use std::collections::HashMap; +use std::sync::Arc; + +/// Context passed to all host function calls +pub struct HostContext { + pub plugin_id: String, + pub scope: String, // user DID or OAuth state + pub secrets: HashMap, + pub config: serde_json::Value, + pub db: sqlx::AnyPool, + pub db_backend: crate::db::DatabaseBackend, + pub http_client: reqwest::Client, + pub lexicons: Arc, +} + +/// Resource usage tracking for limits +#[derive(Default)] +pub struct ResourceUsage { + pub http_requests: u32, + pub http_bytes_transferred: u64, + pub kv_bytes_used: u64, +} + +/// Resource limits from spec +pub const MAX_HTTP_REQUESTS: u32 = 100; +pub const MAX_HTTP_RESPONSE_SIZE: u64 = 100 * 1024 * 1024; // 100 MB +pub const MAX_HTTP_TOTAL_TRANSFER: u64 = 500 * 1024 * 1024; // 500 MB +pub const MAX_HTTP_CONCURRENT: usize = 5; +pub const MAX_KV_SIZE_PER_USER: u64 = 1024 * 1024; // 1 MB diff --git a/src/plugin/host/secrets.rs b/src/plugin/host/secrets.rs new file mode 100644 --- /dev/null +++ b/src/plugin/host/secrets.rs @@ -0,0 +1,33 @@ +use super::HostContext; + +/// Get a secret value by name (from pre-loaded secrets map) +pub fn get_secret(ctx: &HostContext, name: &str) -> Option { + ctx.secrets.get(name).cloned() +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + #[test] + fn test_get_existing_secret() { + let mut secrets: HashMap = HashMap::new(); + secrets.insert("API_KEY".to_string(), "abc123".to_string()); + assert_eq!(secrets.get("API_KEY").cloned(), Some("abc123".to_string())); + } + + #[test] + fn test_get_missing_secret() { + let secrets: HashMap = HashMap::new(); + assert_eq!(secrets.get("MISSING").cloned(), None); + } + + #[test] + fn test_get_secret_case_sensitive() { + let mut secrets: HashMap = HashMap::new(); + secrets.insert("api_key".to_string(), "lower".to_string()); + // Keys are case-sensitive + assert_eq!(secrets.get("API_KEY").cloned(), None); + assert_eq!(secrets.get("api_key").cloned(), Some("lower".to_string())); + } +} diff --git a/src/plugin/loader.rs b/src/plugin/loader.rs new file mode 100644 --- /dev/null +++ b/src/plugin/loader.rs @@ -0,0 +1,201 @@ +use crate::plugin::{LoadedPlugin, PluginInfo, PluginSource}; +use sha2::{Digest, Sha256}; +use std::path::Path; + +const SUPPORTED_API_VERSION: &str = "1"; + +#[derive(Debug, thiserror::Error)] +pub enum LoadError { + #[error("Failed to read plugin file: {0}")] + ReadFile(#[from] std::io::Error), + #[error("Failed to download plugin: {0}")] + Download(#[from] reqwest::Error), + #[error("SHA256 mismatch: expected {expected}, got {actual}")] + Sha256Mismatch { expected: String, actual: String }, + #[error("Failed to parse plugin info: {0}")] + ParseInfo(#[from] serde_json::Error), + #[error("Plugin API version {0} not supported (requires {SUPPORTED_API_VERSION})")] + UnsupportedApiVersion(String), + #[error("Missing required secret: {0}")] + MissingSecret(String), + #[error("WASM validation failed: {0}")] + WasmValidation(String), +} + +/// Load a plugin from a file path +pub async fn load_from_file(path: &Path) -> Result { + let wasm_path = path.join("plugin.wasm"); + let wasm_bytes = tokio::fs::read(&wasm_path).await?; + + // Try to load plugin.toml for metadata override + let toml_path = path.join("plugin.toml"); + let _toml_content = tokio::fs::read_to_string(&toml_path).await.ok(); + + // Extract plugin info by instantiating WASM and calling plugin_info() + // For now, create placeholder - full implementation needs wasmtime integration + let info = extract_plugin_info(&wasm_bytes)?; + + validate_api_version(&info)?; + + Ok(LoadedPlugin { + info, + source: PluginSource::File { + path: path.to_path_buf(), + }, + wasm_bytes, + }) +} + +/// Load a plugin from a URL +pub async fn load_from_url( + client: &reqwest::Client, + url: &str, + expected_sha256: Option<&str>, +) -> Result { + let response = client.get(url).send().await?.error_for_status()?; + let wasm_bytes = response.bytes().await?.to_vec(); + + // Verify SHA256 if provided + if let Some(expected) = expected_sha256 { + let mut hasher = Sha256::new(); + hasher.update(&wasm_bytes); + let actual = hex::encode(hasher.finalize()); + + if actual != expected { + return Err(LoadError::Sha256Mismatch { + expected: expected.to_string(), + actual, + }); + } + } + + let info = extract_plugin_info(&wasm_bytes)?; + validate_api_version(&info)?; + + Ok(LoadedPlugin { + info, + source: PluginSource::Url { + url: url.to_string(), + sha256: expected_sha256.map(String::from), + }, + wasm_bytes, + }) +} + +/// Extract plugin info by instantiating WASM and calling plugin_info() +fn extract_plugin_info(wasm_bytes: &[u8]) -> Result { + // TODO: Full implementation with wasmtime + // For now, this is a placeholder that will be filled in when we integrate wasmtime calls + + // Validate it's valid WASM + wasmtime::Module::validate(&wasmtime::Engine::default(), wasm_bytes) + .map_err(|e| LoadError::WasmValidation(e.to_string()))?; + + // Return placeholder - real implementation calls plugin_info() export + Ok(PluginInfo { + id: "placeholder".into(), + name: "Placeholder".into(), + version: "0.0.0".into(), + api_version: SUPPORTED_API_VERSION.into(), + icon_url: None, + required_secrets: vec![], + config_schema: None, + }) +} + +fn validate_api_version(info: &PluginInfo) -> Result<(), LoadError> { + // Parse as integer for comparison + let plugin_version: u32 = info.api_version.parse().unwrap_or(0); + let supported_version: u32 = SUPPORTED_API_VERSION.parse().unwrap_or(1); + + if plugin_version > supported_version { + return Err(LoadError::UnsupportedApiVersion(info.api_version.clone())); + } + + Ok(()) +} + +/// Validate that all required secrets are present +pub fn validate_secrets( + info: &PluginInfo, + available_secrets: &std::collections::HashMap, +) -> Result<(), LoadError> { + for secret in &info.required_secrets { + if !available_secrets.contains_key(secret) { + return Err(LoadError::MissingSecret(secret.clone())); + } + } + Ok(()) +} + +/// Parse PLUGIN_URLS environment variable +/// Format: id|url|sha256:hash,id|url|sha256:hash,... +pub fn parse_plugin_urls(env_value: &str) -> Vec<(String, String, Option)> { + env_value + .split(',') + .filter_map(|entry| { + let parts: Vec<&str> = entry.trim().split('|').collect(); + if parts.len() >= 2 { + let id = parts[0].to_string(); + let url = parts[1].to_string(); + let sha256 = parts + .get(2) + .and_then(|s| s.strip_prefix("sha256:").map(String::from)); + Some((id, url, sha256)) + } else { + None + } + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_plugin_urls() { + let input = + "steam|https://example.com/steam.wasm|sha256:abc123,gog|https://example.com/gog.wasm"; + let result = parse_plugin_urls(input); + + assert_eq!(result.len(), 2); + assert_eq!( + result[0], + ( + "steam".into(), + "https://example.com/steam.wasm".into(), + Some("abc123".into()) + ) + ); + assert_eq!( + result[1], + ("gog".into(), "https://example.com/gog.wasm".into(), None) + ); + } + + #[test] + fn test_validate_api_version() { + let info = PluginInfo { + id: "test".into(), + name: "Test".into(), + version: "1.0.0".into(), + api_version: "1".into(), + icon_url: None, + required_secrets: vec![], + config_schema: None, + }; + + assert!(validate_api_version(&info).is_ok()); + + let future_info = PluginInfo { + api_version: "99".into(), + ..info + }; + + assert!(matches!( + validate_api_version(&future_info), + Err(LoadError::UnsupportedApiVersion(_)) + )); + } +} diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs new file mode 100644 --- /dev/null +++ b/src/plugin/mod.rs @@ -0,0 +1,41 @@ +pub mod encryption; +pub mod host; +pub mod loader; +mod runtime; +mod types; + +pub use runtime::WasmRuntime; +pub use types::*; + +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; + +/// Registry of loaded plugins +#[derive(Default)] +pub struct PluginRegistry { + plugins: RwLock>>, +} + +impl PluginRegistry { + pub fn new() -> Self { + Self::default() + } + + pub async fn register(&self, plugin: LoadedPlugin) { + let id = plugin.info.id.clone(); + self.plugins.write().await.insert(id, Arc::new(plugin)); + } + + pub async fn get(&self, id: &str) -> Option> { + self.plugins.read().await.get(id).cloned() + } + + pub async fn list(&self) -> Vec> { + self.plugins.read().await.values().cloned().collect() + } + + pub async fn remove(&self, id: &str) -> Option> { + self.plugins.write().await.remove(id) + } +} diff --git a/src/plugin/runtime.rs b/src/plugin/runtime.rs new file mode 100644 --- /dev/null +++ b/src/plugin/runtime.rs @@ -0,0 +1,27 @@ +use wasmtime::*; + +/// WASM runtime for executing plugins +pub struct WasmRuntime { + engine: Engine, +} + +impl WasmRuntime { + pub fn new() -> Result { + let mut config = Config::new(); + config.async_support(true); + + let engine = Engine::new(&config)?; + + Ok(Self { engine }) + } + + pub fn engine(&self) -> &Engine { + &self.engine + } +} + +impl Default for WasmRuntime { + fn default() -> Self { + Self::new().expect("Failed to create WASM runtime") + } +} diff --git a/src/plugin/types.rs b/src/plugin/types.rs new file mode 100644 --- /dev/null +++ b/src/plugin/types.rs @@ -0,0 +1,98 @@ +use serde::{Deserialize, Serialize}; + +/// Plugin metadata returned by plugin_info() +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PluginInfo { + pub id: String, + pub name: String, + pub version: String, + pub api_version: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub icon_url: Option, + #[serde(default)] + pub required_secrets: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub config_schema: Option, +} + +/// OAuth callback parameters passed to handle_callback() +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CallbackParams { + pub code: Option, + pub state: Option, + pub error: Option, + #[serde(flatten)] + pub extra: std::collections::HashMap, +} + +/// Tokens returned by handle_callback() and refresh_tokens() +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TokenSet { + pub access_token: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub expires_at: Option>, + pub token_type: String, +} + +/// Error returned by plugin functions +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PluginError { + pub code: PluginErrorCode, + pub message: String, + #[serde(default)] + pub retryable: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PluginErrorCode { + UserDenied, + InvalidToken, + ServiceUnavailable, + InvalidResponse, + Unknown, +} + +/// External profile returned by get_profile() +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExternalProfile { + pub account_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub profile_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar_url: Option, +} + +/// Record returned by sync_account() - lexicon-aware +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SyncRecord { + pub collection: String, + pub record: serde_json::Value, + #[serde(skip_serializing_if = "Option::is_none")] + pub dedup_key: Option, +} + +/// Strong reference to an AT Protocol record +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StrongRef { + pub uri: String, + pub cid: String, +} + +/// Plugin source - file or URL +#[derive(Debug, Clone)] +pub enum PluginSource { + File { path: std::path::PathBuf }, + Url { url: String, sha256: Option }, +} + +/// Loaded plugin with runtime state +pub struct LoadedPlugin { + pub info: PluginInfo, + pub source: PluginSource, + pub wasm_bytes: Vec, +} diff --git a/src/server.rs b/src/server.rs --- a/src/server.rs +++ b/src/server.rs @@ -67,6 +67,7 @@ .route("/health", get(health)) .route("/settings/logo", get(crate::admin::settings::serve_logo)) .nest("/admin", admin::admin_routes(state.clone())) .nest("/auth", crate::auth::routes::routes()) + .nest("/external-auth", crate::external_auth::routes()) .route("/oauth/client-metadata.json", get(client_metadata)) .route("/xrpc/app.bsky.actor.getProfile", get(get_profile)) .route( diff --git a/tests/common/app.rs b/tests/common/app.rs --- a/tests/common/app.rs +++ b/tests/common/app.rs @@ -51,6 +51,7 @@ app_name: None, logo_uri: None, tos_uri: None, policy_uri: None, + token_encryption_key: None, }; let sql = adapt_sql( @@ -129,6 +130,7 @@ oauth: std::sync::Arc::new(oauth), cookie_key: axum_extra::extract::cookie::Key::derive_from( b"test-secret-that-is-at-least-32-bytes-long", ), + plugin_registry: std::sync::Arc::new(happyview::plugin::PluginRegistry::new()), }; let router = server::router(state.clone()); diff --git a/tests/lua_atproto_api.rs b/tests/lua_atproto_api.rs --- a/tests/lua_atproto_api.rs +++ b/tests/lua_atproto_api.rs @@ -33,6 +33,7 @@ app_name: None, logo_uri: None, tos_uri: None, policy_uri: None, + token_encryption_key: None, }; let (tx, _) = watch::channel(vec![]); let (labeler_tx, _) = watch::channel(()); @@ -83,6 +84,7 @@ vec![], ), oauth: std::sync::Arc::new(oauth), cookie_key: axum_extra::extract::cookie::Key::derive_from(b"test-secret"), + plugin_registry: std::sync::Arc::new(happyview::plugin::PluginRegistry::new()), } } diff --git a/tests/lua_db_api.rs b/tests/lua_db_api.rs --- a/tests/lua_db_api.rs +++ b/tests/lua_db_api.rs @@ -36,6 +36,7 @@ app_name: None, logo_uri: None, tos_uri: None, policy_uri: None, + token_encryption_key: None, }; let (tx, _) = watch::channel(vec![]); let (labeler_tx, _) = watch::channel(()); @@ -86,6 +87,7 @@ vec![], ), oauth: std::sync::Arc::new(oauth), cookie_key: axum_extra::extract::cookie::Key::derive_from(b"test-secret"), + plugin_registry: std::sync::Arc::new(happyview::plugin::PluginRegistry::new()), } } diff --git a/tests/plugin_integration.rs b/tests/plugin_integration.rs new file mode 100644 --- /dev/null +++ b/tests/plugin_integration.rs @@ -0,0 +1,73 @@ +//! Integration tests for the plugin system +//! +//! Note: These tests require a valid WASM plugin to test against. +//! For now, we test the infrastructure without actual WASM execution. + +use happyview::plugin::{LoadedPlugin, PluginInfo, PluginRegistry, PluginSource}; + +#[tokio::test] +async fn test_plugin_registry_crud() { + let registry = PluginRegistry::new(); + + // Create test plugin + let plugin = LoadedPlugin { + info: PluginInfo { + id: "test-plugin".into(), + name: "Test Plugin".into(), + version: "1.0.0".into(), + api_version: "1".into(), + icon_url: None, + required_secrets: vec![], + config_schema: None, + }, + source: PluginSource::File { + path: "/tmp/test".into(), + }, + wasm_bytes: vec![], + }; + + // Register + registry.register(plugin).await; + + // Get + let retrieved = registry.get("test-plugin").await; + assert!(retrieved.is_some()); + assert_eq!(retrieved.unwrap().info.name, "Test Plugin"); + + // List + let all = registry.list().await; + assert_eq!(all.len(), 1); + + // Remove + let removed = registry.remove("test-plugin").await; + assert!(removed.is_some()); + + // Verify removed + assert!(registry.get("test-plugin").await.is_none()); +} + +#[tokio::test] +async fn test_plugin_registry_multiple() { + let registry = PluginRegistry::new(); + + for i in 0..5 { + let plugin = LoadedPlugin { + info: PluginInfo { + id: format!("plugin-{}", i), + name: format!("Plugin {}", i), + version: "1.0.0".into(), + api_version: "1".into(), + icon_url: None, + required_secrets: vec![], + config_schema: None, + }, + source: PluginSource::File { + path: "/tmp/test".into(), + }, + wasm_bytes: vec![], + }; + registry.register(plugin).await; + } + + assert_eq!(registry.list().await.len(), 5); +} -- tangled.sh