diff --git a/Cargo.lock b/Cargo.lock index 324bafca..5192901d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,7 +101,7 @@ dependencies = [ "k256", "once_cell", "rand 0.8.5", - "secp256k1", + "secp256k1 0.30.0", "serde", "serde_json", "serde_with", @@ -1026,6 +1026,19 @@ dependencies = [ "virtue", ] +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.5", + "rand_core 0.6.4", + "serde", + "unicode-normalization", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -1145,6 +1158,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "blstrs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a8a8ed6fefbeef4a8c7b460e4110e12c5e22a5b7cf32621aae6ad650c4dcf29" +dependencies = [ + "blst", + "byte-slice-cast", + "ff", + "group", + "pairing", + "rand_core 0.6.4", + "serde", + "subtle", +] + [[package]] name = "bollard" version = "0.18.1" @@ -1237,6 +1266,21 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bstr" version = "1.12.1" @@ -1750,6 +1794,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-mac" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bcd97a54c7ca5ce2f6eb16f6bede5b0ab5f0055fedc17d2f0b4466e21671ca" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "crypto-mac" version = "0.11.0" @@ -2383,24 +2437,34 @@ name = "dstack-kms" version = "0.5.6" dependencies = [ "anyhow", + "base64 0.22.1", + "blstrs", + "bs58 0.5.1", + "byte-slice-cast", "chrono", "clap", + "dcap-qvl", "dstack-guest-agent-rpc", "dstack-kms-rpc", "dstack-mr", "dstack-types", "dstack-verifier", + "elliptic-curve", "fs-err", "git-version", "hex", "hex_fmt", + "hkdf", "http-client", "k256", "load_config", + "near-api", + "near-crypto", "parity-scale-codec", "ra-rpc", "ra-tls", "rand 0.8.5", + "rand_core 0.6.4", "reqwest", "ring", "rocket", @@ -2415,6 +2479,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "url", "x25519-dalek", "x509-parser", "yasna", @@ -2956,6 +3021,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ + "bitvec", "rand_core 0.6.4", "subtle", ] @@ -2998,6 +3064,15 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixed-hash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" +dependencies = [ + "static_assertions", +] + [[package]] name = "fixed-hash" version = "0.8.0" @@ -3056,6 +3131,21 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -3314,7 +3404,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", + "rand 0.8.5", "rand_core 0.6.4", + "rand_xorshift 0.3.0", "subtle", ] @@ -3439,6 +3531,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hex-conservative" @@ -3567,13 +3662,23 @@ dependencies = [ "hmac 0.12.1", ] +[[package]] +name = "hmac" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deae6d9dbb35ec2c502d62b8f7b1c000a0822c3b0794ba36b3149c0a1c840dff" +dependencies = [ + "crypto-mac 0.9.1", + "digest 0.9.0", +] + [[package]] name = "hmac" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" dependencies = [ - "crypto-mac", + "crypto-mac 0.11.0", "digest 0.9.0", ] @@ -3759,6 +3864,22 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.19" @@ -3778,9 +3899,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -4194,6 +4317,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json_comments" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dbbfed4e59ba9750e15ba154fdfd9329cee16ff3df539c2666b70f58cc32105" + [[package]] name = "k256" version = "0.13.4" @@ -4597,6 +4726,215 @@ dependencies = [ "serde", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe 0.1.6", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "near-abi" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fe0c8cdaf8369d8c78f2577d95673007c7f256c85752a66672be5244e2681a6" +dependencies = [ + "borsh", + "schemars 0.8.22", + "semver 1.0.27", + "serde", +] + +[[package]] +name = "near-account-id" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91f75ff8eee73815c247d0e17f3c0b705f0e993922a5548acd2ad377aeb67fca" +dependencies = [ + "borsh", + "serde", +] + +[[package]] +name = "near-api" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321d2778c3cfb314cf6410376788a17b5c34f6154ee61bcd1579748edda454bb" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bip39", + "borsh", + "futures", + "near-api-types", + "near-openapi-client", + "openssl", + "reqwest", + "serde", + "serde_dbgfmt", + "serde_json", + "slipped10", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", + "zstd", +] + +[[package]] +name = "near-api-types" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4740cd9996fda91dd1cc9034528f0e987ac2c2266f15c6459df7a595e11eb71" +dependencies = [ + "base64 0.22.1", + "borsh", + "bs58 0.5.1", + "ed25519-dalek", + "near-abi", + "near-account-id", + "near-gas", + "near-openapi-types", + "near-token", + "primitive-types 0.10.1", + "secp256k1 0.27.0", + "serde", + "serde_json", + "serde_with", + "sha2 0.10.9", + "thiserror 2.0.18", +] + +[[package]] +name = "near-config-utils" +version = "0.34.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05645e1310050014c787100ec3b34f79381807d8c9cac4d184d3711b15f0dc65" +dependencies = [ + "anyhow", + "json_comments", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "near-crypto" +version = "0.34.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f0ed5bc0e75aa175a6013f92d0ce66a14288b2adfd3ae262062aa220d24c9c4" +dependencies = [ + "blake2", + "borsh", + "bs58 0.4.0", + "curve25519-dalek", + "derive_more 2.1.1", + "ed25519-dalek", + "hex", + "near-account-id", + "near-config-utils", + "near-schema-checker-lib", + "near-stdx", + "primitive-types 0.10.1", + "rand 0.8.5", + "secp256k1 0.27.0", + "serde", + "serde_json", + "subtle", + "thiserror 2.0.18", +] + +[[package]] +name = "near-gas" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cecd5d9463587f34f2b7ff4c7104297483a543be8a68a4a04a8ac96c419d1a1" +dependencies = [ + "borsh", + "serde", +] + +[[package]] +name = "near-openapi-client" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6d2a3dd4e669e14cdd0748b449d7539d0b0fefba17ed8e2d1404a0826908630" +dependencies = [ + "bytes", + "chrono", + "futures-core", + "near-openapi-types", + "progenitor-client", + "reqwest", + "serde", + "serde_json", + "serde_urlencoded", +] + +[[package]] +name = "near-openapi-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e0c9eaf432ddedd9409d6b1477b49e94679c077adbadce173292f759454afc7" +dependencies = [ + "bs58 0.5.1", + "chrono", + "near-account-id", + "near-gas", + "near-token", + "serde", + "serde_json", + "strum_macros", + "thiserror 2.0.18", +] + +[[package]] +name = "near-schema-checker-core" +version = "0.34.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "041c3f48dac0f8986d2cce36ca9ee4fc34967a6a8537750c117c26f58b6bd098" + +[[package]] +name = "near-schema-checker-lib" +version = "0.34.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84fb5f0f897b691d0a197877e6839373ce6f0bbd4ab718d7c8c98472272f659" +dependencies = [ + "near-schema-checker-core", + "near-schema-checker-macro", +] + +[[package]] +name = "near-schema-checker-macro" +version = "0.34.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2a9928bf41d38efba9e4a3afc10842fd28505b9f2e4ca36f0cf8c58246d96d2" + +[[package]] +name = "near-stdx" +version = "0.34.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dba5c1d9236e8e08f9a9ea6519a7b75f6bad1fe21dfb326e71a7f361170055ca" + +[[package]] +name = "near-token" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34de6b54d82d0790b2a56b677e7b4ecb7f021a7e8559f8611065c890d56cfcda" +dependencies = [ + "borsh", + "serde", +] + [[package]] name = "netlink-packet-core" version = "0.7.0" @@ -4899,12 +5237,66 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + [[package]] name = "openssl-probe" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "openssl-src" +version = "300.5.4+3.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + [[package]] name = "optfield" version = "0.4.0" @@ -4962,6 +5354,15 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + [[package]] name = "parcelona" version = "0.4.3" @@ -5090,7 +5491,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" dependencies = [ - "crypto-mac", + "crypto-mac 0.11.0", ] [[package]] @@ -5266,6 +5667,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "poly1305" version = "0.8.0" @@ -5338,13 +5745,23 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "primitive-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373" +dependencies = [ + "fixed-hash 0.7.0", + "uint", +] + [[package]] name = "primitive-types" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" dependencies = [ - "fixed-hash", + "fixed-hash 0.8.0", "impl-codec", "uint", ] @@ -5426,6 +5843,21 @@ dependencies = [ "yansi", ] +[[package]] +name = "progenitor-client" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71a0beb939758f229cbae70a4889c7c76a4ac0e90f0b1e7ae9b4636a927d1018" +dependencies = [ + "bytes", + "futures-core", + "percent-encoding", + "reqwest", + "serde", + "serde_json", + "serde_urlencoded", +] + [[package]] name = "proptest" version = "1.9.0" @@ -5438,7 +5870,7 @@ dependencies = [ "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", - "rand_xorshift", + "rand_xorshift 0.4.0", "regex-syntax", "rusty-fork", "tempfile", @@ -5841,6 +6273,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rand_xorshift" version = "0.4.0" @@ -5978,16 +6419,19 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", + "h2", "hickory-resolver 0.25.2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", + "hyper-tls", "hyper-util", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -5999,13 +6443,16 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots", ] @@ -6262,7 +6709,7 @@ dependencies = [ "num-integer", "num-traits", "parity-scale-codec", - "primitive-types", + "primitive-types 0.12.2", "proptest", "rand 0.8.5", "rand 0.9.2", @@ -6378,10 +6825,10 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.5.1", ] [[package]] @@ -6639,6 +7086,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + [[package]] name = "schemars" version = "0.9.0" @@ -6663,6 +7122,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.114", +] + [[package]] name = "schnorrkel" version = "0.11.5" @@ -6718,6 +7189,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "rand 0.8.5", + "secp256k1-sys 0.8.2", +] + [[package]] name = "secp256k1" version = "0.30.0" @@ -6726,10 +7207,19 @@ checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ "bitcoin_hashes", "rand 0.8.5", - "secp256k1-sys", + "secp256k1-sys 0.10.1", "serde", ] +[[package]] +name = "secp256k1-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4473013577ec77b4ee3668179ef1186df3146e2cf2d927bd200974c6fe60fd99" +dependencies = [ + "cc", +] + [[package]] name = "secp256k1-sys" version = "0.10.1" @@ -6748,6 +7238,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.5.1" @@ -6851,6 +7354,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_dbgfmt" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a8a448a7c23464ff14a8e9a31594ca91ea04fc36314302f3c7acb27717897c" +dependencies = [ + "serde", + "unicode-ident", +] + [[package]] name = "serde_derive" version = "1.0.228" @@ -6862,6 +7375,17 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "serde_ini" version = "0.2.0" @@ -7138,6 +7662,17 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slipped10" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a45443e66aa5d96db5e02d17db056e1ca970232a4fe73e1f9bc1816d68f4e98" +dependencies = [ + "ed25519-dalek", + "hmac 0.9.0", + "sha2 0.9.9", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -7641,6 +8176,16 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" @@ -7944,6 +8489,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -7994,6 +8548,7 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -8031,6 +8586,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -8176,6 +8737,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wavekv" version = "1.0.0" @@ -8401,6 +8975,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -8948,3 +9533,31 @@ name = "zmij" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" + +[[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/docs/onchain-governance.md b/docs/onchain-governance.md index 67497f8b..2a2e2ccb 100644 --- a/docs/onchain-governance.md +++ b/docs/onchain-governance.md @@ -1,6 +1,6 @@ # On-Chain Governance -This guide covers setting up on-chain governance for dstack using smart contracts on Ethereum. +This guide covers setting up on-chain governance for dstack using smart contracts on Ethereum or NEAR Protocol. ## Overview @@ -9,14 +9,27 @@ On-chain governance adds: - **Decentralized trust**: No single operator controls keys - **Transparent policies**: Anyone can verify authorization rules on-chain +dstack supports two blockchain platforms for on-chain governance: +- **Ethereum**: Using Solidity smart contracts (see [Ethereum Auth](#ethereum-auth) section) +- **NEAR Protocol**: Using Rust smart contracts (see [NEAR Auth](#near-auth) section) + ## Prerequisites - Production dstack deployment with KMS and Gateway as CVMs (see [Deployment Guide](./deployment.md)) + +**For Ethereum:** - Ethereum wallet with funds on Sepolia testnet (or your target network) - Node.js and npm installed - Alchemy API key (for Sepolia) - get one at https://www.alchemy.com/ -## Deploy DstackKms Contract +**For NEAR:** +- NEAR account with funds on testnet or mainnet +- Bun runtime (or Node.js) installed +- NEAR RPC endpoint access (default: `https://free.rpc.fastnear.com`) + +## Ethereum Auth + +### Deploy DstackKms Contract ```bash cd dstack/kms/auth-eth @@ -43,7 +56,7 @@ export PRIVATE_KEY="" export ALCHEMY_API_KEY="" ``` -## Configure KMS for On-Chain Auth +### Configure KMS for On-Chain Auth The KMS CVM includes an auth-api service that connects to your DstackKms contract. Configure it via environment variables in the KMS CVM: @@ -56,7 +69,7 @@ Note: The auth-api uses `KMS_CONTRACT_ADDR`, while Hardhat tasks use `KMS_CONTRA The auth-api validates boot requests against the smart contract. See [Deployment Guide](./deployment.md#2-deploy-kms-as-cvm) for complete setup instructions. -## Whitelist OS Image +### Whitelist OS Image ```bash npx hardhat kms:add-image --network sepolia 0x @@ -66,7 +79,7 @@ Output: `Image added successfully` The `os_image_hash` is in the `digest.txt` file from the guest OS image build (see [Building Guest Images](./deployment.md#building-guest-images)). -## Register Gateway App +### Register Gateway App ```bash npx hardhat kms:create-app --network sepolia --allow-any-device @@ -103,7 +116,7 @@ npx hardhat app:add-hash --network sepolia --app-id Output: `Compose hash added successfully` -## Register Apps On-Chain +### Register Apps On-Chain For each app you want to deploy: @@ -133,9 +146,9 @@ npx hardhat app:add-hash --network sepolia --app-id Use the App ID when deploying through the VMM dashboard or [VMM CLI](./vmm-cli-user-guide.md). -## Smart Contract Reference +### Ethereum Smart Contract Reference -### DstackKms (Main Contract) +#### DstackKms (Main Contract) The central governance contract that manages OS image whitelisting, app registration, and KMS authorization. @@ -149,7 +162,7 @@ The central governance contract that manages OS image whitelisting, app registra | `isAppAllowed(AppBootInfo)` | Check if an app is allowed to boot | | `isKmsAllowed(AppBootInfo)` | Check if KMS is allowed to boot | -### DstackApp (Per-App Contract) +#### DstackApp (Per-App Contract) Each app has its own contract controlling which compose hashes and devices are allowed. @@ -163,7 +176,7 @@ Each app has its own contract controlling which compose hashes and devices are a | `isAppAllowed(AppBootInfo)` | Check if app can boot with given config | | `disableUpgrades()` | Permanently disable contract upgrades | -### AppBootInfo Structure +#### AppBootInfo Structure Both `isAppAllowed` and `isKmsAllowed` take an `AppBootInfo` struct: @@ -183,6 +196,275 @@ struct AppBootInfo { Source: [`kms/auth-eth/contracts/`](../kms/auth-eth/contracts/) +## NEAR Auth + +### Deploy NEAR KMS Contract + +The NEAR KMS contract must be deployed to a NEAR account. You can deploy it using NEAR CLI or the deployment scripts in the `near-kms` repository. + +**Prerequisites:** +- NEAR account with sufficient balance (for contract deployment and storage) +- NEAR CLI installed and configured +- MPC contract ID (for key derivation) + +**Deploy the contract:** + +```bash +cd near-kms/contracts/kms +near deploy --wasmFile res/near_dstack_kms.wasm \ + --accountId \ + --initFunction new \ + --initArgs '{"owner_id": "", "mpc_contract_id": "", "mpc_domain_id": 2}' +``` + +Note the KMS contract account ID (e.g., `kms.dstack.testnet`). + +Set environment variables for subsequent commands: + +```bash +export KMS_CONTRACT_ID="" +export NEAR_ACCOUNT_ID="" +export NEAR_PRIVATE_KEY="ed25519:" +export NEAR_NETWORK_ID="testnet" # or "mainnet" +export NEAR_RPC_URL="https://free.rpc.fastnear.com" # optional, auto-detected if not set +``` + +### Configure KMS for NEAR Auth + +The KMS CVM can be configured to use NEAR contracts in two ways: + +**Option 1: Direct NEAR integration (recommended)** + +Configure via TOML config file: + +```toml +[core.auth_api] +type = "near" + +[core.auth_api.near] +url = "http://auth-near:3000" # Optional: auth-near webhook service URL +rpc_url = "https://free.rpc.fastnear.com" +network_id = "testnet" +contract_id = "" +mpc_contract_id = "" # Optional: for MPC key derivation +mpc_domain_id = 2 # Optional: default is 2 +``` + +**Option 2: Via webhook service** + +Deploy the `auth-near` webhook service and configure KMS to use it: + +```toml +[core.auth_api] +type = "webhook" + +[core.auth_api.webhook] +url = "http://auth-near:3000" +``` + +The `auth-near` service validates boot requests against NEAR smart contracts. See [auth-near README](../kms/auth-near/README.md) for complete setup instructions. + +### Whitelist OS Image + +Add an OS image hash to the KMS contract's allowed list: + +```bash +cd dstack/kms/auth-near +bun install +bun cli.ts add-os-image 0x +``` + +Output: `✅ OS image hash added successfully` + +The `os_image_hash` is in the `digest.txt` file from the guest OS image build (see [Building Guest Images](./deployment.md#building-guest-images)). + +**Remove an OS image:** + +```bash +bun cli.ts remove-os-image 0x +``` + +### Register Gateway App + +Deploy and register a gateway app contract: + +```bash +bun cli.ts deploy \ + --allow-any-device \ + --compose-hash 0x \ + --deposit 30 +``` + +Sample output: + +``` +✅ App contract deployed successfully! + App Account: . + Transaction: +``` + +Note the App Account ID (e.g., `gateway.kms.dstack.testnet`). + +Set it as the gateway app: + +```bash +near call set_gateway_app_id \ + '{"app_id": ""}' \ + --accountId \ + --deposit 1 +``` + +Output: `Gateway App ID set successfully` + +**Add compose hash to gateway:** + +```bash +bun cli.ts add-hash 0x +``` + +### Register Apps On-Chain + +For each app you want to deploy: + +#### Create App + +Deploy and register an app contract: + +```bash +bun cli.ts deploy \ + --allow-any-device \ + --compose-hash 0x \ + --deposit 30 +``` + +Note the App Account ID from the output (format: `.`). + +#### Add Compose Hash + +Compute your app's compose hash: + +```bash +sha256sum /path/to/your-app-compose.json | awk '{print "0x"$1}' +``` + +Then add it: + +```bash +bun cli.ts add-hash 0x +``` + +#### Add Device ID (Optional) + +If not using `--allow-any-device`, add specific device IDs: + +```bash +near call add_device \ + '{"device_id": "0x"}' \ + --accountId \ + --deposit 1 +``` + +#### Deploy via VMM + +Use the App Account ID when deploying through the VMM dashboard or [VMM CLI](./vmm-cli-user-guide.md). + +### NEAR Smart Contract Reference + +#### KMS Contract (Main Contract) + +The central governance contract that manages OS image whitelisting, app registration, and KMS authorization. + +| Function | Description | +|----------|-------------| +| `new(owner_id, mpc_contract_id, mpc_domain_id)` | Initialize the KMS contract | +| `add_os_image_hash(os_image_hash)` | Whitelist an OS image hash | +| `remove_os_image_hash(os_image_hash)` | Remove an OS image from whitelist | +| `add_kms_aggregated_mr(mr_aggregated)` | Whitelist an aggregated MR for KMS | +| `remove_kms_aggregated_mr(mr_aggregated)` | Remove an aggregated MR from whitelist | +| `add_kms_device(device_id)` | Whitelist a device ID for KMS | +| `remove_kms_device(device_id)` | Remove a device ID from whitelist | +| `add_kms_compose_hash(compose_hash)` | Whitelist a compose hash for KMS | +| `remove_kms_compose_hash(compose_hash)` | Remove a compose hash from whitelist | +| `set_gateway_app_id(app_id)` | Set the trusted Gateway app ID | +| `register_app(app_id, owner_id, ...)` | Deploy and register an app contract | +| `is_app_registered(app_id)` | Check if an app is registered | +| `is_kms_allowed(AppBootInfo)` | Check if KMS is allowed to boot | +| `request_kms_root_key(...)` | Request KMS root key from MPC network | + +#### App Contract (Per-App Contract) + +Each app has its own contract controlling which compose hashes and devices are allowed. + +| Function | Description | +|----------|-------------| +| `new(owner_id, kms_contract_id, ...)` | Initialize the app contract | +| `add_compose_hash(compose_hash)` | Whitelist a compose hash | +| `remove_compose_hash(compose_hash)` | Remove a compose hash from whitelist | +| `add_device(device_id)` | Whitelist a device ID | +| `remove_device(device_id)` | Remove a device from whitelist | +| `set_allow_any_device(allow)` | Allow any device to run this app | +| `is_app_allowed(AppBootInfo)` | Check if app can boot with given config | +| `disable_upgrades()` | Permanently disable contract upgrades | + +#### AppBootInfo Structure + +Both `is_app_allowed` and `is_kms_allowed` take an `AppBootInfo` struct: + +```rust +pub struct AppBootInfo { + pub app_id: AccountId, // Unique app identifier (account ID) + pub compose_hash: String, // Hash of docker-compose configuration + pub instance_id: AccountId, // Unique instance identifier + pub device_id: String, // Hardware device identifier + pub mr_aggregated: String, // Aggregated measurement register + pub mr_system: String, // System measurement register + pub os_image_hash: String, // OS image hash + pub tcb_status: String, // TCB status (e.g., "UpToDate") + pub advisory_ids: Vec, // Security advisory IDs +} +``` + +Source: [`near-kms/contracts/kms/`](../../near-kms/contracts/kms/) + +### NEAR CLI Commands Reference + +The `auth-near` package provides CLI commands for managing NEAR contracts: + +**App Management:** +```bash +# Deploy app contract +bun cli.ts deploy [options] + +# Add compose hash +bun cli.ts add-hash + +# Remove compose hash +bun cli.ts remove-hash +``` + +**KMS Configuration:** +```bash +# Add OS image hash +bun cli.ts add-os-image + +# Remove OS image hash +bun cli.ts remove-os-image + +# Add KMS device ID +bun cli.ts add-device + +# Remove KMS device ID +bun cli.ts remove-device + +# Add KMS aggregated MR +bun cli.ts add-mr + +# Remove KMS aggregated MR +bun cli.ts remove-mr +``` + +See [auth-near README](../kms/auth-near/README.md) for detailed CLI documentation. + ## See Also - [Deployment Guide](./deployment.md) - Setting up dstack infrastructure diff --git a/kms/Cargo.toml b/kms/Cargo.toml index bc33bc6a..6efaec51 100644 --- a/kms/Cargo.toml +++ b/kms/Cargo.toml @@ -47,6 +47,23 @@ tempfile.workspace = true serde-duration.workspace = true dstack-verifier = { workspace = true, default-features = false } dstack-mr.workspace = true +base64.workspace = true + +# BLS12-381 for MPC key derivation +blstrs = "0.7" +elliptic-curve = "0.13" +hkdf = "0.12.4" +bs58 = "0.5" +rand_core = { version = "0.6", features = ["getrandom"] } + +# NEAR integration +near-api = "0.8.3" +near-crypto = "0.34.6" # Still needed for InMemorySigner in onboard_service.rs +byte-slice-cast = "1.2" +url.workspace = true + +# DCAP QVL for TDX quote verification +dcap-qvl.workspace = true [features] default = [] diff --git a/kms/README.md b/kms/README.md index 08d05b1c..00b063a4 100644 --- a/kms/README.md +++ b/kms/README.md @@ -33,6 +33,9 @@ CVMs running in dstack support three boot modes: ## KMS Implementation ### Components + +#### Ethereum/Base/Phala KMS + 1. **dstack-kms** - Main RPC service for app key requests - Quote verification and boot info validation @@ -56,11 +59,49 @@ CVMs running in dstack support three boot modes: - Controls permissions for individual apps - Maintains the allowed compose hashes for each app +#### NEAR KMS + +1. **dstack-kms** (same as above) + - Can be configured to use NEAR auth API instead of Ethereum + +2. **dstack-kms-auth-near** + - NEAR Protocol chain interface for permission checks + - Two-step validation (same as auth-eth): + 1. KMS control contract check + 2. App control contract check + - Handles hex → NEAR AccountId conversion + - See [auth-near README](auth-near/README.md) for details + +3. **NEAR Authorization Contracts** + - `near-dstack-kms` (Rust contract) + - Maintains a registry for all Applications + - Maintains the allowed KMS Instance MRs + - Maintains the allowed OS Images + - Supports MPC-based root key derivation from NEAR MPC network + - Factory method `register_app()` for deploying app contracts + - `near-dstack-app` (Rust contract) + - Deployed as subaccounts: `{app_id}.{kms_contract_id}` + - Controls permissions for individual apps + - Maintains the allowed compose hashes and device IDs per app + +4. **MPC Key Derivation** (NEAR-specific) + - KMS can derive deterministic root keys from NEAR MPC network + - Uses BLS12-381 cryptography for secure key derivation + - All KMS instances derive the same keys when using the same MPC domain + - Falls back to local key generation if MPC config is incomplete + ### Deployment -The first two components are deployed as an dstack app on dstack in Local-Key-Provider mode. + +The KMS components are deployed as a dstack app on dstack in Local-Key-Provider mode. The docker compose file would look like [this](dstack-app/docker-compose.yaml). -The solidity contracts are deployed on an ethereum compatible chain. +**Authorization Contracts:** +- **Ethereum/Base/Phala**: Solidity contracts deployed on an Ethereum-compatible chain +- **NEAR**: Rust contracts deployed on NEAR Protocol (see [near-kms README](../near-kms/README.md)) + +**App Contract Deployment:** +- **Ethereum**: Use Hardhat tasks (`app:deploy`, `app:add-hash`) or factory method `deployAndRegisterApp()` +- **NEAR**: Use CLI commands (`bun run app:deploy`, `bun run app:add-hash`) or KMS contract's `register_app()` method ## Trustness @@ -89,12 +130,25 @@ On startup, the KMS node will either: - Onboard: Obtain root keys from an existing KMS instance #### Bootstrapping + +**Ethereum/Base/Phala KMS:** During bootstrapping, the KMS node generates two root keys: 1. CA root key: Used to issue x509 certificates for Apps, enabling HTTPS traffic 2. K256 root key: Used to derive Ethereum-compatible keys for Apps After generating the root keys, their public portions can be obtained along with the corresponding TDX quote and registered in the DstackKms contract. +**NEAR KMS with MPC:** +When configured with NEAR MPC, the KMS node derives deterministic root keys from the NEAR MPC network: +1. Generates ephemeral BLS12-381 G1 keypair +2. Calls MPC contract's `request_app_private_key()` with derivation path "kms-root-key" +3. Receives encrypted response and decrypts using ephemeral private key +4. Verifies MPC signature using BLS pairing +5. Derives 32-byte root key using HKDF +6. Converts root key to CA, tmp CA, RPC, and K256 keys using deterministic key derivation + +All KMS instances using the same MPC domain will derive identical root keys, enabling seamless replication without key transfer. If MPC configuration is incomplete, the system automatically falls back to local key generation (same as Ethereum). + #### KMS Self Replication When deploying a new KMS instance (`B`) using an existing instance (`A`), the process follows these steps: @@ -117,9 +171,14 @@ Once onboarded, the KMS node begins listening for app key provisioning requests. When a KMS node receives a key provisioning request, it: 1. Validates the TDX quote of the requesting App -2. Queries the DstackKms contract for provisioning allowance +2. Queries the authorization contract (DstackKms or near-dstack-kms) for provisioning allowance 3. If allowed, generates and sends the keys to the App +**NEAR-specific notes:** +- App contracts are deployed as subaccounts: `{app_id}.{kms_contract_id}` +- Compose hashes are stored as hex strings (without `0x` prefix) +- Use `auth-near` CLI commands for app deployment and compose hash management (see [auth-near README](auth-near/README.md#cli-commands)) + ### Attestation #### Vanilla TDX Quote attestation diff --git a/kms/auth-near/COMPATIBILITY.md b/kms/auth-near/COMPATIBILITY.md new file mode 100644 index 00000000..554e9ebc --- /dev/null +++ b/kms/auth-near/COMPATIBILITY.md @@ -0,0 +1,32 @@ +# NEAR Integration Compatibility Notes + +## Gateway Compatibility + +The gateway handles app IDs as hex-encoded strings internally. For NEAR: +- App IDs in attestations are still bytes (as per dstack spec) +- auth-near converts hex app_id to NEAR AccountId format when calling contracts +- Gateway routing uses app_id from SNI parsing - works with any string format +- **Status**: ✅ Compatible - no changes needed + +## Guest Agent Compatibility + +The guest-agent uses app_id as `Vec` internally: +- App IDs come from attestation (bytes) +- Encoded/decoded as hex strings when needed +- auth-near handles the conversion to NEAR AccountId format +- **Status**: ✅ Compatible - no changes needed + +## App ID Format Considerations + +- **Attestation**: App IDs are always bytes (hex-encoded) +- **NEAR Contracts**: Expect AccountId (string format like "app.near") +- **Conversion**: auth-near converts hex → AccountId when calling NEAR contracts +- **Storage**: Gateway/guest-agent store app_id as hex strings internally + +## Potential Future Enhancements + +1. **App ID Mapping**: Consider a mapping layer if hex addresses need to map to NEAR AccountIds +2. **SNI Parsing**: Gateway SNI parsing works with any string format, but ensure AccountId format is URL-safe +3. **Validation**: Add validation to ensure AccountId format is valid NEAR account ID + + diff --git a/kms/auth-near/README.md b/kms/auth-near/README.md new file mode 100644 index 00000000..b02cb087 --- /dev/null +++ b/kms/auth-near/README.md @@ -0,0 +1,355 @@ +# dstack auth-near + +A NEAR Protocol backend for dstack KMS webhook authorization. Validates boot requests against NEAR smart contracts. + +## Overview + +This module provides on-chain governance authentication for dstack KMS using NEAR Protocol smart contracts. It's similar to `auth-eth` but uses NEAR's contract system instead of Ethereum. + +## Features + +- **NEAR Contract Integration**: Calls `is_kms_allowed()` and `is_app_allowed()` view methods on NEAR contracts +- **Multi-Contract Support**: Validates both KMS and App contracts +- **Account ID Handling**: Converts hex addresses to NEAR AccountId format +- **Compatible API**: Same HTTP endpoints as `auth-eth` for seamless integration + +## Prerequisites + +- Bun runtime (or Node.js with appropriate setup) +- NEAR RPC endpoint access +- Deployed NEAR KMS contract +- Deployed NEAR App contracts (for app validation) + +## Installation + +```bash +bun install +``` + +## Configuration + +Set the following environment variables: + +| Variable | Description | Default | +|----------|-------------|---------| +| `NEAR_RPC_URL` | NEAR RPC endpoint | `https://free.rpc.fastnear.com` | +| `NEAR_NETWORK_ID` | NEAR network ID (`mainnet`, `testnet`, `betanet`) | `mainnet` | +| `KMS_CONTRACT_ID` | NEAR account ID of the KMS contract | (required) | +| `PORT` | Server port | `3000` | + +## Usage + +### Start the server + +```bash +bun run start +``` + +Or in development mode with auto-reload: + +```bash +bun run dev +``` + +### Endpoints + +#### GET / + +Health check and info endpoint. Returns contract information. + +**Response:** +```json +{ + "status": "ok", + "kmsContractAddr": "kms.dstack.near", + "gatewayAppId": "gateway.dstack.near", + "chainId": "mainnet", + "appAuthImplementation": "", + "appImplementation": "" +} +``` + +#### POST /bootAuth/app + +App boot authorization. + +**Request:** JSON body matching `BootInfo` schema: +```json +{ + "mrAggregated": "hex_string", + "osImageHash": "hex_string", + "appId": "hex_string", + "composeHash": "hex_string", + "instanceId": "hex_string", + "deviceId": "hex_string", + "tcbStatus": "UpToDate", + "advisoryIds": [], + "mrSystem": "hex_string" +} +``` + +**Response:** +```json +{ + "isAllowed": true, + "reason": "", + "gatewayAppId": "gateway.dstack.near" +} +``` + +#### POST /bootAuth/kms + +KMS boot authorization. + +**Request:** Same as `/bootAuth/app` + +**Response:** Same as `/bootAuth/app` + +## Integration with dstack-kms + +Configure KMS to use webhook auth pointing to this server: + +```toml +[core.auth_api] +type = "webhook" + +[core.auth_api.webhook] +url = "http://auth-near:3000" +``` + +Or use the new NEAR-specific config: + +```toml +[core.auth_api] +type = "near" + +[core.auth_api.near] +rpc_url = "https://free.rpc.fastnear.com" +network_id = "mainnet" +contract_id = "kms.dstack.near" +``` + +## Differences from auth-eth + +| Aspect | auth-eth | auth-near | +|--------|----------|-----------| +| Blockchain | Ethereum/Base | NEAR Protocol | +| SDK | viem | near-api-js | +| Address Format | 20-byte hex (0x...) | AccountId (string) | +| Contract Calls | `readContract()` | `viewFunction()` | +| Network Config | Chain ID (number) | Network ID (string) | + +## CLI Commands + +The `auth-near` package includes CLI commands for deploying app contracts and managing compose hashes, similar to Hardhat tasks in `auth-eth`. + +### Environment Variables + +For CLI commands, you need to set: + +| Variable | Description | Required | +|----------|-------------|----------| +| `NEAR_ACCOUNT_ID` | NEAR account ID for signing transactions | Yes | +| `NEAR_PRIVATE_KEY` | NEAR account private key (ed25519:...) | Yes | +| `KMS_CONTRACT_ID` | NEAR account ID of the KMS contract | Yes (for deploy) | +| `NEAR_NETWORK_ID` | Network ID (`testnet`, `mainnet`) | No (default: `testnet`) | +| `NEAR_RPC_URL` | NEAR RPC endpoint | No (auto-detected) | + +### Deploy App Contract + +Deploy a new app contract via the KMS contract's `register_app` function: + +```bash +bun run app:deploy [options] +``` + +**Example:** +```bash +export NEAR_ACCOUNT_ID=owner.testnet +export NEAR_PRIVATE_KEY=ed25519:... +export KMS_CONTRACT_ID=kms.testnet +export NEAR_NETWORK_ID=testnet + +bun run app:deploy myapp owner.testnet \ + --allow-any-device \ + --compose-hash 0x1234... +``` + +**Options:** +- `--disable-upgrades` - Disable contract upgrades +- `--allow-any-device` - Allow any device to boot this app +- `--device-id ` - Initial device ID to allow +- `--compose-hash ` - Initial compose hash to allow (hex string) +- `--deposit ` - Deposit amount in NEAR (default: 30 NEAR) + +**Note:** The app contract will be deployed as a subaccount: `{app_id}.{kms_contract_id}` + +### Add Compose Hash + +Add a compose hash to an existing app contract: + +```bash +bun run app:add-hash +``` + +**Example:** +```bash +export NEAR_ACCOUNT_ID=owner.testnet +export NEAR_PRIVATE_KEY=ed25519:... + +bun run app:add-hash myapp.kms.testnet 0xabcd1234... +``` + +**Note:** The `app_account_id` should be the full account ID (e.g., `myapp.kms.testnet`), not just the app ID. + +### Remove Compose Hash + +Remove a compose hash from an app contract: + +```bash +bun run app:remove-hash +``` + +**Example:** +```bash +bun run app:remove-hash myapp.kms.testnet 0xabcd1234... +``` + +### Add OS Image Hash + +Add an OS image hash to the KMS contract's allowed list: + +```bash +bun cli.ts add-os-image +``` + +**Example:** +```bash +export NEAR_ACCOUNT_ID=owner.testnet +export NEAR_PRIVATE_KEY=ed25519:... +export KMS_CONTRACT_ID=kms.testnet + +bun cli.ts add-os-image 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef +``` + +**Note:** The hash can be provided with or without the `0x` prefix. The CLI will automatically strip it if present. + +### Remove OS Image Hash + +Remove an OS image hash from the KMS contract: + +```bash +bun cli.ts remove-os-image +``` + +**Example:** +```bash +bun cli.ts remove-os-image 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef +``` + +### Add KMS Device ID + +Add a device ID to the KMS contract's allowed device list: + +```bash +bun cli.ts add-device +``` + +**Example:** +```bash +export NEAR_ACCOUNT_ID=owner.testnet +export NEAR_PRIVATE_KEY=ed25519:... +export KMS_CONTRACT_ID=kms.testnet + +bun cli.ts add-device 0xdevice1234567890abcdef1234567890abcdef1234567890abcdef1234567890 +``` + +**Note:** Device IDs are typically hex strings. The `0x` prefix is optional. + +### Remove KMS Device ID + +Remove a device ID from the KMS contract: + +```bash +bun cli.ts remove-device +``` + +**Example:** +```bash +bun cli.ts remove-device 0xdevice1234567890abcdef1234567890abcdef1234567890abcdef1234567890 +``` + +### Add KMS Aggregated MR + +Add an aggregated MR (measurement) to the KMS contract's allowed list: + +```bash +bun cli.ts add-mr +``` + +**Example:** +```bash +export NEAR_ACCOUNT_ID=owner.testnet +export NEAR_PRIVATE_KEY=ed25519:... +export KMS_CONTRACT_ID=kms.testnet + +bun cli.ts add-mr 0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 +``` + +**Note:** Aggregated MRs are hex strings representing TEE measurements. The `0x` prefix is optional. + +### Remove KMS Aggregated MR + +Remove an aggregated MR from the KMS contract: + +```bash +bun cli.ts remove-mr +``` + +**Example:** +```bash +bun cli.ts remove-mr 0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 +``` + +### Direct CLI Usage + +You can also use the CLI directly with all available commands: + +```bash +# App management +bun cli.ts deploy [options] +bun cli.ts add-hash +bun cli.ts remove-hash + +# KMS configuration +bun cli.ts add-os-image +bun cli.ts remove-os-image +bun cli.ts add-device +bun cli.ts remove-device +bun cli.ts add-mr +bun cli.ts remove-mr +``` + +**Note:** All KMS configuration commands require the `KMS_CONTRACT_ID` environment variable to be set. + +## Development + +### Running Tests + +```bash +bun run test +``` + +### Linting + +```bash +bun run lint +``` + +## See Also + +- [auth-eth](../auth-eth/) - Ethereum-based auth server +- [auth-simple](../auth-simple/) - Config-based auth server +- [auth-mock](../auth-mock/) - Development/testing auth server (always allows) + + diff --git a/kms/auth-near/bun.lock b/kms/auth-near/bun.lock new file mode 100644 index 00000000..63870580 --- /dev/null +++ b/kms/auth-near/bun.lock @@ -0,0 +1,446 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "auth-near", + "dependencies": { + "@hono/zod-validator": "0.2.2", + "hono": "4.11.4", + "near-api-js": "^2.1.4", + "zod": "3.25.76", + }, + "devDependencies": { + "@types/bun": "1.2.18", + "@types/node": "^24.0.14", + "@vitest/ui": "1.6.1", + "openapi-types": "12.1.3", + "oxlint": "0.9.10", + "typescript": "5.8.3", + "vitest": "1.6.1", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "@hono/zod-validator": ["@hono/zod-validator@0.2.2", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-dSDxaPV70Py8wuIU2QNpoVEIOSzSXZ/6/B/h4xA7eOMz7+AarKTSGV8E6QwrdcCbBLkpqfJ4Q2TmBO0eP1tCBQ=="], + + "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@near-js/accounts": ["@near-js/accounts@0.1.4", "", { "dependencies": { "@near-js/crypto": "0.0.5", "@near-js/providers": "0.0.7", "@near-js/signers": "0.0.5", "@near-js/transactions": "0.2.1", "@near-js/types": "0.0.4", "@near-js/utils": "0.0.4", "ajv": "^8.11.2", "ajv-formats": "^2.1.1", "bn.js": "5.2.1", "borsh": "^0.7.0", "depd": "^2.0.0", "near-abi": "0.1.1" } }, "sha512-zHFmL4OUZ4qHXOE+dDBkYgTNHLWC5RmYUVp9LiuGciO5zFPp7WlxmowJL0QjgXqV1w+dNXq3mgmkfAgYVS8Xjw=="], + + "@near-js/crypto": ["@near-js/crypto@0.0.5", "", { "dependencies": { "@near-js/types": "0.0.4", "bn.js": "5.2.1", "borsh": "^0.7.0", "tweetnacl": "^1.0.1" } }, "sha512-nbQ971iYES5Spiolt+p568gNuZ//HeMHm3qqT3xT+i8ZzgbC//l6oRf48SUVTPAboQ1TJ5dW/NqcxOY0pe7b4g=="], + + "@near-js/keystores": ["@near-js/keystores@0.0.5", "", { "dependencies": { "@near-js/crypto": "0.0.5", "@near-js/types": "0.0.4" } }, "sha512-kxqV+gw/3L8/axe9prhlU+M0hfybkxX54xfI0EEpWP2QiUV+qw+jkKolYIbdk5tdEZrGf9jHawh1yFtwP7APPQ=="], + + "@near-js/keystores-browser": ["@near-js/keystores-browser@0.0.5", "", { "dependencies": { "@near-js/crypto": "0.0.5", "@near-js/keystores": "0.0.5" } }, "sha512-mHF3Vcvsr7xnkaM/reOyxtykbE3OWKV6vQzqyTH2tZYT2OTEnj0KhRT9BCFC0Ra67K1zQLbg49Yc/kDCc5qupA=="], + + "@near-js/keystores-node": ["@near-js/keystores-node@0.0.5", "", { "dependencies": { "@near-js/crypto": "0.0.5", "@near-js/keystores": "0.0.5" } }, "sha512-BYmWyGNydfAqi7eYA1Jo8zULL13cxejD2VBr0BBIXx5bJ+BO4TLecsY1xdTBEq06jyWXHa7kV4h8BJzAjvpTLg=="], + + "@near-js/providers": ["@near-js/providers@0.0.7", "", { "dependencies": { "@near-js/transactions": "0.2.1", "@near-js/types": "0.0.4", "@near-js/utils": "0.0.4", "bn.js": "5.2.1", "borsh": "^0.7.0", "http-errors": "^1.7.2" }, "optionalDependencies": { "node-fetch": "^2.6.1" } }, "sha512-qj16Ey+vSw7lHE85xW+ykYJoLPr4A6Q/TsfpwhJLS6zBInSC6sKVqPO1L8bK4VA/yB7V7JJPor9UVCWgRXdNEA=="], + + "@near-js/signers": ["@near-js/signers@0.0.5", "", { "dependencies": { "@near-js/crypto": "0.0.5", "@near-js/keystores": "0.0.5", "js-sha256": "^0.9.0" } }, "sha512-XJjYYatehxHakHa7WAoiQ8uIBSWBR2EnO4GzlIe8qpWL+LoH4t68MSezC1HwT546y9YHIvePjwDrBeYk8mg20w=="], + + "@near-js/transactions": ["@near-js/transactions@0.2.1", "", { "dependencies": { "@near-js/crypto": "0.0.5", "@near-js/signers": "0.0.5", "@near-js/types": "0.0.4", "@near-js/utils": "0.0.4", "bn.js": "5.2.1", "borsh": "^0.7.0", "js-sha256": "^0.9.0" } }, "sha512-V9tXzkICDPruSxihKXkBhUgsI4uvW7TwXlnZS2GZpPsFFiIUeGrso0wo4uiQwB6miFA5q6fKaAtQa4F2v1s+zg=="], + + "@near-js/types": ["@near-js/types@0.0.4", "", { "dependencies": { "bn.js": "5.2.1" } }, "sha512-8TTMbLMnmyG06R5YKWuS/qFG1tOA3/9lX4NgBqQPsvaWmDsa+D+QwOkrEHDegped0ZHQwcjAXjKML1S1TyGYKg=="], + + "@near-js/utils": ["@near-js/utils@0.0.4", "", { "dependencies": { "@near-js/types": "0.0.4", "bn.js": "5.2.1", "depd": "^2.0.0", "mustache": "^4.0.0" } }, "sha512-mPUEPJbTCMicGitjEGvQqOe8AS7O4KkRCxqd0xuE/X6gXF1jz1pYMZn4lNUeUz2C84YnVSGLAM0o9zcN6Y4hiA=="], + + "@near-js/wallet-account": ["@near-js/wallet-account@0.0.7", "", { "dependencies": { "@near-js/accounts": "0.1.4", "@near-js/crypto": "0.0.5", "@near-js/keystores": "0.0.5", "@near-js/signers": "0.0.5", "@near-js/transactions": "0.2.1", "@near-js/types": "0.0.4", "@near-js/utils": "0.0.4", "bn.js": "5.2.1", "borsh": "^0.7.0" } }, "sha512-tmRyieG/wHmuNkg/WGFyKD6iH6atHPbY0rZ5OjOIiteuhZEPgp+z8OBpiQ4qumTa63q46aj/QVSQL0J3+JmBfw=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@0.9.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eOXKZYq5bnCSgDefgM5bzAg+4Fc//Rc4yjgKN8iDWUARweCaChiQXb6TXX8MfEfs6qayEMy6yVj0pqoFz0B1aw=="], + + "@oxlint/darwin-x64": ["@oxlint/darwin-x64@0.9.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-UeYICDvLUaUOcY+0ugZUEmBMRLP+x8iTgL7TeY6BlpGw2ahbtUOTbyIIRWtr/0O++TnjZ+v8TzhJ9crw6Ij6dg=="], + + "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@0.9.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-0Zn+vqHhrZyufFBfq9WOgiIool0gCR14BLsdS+0Dwd9o+kNxPGA5q7erQFkiC4rpkxtfBHeD3iIKMMt7d29Kyw=="], + + "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@0.9.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-tkQcWpYwF42bA/uRaV2iMFePHkBjTTgomOgeEaiw6XOSJX4nBEqGIIboqqLBWT4JnKCf/L+IG3y/e1MflhKByw=="], + + "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@0.9.10", "", { "os": "linux", "cpu": "x64" }, "sha512-JHbkMUnibqaSMBvLHyqTL5cWxcGW+jw+Ppt2baLISpvo34a6fBR+PI7v/A92sEDWe0W1rPhypzCwA8mKpkQ3DA=="], + + "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@0.9.10", "", { "os": "linux", "cpu": "x64" }, "sha512-aBBwN7bQzidwHwEXr7BAdVvMTLWstCy5gikerjLnGDeCSXX9r+o6+yUzTOqZvOo66E+XBgOJaVbY8rsL1MLE0g=="], + + "@oxlint/win32-arm64": ["@oxlint/win32-arm64@0.9.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-LXDnk7vKHT3IY6G1jq0O7+XMhtcHOYuxLGIx4KP+4xS6vKgBY+Bsq4xV3AtmtKlvnXkP5FxHpfLmcEtm5AWysA=="], + + "@oxlint/win32-x64": ["@oxlint/win32-x64@0.9.10", "", { "os": "win32", "cpu": "x64" }, "sha512-w5XRAV4bhgwenjjpGYZGglqzG9Wv/sI+cjQWJBQsvfDXsr2w4vOBXzt1j3/Z3EcSqf4KtkCa/IIuAhQyeShUbA=="], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.56.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.56.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + + "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="], + + "@types/react": ["@types/react@19.2.9", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA=="], + + "@vitest/expect": ["@vitest/expect@1.6.1", "", { "dependencies": { "@vitest/spy": "1.6.1", "@vitest/utils": "1.6.1", "chai": "^4.3.10" } }, "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog=="], + + "@vitest/runner": ["@vitest/runner@1.6.1", "", { "dependencies": { "@vitest/utils": "1.6.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" } }, "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA=="], + + "@vitest/snapshot": ["@vitest/snapshot@1.6.1", "", { "dependencies": { "magic-string": "^0.30.5", "pathe": "^1.1.1", "pretty-format": "^29.7.0" } }, "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ=="], + + "@vitest/spy": ["@vitest/spy@1.6.1", "", { "dependencies": { "tinyspy": "^2.2.0" } }, "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw=="], + + "@vitest/ui": ["@vitest/ui@1.6.1", "", { "dependencies": { "@vitest/utils": "1.6.1", "fast-glob": "^3.3.2", "fflate": "^0.8.1", "flatted": "^3.2.9", "pathe": "^1.1.1", "picocolors": "^1.0.0", "sirv": "^2.0.4" }, "peerDependencies": { "vitest": "1.6.1" } }, "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg=="], + + "@vitest/utils": ["@vitest/utils@1.6.1", "", { "dependencies": { "diff-sequences": "^29.6.3", "estree-walker": "^3.0.3", "loupe": "^2.3.7", "pretty-format": "^29.7.0" } }, "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "assertion-error": ["assertion-error@1.1.0", "", {}, "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw=="], + + "base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], + + "bn.js": ["bn.js@5.2.1", "", {}, "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ=="], + + "borsh": ["borsh@0.7.0", "", { "dependencies": { "bn.js": "^5.2.0", "bs58": "^4.0.0", "text-encoding-utf-8": "^1.0.2" } }, "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], + + "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "capability": ["capability@0.2.5", "", {}, "sha512-rsJZYVCgXd08sPqwmaIqjAd5SUTfonV0z/gDJ8D6cN8wQphky1kkAYEqQ+hmDxTw7UihvBfjUVUSY+DBEe44jg=="], + + "chai": ["chai@4.5.0", "", { "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", "deep-eql": "^4.1.3", "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", "type-detect": "^4.1.0" } }, "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw=="], + + "check-error": ["check-error@1.0.3", "", { "dependencies": { "get-func-name": "^2.0.2" } }, "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg=="], + + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-eql": ["deep-eql@4.1.4", "", { "dependencies": { "type-detect": "^4.0.0" } }, "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="], + + "error-polyfill": ["error-polyfill@0.1.3", "", { "dependencies": { "capability": "^0.2.5", "o3": "^1.0.3", "u3": "^0.1.1" } }, "sha512-XHJk60ufE+TG/ydwp4lilOog549iiQF2OAPhkk9DdiYWMrltz5yhDz/xnKuenNwP7gy3dsibssO5QpVhkrSzzg=="], + + "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "get-func-name": ["get-func-name@2.0.2", "", {}, "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ=="], + + "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="], + + "http-errors": ["http-errors@1.8.1", "", { "dependencies": { "depd": "~1.1.2", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": ">= 1.5.0 < 2", "toidentifier": "1.0.1" } }, "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g=="], + + "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "js-sha256": ["js-sha256@0.9.0", "", {}, "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="], + + "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "local-pkg": ["local-pkg@0.5.1", "", { "dependencies": { "mlly": "^1.7.3", "pkg-types": "^1.2.1" } }, "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ=="], + + "loupe": ["loupe@2.3.7", "", { "dependencies": { "get-func-name": "^2.0.1" } }, "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "near-abi": ["near-abi@0.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.11" } }, "sha512-RVDI8O+KVxRpC3KycJ1bpfVj9Zv+xvq9PlW1yIFl46GhrnLw83/72HqHGjGDjQ8DtltkcpSjY9X3YIGZ+1QyzQ=="], + + "near-api-js": ["near-api-js@2.1.4", "", { "dependencies": { "@near-js/accounts": "0.1.4", "@near-js/crypto": "0.0.5", "@near-js/keystores": "0.0.5", "@near-js/keystores-browser": "0.0.5", "@near-js/keystores-node": "0.0.5", "@near-js/providers": "0.0.7", "@near-js/signers": "0.0.5", "@near-js/transactions": "0.2.1", "@near-js/types": "0.0.4", "@near-js/utils": "0.0.4", "@near-js/wallet-account": "0.0.7", "ajv": "^8.11.2", "ajv-formats": "^2.1.1", "bn.js": "5.2.1", "borsh": "^0.7.0", "depd": "^2.0.0", "error-polyfill": "^0.1.3", "http-errors": "^1.7.2", "near-abi": "0.1.1", "node-fetch": "^2.6.1", "tweetnacl": "^1.0.1" } }, "sha512-e1XicyvJvQMtu7qrG8oWyAdjHJJCoy+cvbW6h2Dky4yj7vC85omQz/x7IgKl71VhzDj2/TGUwjTVESp6NSe75A=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], + + "o3": ["o3@1.0.3", "", { "dependencies": { "capability": "^0.2.5" } }, "sha512-f+4n+vC6s4ysy7YO7O2gslWZBUu8Qj2i2OUJOvjRxQva7jVjYjB29jrr9NCjmxZQR0gzrOcv1RnqoYOeMs5VRQ=="], + + "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "oxlint": ["oxlint@0.9.10", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "0.9.10", "@oxlint/darwin-x64": "0.9.10", "@oxlint/linux-arm64-gnu": "0.9.10", "@oxlint/linux-arm64-musl": "0.9.10", "@oxlint/linux-x64-gnu": "0.9.10", "@oxlint/linux-x64-musl": "0.9.10", "@oxlint/win32-arm64": "0.9.10", "@oxlint/win32-x64": "0.9.10" }, "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-bKiiFN7Hnoaist/rditTRBXz+GXKYuLd53/NB7Q6zHB/bifELJarSoRLkAUGElIJKl4PSr3lTh1g6zehh+rX0g=="], + + "p-limit": ["p-limit@5.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "pathval": ["pathval@1.1.1", "", {}, "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rollup": ["rollup@4.56.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.56.0", "@rollup/rollup-android-arm64": "4.56.0", "@rollup/rollup-darwin-arm64": "4.56.0", "@rollup/rollup-darwin-x64": "4.56.0", "@rollup/rollup-freebsd-arm64": "4.56.0", "@rollup/rollup-freebsd-x64": "4.56.0", "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", "@rollup/rollup-linux-arm-musleabihf": "4.56.0", "@rollup/rollup-linux-arm64-gnu": "4.56.0", "@rollup/rollup-linux-arm64-musl": "4.56.0", "@rollup/rollup-linux-loong64-gnu": "4.56.0", "@rollup/rollup-linux-loong64-musl": "4.56.0", "@rollup/rollup-linux-ppc64-gnu": "4.56.0", "@rollup/rollup-linux-ppc64-musl": "4.56.0", "@rollup/rollup-linux-riscv64-gnu": "4.56.0", "@rollup/rollup-linux-riscv64-musl": "4.56.0", "@rollup/rollup-linux-s390x-gnu": "4.56.0", "@rollup/rollup-linux-x64-gnu": "4.56.0", "@rollup/rollup-linux-x64-musl": "4.56.0", "@rollup/rollup-openbsd-x64": "4.56.0", "@rollup/rollup-openharmony-arm64": "4.56.0", "@rollup/rollup-win32-arm64-msvc": "4.56.0", "@rollup/rollup-win32-ia32-msvc": "4.56.0", "@rollup/rollup-win32-x64-gnu": "4.56.0", "@rollup/rollup-win32-x64-msvc": "4.56.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "sirv": ["sirv@2.0.4", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + + "strip-literal": ["strip-literal@2.1.1", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q=="], + + "text-encoding-utf-8": ["text-encoding-utf-8@1.0.2", "", {}, "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinypool": ["tinypool@0.8.4", "", {}, "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ=="], + + "tinyspy": ["tinyspy@2.2.1", "", {}, "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="], + + "type-detect": ["type-detect@4.1.0", "", {}, "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "u3": ["u3@0.1.1", "", {}, "sha512-+J5D5ir763y+Am/QY6hXNRlwljIeRMZMGs0cT6qqZVVzzT3X3nFPXVyPOFRMOR4kupB0T8JnCdpWdp6Q/iXn3w=="], + + "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + + "vite-node": ["vite-node@1.6.1", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.4", "pathe": "^1.1.1", "picocolors": "^1.0.0", "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA=="], + + "vitest": ["vitest@1.6.1", "", { "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1", "@vitest/snapshot": "1.6.1", "@vitest/spy": "1.6.1", "@vitest/utils": "1.6.1", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", "execa": "^8.0.1", "local-pkg": "^0.5.0", "magic-string": "^0.30.5", "pathe": "^1.1.1", "picocolors": "^1.0.0", "std-env": "^3.5.0", "strip-literal": "^2.0.0", "tinybench": "^2.5.1", "tinypool": "^0.8.3", "vite": "^5.0.0", "vite-node": "1.6.1", "why-is-node-running": "^2.2.2" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", "@vitest/browser": "1.6.1", "@vitest/ui": "1.6.1", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="], + + "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + } +} diff --git a/kms/auth-near/cli.ts b/kms/auth-near/cli.ts new file mode 100644 index 00000000..f72ea7dd --- /dev/null +++ b/kms/auth-near/cli.ts @@ -0,0 +1,603 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +import { connect, keyStores, Near, Account, KeyPair, utils } from 'near-api-js'; +import { parseNearAmount } from 'near-api-js/lib/utils/format'; + +// Helper function to load account from private key +async function getAccount( + accountId: string, + privateKey: string, + networkId: string, + rpcUrl: string +): Promise { + const keyStore = new keyStores.InMemoryKeyStore(); + const keyPair = KeyPair.fromString(privateKey); + keyStore.setKey(networkId, accountId, keyPair); + + const near = await connect({ + networkId, + nodeUrl: rpcUrl, + keyStore, + }); + + return await near.account(accountId); +} + +// Helper function to get app account ID (subaccount of KMS) +function getAppAccountId(appId: string, kmsContractId: string): string { + return `${appId}.${kmsContractId}`; +} + +// Deploy app contract via KMS register_app +async function deployApp( + kmsContractId: string, + appId: string, + ownerId: string, + accountId: string, + privateKey: string, + networkId: string, + rpcUrl: string, + options: { + disableUpgrades?: boolean; + allowAnyDevice?: boolean; + initialDeviceId?: string; + initialComposeHash?: string; + deposit?: string; // in NEAR + } = {} +): Promise { + const account = await getAccount(accountId, privateKey, networkId, rpcUrl); + const appAccountId = getAppAccountId(appId, kmsContractId); + + console.log(`Deploying app contract...`); + console.log(` KMS Contract: ${kmsContractId}`); + console.log(` App ID: ${appId}`); + console.log(` App Account: ${appAccountId}`); + console.log(` Owner: ${ownerId}`); + + const args: any = { + app_id: appId, + owner_id: ownerId, + disable_upgrades: options.disableUpgrades ?? false, + allow_any_device: options.allowAnyDevice ?? false, + }; + + if (options.initialDeviceId) { + args.initial_device_id = options.initialDeviceId; + } else { + args.initial_device_id = null; + } + + if (options.initialComposeHash) { + args.initial_compose_hash = options.initialComposeHash; + } else { + args.initial_compose_hash = null; + } + + const deposit = options.deposit + ? parseNearAmount(options.deposit) + : parseNearAmount('30'); // Default 30 NEAR for account creation + deployment + + if (!deposit) { + throw new Error('Failed to parse deposit amount'); + } + + try { + const result = await account.functionCall({ + contractId: kmsContractId, + methodName: 'register_app', + args, + gas: BigInt('300000000000000'), // 300 TGas + attachedDeposit: BigInt(deposit), + }); + + console.log(`✅ App contract deployed successfully!`); + console.log(` App Account: ${appAccountId}`); + console.log(` Transaction: ${result.transaction.hash}`); + return appAccountId; + } catch (error) { + console.error('❌ Failed to deploy app contract:', error); + throw error; + } +} + +// Add compose hash to app contract +async function addComposeHash( + appAccountId: string, + composeHash: string, + accountId: string, + privateKey: string, + networkId: string, + rpcUrl: string +): Promise { + const account = await getAccount(accountId, privateKey, networkId, rpcUrl); + + // Convert hex hash to string (remove 0x prefix if present) + const hashString = composeHash.startsWith('0x') ? composeHash.slice(2) : composeHash; + + console.log(`Adding compose hash to app contract...`); + console.log(` App Account: ${appAccountId}`); + console.log(` Compose Hash: ${hashString}`); + + try { + const result = await account.functionCall({ + contractId: appAccountId, + methodName: 'add_compose_hash', + args: { compose_hash: hashString }, + gas: BigInt('100000000000000'), // 100 TGas + attachedDeposit: BigInt('1'), // 1 yoctoNEAR + }); + + console.log(`✅ Compose hash added successfully!`); + console.log(` Transaction: ${result.transaction.hash}`); + } catch (error) { + console.error('❌ Failed to add compose hash:', error); + throw error; + } +} + +// Remove compose hash from app contract +async function removeComposeHash( + appAccountId: string, + composeHash: string, + accountId: string, + privateKey: string, + networkId: string, + rpcUrl: string +): Promise { + const account = await getAccount(accountId, privateKey, networkId, rpcUrl); + + // Convert hex hash to string (remove 0x prefix if present) + const hashString = composeHash.startsWith('0x') ? composeHash.slice(2) : composeHash; + + console.log(`Removing compose hash from app contract...`); + console.log(` App Account: ${appAccountId}`); + console.log(` Compose Hash: ${hashString}`); + + try { + const result = await account.functionCall({ + contractId: appAccountId, + methodName: 'remove_compose_hash', + args: { compose_hash: hashString }, + gas: BigInt('100000000000000'), // 100 TGas + attachedDeposit: BigInt('1'), // 1 yoctoNEAR + }); + + console.log(`✅ Compose hash removed successfully!`); + console.log(` Transaction: ${result.transaction.hash}`); + } catch (error) { + console.error('❌ Failed to remove compose hash:', error); + throw error; + } +} + +// Add OS image hash to KMS contract +async function addOsImageHash( + kmsContractId: string, + osImageHash: string, + accountId: string, + privateKey: string, + networkId: string, + rpcUrl: string +): Promise { + const account = await getAccount(accountId, privateKey, networkId, rpcUrl); + + // Convert hex hash to string (remove 0x prefix if present) + const hashString = osImageHash.startsWith('0x') ? osImageHash.slice(2) : osImageHash; + + console.log(`Adding OS image hash to KMS contract...`); + console.log(` KMS Contract: ${kmsContractId}`); + console.log(` OS Image Hash: ${hashString}`); + + try { + const result = await account.functionCall({ + contractId: kmsContractId, + methodName: 'add_os_image_hash', + args: { os_image_hash: hashString }, + gas: BigInt('100000000000000'), // 100 TGas + attachedDeposit: BigInt('1'), // 1 yoctoNEAR + }); + + console.log(`✅ OS image hash added successfully!`); + console.log(` Transaction: ${result.transaction.hash}`); + } catch (error) { + console.error('❌ Failed to add OS image hash:', error); + throw error; + } +} + +// Remove OS image hash from KMS contract +async function removeOsImageHash( + kmsContractId: string, + osImageHash: string, + accountId: string, + privateKey: string, + networkId: string, + rpcUrl: string +): Promise { + const account = await getAccount(accountId, privateKey, networkId, rpcUrl); + + // Convert hex hash to string (remove 0x prefix if present) + const hashString = osImageHash.startsWith('0x') ? osImageHash.slice(2) : osImageHash; + + console.log(`Removing OS image hash from KMS contract...`); + console.log(` KMS Contract: ${kmsContractId}`); + console.log(` OS Image Hash: ${hashString}`); + + try { + const result = await account.functionCall({ + contractId: kmsContractId, + methodName: 'remove_os_image_hash', + args: { os_image_hash: hashString }, + gas: BigInt('100000000000000'), // 100 TGas + attachedDeposit: BigInt('1'), // 1 yoctoNEAR + }); + + console.log(`✅ OS image hash removed successfully!`); + console.log(` Transaction: ${result.transaction.hash}`); + } catch (error) { + console.error('❌ Failed to remove OS image hash:', error); + throw error; + } +} + +// Add KMS device ID to KMS contract +async function addKmsDevice( + kmsContractId: string, + deviceId: string, + accountId: string, + privateKey: string, + networkId: string, + rpcUrl: string +): Promise { + const account = await getAccount(accountId, privateKey, networkId, rpcUrl); + + // Convert hex device ID to string (remove 0x prefix if present) + const deviceIdString = deviceId.startsWith('0x') ? deviceId.slice(2) : deviceId; + + console.log(`Adding KMS device ID to KMS contract...`); + console.log(` KMS Contract: ${kmsContractId}`); + console.log(` Device ID: ${deviceIdString}`); + + try { + const result = await account.functionCall({ + contractId: kmsContractId, + methodName: 'add_kms_device', + args: { device_id: deviceIdString }, + gas: BigInt('100000000000000'), // 100 TGas + attachedDeposit: BigInt('1'), // 1 yoctoNEAR + }); + + console.log(`✅ KMS device ID added successfully!`); + console.log(` Transaction: ${result.transaction.hash}`); + } catch (error) { + console.error('❌ Failed to add KMS device ID:', error); + throw error; + } +} + +// Remove KMS device ID from KMS contract +async function removeKmsDevice( + kmsContractId: string, + deviceId: string, + accountId: string, + privateKey: string, + networkId: string, + rpcUrl: string +): Promise { + const account = await getAccount(accountId, privateKey, networkId, rpcUrl); + + // Convert hex device ID to string (remove 0x prefix if present) + const deviceIdString = deviceId.startsWith('0x') ? deviceId.slice(2) : deviceId; + + console.log(`Removing KMS device ID from KMS contract...`); + console.log(` KMS Contract: ${kmsContractId}`); + console.log(` Device ID: ${deviceIdString}`); + + try { + const result = await account.functionCall({ + contractId: kmsContractId, + methodName: 'remove_kms_device', + args: { device_id: deviceIdString }, + gas: BigInt('100000000000000'), // 100 TGas + attachedDeposit: BigInt('1'), // 1 yoctoNEAR + }); + + console.log(`✅ KMS device ID removed successfully!`); + console.log(` Transaction: ${result.transaction.hash}`); + } catch (error) { + console.error('❌ Failed to remove KMS device ID:', error); + throw error; + } +} + +// Add KMS aggregated MR to KMS contract +async function addKmsAggregatedMr( + kmsContractId: string, + mrAggregated: string, + accountId: string, + privateKey: string, + networkId: string, + rpcUrl: string +): Promise { + const account = await getAccount(accountId, privateKey, networkId, rpcUrl); + + // Convert hex MR to string (remove 0x prefix if present) + const mrString = mrAggregated.startsWith('0x') ? mrAggregated.slice(2) : mrAggregated; + + console.log(`Adding KMS aggregated MR to KMS contract...`); + console.log(` KMS Contract: ${kmsContractId}`); + console.log(` Aggregated MR: ${mrString}`); + + try { + const result = await account.functionCall({ + contractId: kmsContractId, + methodName: 'add_kms_aggregated_mr', + args: { mr_aggregated: mrString }, + gas: BigInt('100000000000000'), // 100 TGas + attachedDeposit: BigInt('1'), // 1 yoctoNEAR + }); + + console.log(`✅ KMS aggregated MR added successfully!`); + console.log(` Transaction: ${result.transaction.hash}`); + } catch (error) { + console.error('❌ Failed to add KMS aggregated MR:', error); + throw error; + } +} + +// Remove KMS aggregated MR from KMS contract +async function removeKmsAggregatedMr( + kmsContractId: string, + mrAggregated: string, + accountId: string, + privateKey: string, + networkId: string, + rpcUrl: string +): Promise { + const account = await getAccount(accountId, privateKey, networkId, rpcUrl); + + // Convert hex MR to string (remove 0x prefix if present) + const mrString = mrAggregated.startsWith('0x') ? mrAggregated.slice(2) : mrAggregated; + + console.log(`Removing KMS aggregated MR from KMS contract...`); + console.log(` KMS Contract: ${kmsContractId}`); + console.log(` Aggregated MR: ${mrString}`); + + try { + const result = await account.functionCall({ + contractId: kmsContractId, + methodName: 'remove_kms_aggregated_mr', + args: { mr_aggregated: mrString }, + gas: BigInt('100000000000000'), // 100 TGas + attachedDeposit: BigInt('1'), // 1 yoctoNEAR + }); + + console.log(`✅ KMS aggregated MR removed successfully!`); + console.log(` Transaction: ${result.transaction.hash}`); + } catch (error) { + console.error('❌ Failed to remove KMS aggregated MR:', error); + throw error; + } +} + +// CLI interface +async function main() { + const args = process.argv.slice(2); + const command = args[0]; + + // Get environment variables + const networkId = process.env.NEAR_NETWORK_ID || process.env.NEAR_ENV || 'testnet'; + const rpcUrl = + process.env.NEAR_RPC_URL || + (networkId === 'mainnet' ? 'https://rpc.near.org' : 'https://rpc.testnet.near.org'); + const kmsContractId = process.env.KMS_CONTRACT_ID || ''; + const accountId = process.env.NEAR_ACCOUNT_ID || ''; + const privateKey = process.env.NEAR_PRIVATE_KEY || ''; + + if (!accountId || !privateKey) { + console.error('❌ NEAR_ACCOUNT_ID and NEAR_PRIVATE_KEY environment variables are required'); + process.exit(1); + } + + if (command === 'deploy') { + // Usage: bun cli.ts deploy [options] + if (args.length < 3) { + console.error('Usage: bun cli.ts deploy [--disable-upgrades] [--allow-any-device] [--device-id ] [--compose-hash ] [--deposit ]'); + process.exit(1); + } + + if (!kmsContractId) { + console.error('❌ KMS_CONTRACT_ID environment variable is required for deployment'); + process.exit(1); + } + + const appId = args[1]; + const ownerId = args[2]; + const options: any = {}; + + // Parse options + for (let i = 3; i < args.length; i++) { + if (args[i] === '--disable-upgrades') { + options.disableUpgrades = true; + } else if (args[i] === '--allow-any-device') { + options.allowAnyDevice = true; + } else if (args[i] === '--device-id' && i + 1 < args.length) { + options.initialDeviceId = args[++i]; + } else if (args[i] === '--compose-hash' && i + 1 < args.length) { + options.initialComposeHash = args[++i]; + } else if (args[i] === '--deposit' && i + 1 < args.length) { + options.deposit = args[++i]; + } + } + + await deployApp( + kmsContractId, + appId, + ownerId, + accountId, + privateKey, + networkId, + rpcUrl, + options + ); + } else if (command === 'add-hash') { + // Usage: bun cli.ts add-hash + if (args.length < 3) { + console.error('Usage: bun cli.ts add-hash '); + console.error(' app_account_id: Full account ID (e.g., app-id.kms-contract.near)'); + console.error(' compose_hash: Hex string (with or without 0x prefix)'); + process.exit(1); + } + + const appAccountId = args[1]; + const composeHash = args[2]; + + await addComposeHash(appAccountId, composeHash, accountId, privateKey, networkId, rpcUrl); + } else if (command === 'remove-hash') { + // Usage: bun cli.ts remove-hash + if (args.length < 3) { + console.error('Usage: bun cli.ts remove-hash '); + process.exit(1); + } + + const appAccountId = args[1]; + const composeHash = args[2]; + + await removeComposeHash(appAccountId, composeHash, accountId, privateKey, networkId, rpcUrl); + } else if (command === 'add-os-image') { + // Usage: bun cli.ts add-os-image + if (args.length < 2) { + console.error('Usage: bun cli.ts add-os-image '); + console.error(' os_image_hash: Hex string (with or without 0x prefix)'); + process.exit(1); + } + + if (!kmsContractId) { + console.error('❌ KMS_CONTRACT_ID environment variable is required'); + process.exit(1); + } + + const osImageHash = args[1]; + await addOsImageHash(kmsContractId, osImageHash, accountId, privateKey, networkId, rpcUrl); + } else if (command === 'remove-os-image') { + // Usage: bun cli.ts remove-os-image + if (args.length < 2) { + console.error('Usage: bun cli.ts remove-os-image '); + console.error(' os_image_hash: Hex string (with or without 0x prefix)'); + process.exit(1); + } + + if (!kmsContractId) { + console.error('❌ KMS_CONTRACT_ID environment variable is required'); + process.exit(1); + } + + const osImageHash = args[1]; + await removeOsImageHash(kmsContractId, osImageHash, accountId, privateKey, networkId, rpcUrl); + } else if (command === 'add-device') { + // Usage: bun cli.ts add-device + if (args.length < 2) { + console.error('Usage: bun cli.ts add-device '); + console.error(' device_id: Hex string (with or without 0x prefix)'); + process.exit(1); + } + + if (!kmsContractId) { + console.error('❌ KMS_CONTRACT_ID environment variable is required'); + process.exit(1); + } + + const deviceId = args[1]; + await addKmsDevice(kmsContractId, deviceId, accountId, privateKey, networkId, rpcUrl); + } else if (command === 'remove-device') { + // Usage: bun cli.ts remove-device + if (args.length < 2) { + console.error('Usage: bun cli.ts remove-device '); + console.error(' device_id: Hex string (with or without 0x prefix)'); + process.exit(1); + } + + if (!kmsContractId) { + console.error('❌ KMS_CONTRACT_ID environment variable is required'); + process.exit(1); + } + + const deviceId = args[1]; + await removeKmsDevice(kmsContractId, deviceId, accountId, privateKey, networkId, rpcUrl); + } else if (command === 'add-mr') { + // Usage: bun cli.ts add-mr + if (args.length < 2) { + console.error('Usage: bun cli.ts add-mr '); + console.error(' mr_aggregated: Hex string (with or without 0x prefix)'); + process.exit(1); + } + + if (!kmsContractId) { + console.error('❌ KMS_CONTRACT_ID environment variable is required'); + process.exit(1); + } + + const mrAggregated = args[1]; + await addKmsAggregatedMr(kmsContractId, mrAggregated, accountId, privateKey, networkId, rpcUrl); + } else if (command === 'remove-mr') { + // Usage: bun cli.ts remove-mr + if (args.length < 2) { + console.error('Usage: bun cli.ts remove-mr '); + console.error(' mr_aggregated: Hex string (with or without 0x prefix)'); + process.exit(1); + } + + if (!kmsContractId) { + console.error('❌ KMS_CONTRACT_ID environment variable is required'); + process.exit(1); + } + + const mrAggregated = args[1]; + await removeKmsAggregatedMr(kmsContractId, mrAggregated, accountId, privateKey, networkId, rpcUrl); + } else { + console.error('Unknown command:', command); + console.error(''); + console.error('Available commands:'); + console.error(' deploy [options] - Deploy app contract via KMS'); + console.error(' add-hash - Add compose hash to app contract'); + console.error(' remove-hash - Remove compose hash from app contract'); + console.error(' add-os-image - Add OS image hash to KMS contract'); + console.error(' remove-os-image - Remove OS image hash from KMS contract'); + console.error(' add-device - Add KMS device ID to KMS contract'); + console.error(' remove-device - Remove KMS device ID from KMS contract'); + console.error(' add-mr - Add KMS aggregated MR to KMS contract'); + console.error(' remove-mr - Remove KMS aggregated MR from KMS contract'); + console.error(''); + console.error('Environment variables:'); + console.error(' NEAR_ACCOUNT_ID - NEAR account ID (required)'); + console.error(' NEAR_PRIVATE_KEY - NEAR account private key (required)'); + console.error(' KMS_CONTRACT_ID - KMS contract ID (required for KMS operations)'); + console.error(' NEAR_NETWORK_ID - Network ID (testnet/mainnet, default: testnet)'); + console.error(' NEAR_RPC_URL - NEAR RPC URL (optional, auto-detected by network)'); + process.exit(1); + } +} + +// Run CLI if executed directly (Bun supports import.meta.main) +if (import.meta.main) { + main().catch((error) => { + console.error('Fatal error:', error); + process.exit(1); + }); +} + +export { + deployApp, + addComposeHash, + removeComposeHash, + getAppAccountId, + addOsImageHash, + removeOsImageHash, + addKmsDevice, + removeKmsDevice, + addKmsAggregatedMr, + removeKmsAggregatedMr, +}; + diff --git a/kms/auth-near/index.test.ts b/kms/auth-near/index.test.ts new file mode 100644 index 00000000..856a5d94 --- /dev/null +++ b/kms/auth-near/index.test.ts @@ -0,0 +1,182 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest'; + +// Mock near-api-js +const mockViewFunction = vi.fn(); +const mockAccount = { + viewFunction: mockViewFunction, +}; + +const mockConnect = vi.fn(() => ({ + account: vi.fn(() => Promise.resolve(mockAccount)), + config: { networkId: 'testnet' }, +})); + +vi.mock('near-api-js', () => ({ + connect: mockConnect, + keyStores: { + InMemoryKeyStore: vi.fn(), + }, +})); + +// Dynamic import after mocking +let appFetch: any; + +beforeAll(async () => { + // Set environment variables for testing + process.env.NEAR_RPC_URL = 'https://rpc.testnet.fastnear.com'; + process.env.NEAR_NETWORK_ID = 'testnet'; + process.env.KMS_CONTRACT_ID = 'kms.testnet'; + process.env.PORT = '3002'; + + // Import the app after mocking + const indexModule = await import('./index.ts'); + appFetch = indexModule.default.fetch; + + // Wait for NEAR initialization + await new Promise(resolve => setTimeout(resolve, 100)); +}); + +beforeEach(() => { + // Reset mocks before each test + vi.clearAllMocks(); +}); + +describe('auth-near API Tests', () => { + describe('GET /', () => { + it('should return system info', async () => { + // Mock contract calls + mockViewFunction.mockImplementation((params) => { + if (params.methodName === 'get_gateway_app_id') { + return Promise.resolve('gateway.testnet'); + } + return Promise.resolve(''); + }); + + const response = await appFetch(new Request('http://localhost:3002/')); + expect(response.status).toBe(200); + + const data = await response.json(); + expect(data.status).toBe('ok'); + expect(data.kmsContractAddr).toBe('kms.testnet'); + }); + }); + + describe('POST /bootAuth/app', () => { + it('should validate app boot request', async () => { + const bootInfo = { + mrAggregated: '0x' + 'a'.repeat(64), + osImageHash: '0x' + 'b'.repeat(64), + appId: '0x' + 'c'.repeat(40), + composeHash: '0x' + 'd'.repeat(64), + instanceId: '0x' + 'e'.repeat(40), + deviceId: '0x' + 'f'.repeat(64), + tcbStatus: 'UpToDate', + advisoryIds: [], + mrSystem: '0x' + '1'.repeat(64), + }; + + // Mock contract calls + mockViewFunction.mockImplementation((params) => { + if (params.methodName === 'is_app_registered') { + return Promise.resolve(true); + } + if (params.methodName === 'is_os_image_allowed') { + return Promise.resolve(true); + } + if (params.methodName === 'is_app_allowed') { + return Promise.resolve([true, '']); + } + if (params.methodName === 'get_gateway_app_id') { + return Promise.resolve('gateway.testnet'); + } + return Promise.resolve(''); + }); + + const response = await appFetch( + new Request('http://localhost:3002/bootAuth/app', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(bootInfo), + }) + ); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data.isAllowed).toBe(true); + expect(data.gatewayAppId).toBe('gateway.testnet'); + }); + + it('should reject unregistered app', async () => { + const bootInfo = { + mrAggregated: '0x' + 'a'.repeat(64), + osImageHash: '0x' + 'b'.repeat(64), + appId: '0x' + 'c'.repeat(40), + composeHash: '0x' + 'd'.repeat(64), + instanceId: '0x' + 'e'.repeat(40), + deviceId: '0x' + 'f'.repeat(64), + }; + + mockViewFunction.mockImplementation((params) => { + if (params.methodName === 'is_app_registered') { + return Promise.resolve(false); + } + return Promise.resolve(''); + }); + + const response = await appFetch( + new Request('http://localhost:3002/bootAuth/app', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(bootInfo), + }) + ); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data.isAllowed).toBe(false); + expect(data.reason).toContain('not registered'); + }); + }); + + describe('POST /bootAuth/kms', () => { + it('should validate KMS boot request', async () => { + const bootInfo = { + mrAggregated: '0x' + 'a'.repeat(64), + osImageHash: '0x' + 'b'.repeat(64), + appId: '0x' + 'c'.repeat(40), + composeHash: '0x' + 'd'.repeat(64), + instanceId: '0x' + 'e'.repeat(40), + deviceId: '0x' + 'f'.repeat(64), + tcbStatus: 'UpToDate', + }; + + mockViewFunction.mockImplementation((params) => { + if (params.methodName === 'is_kms_allowed') { + return Promise.resolve([true, '']); + } + if (params.methodName === 'get_gateway_app_id') { + return Promise.resolve('gateway.testnet'); + } + return Promise.resolve(''); + }); + + const response = await appFetch( + new Request('http://localhost:3002/bootAuth/kms', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(bootInfo), + }) + ); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data.isAllowed).toBe(true); + }); + }); +}); + + diff --git a/kms/auth-near/index.ts b/kms/auth-near/index.ts new file mode 100644 index 00000000..0294bfc0 --- /dev/null +++ b/kms/auth-near/index.ts @@ -0,0 +1,303 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +import { Hono } from 'hono'; +import { zValidator } from '@hono/zod-validator'; +import { z } from 'zod'; +import { connect, keyStores, Near, Contract, Account } from 'near-api-js'; + +// zod schemas for validation - compatible with original fastify implementation +const BootInfoSchema = z.object({ + // required fields (matching original fastify schema) + mrAggregated: z.string().describe('aggregated MR measurement'), + osImageHash: z.string().describe('OS Image hash'), + appId: z.string().describe('application ID'), + composeHash: z.string().describe('compose hash'), + instanceId: z.string().describe('instance ID'), + deviceId: z.string().describe('device ID'), + // optional fields (for full compatibility with BootInfo interface) + tcbStatus: z.string().optional().default(''), + advisoryIds: z.array(z.string()).optional().default([]), + mrSystem: z.string().optional().default('') +}); + +const BootResponseSchema = z.object({ + isAllowed: z.boolean(), + reason: z.string(), + gatewayAppId: z.string() +}); + +type BootInfo = z.infer; +type BootResponse = z.infer; + +// NEAR backend class +class NearBackend { + private near: Near; + private kmsContractId: string; + private account: Account | null; + + constructor(near: Near, kmsContractId: string, account: Account | null) { + this.near = near; + this.kmsContractId = kmsContractId; + this.account = account; + } + + private ensureAccount(): Account { + if (!this.account) { + throw new Error('NEAR account not initialized'); + } + return this.account; + } + + private hexToAccountId(hex: string): string { + // Remove '0x' prefix if present + hex = hex.startsWith('0x') ? hex.slice(2) : hex; + // For NEAR, we'll use the hex string as-is or convert to a valid account ID format + // Since NEAR account IDs are strings, we'll use the hex as a subaccount or convert + // For now, we'll assume the hex represents a valid account ID or use it directly + // In production, you might want to map hex addresses to NEAR account IDs + return hex; + } + + async checkBoot(bootInfo: BootInfo, isKms: boolean): Promise { + // Create boot info struct for NEAR contract call + const bootInfoStruct = { + app_id: this.hexToAccountId(bootInfo.appId), + compose_hash: bootInfo.composeHash, + instance_id: this.hexToAccountId(bootInfo.instanceId), + device_id: bootInfo.deviceId, + mr_aggregated: bootInfo.mrAggregated, + mr_system: bootInfo.mrSystem || '', + os_image_hash: bootInfo.osImageHash, + tcb_status: bootInfo.tcbStatus || '', + advisory_ids: bootInfo.advisoryIds || [] + }; + + const account = this.ensureAccount(); + let response: [boolean, string]; + if (isKms) { + // Call is_kms_allowed on KMS contract + response = await account.viewFunction({ + contractId: this.kmsContractId, + methodName: 'is_kms_allowed', + args: bootInfoStruct + }); + } else { + // For app boot, follow the same flow as Ethereum contract: + // 1. Check if app is registered in KMS contract + const isRegistered = await account.viewFunction({ + contractId: this.kmsContractId, + methodName: 'is_app_registered', + args: { app_id: bootInfoStruct.app_id } + }); + + if (!isRegistered) { + return { + isAllowed: false, + reason: 'App not registered', + gatewayAppId: '' + }; + } + + // 2. Check if OS image is allowed in KMS contract + const isOsImageAllowed = await account.viewFunction({ + contractId: this.kmsContractId, + methodName: 'is_os_image_allowed', + args: { os_image_hash: bootInfoStruct.os_image_hash } + }); + + if (!isOsImageAllowed) { + return { + isAllowed: false, + reason: 'OS image is not allowed', + gatewayAppId: '' + }; + } + + // 3. Call is_app_allowed on the app contract + // The app_id in bootInfo is the app contract account ID + response = await account.viewFunction({ + contractId: bootInfoStruct.app_id, + methodName: 'is_app_allowed', + args: bootInfoStruct + }); + } + + const [isAllowed, reason] = response; + + // Get gateway app ID from KMS contract + const gatewayAppId = await account.viewFunction({ + contractId: this.kmsContractId, + methodName: 'get_gateway_app_id', + args: {} + }); + + return { + isAllowed, + reason: reason || '', + gatewayAppId: gatewayAppId || '' + }; + } + + async getGatewayAppId(): Promise { + try { + const account = this.ensureAccount(); + const result = await account.viewFunction({ + contractId: this.kmsContractId, + methodName: 'get_gateway_app_id', + args: {} + }); + return result || ''; + } catch (error) { + console.error('Error getting gateway app ID:', error); + return ''; + } + } + + async getNetworkId(): Promise { + return this.near.config.networkId; + } + + async getAppImplementation(): Promise { + // NEAR doesn't have appImplementation like Ethereum + // Return empty string for compatibility + return ''; + } +} + +// Initialize app +const app = new Hono(); + +// Initialize NEAR connection +const rpcUrl = process.env.NEAR_RPC_URL || 'https://free.rpc.fastnear.com'; +const networkId = process.env.NEAR_NETWORK_ID || 'mainnet'; +const kmsContractId = process.env.KMS_CONTRACT_ID || ''; + +if (!kmsContractId) { + console.error('KMS_CONTRACT_ID environment variable is required'); + process.exit(1); +} + +const keyStore = new keyStores.InMemoryKeyStore(); +const nearConfig = { + networkId, + nodeUrl: rpcUrl, + keyStore, + headers: {} +}; + +// Initialize NEAR connection +let nearBackend: NearBackend | null = null; + +// Initialize NEAR connection asynchronously +(async () => { + try { + const near = await connect(nearConfig); + // For view calls, we can use any account ID - it doesn't need to exist or be controlled by us + // Using the contract ID itself as the account for view calls (no private key needed) + const account = await near.account(kmsContractId); + nearBackend = new NearBackend(near, kmsContractId, account); + console.log(`NEAR backend initialized: network=${networkId}, contract=${kmsContractId}`); + } catch (error) { + console.error('Failed to initialize NEAR connection:', error); + // Don't exit - let the health check handle it + } +})(); + +// Health check and info endpoint +app.get('/', async (c) => { + try { + if (!nearBackend) { + return c.json({ + status: 'error', + message: 'NEAR backend not initialized yet' + }, 503); + } + + const batch = await Promise.all([ + nearBackend.getGatewayAppId(), + nearBackend.getNetworkId(), + nearBackend.getAppImplementation(), + ]); + + return c.json({ + status: 'ok', + kmsContractAddr: kmsContractId, + gatewayAppId: batch[0], + chainId: batch[1], // Using network ID as chain identifier + appAuthImplementation: batch[2], // NOTE: for backward compatibility + appImplementation: batch[2], + }); + } catch (error) { + console.error('error in health check:', error); + return c.json({ + status: 'error', + message: error instanceof Error ? error.message : String(error) + }, 500); + } +}); + +// app boot authentication +app.post('/bootAuth/app', + zValidator('json', BootInfoSchema), + async (c) => { + try { + if (!nearBackend) { + return c.json({ + isAllowed: false, + gatewayAppId: '', + reason: 'NEAR backend not initialized' + }, 503); + } + + const bootInfo = c.req.valid('json'); + const result = await nearBackend.checkBoot(bootInfo, false); + return c.json(result); + } catch (error) { + console.error('error in app boot auth:', error); + return c.json({ + isAllowed: false, + gatewayAppId: '', + reason: error instanceof Error ? error.message : String(error) + }); + } + } +); + +// KMS boot authentication +app.post('/bootAuth/kms', + zValidator('json', BootInfoSchema), + async (c) => { + try { + if (!nearBackend) { + return c.json({ + isAllowed: false, + gatewayAppId: '', + reason: 'NEAR backend not initialized' + }, 503); + } + + const bootInfo = c.req.valid('json'); + const result = await nearBackend.checkBoot(bootInfo, true); + return c.json(result); + } catch (error) { + console.error('error in KMS boot auth:', error); + return c.json({ + isAllowed: false, + gatewayAppId: '', + reason: error instanceof Error ? error.message : String(error) + }); + } + } +); + +// start server +const port = parseInt(process.env.PORT || '3000'); +console.log(`starting NEAR auth server on port ${port}`); + +export default { + port, + fetch: app.fetch, +}; + diff --git a/kms/auth-near/openapi.json b/kms/auth-near/openapi.json new file mode 100644 index 00000000..87e667cc --- /dev/null +++ b/kms/auth-near/openapi.json @@ -0,0 +1,234 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "dstack KMS NEAR Protocol Backend API", + "description": "API for dstack KMS NEAR Protocol backend authentication and system information", + "version": "1.0.0" + }, + "servers": [ + { + "url": "http://localhost:3000", + "description": "Development server" + } + ], + "paths": { + "/": { + "get": { + "summary": "Health check and system information", + "description": "Returns system status and configuration information", + "operationId": "getSystemInfo", + "responses": { + "200": { + "description": "System information", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SystemInfo" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/bootAuth/app": { + "post": { + "summary": "Application boot authentication", + "description": "Validates application boot information against NEAR smart contracts", + "operationId": "authenticateAppBoot", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BootInfo" + } + } + } + }, + "responses": { + "200": { + "description": "Authentication result", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BootResponse" + } + } + } + } + } + } + }, + "/bootAuth/kms": { + "post": { + "summary": "KMS boot authentication", + "description": "Validates KMS boot information against NEAR smart contracts", + "operationId": "authenticateKmsBoot", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BootInfo" + } + } + } + }, + "responses": { + "200": { + "description": "Authentication result", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BootResponse" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "BootInfo": { + "type": "object", + "required": [ + "mrAggregated", + "osImageHash", + "appId", + "composeHash", + "instanceId", + "deviceId" + ], + "properties": { + "mrAggregated": { + "type": "string", + "description": "Aggregated MR measurement (hex string)" + }, + "osImageHash": { + "type": "string", + "description": "OS Image hash (hex string)" + }, + "appId": { + "type": "string", + "description": "Application ID (hex string, converted to NEAR AccountId)" + }, + "composeHash": { + "type": "string", + "description": "Compose hash (hex string)" + }, + "instanceId": { + "type": "string", + "description": "Instance ID (hex string, converted to NEAR AccountId)" + }, + "deviceId": { + "type": "string", + "description": "Device ID (hex string)" + }, + "tcbStatus": { + "type": "string", + "description": "TCB status (e.g., 'UpToDate')", + "default": "" + }, + "advisoryIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Security advisory IDs", + "default": [] + }, + "mrSystem": { + "type": "string", + "description": "System MR measurement (hex string)", + "default": "" + } + } + }, + "BootResponse": { + "type": "object", + "required": [ + "isAllowed", + "reason", + "gatewayAppId" + ], + "properties": { + "isAllowed": { + "type": "boolean", + "description": "Whether the boot is allowed" + }, + "reason": { + "type": "string", + "description": "Reason for allow/deny decision" + }, + "gatewayAppId": { + "type": "string", + "description": "Gateway app ID (NEAR AccountId)" + } + } + }, + "SystemInfo": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Service status", + "example": "ok" + }, + "kmsContractAddr": { + "type": "string", + "description": "NEAR KMS contract account ID", + "example": "kms.dstack.near" + }, + "gatewayAppId": { + "type": "string", + "description": "Gateway app ID", + "example": "gateway.dstack.near" + }, + "chainId": { + "type": "string", + "description": "NEAR network ID", + "example": "mainnet" + }, + "appAuthImplementation": { + "type": "string", + "description": "App auth implementation (backward compatibility)", + "example": "" + }, + "appImplementation": { + "type": "string", + "description": "App implementation", + "example": "" + } + } + }, + "Error": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "error" + }, + "message": { + "type": "string", + "description": "Error message" + } + } + } + } + } +} + + diff --git a/kms/auth-near/package.json b/kms/auth-near/package.json new file mode 100644 index 00000000..cf2edf59 --- /dev/null +++ b/kms/auth-near/package.json @@ -0,0 +1,44 @@ +{ + "name": "auth-near", + "version": "1.0.0", + "description": "dstack KMS NEAR Protocol backend with bun + hono + zod", + "main": "index.ts", + "scripts": { + "dev": "bun run --watch index.ts", + "start": "bun run index.ts", + "build": "bun build index.ts --outdir ./dist --target bun", + "test": "vitest", + "test:run": "vitest run", + "lint": "oxlint .", + "lint:fix": "oxlint --fix .", + "fmt": "oxlint --fix .", + "check": "bun run lint && bun run test:run", + "app:deploy": "bun cli.ts deploy", + "app:add-hash": "bun cli.ts add-hash", + "app:remove-hash": "bun cli.ts remove-hash", + "kms:add-os-image": "bun cli.ts add-os-image", + "kms:remove-os-image": "bun cli.ts remove-os-image", + "kms:add-device": "bun cli.ts add-device", + "kms:remove-device": "bun cli.ts remove-device", + "kms:add-mr": "bun cli.ts add-mr", + "kms:remove-mr": "bun cli.ts remove-mr" + }, + "dependencies": { + "hono": "4.11.4", + "@hono/zod-validator": "0.2.2", + "zod": "3.25.76", + "near-api-js": "^2.1.4" + }, + "devDependencies": { + "@types/bun": "1.2.18", + "@types/node": "^24.0.14", + "@vitest/ui": "1.6.1", + "openapi-types": "12.1.3", + "oxlint": "0.9.10", + "typescript": "5.8.3", + "vitest": "1.6.1" + }, + "type": "module" +} + + diff --git a/kms/auth-near/vitest.config.ts b/kms/auth-near/vitest.config.ts new file mode 100644 index 00000000..45a60a6e --- /dev/null +++ b/kms/auth-near/vitest.config.ts @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, +}); + + diff --git a/kms/dstack-app/docker-compose.yaml b/kms/dstack-app/docker-compose.yaml index 3d8a18f5..7ae60829 100644 --- a/kms/dstack-app/docker-compose.yaml +++ b/kms/dstack-app/docker-compose.yaml @@ -27,7 +27,7 @@ services: "--execution-rpc", "https://ethereum-rpc.publicnode.com" ] - # Auth API is a webhook server that authenticates KMS instances and Apps launches + # Auth API is a webhook server that authenticates KMS instances and Apps launches (Ethereum) auth-api: build: context: . @@ -52,6 +52,29 @@ services: depends_on: - helios + # Auth NEAR is a webhook server that authenticates KMS instances and Apps launches (NEAR Protocol) + auth-near: + build: + context: . + dockerfile_inline: | + FROM oven/bun:1.1.0-alpine + WORKDIR /app + + RUN apk add --no-cache git + RUN git clone https://github.com/Dstack-TEE/dstack.git && \ + cd dstack && \ + git checkout 78057c975fe4b9e21f557fb888d72eeecfb21178 + WORKDIR /app/dstack/kms/auth-near + RUN bun install + CMD bun run index.ts + environment: + - PORT=3000 + - NEAR_RPC_URL=${NEAR_RPC_URL:-https://free.rpc.fastnear.com} + - NEAR_NETWORK_ID=${NEAR_NETWORK_ID:-mainnet} + - KMS_CONTRACT_ID=${KMS_CONTRACT_ID} + ports: + - 3000:3000 + # KMS handles the TEE Remote Attestation kms: build: diff --git a/kms/kms.toml b/kms/kms.toml index 1f354066..dfc51841 100644 --- a/kms/kms.toml +++ b/kms/kms.toml @@ -42,6 +42,21 @@ url = "http://auth-api:8000" [core.auth_api.dev] gateway_app_id = "any" +# NEAR Protocol authentication (alternative to webhook) +# [core.auth_api] +# type = "near" +# +# [core.auth_api.near] +# url = "http://auth-near:3000" +# rpc_url = "https://free.rpc.fastnear.com" +# network_id = "mainnet" +# contract_id = "kms.dstack.near" +# mpc_contract_id = "v1.signer.testnet" # MPC contract ID (required for MPC key derivation) +# mpc_domain_id = 2 # MPC domain ID for BLS12-381 (default: 2) +# # Note: MPC public key is automatically fetched from the contract +# # Note: NEAR signer is automatically generated as an implicit account (64 hex chars) on startup +# # The signer account ID is available via GetMeta RPC method + [core.onboard] enabled = true auto_bootstrap_domain = "" diff --git a/kms/rpc/proto/kms_rpc.proto b/kms/rpc/proto/kms_rpc.proto index b927c283..e933a83c 100644 --- a/kms/rpc/proto/kms_rpc.proto +++ b/kms/rpc/proto/kms_rpc.proto @@ -58,6 +58,7 @@ message GetMetaResponse { optional string kms_contract_address = 7; optional uint64 chain_id = 8; optional string app_auth_implementation = 9; + optional string near_signer_account_id = 10; // NEAR implicit account ID for MPC key derivation } message GetKmsKeyRequest { diff --git a/kms/src/ckd.rs b/kms/src/ckd.rs new file mode 100644 index 00000000..cb538916 --- /dev/null +++ b/kms/src/ckd.rs @@ -0,0 +1,259 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! NEAR MPC Chain Key Derivation (CKD) for deterministic root key generation +//! +//! This module implements deterministic root key generation using NEAR MPC network. +//! The root key is derived deterministically and can only be generated inside a verified TEE. +//! +//! Flow: +//! 1. Generate ephemeral BLS12-381 G1 keypair +//! 2. Call NEAR KMS contract's request_kms_root_key() with attestation +//! 3. KMS contract verifies attestation and calls MPC contract +//! 4. MPC contract returns encrypted response (big_y, big_c) +//! 5. Decrypt and verify the response +//! 6. Derive final 32-byte key using HKDF +//! 7. Convert to CA and K256 keys + +use anyhow::{Context, Result}; +use blstrs::{G1Affine, G1Projective, G2Affine, G2Projective, Scalar}; +use elliptic_curve::{group::prime::PrimeCurveAffine as _, Field as _, Group as _}; +use hkdf::Hkdf; +use rand_core::OsRng; +use serde::Deserialize; +use sha2::Sha256; +use sha3::{Digest, Sha3_256}; + +// Constants matching NEAR MPC contract +const BLS12381G1_PUBLIC_KEY_SIZE: usize = 48; +const NEAR_CKD_DOMAIN: &[u8] = b"NEAR BLS12381G1_XMD:SHA-256_SSWU_RO_"; +const OUTPUT_SECRET_SIZE: usize = 32; +const APP_ID_DERIVATION_PREFIX: &str = "near-mpc v0.1.0 app_id derivation:"; +const KMS_ROOT_KEY_DERIVATION_PATH: &str = "kms-root-key"; + +#[derive(Clone, Debug, Deserialize)] +pub struct Bls12381G1PublicKey(String); + +/// MPC CKD response (big_y, big_c from MPC network) +#[derive(Clone, Debug, Deserialize)] +pub struct CkdResponse { + pub big_y: Bls12381G1PublicKey, + pub big_c: Bls12381G1PublicKey, +} + +/// Derive app_id the same way MPC contract does +/// app_id = SHA3-256("{prefix}{account_id},{derivation_path}") +fn derive_app_id(account_id: &str, derivation_path: &str) -> [u8; 32] { + let derivation_string = format!( + "{}{},{}", + APP_ID_DERIVATION_PREFIX, account_id, derivation_path + ); + let mut hasher = Sha3_256::new(); + hasher.update(derivation_string.as_bytes()); + hasher.finalize().into() +} + +/// Generate ephemeral BLS12-381 G1 keypair +pub fn generate_ephemeral_keypair() -> (Scalar, G1Projective) { + let mut rng = OsRng; + let private_key = Scalar::random(&mut rng); + let public_key = G1Projective::generator() * private_key; + (private_key, public_key) +} + +/// Convert G1 point to NEAR format (bls12381g1:base58...) +pub fn g1_to_near_format(point: G1Projective) -> Result { + let compressed = point.to_compressed(); + let base58 = bs58::encode(&compressed).into_string(); + Ok(format!("bls12381g1:{}", base58)) +} + +/// Parse NEAR format to G1 point +pub fn near_format_to_g1(s: &str) -> Result { + let base58_part = s + .strip_prefix("bls12381g1:") + .context("Invalid BLS12-381 G1 format - missing prefix")?; + + let bytes = bs58::decode(base58_part) + .into_vec() + .context("Invalid base58 encoding")?; + + if bytes.len() != 48 { + anyhow::bail!( + "Invalid G1 point length: expected 48 bytes, got {}", + bytes.len() + ); + } + + let mut compressed = [0u8; 48]; + compressed.copy_from_slice(&bytes[..48]); + + G1Projective::from_compressed(&compressed) + .into_option() + .context("Invalid G1 point - not on curve") +} + +/// Parse NEAR format to G2 point +pub fn near_format_to_g2(s: &str) -> Result { + let base58_part = s + .strip_prefix("bls12381g2:") + .context("Invalid BLS12-381 G2 format - missing prefix")?; + + let bytes = bs58::decode(base58_part) + .into_vec() + .context("Invalid base58 encoding")?; + + if bytes.len() != 96 { + anyhow::bail!( + "Invalid G2 point length: expected 96 bytes, got {}", + bytes.len() + ); + } + + let mut compressed = [0u8; 96]; + compressed.copy_from_slice(&bytes[..96]); + + G2Projective::from_compressed(&compressed) + .into_option() + .context("Invalid G2 point - not on curve") +} + +/// Decrypt MPC response and verify signature +pub fn decrypt_and_verify_mpc_response( + big_y: &Bls12381G1PublicKey, + big_c: &Bls12381G1PublicKey, + ephemeral_private_key: Scalar, + mpc_public_key: &str, + app_id: &[u8], +) -> Result<[u8; BLS12381G1_PUBLIC_KEY_SIZE]> { + // Parse G1 points + let big_y_point = near_format_to_g1(&big_y.0)?; + let big_c_point = near_format_to_g1(&big_c.0)?; + + // Parse MPC public key (G2) + let mpc_pk = near_format_to_g2(mpc_public_key)?; + + // Decrypt the secret: secret = big_c - big_y * private_key + let secret = big_c_point - big_y_point * ephemeral_private_key; + + // Verify the signature using pairing + if !verify_mpc_signature(&mpc_pk, app_id, &secret) { + anyhow::bail!("MPC signature verification failed"); + } + + // Return secret as compressed bytes + Ok(secret.to_compressed()) +} + +/// Verify MPC signature using BLS pairing +fn verify_mpc_signature( + public_key: &G2Projective, + app_id: &[u8], + signature: &G1Projective, +) -> bool { + let element1: G1Affine = signature.into(); + if (!element1.is_on_curve() | !element1.is_torsion_free() | element1.is_identity()).into() { + return false; + } + + let element2: G2Affine = public_key.into(); + if (!element2.is_on_curve() | !element2.is_torsion_free() | element2.is_identity()).into() { + return false; + } + + // Hash input = MPC public key || app_id (must match MPC contract) + let hash_input = [public_key.to_compressed().as_slice(), app_id].concat(); + let base1 = G1Projective::hash_to_curve(&hash_input, NEAR_CKD_DOMAIN, &[]).into(); + let base2 = G2Affine::generator(); + + // Verify pairing equation: e(H(mpk||app_id), mpk) == e(signature, G2) + blstrs::pairing(&base1, &element2) == blstrs::pairing(&element1, &base2) +} + +/// Derive final 32-byte key using HKDF +pub fn derive_final_key( + ikm: [u8; BLS12381G1_PUBLIC_KEY_SIZE], + info: &[u8], +) -> Result<[u8; OUTPUT_SECRET_SIZE]> { + let hk = Hkdf::::new(None, &ikm); + let mut okm = [0u8; OUTPUT_SECRET_SIZE]; + hk.expand(info, &mut okm) + .map_err(|e| anyhow::anyhow!("HKDF expansion failed: {}", e))?; + Ok(okm) +} + +/// Derive root keys from MPC response +/// +/// This function: +/// 1. Decrypts the MPC response using the ephemeral private key +/// 2. Verifies the MPC signature +/// 3. Derives the final 32-byte key using HKDF +/// 4. Returns the key that can be used as K256 signing key +pub fn derive_root_key_from_mpc( + mpc_response: &CkdResponse, + ephemeral_private_key: Scalar, + mpc_public_key: &str, + kms_account_id: &str, +) -> Result<[u8; OUTPUT_SECRET_SIZE]> { + // Derive app_id (must match MPC contract derivation) + let app_id = derive_app_id(kms_account_id, KMS_ROOT_KEY_DERIVATION_PATH); + + // Decrypt and verify MPC response + let secret_bytes = decrypt_and_verify_mpc_response( + &mpc_response.big_y, + &mpc_response.big_c, + ephemeral_private_key, + mpc_public_key, + &app_id, + )?; + + // Derive final 32-byte key using HKDF + let final_key = derive_final_key(secret_bytes, b"")?; + + Ok(final_key) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_derive_app_id() { + let app_id = derive_app_id("kms.testnet", "kms-root-key"); + assert_eq!(app_id.len(), 32); + + // Same inputs should produce same app_id + let app_id2 = derive_app_id("kms.testnet", "kms-root-key"); + assert_eq!(app_id, app_id2); + + // Different inputs should produce different app_id + let app_id3 = derive_app_id("kms.testnet", "different-path"); + assert_ne!(app_id, app_id3); + } + + #[test] + fn test_g1_format_conversion() { + let (_, public_key) = generate_ephemeral_keypair(); + let near_format = g1_to_near_format(public_key).unwrap(); + assert!(near_format.starts_with("bls12381g1:")); + + let parsed = near_format_to_g1(&near_format).unwrap(); + assert_eq!(parsed.to_compressed(), public_key.to_compressed()); + } + + #[test] + fn test_derive_final_key() { + let ikm = [0u8; 48]; + let key = derive_final_key(ikm, b"").unwrap(); + assert_eq!(key.len(), 32); + + // Same input should produce same output + let key2 = derive_final_key(ikm, b"").unwrap(); + assert_eq!(key, key2); + + // Different info should produce different output + let key3 = derive_final_key(ikm, b"different").unwrap(); + assert_ne!(key, key3); + } +} diff --git a/kms/src/config.rs b/kms/src/config.rs index 36874e1b..d9e5bb45 100644 --- a/kms/src/config.rs +++ b/kms/src/config.rs @@ -97,6 +97,8 @@ pub(crate) enum AuthApi { Dev { dev: Dev }, #[serde(rename = "webhook")] Webhook { webhook: Webhook }, + #[serde(rename = "near")] + Near { near: Near }, } impl AuthApi { @@ -115,6 +117,28 @@ pub(crate) struct Dev { pub gateway_app_id: String, } +#[derive(Debug, Clone, Deserialize)] +pub(crate) struct Near { + /// URL to the auth-near webhook service + pub url: String, + /// NEAR RPC URL (optional, for info/metadata) + pub rpc_url: Option, + /// NEAR network ID (optional, for info/metadata) + pub network_id: Option, + /// NEAR KMS contract ID (for info/metadata) + pub contract_id: String, + /// MPC contract ID (for key derivation, optional) + #[serde(default)] + pub mpc_contract_id: Option, + /// MPC domain ID (for key derivation, optional, default: 2) + #[serde(default = "default_mpc_domain_id")] + pub mpc_domain_id: u64, +} + +fn default_mpc_domain_id() -> u64 { + 2 +} + #[derive(Debug, Clone, Deserialize)] pub(crate) struct OnboardConfig { pub enabled: bool, diff --git a/kms/src/main.rs b/kms/src/main.rs index eddfbdc9..807d55d8 100644 --- a/kms/src/main.rs +++ b/kms/src/main.rs @@ -17,8 +17,10 @@ use tracing::{info, warn}; mod config; // mod ct_log; +mod ckd; mod crypto; mod main_service; +mod near_kms_client; mod onboard_service; fn app_version() -> String { diff --git a/kms/src/main_service.rs b/kms/src/main_service.rs index 52573d4b..891ec505 100644 --- a/kms/src/main_service.rs +++ b/kms/src/main_service.rs @@ -28,6 +28,7 @@ use upgrade_authority::BootInfo; use crate::{ config::KmsConfig, crypto::{derive_k256_key, sign_message, sign_message_with_timestamp}, + near_kms_client, }; mod upgrade_authority; @@ -52,6 +53,8 @@ pub struct KmsStateInner { temp_ca_cert: String, temp_ca_key: String, verifier: CvmVerifier, + /// NEAR signer for MPC key derivation (generated automatically as implicit account) + near_signer: Option, } impl KmsState { @@ -71,6 +74,10 @@ impl KmsState { config.image.download_timeout, config.pccs_url.clone(), ); + + // Load or generate NEAR implicit account signer if using NEAR auth + let near_signer = near_kms_client::load_or_generate_near_signer(&config)?; + Ok(Self { inner: Arc::new(KmsStateInner { config, @@ -79,9 +86,15 @@ impl KmsState { temp_ca_cert, temp_ca_key, verifier, + near_signer, }), }) } + + /// Get NEAR signer account ID (implicit account) + pub fn near_signer_account_id(&self) -> Option { + self.near_signer.as_ref().map(|s| s.account_id.to_string()) + } } pub struct RpcHandler { @@ -341,6 +354,7 @@ impl KmsRpc for RpcHandler { chain_id: info.chain_id, gateway_app_id: info.gateway_app_id, app_auth_implementation: info.app_implementation, + near_signer_account_id: self.state.near_signer_account_id(), }) } diff --git a/kms/src/main_service/upgrade_authority.rs b/kms/src/main_service/upgrade_authority.rs index 169fd495..fca51d77 100644 --- a/kms/src/main_service/upgrade_authority.rs +++ b/kms/src/main_service/upgrade_authority.rs @@ -82,6 +82,21 @@ impl AuthApi { } Ok(response.json().await?) } + AuthApi::Near { near } => { + // For NEAR, we use webhook-style HTTP calls to auth-near service + let client = reqwest::Client::new(); + let path = if is_kms { + "bootAuth/kms" + } else { + "bootAuth/app" + }; + let url = url_join(&near.url, path); + let response = client.post(&url).json(&boot_info).send().await?; + if !response.status().is_success() { + bail!("Failed to check boot auth: {}", response.text().await?); + } + Ok(response.json().await?) + } } } @@ -107,6 +122,22 @@ impl AuthApi { app_implementation: Some(info.app_implementation.clone()), }) } + AuthApi::Near { near } => { + // For NEAR, fetch info from auth-near service + let client = reqwest::Client::new(); + let response = client.get(&near.url).send().await?; + if !response.status().is_success() { + bail!("Failed to get NEAR auth info: {}", response.text().await?); + } + let info: AuthApiInfoResponse = response.json().await?; + Ok(GetInfoResponse { + is_dev: false, + kms_contract_address: Some(info.kms_contract_addr.clone()), + chain_id: Some(info.chain_id), + gateway_app_id: Some(info.gateway_app_id.clone()), + app_implementation: Some(info.app_implementation.clone()), + }) + } } } } diff --git a/kms/src/near_kms_client.rs b/kms/src/near_kms_client.rs new file mode 100644 index 00000000..cc2d1618 --- /dev/null +++ b/kms/src/near_kms_client.rs @@ -0,0 +1,473 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! NEAR KMS client for requesting root keys via KMS contract +//! +//! This module handles calling the NEAR KMS contract's `request_kms_root_key()` function, +//! which verifies attestation and requests keys from the MPC network. + +use anyhow::{Context, Result}; +use byte_slice_cast::AsByteSlice; +use fs_err as fs; +use near_api::{ + types::{AccountId, Data}, + Contract, NetworkConfig, SecretKey, Signer, +}; +use near_crypto::InMemorySigner; +use serde::{Deserialize, Serialize}; +use sha3::{Digest, Sha3_384}; +use std::str::FromStr; +use std::sync::Arc; + +use crate::ckd::CkdResponse; +use crate::config::KmsConfig; + +const NEAR_SIGNER_FILE: &str = "near_signer.json"; + +/// Request KMS root key arguments (matching NEAR KMS contract interface) +#[derive(Debug, Serialize, Deserialize)] +pub struct RequestKmsRootKeyArgs { + pub quote_hex: String, + pub collateral: String, + pub tcb_info: String, + pub worker_public_key: String, // BLS12-381 G1 public key in NEAR format +} + +/// NEAR KMS client for requesting root keys +pub struct NearKmsClient { + network_config: NetworkConfig, + mpc_contract: Contract, + kms_contract: Contract, + signer_account_id: Option, + signer: Option>, +} + +impl NearKmsClient { + /// Create a new NEAR KMS client + pub fn new( + network_config: NetworkConfig, + mpc_contract_id: String, + kms_contract_id: String, + signer: Option, + ) -> Result { + let mpc_contract_id: AccountId = mpc_contract_id + .parse() + .context("Failed to parse MPC contract ID")?; + + let kms_contract_id: AccountId = kms_contract_id + .parse() + .context("Failed to parse KMS contract ID")?; + + let (signer_account_id, near_api_signer) = if let Some(in_memory_signer) = signer { + // Convert near_crypto::InMemorySigner to near-api Signer + let secret_key_str = in_memory_signer.secret_key.to_string(); + let near_api_secret_key = SecretKey::from_str(&secret_key_str) + .context("Failed to parse secret key for near-api Signer")?; + + // near-api Signer::from_secret_key takes only the secret key + let signer = Signer::from_secret_key(near_api_secret_key) + .context("Failed to create near-api Signer")?; + + let account_id: AccountId = in_memory_signer + .account_id + .to_string() + .parse() + .context("Failed to parse account ID")?; + + (Some(account_id.clone()), Some(signer)) + } else { + (None, None) + }; + + let mpc_contract = Contract(mpc_contract_id); + let kms_contract = Contract(kms_contract_id); + + Ok(Self { + network_config, + mpc_contract, + kms_contract, + signer_account_id, + signer: near_api_signer, + }) + } + + /// Get MPC public key from the contract + pub async fn get_mpc_public_key(&self, domain_id: u64) -> Result { + let current_value: Data = self + .mpc_contract + .call_function("public_key", serde_json::json!({ "domain_id": domain_id })) + .read_only() + .fetch_from(&self.network_config) + .await?; + + Ok(current_value.data) + } + + /// Request root key from KMS contract + /// + /// This calls the KMS contract's `request_kms_root_key()` which: + /// 1. Verifies the TDX attestation (quote, collateral, tcb_info) + /// 2. Calls the MPC contract to derive the key + /// 3. Returns the MPC response (big_y, big_c) + pub async fn request_kms_root_key( + &self, + quote_hex: &str, + collateral: &str, + tcb_info: &str, + worker_public_key: &str, + ) -> Result { + let signer = self + .signer + .as_ref() + .context("NEAR signer required for KMS root key requests")?; + let signer_account_id = self + .signer_account_id + .as_ref() + .context("Signer account ID required for KMS root key requests")?; + + // Create request arguments + let args = RequestKmsRootKeyArgs { + quote_hex: quote_hex.to_string(), + collateral: collateral.to_string(), + tcb_info: tcb_info.to_string(), + worker_public_key: worker_public_key.to_string(), + }; + + // Call the contract method + let execution_result = self + .kms_contract + .call_function("request_kms_root_key", args) + .transaction() + .with_signer(signer_account_id.clone(), signer.clone()) + .send_to(&self.network_config) + .await + .context("Failed to call KMS contract")?; + + // Extract the return value from the transaction result + let execution_success = execution_result + .into_result() + .context("Transaction execution failed")?; + let ckd_response: CkdResponse = execution_success + .json() + .context("Failed to deserialize CkdResponse from transaction return value")?; + + Ok(ckd_response) + } +} + +/// Generate a random NEAR implicit account signer +/// The account ID is derived from the Ed25519 public key (64 hex characters) +pub fn generate_near_implicit_signer() -> Result { + // Generate random Ed25519 keypair + let secret_key = near_crypto::SecretKey::from_random(near_crypto::KeyType::ED25519); + + // Derive implicit account ID from public key (hex encode the 32-byte public key) + let public_key = secret_key.public_key(); + use byte_slice_cast::AsByteSlice; + let account_id = match public_key { + near_crypto::PublicKey::ED25519(pk) => { + // Implicit account ID is the hex-encoded public key (64 characters) + hex::encode(pk.as_byte_slice()) + } + _ => anyhow::bail!("Unexpected key type for NEAR implicit account"), + }; + + let account_id: AccountId = account_id + .parse() + .context("Failed to parse generated account ID")?; + + let public_key = secret_key.public_key(); + Ok(InMemorySigner { + account_id, + secret_key, + public_key, + }) +} + +/// Load or generate NEAR signer (stored persistently) +/// +/// This function checks if a signer already exists in the cert_dir, and if not, +/// generates a new random NEAR implicit account signer and saves it. +pub fn load_or_generate_near_signer(config: &KmsConfig) -> Result> { + // Only generate if using NEAR auth + if !matches!(&config.auth_api, crate::config::AuthApi::Near { .. }) { + return Ok(None); + } + + let signer_path = config.cert_dir.join(NEAR_SIGNER_FILE); + + if signer_path.exists() { + // Load existing signer + let signer_data: serde_json::Value = serde_json::from_slice( + &fs::read(&signer_path).context("Failed to read NEAR signer file")?, + ) + .context("Failed to parse NEAR signer file")?; + + let account_id_str = signer_data["account_id"] + .as_str() + .context("Missing account_id in signer file")?; + let secret_key_str = signer_data["secret_key"] + .as_str() + .context("Missing secret_key in signer file")?; + + let account_id_parsed: AccountId = account_id_str + .parse() + .context("Failed to parse account ID from signer file")?; + let secret_key = near_crypto::SecretKey::from_str(secret_key_str) + .context("Failed to parse secret key from signer file")?; + + tracing::info!("Loaded existing NEAR signer: {}", account_id_str); + let public_key = secret_key.public_key(); + let signer = InMemorySigner { + account_id: account_id_parsed, + secret_key, + public_key, + }; + Ok(Some(signer)) + } else { + // Generate new signer + let signer = generate_near_implicit_signer()?; + let account_id = signer.account_id.to_string(); + let secret_key_str = signer.secret_key.to_string(); + + // Save to file + let signer_data = serde_json::json!({ + "account_id": account_id, + "secret_key": secret_key_str, + }); + fs::write(&signer_path, serde_json::to_string_pretty(&signer_data)?) + .context("Failed to write NEAR signer file")?; + + tracing::info!("Generated new NEAR implicit account signer: {}", account_id); + Ok(Some(signer)) + } +} + +/// Converts a NEAR public key to the report_data format required by the NEAR KMS contract. +/// +/// The format matches `ReportData::new()` in the contract: +/// `[version(2 bytes big endian) || SHA3-384(public_key_bytes[1..]) || zero padding]` +/// +/// # Arguments +/// * `public_key` - The NEAR account public key (must be ED25519) +/// +/// # Returns +/// A 64-byte array containing the report_data in the format expected by the KMS contract +/// +/// # Errors +/// Returns an error if the public key is not ED25519 (only ED25519 keys are supported for NEAR implicit accounts) +pub fn near_public_key_to_report_data(public_key: &near_crypto::PublicKey) -> Result<[u8; 64]> { + const REPORT_DATA_SIZE: usize = 64; + const BINARY_VERSION_SIZE: usize = 2; + const PUBLIC_KEYS_HASH_SIZE: usize = 48; + const PUBLIC_KEYS_OFFSET: usize = BINARY_VERSION_SIZE; + + let mut report_data = [0u8; REPORT_DATA_SIZE]; + + // Version: 1 (2 bytes, big endian) + report_data[0..BINARY_VERSION_SIZE].copy_from_slice(&1u16.to_be_bytes()); + + // Get public key bytes (skip first byte which is curve type identifier) + let public_key_bytes = match public_key { + near_crypto::PublicKey::ED25519(pk) => pk.as_byte_slice(), + _ => anyhow::bail!("Only ED25519 keys are supported for NEAR implicit accounts"), + }; + + // Hash public key bytes (skip first byte) with SHA3-384 + let mut hasher = Sha3_384::new(); + hasher.update(&public_key_bytes[1..]); // Skip first byte (curve type) + let hash: [u8; PUBLIC_KEYS_HASH_SIZE] = hasher.finalize().into(); + + // Copy hash to report_data (offset 2, length 48) + report_data[PUBLIC_KEYS_OFFSET..PUBLIC_KEYS_OFFSET + PUBLIC_KEYS_HASH_SIZE] + .copy_from_slice(&hash); + // Remaining bytes (50..64) are already zero-padded + + Ok(report_data) +} + +#[cfg(test)] +mod tests { + use super::*; + use near_crypto::PublicKey; + use sha3::{Digest, Sha3_384}; + + #[test] + fn test_generate_near_implicit_signer() { + // Test that we can generate a signer + let signer = generate_near_implicit_signer().expect("Should generate a signer"); + + // Verify the signer has valid fields + assert!(!signer.account_id.to_string().is_empty()); + assert_eq!(signer.account_id.to_string().len(), 64); // Hex-encoded 32-byte public key = 64 chars + + // Verify the public key matches the account ID derivation + let public_key_bytes = match &signer.public_key { + PublicKey::ED25519(pk) => pk.as_byte_slice(), + _ => panic!("Expected ED25519 key"), + }; + let expected_account_id = hex::encode(public_key_bytes); + assert_eq!(signer.account_id.to_string(), expected_account_id); + + // Verify the signer can be used (secret key and public key match) + let derived_public_key = signer.secret_key.public_key(); + assert_eq!(signer.public_key, derived_public_key); + } + + #[test] + fn test_generate_multiple_signers_are_different() { + // Generate multiple signers and verify they're different + let signer1 = generate_near_implicit_signer().expect("Should generate signer 1"); + let signer2 = generate_near_implicit_signer().expect("Should generate signer 2"); + let signer3 = generate_near_implicit_signer().expect("Should generate signer 3"); + + // All should have different account IDs + assert_ne!(signer1.account_id, signer2.account_id); + assert_ne!(signer2.account_id, signer3.account_id); + assert_ne!(signer1.account_id, signer3.account_id); + + // All should have different secret keys + assert_ne!( + signer1.secret_key.to_string(), + signer2.secret_key.to_string() + ); + assert_ne!( + signer2.secret_key.to_string(), + signer3.secret_key.to_string() + ); + } + + #[test] + fn test_near_public_key_to_report_data_format() { + // Generate a test public key + let secret_key = near_crypto::SecretKey::from_random(near_crypto::KeyType::ED25519); + let public_key = secret_key.public_key(); + + // Convert to report_data + let report_data = near_public_key_to_report_data(&public_key) + .expect("Should convert public key to report_data"); + + // Verify size + assert_eq!(report_data.len(), 64); + + // Verify version bytes (first 2 bytes should be [0, 1] for version 1) + assert_eq!(report_data[0..2], [0, 1]); + + // Verify hash bytes are non-zero (bytes 2-50 should contain the hash) + let hash_section = &report_data[2..50]; + assert!( + !hash_section.iter().all(|&b| b == 0), + "Hash should not be all zeros" + ); + + // Verify padding bytes are zero (bytes 50-64 should be zero) + let padding_section = &report_data[50..64]; + assert!( + padding_section.iter().all(|&b| b == 0), + "Padding should be zeros" + ); + } + + #[test] + fn test_near_public_key_to_report_data_hash_correctness() { + // Generate a test public key + let secret_key = near_crypto::SecretKey::from_random(near_crypto::KeyType::ED25519); + let public_key = secret_key.public_key(); + + // Convert to report_data + let report_data = near_public_key_to_report_data(&public_key) + .expect("Should convert public key to report_data"); + + // Manually compute the expected hash + let public_key_bytes = match &public_key { + PublicKey::ED25519(pk) => pk.as_byte_slice(), + _ => panic!("Expected ED25519 key"), + }; + + let mut hasher = Sha3_384::new(); + hasher.update(&public_key_bytes[1..]); // Skip first byte (curve type) + let expected_hash: [u8; 48] = hasher.finalize().into(); + + // Verify the hash matches + assert_eq!(&report_data[2..50], &expected_hash); + } + + #[test] + fn test_near_public_key_to_report_data_deterministic() { + // Generate a test public key + let secret_key = near_crypto::SecretKey::from_random(near_crypto::KeyType::ED25519); + let public_key = secret_key.public_key(); + + // Convert multiple times - should be deterministic + let report_data1 = near_public_key_to_report_data(&public_key) + .expect("Should convert public key to report_data"); + let report_data2 = near_public_key_to_report_data(&public_key) + .expect("Should convert public key to report_data"); + let report_data3 = near_public_key_to_report_data(&public_key) + .expect("Should convert public key to report_data"); + + // All should be identical + assert_eq!(report_data1, report_data2); + assert_eq!(report_data2, report_data3); + } + + #[test] + fn test_near_public_key_to_report_data_different_keys_different_output() { + // Generate two different public keys + let secret_key1 = near_crypto::SecretKey::from_random(near_crypto::KeyType::ED25519); + let public_key1 = secret_key1.public_key(); + + let secret_key2 = near_crypto::SecretKey::from_random(near_crypto::KeyType::ED25519); + let public_key2 = secret_key2.public_key(); + + // Convert both to report_data + let report_data1 = near_public_key_to_report_data(&public_key1) + .expect("Should convert public key 1 to report_data"); + let report_data2 = near_public_key_to_report_data(&public_key2) + .expect("Should convert public key 2 to report_data"); + + // They should be different + assert_ne!(report_data1, report_data2); + } + + #[test] + fn test_parse_ckd_response_valid_json() { + // Create valid CkdResponse JSON + let json_data = r#"{ + "big_y": "ed25519:test_y_key", + "big_c": "ed25519:test_c_key" + }"#; + + // Parse the response directly + let result: Result = serde_json::from_str(json_data); + assert!(result.is_ok(), "Should parse valid JSON"); + + let ckd_response = result.unwrap(); + // Verify the response was parsed correctly by checking the string representation + // (Bls12381G1PublicKey wraps a String, but we can't access it directly) + // We can at least verify the struct was created successfully + let _ = ckd_response.big_y; + let _ = ckd_response.big_c; + } + + #[test] + fn test_parse_ckd_response_invalid_json() { + use crate::ckd::CkdResponse; + + // Try to parse invalid JSON + let invalid_json = "{ invalid json }"; + let result: Result = serde_json::from_str(invalid_json); + assert!(result.is_err(), "Should fail to parse invalid JSON"); + } + + #[test] + fn test_parse_ckd_response_missing_fields() { + use crate::ckd::CkdResponse; + + // Try to parse JSON with missing fields + let incomplete_json = r#"{"big_y": "ed25519:test_y_key"}"#; + let result: Result = serde_json::from_str(incomplete_json); + assert!(result.is_err(), "Should fail to parse incomplete JSON"); + } +} diff --git a/kms/src/onboard_service.rs b/kms/src/onboard_service.rs index 4a4107fd..eb184a4e 100644 --- a/kms/src/onboard_service.rs +++ b/kms/src/onboard_service.rs @@ -22,7 +22,15 @@ use ra_tls::{ }; use safe_write::safe_write; -use crate::config::KmsConfig; +use crate::ckd::{derive_root_key_from_mpc, g1_to_near_format, generate_ephemeral_keypair}; +use crate::config::{AuthApi, KmsConfig}; +use crate::near_kms_client::{ + load_or_generate_near_signer, near_public_key_to_report_data, NearKmsClient, +}; +use dcap_qvl::collateral; +use near_api::NetworkConfig; +use ra_tls::kdf; +use serde_json::json; #[derive(Clone)] pub struct OnboardState { @@ -52,9 +60,40 @@ impl RpcCall for OnboardHandler { impl OnboardRpc for OnboardHandler { async fn bootstrap(self, request: BootstrapRequest) -> Result { let quote_enabled = self.state.config.onboard.quote_enabled; - let keys = Keys::generate(&request.domain, quote_enabled) - .await - .context("Failed to generate keys")?; + + // Check if we're using NEAR auth API + let use_near_mpc = matches!(&self.state.config.auth_api, AuthApi::Near { .. }); + + let keys = if use_near_mpc { + // Attempt MPC key derivation if config is available + match try_derive_keys_from_mpc(&self.state.config, &request.domain, quote_enabled).await + { + Ok(Some(keys)) => { + tracing::info!("✅ Successfully derived keys from NEAR MPC network"); + keys + } + Ok(None) => { + tracing::warn!("MPC config incomplete, falling back to local key generation"); + Keys::generate(&request.domain, quote_enabled) + .await + .context("Failed to generate keys")? + } + Err(e) => { + tracing::warn!( + "MPC key derivation failed: {}, falling back to local generation", + e + ); + Keys::generate(&request.domain, quote_enabled) + .await + .context("Failed to generate keys")? + } + } + } else { + // Ethereum/Base/Phala: Generate keys locally + Keys::generate(&request.domain, quote_enabled) + .await + .context("Failed to generate keys")? + }; let k256_pubkey = keys.k256_key.verifying_key().to_sec1_bytes().to_vec(); let ca_pubkey = keys.ca_key.public_key_der(); @@ -115,6 +154,32 @@ impl Keys { Self::from_keys(tmp_ca_key, ca_key, rpc_key, k256_key, domain, quote_enabled).await } + /// Create Keys from MPC-derived root key + /// The root_key is a 32-byte key derived from MPC + async fn from_mpc_root_key( + root_key: [u8; 32], + domain: &str, + quote_enabled: bool, + ) -> Result { + // Derive CA key from root key using deterministic key derivation + let ca_key = kdf::derive_p256_key_pair_from_bytes(&root_key, &[b"ca-key"]) + .context("Failed to derive CA key from MPC root key")?; + + // Derive tmp CA key from root key + let tmp_ca_key = kdf::derive_p256_key_pair_from_bytes(&root_key, &[b"tmp-ca-key"]) + .context("Failed to derive tmp CA key from MPC root key")?; + + // Derive RPC key from root key + let rpc_key = kdf::derive_p256_key_pair_from_bytes(&root_key, &[b"rpc-key"]) + .context("Failed to derive RPC key from MPC root key")?; + + // Use root key directly as K256 key (it's already 32 bytes) + let k256_key = SigningKey::from_bytes(&root_key.into()) + .context("Failed to create K256 key from root key")?; + + Self::from_keys(tmp_ca_key, ca_key, rpc_key, k256_key, domain, quote_enabled).await + } + async fn from_keys( tmp_ca_key: KeyPair, ca_key: KeyPair, @@ -161,7 +226,8 @@ impl Keys { .key(&rpc_key) .build() .signed_by(&ca_cert, &ca_key)?; - Ok(Keys { + + Ok(Self { k256_key, tmp_ca_key, tmp_ca_cert, @@ -203,7 +269,6 @@ impl Keys { let tmp_ca_key_pem = keys_res.temp_ca_key; let root_ca_key_pem = keys.ca_key; let root_k256_key = keys.k256_key; - let rpc_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?; let ca_key = KeyPair::from_pem(&root_ca_key_pem).context("Failed to parse CA key")?; let tmp_ca_key = @@ -280,16 +345,222 @@ pub(crate) async fn update_certs(cfg: &KmsConfig) -> Result<()> { } pub(crate) async fn bootstrap_keys(cfg: &KmsConfig) -> Result<()> { - let keys = Keys::generate( - &cfg.onboard.auto_bootstrap_domain, - cfg.onboard.quote_enabled, - ) - .await - .context("Failed to generate keys")?; + // Check if we're using NEAR auth API + let use_near_mpc = matches!(&cfg.auth_api, AuthApi::Near { .. }); + + let keys = if use_near_mpc { + // Attempt MPC key derivation + match try_derive_keys_from_mpc( + cfg, + &cfg.onboard.auto_bootstrap_domain, + cfg.onboard.quote_enabled, + ) + .await + { + Ok(Some(keys)) => { + tracing::info!("✅ Successfully derived keys from NEAR MPC network"); + keys + } + Ok(None) => { + tracing::warn!("MPC config incomplete, falling back to local key generation"); + Keys::generate( + &cfg.onboard.auto_bootstrap_domain, + cfg.onboard.quote_enabled, + ) + .await + .context("Failed to generate keys")? + } + Err(e) => { + tracing::warn!( + "MPC key derivation failed: {}, falling back to local generation", + e + ); + Keys::generate( + &cfg.onboard.auto_bootstrap_domain, + cfg.onboard.quote_enabled, + ) + .await + .context("Failed to generate keys")? + } + } + } else { + // Ethereum/Base/Phala: Generate keys locally + Keys::generate( + &cfg.onboard.auto_bootstrap_domain, + cfg.onboard.quote_enabled, + ) + .await + .context("Failed to generate keys")? + }; + keys.store(cfg)?; Ok(()) } +/// Attempt to derive keys from NEAR MPC network +/// Returns Ok(Some(keys)) if successful, Ok(None) if config is incomplete, Err if derivation failed +async fn try_derive_keys_from_mpc( + cfg: &KmsConfig, + domain: &str, + quote_enabled: bool, +) -> Result> { + let AuthApi::Near { near } = &cfg.auth_api else { + return Ok(None); + }; + + let rpc_url = near + .rpc_url + .as_deref() + .unwrap_or("https://free.rpc.fastnear.com") + .to_string(); + + // Check if MPC configuration is complete + let mpc_contract_id = match &near.mpc_contract_id { + Some(id) => id.clone(), + None => { + tracing::debug!("MPC contract ID not configured, skipping MPC derivation"); + return Ok(None); + } + }; + let kms_contract_id = &near.contract_id; + let mpc_domain_id = near.mpc_domain_id; + + let network_id = near.network_id.as_deref().unwrap_or("testnet"); + let network_config = NetworkConfig::from_rpc_url( + network_id, + rpc_url.parse().context("Failed to parse RPC URL")?, + ); + + // Load or generate NEAR signer (implicit account) + let signer = load_or_generate_near_signer(cfg)?; + let signer = match signer { + Some(s) => s, + None => { + tracing::warn!("Failed to load or generate NEAR signer, cannot call KMS contract"); + return Ok(None); + } + }; + + tracing::info!("Attempting MPC key derivation from NEAR network..."); + tracing::info!(" Network ID: {}", network_id); + tracing::info!(" RPC URL: {}", rpc_url); + tracing::info!(" MPC Contract: {}", mpc_contract_id); + tracing::info!(" KMS Contract: {}", kms_contract_id); + tracing::info!(" Domain ID: {}", mpc_domain_id); + tracing::info!(" Signer Account ID: {}", signer.account_id); + + // Get the NEAR account public key for report_data (clone before moving signer) + // The signer's public_key field contains the NEAR account public key + let near_account_public_key = signer.public_key.clone(); + + let kms_client = NearKmsClient::new( + network_config, + mpc_contract_id.clone(), + kms_contract_id.clone(), + Some(signer), + )?; + + let mpc_public_key = kms_client.get_mpc_public_key(mpc_domain_id).await?; + + tracing::info!( + "✅ Fetched MPC public key from contract: {}", + mpc_public_key + ); + + // Generate ephemeral BLS12-381 G1 keypair + let (ephemeral_private_key, ephemeral_public_key) = generate_ephemeral_keypair(); + let worker_public_key = g1_to_near_format(ephemeral_public_key) + .context("Failed to convert ephemeral public key to NEAR format")?; + + tracing::debug!("Generated ephemeral BLS12-381 keypair"); + + // Get TDX attestation (quote, collateral, tcb_info) for KMS contract + // The KMS contract's request_kms_root_key() requires attestation verification + let (quote_hex, collateral_json, tcb_info_json) = if quote_enabled { + // Generate report_data from NEAR account public key using helper function + let report_data = near_public_key_to_report_data(&near_account_public_key) + .context("Failed to convert NEAR public key to report_data format")?; + + let attest_response = app_attest(report_data.to_vec()) + .await + .context("Failed to get TDX quote for MPC request")?; + + let attestation = VersionedAttestation::from_scale(&attest_response.attestation) + .context("Failed to parse attestation")?; + + // Get quote bytes - need to call into_inner() first to get Attestation + let quote_bytes = attestation + .into_inner() + .tdx_quote() + .map(|q| q.quote.clone()) + .context("Failed to get TDX quote bytes")?; + let quote_hex = hex::encode("e_bytes); + + // Get collateral and tcb_info + let pccs_url = cfg.pccs_url.as_deref(); + let verified_report = collateral::get_collateral_and_verify("e_bytes, pccs_url) + .await + .context("Failed to get collateral and verify quote")?; + + // Extract collateral from verified_report + // The verified_report contains the quote verification result + // We need to serialize the entire verified_report or extract the needed fields + let collateral_json = serde_json::to_string(&verified_report) + .context("Failed to serialize verified report as collateral")?; + + // Get TCB info from verified report + let td_report = verified_report + .report + .as_td10() + .context("Failed to get TD10 report")?; + + let tcb_info_json = serde_json::to_string(&json!({ + "mrtd": hex::encode(td_report.mr_td), + "rtmr0": hex::encode(td_report.rt_mr0), + "rtmr1": hex::encode(td_report.rt_mr1), + "rtmr2": hex::encode(td_report.rt_mr2), + "rtmr3": hex::encode(td_report.rt_mr3), + })) + .context("Failed to serialize TCB info")?; + + (quote_hex, collateral_json, tcb_info_json) + } else { + anyhow::bail!("Quote must be enabled for NEAR MPC key derivation (attestation required)"); + }; + + // Request root key from KMS contract (which will verify attestation and call MPC) + tracing::info!("Calling KMS contract's request_kms_root_key() with attestation..."); + let mpc_response = kms_client + .request_kms_root_key( + "e_hex, + &collateral_json, + &tcb_info_json, + &worker_public_key, + ) + .await + .context("Failed to request root key from KMS contract")?; + + tracing::info!("Received MPC response (big_y, big_c)"); + + // Derive root key from MPC response + let root_key = derive_root_key_from_mpc( + &mpc_response, + ephemeral_private_key, + &mpc_public_key, + kms_contract_id, + ) + .context("Failed to derive root key from MPC response")?; + + tracing::info!("✅ Successfully derived 32-byte root key from MPC"); + + // Convert root key to Keys structure + let keys = Keys::from_mpc_root_key(root_key, domain, quote_enabled) + .await + .context("Failed to create keys from MPC root key")?; + + Ok(Some(keys)) +} + fn dstack_client() -> DstackGuestClient { let address = dstack_types::dstack_agent_address(); let http_client = PrpcClient::new(address); diff --git a/port-forward/src/tcp.rs b/port-forward/src/tcp.rs index 50878552..ade787f2 100644 --- a/port-forward/src/tcp.rs +++ b/port-forward/src/tcp.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 use std::net::SocketAddr; +#[cfg(target_os = "linux")] use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, OwnedFd}; use tokio::io; @@ -85,6 +86,7 @@ async fn relay(client: &mut TcpStream, server: &mut TcpStream) -> io::Result<()> /// Zero-copy bidirectional TCP relay using Linux splice(2). /// /// When one direction hits EOF, select! drops the other direction. +#[cfg(target_os = "linux")] async fn splice_bidirectional(a: &TcpStream, b: &TcpStream) -> io::Result<()> { let a_fd = a.as_raw_fd(); let b_fd = b.as_raw_fd(); @@ -95,11 +97,21 @@ async fn splice_bidirectional(a: &TcpStream, b: &TcpStream) -> io::Result<()> { } } +/// On non-Linux platforms, splice is not available, so return Unsupported. +#[cfg(not(target_os = "linux"))] +async fn splice_bidirectional(_a: &TcpStream, _b: &TcpStream) -> io::Result<()> { + Err(io::Error::new( + io::ErrorKind::Unsupported, + "splice not supported on this platform", + )) +} + /// Splice data from src fd to dst fd via an intermediate pipe. /// /// Uses `TcpStream::try_io` for proper readiness handling: when splice returns /// EAGAIN, try_io automatically clears the readiness flag so the next /// `readable().await` / `writable().await` blocks until the fd is truly ready. +#[cfg(target_os = "linux")] async fn splice_one_direction( src: &TcpStream, src_fd: i32,