commit fc47967ff4e288fb2b297b01bc13d01839767669 Author: zyycn Date: Thu Jun 11 20:52:47 2026 +0800 chore: scaffold codex-proxy-rs Co-authored-by: Codex diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..5b3f219b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +/target/ +/data/ +/logs/ +/.env +/.env.* +!/.env.example +/config/local.yaml +/config/local.yml +/.worktrees/ +/worktrees/ +*.sqlite +*.sqlite3 +*.db +*.db-shm +*.db-wal +*.log +*.tmp +*.swp +*~ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 000000000..ff74983ad --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3577 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.6", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash", +] + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.6", + "inout", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "codex-proxy-rs" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "anyhow", + "argon2", + "async-trait", + "axum", + "base64", + "chrono", + "config", + "dotenvy", + "futures", + "hmac", + "insta", + "pretty_assertions", + "rand 0.10.1", + "reqwest", + "rustls", + "secrecy", + "serde", + "serde_json", + "serde_yaml", + "sha2 0.11.0", + "sqlx", + "tempfile", + "thiserror", + "tokio", + "tower", + "tower-http", + "tracing", + "tracing-appender", + "tracing-subscriber", + "uuid", + "wiremock", + "zeroize", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "config" +version = "0.15.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f316c6237b2d38be61949ecd15268a4c6ca32570079394a2444d9ce2c72a72d8" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde-untagged", + "serde_core", + "serde_json", + "toml", + "winnow", + "yaml-rust2", +] + +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.6", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +dependencies = [ + "serde", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "insta" +version = "1.47.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" +dependencies = [ + "console", + "once_cell", + "similar", + "tempfile", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2 0.10.9", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "cookie", + "cookie_store", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ron" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" +dependencies = [ + "bitflags", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64", + "bytes", + "cfg-if", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink", + "indexmap", + "log", + "memchr", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +dependencies = [ + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest 0.11.3", + "dotenvy", + "either", + "futures-core", + "futures-util", + "generic-array", + "log", + "percent-encoding", + "serde", + "sha1", + "sha2 0.11.0", + "sqlx-core", + "thiserror", + "tracing", + "uuid", +] + +[[package]] +name = "sqlx-postgres" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "rand 0.10.1", + "serde", + "serde_json", + "sha2 0.11.0", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +dependencies = [ + "atoi", + "chrono", + "flume", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "thiserror", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[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-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.6", + "subtle", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[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 = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wiremock" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" +dependencies = [ + "assert-json-diff", + "base64", + "deadpool", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yaml-rust2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "631a50d867fafb7093e709d75aaee9e0e0d5deb934021fcea25ac2fe09edc51e" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..5feefede9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "codex-proxy-rs" +version = "0.1.0" +edition = "2021" +license = "MIT" +publish = false +description = "A lean Rust Codex proxy backed only by ChatGPT/Codex accounts." +repository = "https://example.invalid/codex-proxy-rs" +rust-version = "1.95" + +[dependencies] +aes-gcm = "0.10.3" +anyhow = "1.0.102" +argon2 = "0.5.3" +async-trait = "0.1.89" +axum = "0.8.9" +base64 = "0.22.1" +chrono = { version = "0.4.45", features = ["serde", "clock"] } +config = "0.15.23" +dotenvy = "0.15.7" +futures = "0.3.32" +hmac = "0.13.0" +rand = "0.10.1" +reqwest = { version = "=0.12.28", default-features = false, features = ["json", "stream", "cookies", "rustls-tls-native-roots"] } +rustls = { version = "=0.23.36", default-features = false, features = ["ring", "std", "tls12"] } +secrecy = "0.10.3" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.150" +serde_yaml = "0.9.34" +sha2 = "0.11.0" +sqlx = { version = "0.9.0", features = ["runtime-tokio", "tls-rustls", "sqlite", "chrono", "uuid", "migrate"] } +thiserror = "2.0.18" +tokio = { version = "1.52.3", features = ["macros", "rt-multi-thread", "signal", "fs", "time", "sync"] } +tower = "0.5.3" +tower-http = { version = "0.6.11", features = ["trace", "cors", "request-id", "timeout"] } +tracing = "0.1.44" +tracing-appender = "0.2.5" +tracing-subscriber = { version = "0.3.23", features = ["env-filter", "json"] } +uuid = { version = "1.23.3", features = ["v4", "v7", "serde"] } +zeroize = "1.8.2" + +[dev-dependencies] +insta = "1.47.2" +pretty_assertions = "1.4.1" +tempfile = "3.27.0" +wiremock = "0.6.5" + +[lints.rust] +future_incompatible = { level = "warn", priority = -1 } +nonstandard_style = { level = "deny", priority = -1 } +unsafe_code = "forbid" + +[lints.clippy] +all = { level = "deny", priority = 10 } +redundant_clone = { level = "deny", priority = 9 } +large_enum_variant = { level = "warn", priority = 8 } +needless_collect = { level = "deny", priority = 7 } diff --git a/README.md b/README.md new file mode 100644 index 000000000..2acec634a --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# codex-proxy-rs + +`codex-proxy-rs` is a lean Rust rewrite of `codex-proxy`. + +The service exposes OpenAI-compatible local endpoints backed only by ChatGPT/Codex +accounts and the official Codex backend: + +- Keep `/v1/chat/completions`, `/v1/responses`, and `/v1/models`. +- Remove OpenAI official API key direct upstream. +- Remove Anthropic, Gemini, custom provider routing, Ollama, Electron, and per-account proxy assignment. +- Keep Codex Desktop TLS/header parity, Cloudflare Cookie persistence, and fingerprint auto-update as first-class subsystems. + +Current implementation status is tracked in `docs/implementation-status.md`. diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 000000000..ae9da4cb5 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,87 @@ +# API Contract + +`codex-proxy-rs` exposes two API families. + +## `/v1/*` + +These endpoints are OpenAI-compatible and are authenticated only by client API keys using `Authorization: Bearer cpr_...`. +Responses include an `X-Request-Id` header for tracing, but the body stays OpenAI-compatible and does not include a custom `requestId` field. + +Error body: + +```json +{ + "error": { + "message": "Invalid client API key", + "type": "invalid_request_error", + "param": null, + "code": "invalid_api_key" + } +} +``` + +Streaming errors after SSE has started use: + +```text +event: error +data: {"error":{"message":"Upstream failed","type":"server_error","param":null,"code":"upstream_error"}} +``` + +## `/admin/*` + +Admin endpoints are authenticated only by HttpOnly admin session cookies. +Admin JSON uses lower camelCase field names. Every admin response includes an `X-Request-Id` header, and every JSON body includes `requestId`. + +Use real HTTP status codes and body-level frontend codes together. Do not return HTTP `200` for failed requests. The body `code` exists for frontend branching; the HTTP status remains the transport truth. + +Success body: + +```json +{ + "code": 200, + "message": "OK", + "data": {}, + "requestId": "req_01" +} +``` + +List body: + +```json +{ + "code": 200, + "message": "OK", + "data": [], + "page": { + "limit": 50, + "nextCursor": null + }, + "requestId": "req_01" +} +``` + +Error body: + +```json +{ + "code": 40101, + "message": "Admin login required", + "data": null, + "requestId": "req_01" +} +``` + +Pagination uses cursor ordering by `(created_at desc, id desc)`, default `limit=50`, max `limit=200`. + +Rust structs use `PascalCase` type names and `snake_case` fields internally, then expose lower camelCase through serde: + +```rust +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiEnvelope { + pub code: u32, + pub message: String, + pub data: T, + pub request_id: String, +} +``` diff --git a/docs/dependency-policy.md b/docs/dependency-policy.md new file mode 100644 index 000000000..21bd16e8c --- /dev/null +++ b/docs/dependency-policy.md @@ -0,0 +1,14 @@ +# Dependency Policy + +- Use current stable crate releases for normal Rust dependencies. +- Commit `Cargo.lock` because this is an application. +- Run `cargo update` in dedicated dependency-refresh changes. +- Keep `reqwest = 0.12.28` and `rustls = 0.23.36` pinned until a TLS fingerprint review proves a newer pair matches real Codex Desktop behavior. +- Do not add unmaintained crates when a maintained community-standard crate exists. +- Every dependency change must run: + +```bash +cargo fmt --check +cargo clippy --all-targets --all-features --locked -- -D warnings +cargo test +``` diff --git a/docs/implementation-status.md b/docs/implementation-status.md new file mode 100644 index 000000000..89c2fa5ae --- /dev/null +++ b/docs/implementation-status.md @@ -0,0 +1,21 @@ +# Implementation Status + +Update this file before each feature commit. + +| Area | Status | Commit | Verification | Notes | +| --- | --- | --- | --- | --- | +| Scaffold | Completed | initial scaffold commit | `cargo fmt --check`; `cargo test`; `cargo clippy --all-targets --all-features --locked -- -D warnings` | Rust crate, lint policy, pinned TLS-sensitive dependencies, `.gitignore`, and minimal entry points are in place. | +| API contract docs | Completed | initial scaffold commit | `cargo fmt --check`; `cargo test`; `cargo clippy --all-targets --all-features --locked -- -D warnings` | `/v1/*` OpenAI-compatible body, `/admin/*` frontend envelope, body codes, camelCase, and request ID policy are documented. | +| Configuration | Planned | | | | +| SQLite storage | Planned | | | | +| Admin auth and client API keys | Planned | | | | +| Secret encryption | Planned | | | | +| Logging and pagination | Planned | | | | +| TLS headers and fingerprint | Planned | | | | +| Cookie persistence | Planned | | | | +| Account pool and refresh | Planned | | | | +| Translation | Planned | | | | +| HTTP routes | Planned | | | | +| Upstream lifecycle | Planned | | | | +| Fingerprint updates | Planned | | | | +| Runtime docs and packaging | Planned | | | | diff --git a/docs/status-codes.md b/docs/status-codes.md new file mode 100644 index 000000000..c722d7eef --- /dev/null +++ b/docs/status-codes.md @@ -0,0 +1,55 @@ +# Status Codes + +HTTP status codes are not replaced by body codes. Admin/frontend APIs return both an accurate HTTP status and a JSON body `code` unless the status is `204 No Content`. + +## Health + +| Status | Meaning | +| --- | --- | +| `200` | Service is running. | + +## Admin and Frontend APIs + +| HTTP status | Body code range | Meaning | +| --- | --- | --- | +| `200` | `200` | Successful read, login, or mutation with response body. | +| `201` | `201` | Resource created. | +| `204` | `204` | Successful mutation with no response body. No JSON envelope is sent. | +| `400` | `40000`-`40099` | Malformed JSON, invalid parameters, or invalid cursor. | +| `401` | `40100`-`40199` | Missing/expired admin session or bad admin password. | +| `403` | `40300`-`40399` | Authenticated admin lacks permission for a local-only/bootstrap action. | +| `404` | `40400`-`40499` | Resource not found. | +| `409` | `40900`-`40999` | Duplicate resource or stale state transition. | +| `422` | `42200`-`42299` | Well-formed request failed domain validation. | +| `429` | `42900`-`42999` | Login or operation rate limit. | +| `500` | `50000`-`50099` | Internal service error. | + +Recommended initial body codes: + +| Body code | HTTP status | Meaning | +| --- | --- | --- | +| `40001` | `400` | Validation failed. | +| `40002` | `400` | Invalid cursor. | +| `40101` | `401` | Admin session required. | +| `40102` | `401` | Admin password invalid. | +| `40301` | `403` | Bootstrap action denied. | +| `40401` | `404` | Resource not found. | +| `40901` | `409` | Duplicate resource. | +| `42201` | `422` | Domain validation failed. | +| `42901` | `429` | Login rate limited. | +| `50001` | `500` | Internal service error. | + +## OpenAI-Compatible `/v1/*` + +| Status | Meaning | +| --- | --- | +| `200` | Successful response or SSE stream accepted. | +| `400` | Invalid OpenAI/Responses request body. | +| `401` | Missing or invalid client API key. | +| `404` | Requested model is not a supported Codex model. | +| `413` | Request payload too large for safe replay or forwarding. | +| `429` | Codex quota or rate limit surfaced to client. | +| `499` | Client aborted; log internally only. | +| `502` | Upstream Codex transport or protocol failure. | +| `503` | No usable Codex account is available. | +| `504` | Upstream timeout. | diff --git a/docs/superpowers/plans/2026-06-11-codex-proxy-rs.md b/docs/superpowers/plans/2026-06-11-codex-proxy-rs.md new file mode 100644 index 000000000..de5778897 --- /dev/null +++ b/docs/superpowers/plans/2026-06-11-codex-proxy-rs.md @@ -0,0 +1,2985 @@ +# Codex Proxy RS Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build `codex-proxy-rs`, a lean Rust rewrite of `codex-proxy` that exposes OpenAI-compatible local endpoints backed only by ChatGPT/Codex accounts and the official Codex backend. + +**Architecture:** Create a new Rust service at `/home/zyy/桌面/Codes/codex-proxy-rs`. The only upstream is `https://chatgpt.com/backend-api`; remove OpenAI official API key passthrough, Anthropic, Gemini, custom providers, Ollama, Electron, and per-account proxy assignment. Treat Codex Desktop impersonation as a first-class subsystem: locked `reqwest` + `rustls`, exact headers, account-scoped Cookie replay, and fingerprint auto-update. Keep `src/main.rs` as the binary/bootstrap layer and keep reusable logic in `src/lib.rs` modules with typed `thiserror` errors. + +**Tech Stack:** Rust 2021, `tokio`, `axum`, `tower-http`, `sqlx` + SQLite WAL, `config`, `dotenvy`, `serde`, `serde_json`, `serde_yaml`, `tracing`, `tracing-subscriber`, rotating file logging, `argon2`, `rand`, `uuid`, `chrono`, `thiserror`, `anyhow` only in `main.rs` and tests, `base64`, `aes-gcm`, `hmac`, `sha2`, `secrecy`, `zeroize`, `wiremock`, `insta`, `tempfile`. Use the latest stable crate versions at implementation time except dependencies intentionally pinned for Codex Desktop TLS fingerprint parity: `reqwest = 0.12.28` and `rustls = 0.23.36` until a fingerprint review proves a newer pair is equivalent. + +**Execution Mode:** Subagent-Driven. Execute one task at a time with implementation, spec review, and code-quality review before moving to the next task. After each completed task, update this plan's checkbox state and `docs/implementation-status.md` before committing. + +--- + +## Locked Scope + +Keep: + +- OpenAI-compatible inbound API: `/v1/chat/completions`, `/v1/responses`, `/v1/models`. +- Codex Responses passthrough for clients that already speak Responses format. +- ChatGPT/Codex OAuth login, device login, CLI token import, manual access-token import. +- Account pool, account rotation, quota state, token refresh behavior compatible with the current TypeScript implementation. +- Rust native TLS path using pinned `reqwest` and `rustls`. +- Codex Desktop fingerprint headers and fingerprint auto-update. +- Account-scoped Cloudflare Cookie capture and replay. +- Structured logs, queryable event records, cursor pagination, and log rotation. + +Remove: + +- OpenAI official API Key direct upstream. +- Anthropic, Gemini, custom provider routing, OpenRouter-style provider pools. +- Ollama bridge and Ollama settings. +- Per-account proxy assignment, proxy health checks, proxy UI, proxy routing. +- Electron desktop packaging and large dashboard UI. + +Security split: + +- Admin login is not API key auth. +- Client API keys call `/v1/*` only and cannot log in to admin endpoints. +- Upstream Codex account tokens are internal credentials and cannot be used as client API keys or admin passwords. + +## Rust Architecture Adjustments + +These adjustments come from the `rust-best-practices` review and should be treated as implementation constraints: + +- Use module-local `thiserror` error enums for library code. Reserve `anyhow::Result` for `src/main.rs`, tests, and one-off setup commands. +- Add `Cargo.toml` lint configuration early. Run `cargo clippy --all-targets --all-features --locked -- -D warnings` before each task commit. +- Store secrets with explicit secret types: `secrecy::SecretString` for in-memory tokens/passwords and AES-GCM ciphertext for persisted access tokens, refresh tokens, Cookies, and API key pepper material. +- Hash admin passwords with Argon2id. Hash high-entropy client API keys with HMAC-SHA256 using a server-side pepper, not with the API key display name. +- Prefer concrete service structs over early `dyn Trait` abstraction. Use generics for test seams where useful; introduce trait objects only at boundaries that actually need runtime polymorphism. +- Keep shared runtime state in an `Arc` container that holds cheap-clone handles (`SqlitePool`, `reqwest::Client`, repositories, secret box, config). Avoid mutable globals and avoid `std::sync::Mutex` across async `.await`; use SQLite, channels, or `tokio::sync` primitives. +- Use the `config` crate with `config/default.yaml`, optional `config/local.yaml`, and `CPRS_*` environment overrides. Keep `.env` support via `dotenvy` for local development only. +- Prefer borrowed parameters in public APIs (`&str`, `&[T]`, references to request structs) unless ownership transfer is part of the domain model. +- Keep tests descriptive and targeted. Unit tests live beside modules for private behavior; integration tests under `tests/` cover route and storage behavior. Use `insta` only for structured payload snapshots that are worth reviewing. +- Add sparse Chinese comments only where the reason is not obvious from names or types. Good targets are protocol/security/business invariants: TLS fingerprint pinning, Codex Desktop header parity, account-scoped Cookie replay, admin-session versus client-API-key boundaries, and token refresh compatibility with the TypeScript implementation. Do not comment every line, do not restate obvious code, and do not leave untracked TODO comments. +- Keep Rust identifiers idiomatic: type names use `PascalCase`, functions/variables/fields use `snake_case`, constants use `SCREAMING_SNAKE_CASE`. JSON fields exposed to the frontend use lower camelCase via `#[serde(rename_all = "camelCase")]`; do not expose PascalCase or snake_case JSON fields in admin/frontend APIs. + +## Dependency Version Policy + +- Resolve non-fingerprint Rust dependencies with `cargo add` at implementation time so the project starts from current stable crate releases. +- Commit `Cargo.lock` for reproducible application builds. +- Run `cargo update` during dependency-refresh tasks and include the update in a dedicated commit with test evidence. +- Keep `reqwest = 0.12.28` and `rustls = 0.23.36` pinned for the initial TLS fingerprint implementation. Upgrading either requires a separate fingerprint verification task that compares the real Codex Desktop TLS behavior, request headers, and Cloudflare response behavior before changing the pin. +- Do not introduce an unmaintained crate when a maintained Rust community standard exists. Prefer widely adopted crates already listed in the tech stack. + +## API Contract and Status Codes + +`codex-proxy-rs` exposes two response families. Do not mix them. + +OpenAI-compatible `/v1/*` endpoints: + +- Success responses follow the OpenAI-compatible shape for the endpoint. +- Error responses always use OpenAI error format: + +```json +{ + "error": { + "message": "Human readable message", + "type": "invalid_request_error", + "param": null, + "code": "invalid_api_key" + } +} +``` + +- Responses include an `X-Request-Id` header for tracing, but do not add `requestId` to the JSON body because that would break OpenAI compatibility. +- Streaming errors before any upstream bytes are sent return the normal HTTP status plus OpenAI error JSON. +- Streaming errors after SSE starts are sent as an SSE error event using the same `error` object. + +Admin/frontend `/admin/*` endpoints: + +- Use real HTTP status codes and a body-level frontend code. The HTTP status is still authoritative for transport, browser, gateway, and observability behavior. The body `code` is for frontend branching and product error handling. +- JSON field names use lower camelCase (`requestId`, `nextCursor`, `createdAt`). Rust structs keep idiomatic Rust naming and apply `#[serde(rename_all = "camelCase")]`. +- All responses include an `X-Request-Id` response header. Admin/frontend JSON bodies also include `requestId` except `204 No Content`, because a `204` response has no body. +- Success envelope: + +```json +{ + "code": 200, + "message": "OK", + "data": {}, + "requestId": "req_..." +} +``` + +- List envelope: + +```json +{ + "code": 200, + "message": "OK", + "data": [], + "page": { + "limit": 50, + "nextCursor": null + }, + "requestId": "req_..." +} +``` + +- Error envelope: + +```json +{ + "code": 40101, + "message": "Admin login required", + "data": null, + "requestId": "req_..." +} +``` + +Body code ranges: + +| Body code | HTTP status | Meaning | +| --- | --- | --- | +| `200` | `200` | Successful read/login/mutation with body. | +| `201` | `201` | Resource created. | +| `204` | `204` | No body; use only when the frontend does not need an envelope. | +| `40000`-`40099` | `400` | Malformed JSON, invalid parameters, or invalid cursor. | +| `40100`-`40199` | `401` | Missing/expired admin session or bad admin password. | +| `40300`-`40399` | `403` | Authenticated admin lacks permission for a local-only/bootstrap action. | +| `40400`-`40499` | `404` | Admin resource not found. | +| `40900`-`40999` | `409` | Conflict, such as duplicate account import or stale state transition. | +| `42200`-`42299` | `422` | Well-formed request that fails domain validation. | +| `42900`-`42999` | `429` | Login rate limit or admin operation rate limit. | +| `50000`-`50099` | `500` | Internal service error. | + +Status code matrix: + +| Area | Status | Meaning | +| --- | --- | --- | +| Health | `200` | Service is running. | +| Admin | `200` | Successful read, login, or mutation with response body. | +| Admin | `201` | Resource created, such as a client API key or imported account. | +| Admin | `204` | Successful mutation with no response body, such as logout/delete when no body is useful. | +| Admin | `400` | Malformed JSON, invalid parameters, or invalid cursor. | +| Admin | `401` | Missing/expired admin session or bad admin password. | +| Admin | `403` | Authenticated admin lacks permission for a local-only/bootstrap action. | +| Admin | `404` | Admin resource not found. | +| Admin | `409` | Conflict, such as duplicate account import or stale state transition. | +| Admin | `422` | Well-formed request that fails domain validation. | +| Admin | `429` | Login rate limit or admin operation rate limit. | +| Admin | `500` | Internal service error. | +| `/v1/*` | `200` | Successful non-streaming response or SSE stream accepted. | +| `/v1/*` | `400` | Invalid OpenAI/Responses request body. | +| `/v1/*` | `401` | Missing or invalid client API key. Never use admin session as API auth. | +| `/v1/*` | `404` | Requested model is not a supported Codex model. | +| `/v1/*` | `413` | Request payload too large for safe replay/forwarding. | +| `/v1/*` | `429` | Codex quota/rate limit surfaced to client. | +| `/v1/*` | `499` | Client aborted; log internally only. Do not send this after disconnect. | +| `/v1/*` | `502` | Upstream Codex transport/protocol failure. | +| `/v1/*` | `503` | No usable Codex account is available. | +| `/v1/*` | `504` | Upstream timeout. | + +All paginated admin APIs use cursor pagination with `limit` default `50`, max `200`, and stable ordering by `(created_at desc, id desc)`. + +## Documentation Progress Discipline + +- Each implementation task must update the matching checkboxes in this plan before its commit. +- Each completed feature must update `docs/implementation-status.md` with status, commit hash, test command, and any known limitations. +- API shape or status-code changes must update `docs/api.md` and `docs/status-codes.md` in the same commit as the code change. +- Dependency changes must update `docs/dependency-policy.md` and mention whether the change is a normal latest-stable refresh or a TLS-fingerprint-sensitive change. +- A task is not complete until code, tests, and documentation status are all updated. + +## Target File Structure + +Create the new project at `/home/zyy/桌面/Codes/codex-proxy-rs`: + +```text +codex-proxy-rs/ + Cargo.toml + rust-toolchain.toml + README.md + config/default.yaml + docs/api.md + docs/dependency-policy.md + docs/implementation-status.md + docs/status-codes.md + migrations/0001_initial.sql + migrations/0002_events_indexes.sql + src/ + main.rs + lib.rs + app.rs + config.rs + error.rs + state.rs + crypto.rs + pagination.rs + http/ + mod.rs + middleware.rs + admin.rs + auth.rs + v1.rs + health.rs + auth/ + mod.rs + error.rs + admin_session.rs + api_key.rs + oauth.rs + refresh.rs + token.rs + accounts/ + mod.rs + model.rs + pool.rs + repository.rs + lifecycle.rs + codex/ + mod.rs + client.rs + headers.rs + sse.rs + types.rs + usage.rs + websocket.rs + fingerprint/ + mod.rs + model.rs + updater.rs + cookies/ + mod.rs + repository.rs + jar.rs + logs/ + mod.rs + event.rs + repository.rs + rotation.rs + models/ + mod.rs + catalog.rs + storage/ + mod.rs + db.rs + migrations.rs + translation/ + mod.rs + openai_to_codex.rs + codex_to_openai.rs + schema.rs + tests/ + admin_auth_test.rs + api_key_auth_test.rs + codex_headers_test.rs + cookie_store_test.rs + logs_pagination_test.rs + refresh_scheduler_test.rs + routes_chat_test.rs + routes_responses_test.rs +``` + +--- + +### Task 1: Scaffold Rust Workspace + +**Files:** +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/Cargo.toml` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/rust-toolchain.toml` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/main.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/error.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/README.md` + +- [x] **Step 1: Create project directory** + +Run: + +```bash +cd /home/zyy/Codes +mkdir codex-proxy-rs +cd codex-proxy-rs +git init +mkdir -p src +``` + +Expected: `git status --short` prints no tracked files yet. + +- [x] **Step 2: Resolve current crate versions and write Cargo manifest** + +Run these commands so non-fingerprint dependencies resolve to the latest stable versions available at implementation time: + +```bash +cargo add anyhow aes-gcm argon2 async-stream base64 bytes dotenvy futures-util hmac http http-body-util rand serde_json serde_yaml sha2 thiserror tracing tracing-appender uuid zeroize +cargo add axum --features macros,ws +cargo add chrono --features serde +cargo add config +cargo add reqwest@=0.12.28 --no-default-features --features rustls-tls-native-roots,stream,gzip,brotli,zstd,deflate,http2,json +cargo add rustls@=0.23.36 +cargo add secrecy --features serde +cargo add serde --features derive +cargo add sqlx --features runtime-tokio-rustls,sqlite,chrono,uuid,json,migrate +cargo add tokio --features macros,rt-multi-thread,signal,time,fs +cargo add tower +cargo add tower-http --features cors,trace,request-id,timeout +cargo add --dev insta --features json +cargo add --dev tempfile wiremock +``` + +Then edit `/home/zyy/桌面/Codes/codex-proxy-rs/Cargo.toml` so it includes package metadata and lint policy. The exact non-pinned versions may be newer than this example because `cargo add` resolves them at execution time: + +```toml +[package] +name = "codex-proxy-rs" +version = "0.1.0" +edition = "2021" +license = "LicenseRef-Non-Commercial" + +[dependencies] +anyhow = "1" +aes-gcm = "0.10" +argon2 = "0.5" +async-stream = "0.3" +axum = { version = "0.7", features = ["macros", "ws"] } +base64 = "0.22" +bytes = "1" +chrono = { version = "0.4", features = ["serde"] } +config = "0.15" +dotenvy = "0.15" +futures-util = "0.3" +hmac = "0.12" +http = "1" +http-body-util = "0.1" +rand = "0.8" +reqwest = { version = "=0.12.28", default-features = false, features = ["rustls-tls-native-roots", "stream", "gzip", "brotli", "zstd", "deflate", "http2", "json"] } +rustls = "=0.23.36" +secrecy = { version = "0.8", features = ["serde"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_yaml = "0.9" +sha2 = "0.10" +sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "chrono", "uuid", "json", "migrate"] } +thiserror = "1" +tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "time", "fs"] } +tower = "0.5" +tower-http = { version = "0.6", features = ["cors", "trace", "request-id", "timeout"] } +tracing = "0.1" +tracing-appender = "0.2" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +uuid = { version = "1", features = ["v4", "serde"] } +zeroize = "1" + +[dev-dependencies] +insta = { version = "1", features = ["json"] } +tempfile = "3" +wiremock = "0.6" + +[lints.rust] +future_incompatible = "warn" +nonstandard_style = "deny" +unsafe_code = "forbid" + +[lints.clippy] +all = { level = "deny", priority = 10 } +redundant_clone = { level = "deny", priority = 9 } +needless_collect = { level = "deny", priority = 9 } +large_enum_variant = { level = "deny", priority = 9 } +manual_ok_or = { level = "deny", priority = 9 } +pedantic = { level = "warn", priority = 3 } +``` + +- [x] **Step 3: Pin Rust toolchain** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/rust-toolchain.toml`: + +```toml +[toolchain] +channel = "stable" +components = ["rustfmt", "clippy"] +``` + +- [x] **Step 4: Add minimal entry points** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs`: + +```rust +pub mod error; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/error.rs`: + +```rust +use axum::{http::StatusCode, response::{IntoResponse, Response}, Json}; +use serde::Serialize; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum AppError { + #[error("configuration error: {0}")] + Config(String), + #[error("database error: {0}")] + Database(#[from] sqlx::Error), + #[error("upstream error: {0}")] + Upstream(String), + #[error("unauthorized")] + Unauthorized, + #[error("forbidden")] + Forbidden, + #[error("bad request: {0}")] + BadRequest(String), +} + +#[derive(Serialize)] +struct ErrorBody<'a> { + error: ErrorMessage<'a>, +} + +#[derive(Serialize)] +struct ErrorMessage<'a> { + message: &'a str, + #[serde(rename = "type")] + kind: &'a str, + code: &'a str, +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let status = match self { + AppError::Unauthorized => StatusCode::UNAUTHORIZED, + AppError::Forbidden => StatusCode::FORBIDDEN, + AppError::BadRequest(_) => StatusCode::BAD_REQUEST, + AppError::Config(_) | AppError::Database(_) | AppError::Upstream(_) => StatusCode::INTERNAL_SERVER_ERROR, + }; + let message = self.to_string(); + let body = Json(ErrorBody { + error: ErrorMessage { + message: &message, + kind: "server_error", + code: "codex_proxy_rs_error", + }, + }); + (status, body).into_response() + } +} + +pub type AppResult = Result; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/main.rs`: + +```rust +#[tokio::main] +async fn main() -> anyhow::Result<()> { + println!("codex-proxy-rs bootstrap"); + Ok(()) +} +``` + +- [x] **Step 5: Verify scaffold builds** + +Run: + +```bash +cargo test +cargo clippy --all-targets --all-features --locked -- -D warnings +``` + +Expected: both commands pass. + +- [x] **Step 6: Commit scaffold** + +```bash +git add Cargo.toml rust-toolchain.toml src README.md +git commit -m "chore: scaffold codex-proxy-rs" +``` + +--- + +### Task 1A: API Contract and Documentation Baseline + +**Files:** +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/docs/api.md` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/docs/status-codes.md` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/docs/dependency-policy.md` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/docs/implementation-status.md` + +- [x] **Step 1: Create API contract document** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/docs/api.md`: + +````markdown +# API Contract + +`codex-proxy-rs` exposes two API families. + +## `/v1/*` + +These endpoints are OpenAI-compatible and are authenticated only by client API keys using `Authorization: Bearer cpr_...`. +Responses include an `X-Request-Id` header for tracing, but the body stays OpenAI-compatible and does not include a custom `requestId` field. + +Error body: + +```json +{ + "error": { + "message": "Invalid client API key", + "type": "invalid_request_error", + "param": null, + "code": "invalid_api_key" + } +} +``` + +Streaming errors after SSE has started use: + +```text +event: error +data: {"error":{"message":"Upstream failed","type":"server_error","param":null,"code":"upstream_error"}} +``` + +## `/admin/*` + +Admin endpoints are authenticated only by HttpOnly admin session cookies. +Admin JSON uses lower camelCase field names. Every admin response includes an `X-Request-Id` header, and every JSON body includes `requestId`. + +Use real HTTP status codes and body-level frontend codes together. Do not return HTTP `200` for failed requests. The body `code` exists for frontend branching; the HTTP status remains the transport truth. + +Success body: + +```json +{ + "code": 200, + "message": "OK", + "data": {}, + "requestId": "req_01" +} +``` + +List body: + +```json +{ + "code": 200, + "message": "OK", + "data": [], + "page": { + "limit": 50, + "nextCursor": null + }, + "requestId": "req_01" +} +``` + +Error body: + +```json +{ + "code": 40101, + "message": "Admin login required", + "data": null, + "requestId": "req_01" +} +``` + +Pagination uses cursor ordering by `(created_at desc, id desc)`, default `limit=50`, max `limit=200`. + +Rust structs use `PascalCase` type names and `snake_case` fields internally, then expose lower camelCase through serde: + +```rust +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiEnvelope { + pub code: u32, + pub message: String, + pub data: T, + pub request_id: String, +} +``` +```` + +- [x] **Step 2: Create status-code document** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/docs/status-codes.md`: + +````markdown +# Status Codes + +HTTP status codes are not replaced by body codes. Admin/frontend APIs return both an accurate HTTP status and a JSON body `code` unless the status is `204 No Content`. + +## Health + +| Status | Meaning | +| --- | --- | +| `200` | Service is running. | + +## Admin and Frontend APIs + +| HTTP status | Body code range | Meaning | +| --- | --- | --- | +| `200` | `200` | Successful read, login, or mutation with response body. | +| `201` | `201` | Resource created. | +| `204` | `204` | Successful mutation with no response body. No JSON envelope is sent. | +| `400` | `40000`-`40099` | Malformed JSON, invalid parameters, or invalid cursor. | +| `401` | `40100`-`40199` | Missing/expired admin session or bad admin password. | +| `403` | `40300`-`40399` | Authenticated admin lacks permission for a local-only/bootstrap action. | +| `404` | `40400`-`40499` | Resource not found. | +| `409` | `40900`-`40999` | Duplicate resource or stale state transition. | +| `422` | `42200`-`42299` | Well-formed request failed domain validation. | +| `429` | `42900`-`42999` | Login or operation rate limit. | +| `500` | `50000`-`50099` | Internal service error. | + +Recommended initial body codes: + +| Body code | HTTP status | Meaning | +| --- | --- | --- | +| `40001` | `400` | Validation failed. | +| `40002` | `400` | Invalid cursor. | +| `40101` | `401` | Admin session required. | +| `40102` | `401` | Admin password invalid. | +| `40301` | `403` | Bootstrap action denied. | +| `40401` | `404` | Resource not found. | +| `40901` | `409` | Duplicate resource. | +| `42201` | `422` | Domain validation failed. | +| `42901` | `429` | Login rate limited. | +| `50001` | `500` | Internal service error. | + +## OpenAI-Compatible `/v1/*` + +| Status | Meaning | +| --- | --- | +| `200` | Successful response or SSE stream accepted. | +| `400` | Invalid OpenAI/Responses request body. | +| `401` | Missing or invalid client API key. | +| `404` | Requested model is not a supported Codex model. | +| `413` | Request payload too large for safe replay or forwarding. | +| `429` | Codex quota or rate limit surfaced to client. | +| `499` | Client aborted; log internally only. | +| `502` | Upstream Codex transport or protocol failure. | +| `503` | No usable Codex account is available. | +| `504` | Upstream timeout. | +```` + +- [x] **Step 3: Create dependency policy document** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/docs/dependency-policy.md`: + +````markdown +# Dependency Policy + +- Use current stable crate releases for normal Rust dependencies. +- Commit `Cargo.lock` because this is an application. +- Run `cargo update` in dedicated dependency-refresh changes. +- Keep `reqwest = 0.12.28` and `rustls = 0.23.36` pinned until a TLS fingerprint review proves a newer pair matches real Codex Desktop behavior. +- Do not add unmaintained crates when a maintained community-standard crate exists. +- Every dependency change must run: + +```bash +cargo fmt --check +cargo clippy --all-targets --all-features --locked -- -D warnings +cargo test +``` +```` + +- [x] **Step 4: Create implementation status document** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/docs/implementation-status.md`: + +````markdown +# Implementation Status + +Update this file before each feature commit. + +| Area | Status | Commit | Verification | Notes | +| --- | --- | --- | --- | --- | +| Scaffold | Planned | | | | +| API contract docs | Planned | | | | +| Configuration | Planned | | | | +| SQLite storage | Planned | | | | +| Admin auth and client API keys | Planned | | | | +| Secret encryption | Planned | | | | +| Logging and pagination | Planned | | | | +| TLS headers and fingerprint | Planned | | | | +| Cookie persistence | Planned | | | | +| Account pool and refresh | Planned | | | | +| Translation | Planned | | | | +| HTTP routes | Planned | | | | +| Upstream lifecycle | Planned | | | | +| Fingerprint updates | Planned | | | | +| Runtime docs and packaging | Planned | | | | +```` + +- [x] **Step 5: Commit API contract docs** + +```bash +git add docs/api.md docs/status-codes.md docs/dependency-policy.md docs/implementation-status.md docs/superpowers/plans/2026-06-11-codex-proxy-rs.md +git commit -m "docs: define api contracts and status codes" +``` + +--- + +### Task 2: Configuration and Application State + +**Files:** +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/config/default.yaml` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/config.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/state.rs` +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs` +- Test: `/home/zyy/桌面/Codes/codex-proxy-rs/tests/config_test.rs` + +- [ ] **Step 1: Write failing config test** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/tests/config_test.rs`: + +```rust +use codex_proxy_rs::config::AppConfig; + +#[test] +fn default_config_keeps_only_codex_backend() { + let yaml = r#" +server: + host: 127.0.0.1 + port: 8080 +api: + base_url: https://chatgpt.com/backend-api +auth: + refresh_margin_seconds: 300 + refresh_enabled: true + refresh_concurrency: 2 +database: + url: sqlite://data/codex-proxy-rs.sqlite +security: + master_key_file: data/master.key + api_key_pepper_file: data/api-key-pepper.key +tls: + force_http11: false +admin: + session_ttl_minutes: 1440 +logging: + directory: logs + max_file_bytes: 10485760 + retention_days: 14 +"#; + let cfg: AppConfig = serde_yaml::from_str(yaml).unwrap(); + assert_eq!(cfg.api.base_url, "https://chatgpt.com/backend-api"); + assert_eq!(cfg.auth.refresh_margin_seconds, 300); + assert_eq!(cfg.database.url, "sqlite://data/codex-proxy-rs.sqlite"); + assert_eq!(cfg.security.master_key_file, "data/master.key"); +} +``` + +- [ ] **Step 2: Run test and verify RED** + +Run: + +```bash +cargo test default_config_keeps_only_codex_backend +``` + +Expected: compile failure because `codex_proxy_rs::config` does not exist. + +- [ ] **Step 3: Implement config types** + +Modify `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs`: + +```rust +pub mod config; +pub mod error; +pub mod state; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/config.rs`: + +```rust +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct AppConfig { + pub server: ServerConfig, + pub api: ApiConfig, + pub auth: AuthConfig, + pub database: DatabaseConfig, + pub security: SecurityConfig, + pub tls: TlsConfig, + pub admin: AdminConfig, + pub logging: LoggingConfig, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct ServerConfig { + pub host: String, + pub port: u16, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct ApiConfig { + pub base_url: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct AuthConfig { + pub refresh_margin_seconds: u64, + pub refresh_enabled: bool, + pub refresh_concurrency: u32, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct DatabaseConfig { + pub url: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct SecurityConfig { + pub master_key_file: String, + pub api_key_pepper_file: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct TlsConfig { + pub force_http11: bool, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct AdminConfig { + pub session_ttl_minutes: u64, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct LoggingConfig { + pub directory: String, + pub max_file_bytes: u64, + pub retention_days: u64, +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/state.rs`: + +```rust +use std::sync::Arc; + +use crate::config::AppConfig; + +#[derive(Clone)] +pub struct AppState { + pub services: Arc, +} + +pub struct AppServices { + pub config: AppConfig, +} + +impl AppState { + pub fn new(config: AppConfig) -> Self { + Self { + services: Arc::new(AppServices { config }), + } + } + + pub fn config(&self) -> &AppConfig { + &self.services.config + } +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/config/default.yaml`: + +```yaml +server: + host: 127.0.0.1 + port: 8080 +api: + base_url: https://chatgpt.com/backend-api +auth: + refresh_margin_seconds: 300 + refresh_enabled: true + refresh_concurrency: 2 +database: + url: sqlite://data/codex-proxy-rs.sqlite +security: + master_key_file: data/master.key + api_key_pepper_file: data/api-key-pepper.key +tls: + force_http11: false +admin: + session_ttl_minutes: 1440 +logging: + directory: logs + max_file_bytes: 10485760 + retention_days: 14 +``` + +- [ ] **Step 4: Verify GREEN** + +Run: + +```bash +cargo test default_config_keeps_only_codex_backend +``` + +Expected: pass. + +- [ ] **Step 5: Commit config** + +```bash +git add config src tests/config_test.rs +git commit -m "feat: add codex-only configuration" +``` + +--- + +### Task 3: SQLite Storage with sqlx Migrations + +**Files:** +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/migrations/0001_initial.sql` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/migrations/0002_events_indexes.sql` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/storage/mod.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/storage/db.rs` +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs` +- Test: `/home/zyy/桌面/Codes/codex-proxy-rs/tests/storage_migration_test.rs` + +- [ ] **Step 1: Write failing migration test** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/tests/storage_migration_test.rs`: + +```rust +use codex_proxy_rs::storage::db::connect_sqlite; + +#[tokio::test] +async fn migrations_create_accounts_and_event_tables() { + let dir = tempfile::tempdir().unwrap(); + let db = dir.path().join("test.sqlite"); + let url = format!("sqlite://{}", db.display()); + let pool = connect_sqlite(&url).await.unwrap(); + + let row: (i64,) = sqlx::query_as("select count(*) from sqlite_master where type = 'table' and name in ('accounts', 'client_api_keys', 'event_logs')") + .fetch_one(&pool) + .await + .unwrap(); + assert_eq!(row.0, 3); +} +``` + +- [ ] **Step 2: Run test and verify RED** + +Run: + +```bash +cargo test migrations_create_accounts_and_event_tables +``` + +Expected: compile failure because `storage::db` does not exist. + +- [ ] **Step 3: Create migrations** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/migrations/0001_initial.sql`: + +```sql +pragma foreign_keys = on; + +create table if not exists admin_users ( + id text primary key, + password_hash text not null, + created_at text not null, + updated_at text not null +); + +create table if not exists admin_sessions ( + id text primary key, + user_id text not null references admin_users(id) on delete cascade, + expires_at text not null, + created_at text not null +); + +create table if not exists client_api_keys ( + id text primary key, + name text not null, + prefix text not null, + key_hash text not null, + enabled integer not null default 1, + created_at text not null, + last_used_at text +); + +create table if not exists accounts ( + id text primary key, + email text, + account_id text, + user_id text, + label text, + plan_type text, + access_token_cipher text not null, + refresh_token_cipher text, + status text not null, + quota_json text, + quota_fetched_at text, + added_at text not null, + updated_at text not null +); + +create table if not exists account_usage ( + account_id text primary key references accounts(id) on delete cascade, + request_count integer not null default 0, + input_tokens integer not null default 0, + output_tokens integer not null default 0, + cached_tokens integer not null default 0, + last_used_at text +); + +create table if not exists account_cookies ( + id text primary key, + account_id text not null references accounts(id) on delete cascade, + domain text not null, + name text not null, + value_cipher text not null, + path text not null default '/', + expires_at text, + updated_at text not null, + unique(account_id, domain, name, path) +); + +create table if not exists fingerprints ( + id text primary key, + app_version text not null, + build_number text not null, + platform text not null, + arch text not null, + chromium_version text not null, + user_agent_template text not null, + source text not null, + created_at text not null +); + +create table if not exists event_logs ( + id text primary key, + request_id text, + kind text not null, + level text not null, + account_id text, + route text, + model text, + status_code integer, + latency_ms integer, + message text not null, + metadata_json text not null, + created_at text not null +); +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/migrations/0002_events_indexes.sql`: + +```sql +create index if not exists idx_event_logs_created_id on event_logs(created_at desc, id desc); +create index if not exists idx_event_logs_kind_created on event_logs(kind, created_at desc); +create index if not exists idx_event_logs_request_id on event_logs(request_id); +create index if not exists idx_accounts_status on accounts(status); +create index if not exists idx_client_api_keys_prefix on client_api_keys(prefix); +create index if not exists idx_account_cookies_account on account_cookies(account_id); +``` + +- [ ] **Step 4: Implement database connector** + +Modify `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs`: + +```rust +pub mod config; +pub mod error; +pub mod state; +pub mod storage; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/storage/mod.rs`: + +```rust +pub mod db; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/storage/db.rs`: + +```rust +use sqlx::{sqlite::SqliteConnectOptions, SqlitePool}; +use std::{str::FromStr, time::Duration}; + +pub async fn connect_sqlite(database_url: &str) -> Result { + let options = SqliteConnectOptions::from_str(database_url)? + .create_if_missing(true) + .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) + .foreign_keys(true) + .busy_timeout(Duration::from_secs(5)); + let pool = SqlitePool::connect_with(options).await?; + sqlx::migrate!("./migrations").run(&pool).await?; + Ok(pool) +} +``` + +- [ ] **Step 5: Verify migrations** + +Run: + +```bash +cargo test migrations_create_accounts_and_event_tables +``` + +Expected: pass. + +- [ ] **Step 6: Commit storage** + +```bash +git add Cargo.toml migrations src/storage src/lib.rs tests/storage_migration_test.rs +git commit -m "feat: add sqlite storage schema" +``` + +--- + +### Task 4: Split Admin Login, Client API Keys, and Upstream Accounts + +**Files:** +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/auth/mod.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/auth/error.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/auth/admin_session.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/auth/api_key.rs` +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs` +- Test: `/home/zyy/桌面/Codes/codex-proxy-rs/tests/admin_auth_test.rs` +- Test: `/home/zyy/桌面/Codes/codex-proxy-rs/tests/api_key_auth_test.rs` + +- [ ] **Step 1: Write failing tests for separate secret types** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/tests/api_key_auth_test.rs`: + +```rust +use codex_proxy_rs::auth::api_key::ApiKeyHasher; + +#[test] +fn client_api_key_has_proxy_prefix_and_verifies_against_hash() { + let hasher = ApiKeyHasher::new([9u8; 32]); + let generated = hasher.generate_client_api_key("cursor"); + assert!(generated.plaintext.starts_with("cpr_")); + assert_eq!(generated.prefix.len(), 12); + assert!(hasher.verify_client_api_key(&generated.plaintext, &generated.key_hash).unwrap()); + assert!(!hasher.verify_client_api_key("wrong", &generated.key_hash).unwrap()); +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/tests/admin_auth_test.rs`: + +```rust +use codex_proxy_rs::auth::admin_session::{hash_admin_password, verify_admin_password}; + +#[test] +fn admin_password_hash_is_not_a_client_api_key() { + let hash = hash_admin_password("correct horse battery staple").unwrap(); + assert!(verify_admin_password("correct horse battery staple", &hash).unwrap()); + assert!(!verify_admin_password("cpr_fake_client_key", &hash).unwrap()); +} +``` + +- [ ] **Step 2: Run tests and verify RED** + +Run: + +```bash +cargo test client_api_key_has_proxy_prefix_and_verifies_against_hash admin_password_hash_is_not_a_client_api_key +``` + +Expected: compile failure because auth modules do not exist. + +- [ ] **Step 3: Implement hashing helpers** + +Modify `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs`: + +```rust +pub mod auth; +pub mod config; +pub mod crypto; +pub mod error; +pub mod state; +pub mod storage; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/auth/mod.rs`: + +```rust +pub mod admin_session; +pub mod api_key; +pub mod error; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/auth/error.rs`: + +```rust +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum AuthError { + #[error("password hash error: {0}")] + PasswordHash(#[from] argon2::password_hash::Error), + #[error("invalid api key encoding: {0}")] + ApiKeyEncoding(#[from] base64::DecodeError), + #[error("invalid api key pepper length")] + InvalidPepperLength, +} + +pub type AuthResult = Result; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/auth/admin_session.rs`: + +```rust +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Argon2, +}; + +use crate::auth::error::AuthResult; + +pub fn hash_admin_password(password: &str) -> AuthResult { + let salt = SaltString::generate(&mut OsRng); + Ok(Argon2::default().hash_password(password.as_bytes(), &salt)?.to_string()) +} + +pub fn verify_admin_password(password: &str, hash: &str) -> AuthResult { + let parsed = PasswordHash::new(hash)?; + Ok(Argon2::default().verify_password(password.as_bytes(), &parsed).is_ok()) +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/auth/api_key.rs`: + +```rust +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; +use hmac::{Hmac, Mac}; +use rand::RngCore; +use sha2::Sha256; + +use crate::auth::error::{AuthError, AuthResult}; + +type HmacSha256 = Hmac; + +#[derive(Debug, Clone)] +pub struct GeneratedClientApiKey { + pub plaintext: String, + pub prefix: String, + pub key_hash: String, +} + +#[derive(Debug, Clone)] +pub struct ApiKeyHasher { + pepper: [u8; 32], +} + +impl ApiKeyHasher { + pub fn new(pepper: [u8; 32]) -> Self { + Self { pepper } + } + + pub fn try_from_slice(pepper: &[u8]) -> AuthResult { + let pepper: [u8; 32] = pepper.try_into().map_err(|_| AuthError::InvalidPepperLength)?; + Ok(Self::new(pepper)) + } + + pub fn generate_client_api_key(&self, _name: &str) -> GeneratedClientApiKey { + let mut bytes = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut bytes); + let plaintext = format!("cpr_{}", URL_SAFE_NO_PAD.encode(bytes)); + let prefix = plaintext.chars().take(12).collect::(); + let key_hash = self.hash_client_api_key(&plaintext); + GeneratedClientApiKey { plaintext, prefix, key_hash } + } + + pub fn hash_client_api_key(&self, plaintext: &str) -> String { + let mut mac = HmacSha256::new_from_slice(&self.pepper).expect("HMAC accepts any key size"); + mac.update(plaintext.as_bytes()); + URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes()) + } + + pub fn verify_client_api_key(&self, plaintext: &str, key_hash: &str) -> AuthResult { + if !plaintext.starts_with("cpr_") { + return Ok(false); + } + let suffix = plaintext.strip_prefix("cpr_").unwrap_or_default(); + let decoded = URL_SAFE_NO_PAD.decode(suffix)?; + let candidate = format!("cpr_{}", URL_SAFE_NO_PAD.encode(decoded)); + Ok(self.hash_client_api_key(&candidate) == key_hash) + } +} +``` + +- [ ] **Step 4: Verify auth split tests** + +Run: + +```bash +cargo test client_api_key_has_proxy_prefix_and_verifies_against_hash admin_password_hash_is_not_a_client_api_key +``` + +Expected: pass. + +- [ ] **Step 5: Commit auth split** + +```bash +git add Cargo.toml src/auth src/lib.rs tests/admin_auth_test.rs tests/api_key_auth_test.rs +git commit -m "feat: split admin login and client api keys" +``` + +--- + +### Task 4A: Encrypt Stored Upstream Secrets + +**Files:** +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/crypto.rs` +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs` +- Test: `/home/zyy/桌面/Codes/codex-proxy-rs/tests/crypto_test.rs` + +- [ ] **Step 1: Write failing encryption test** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/tests/crypto_test.rs`: + +```rust +use codex_proxy_rs::crypto::SecretBox; +use secrecy::{ExposeSecret, SecretString}; + +#[test] +fn secret_box_encrypts_and_decrypts_without_storing_plaintext() { + let secret_box = SecretBox::new([7u8; 32]); + let plaintext = SecretString::new("rt_example_refresh_token".to_string()); + let ciphertext = secret_box.encrypt(&plaintext).unwrap(); + + assert!(ciphertext.starts_with("v1:")); + assert!(!ciphertext.contains("rt_example_refresh_token")); + assert_eq!(secret_box.decrypt(&ciphertext).unwrap().expose_secret(), "rt_example_refresh_token"); +} +``` + +- [ ] **Step 2: Run test and verify RED** + +Run: + +```bash +cargo test secret_box_encrypts_and_decrypts_without_storing_plaintext +``` + +Expected: compile failure because `codex_proxy_rs::crypto::SecretBox` does not exist. + +- [ ] **Step 3: Implement AES-GCM secret encryption** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/crypto.rs`: + +```rust +use aes_gcm::{ + aead::{Aead, OsRng, rand_core::RngCore}, + Aes256Gcm, KeyInit, Nonce, +}; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; +use secrecy::{ExposeSecret, SecretString}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum CryptoError { + #[error("invalid secret key length")] + InvalidKeyLength, + #[error("secret encryption failed")] + Encrypt, + #[error("secret decryption failed")] + Decrypt, + #[error("invalid secret encoding: {0}")] + Decode(#[from] base64::DecodeError), + #[error("unsupported secret version")] + UnsupportedVersion, + #[error("secret is not valid utf-8: {0}")] + Utf8(#[from] std::string::FromUtf8Error), +} + +pub type CryptoResult = Result; + +#[derive(Clone)] +pub struct SecretBox { + key: [u8; 32], +} + +impl SecretBox { + pub fn new(key: [u8; 32]) -> Self { + Self { key } + } + + pub fn encrypt(&self, plaintext: &SecretString) -> CryptoResult { + let cipher = Aes256Gcm::new_from_slice(&self.key) + .map_err(|_| CryptoError::InvalidKeyLength)?; + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let ciphertext = cipher + .encrypt(Nonce::from_slice(&nonce_bytes), plaintext.expose_secret().as_bytes()) + .map_err(|_| CryptoError::Encrypt)?; + Ok(format!( + "v1:{}:{}", + URL_SAFE_NO_PAD.encode(nonce_bytes), + URL_SAFE_NO_PAD.encode(ciphertext) + )) + } + + pub fn decrypt(&self, encoded: &str) -> CryptoResult { + let mut parts = encoded.split(':'); + let version = parts.next().unwrap_or_default(); + let nonce = parts.next().unwrap_or_default(); + let ciphertext = parts.next().unwrap_or_default(); + if version != "v1" { + return Err(CryptoError::UnsupportedVersion); + } + let nonce = URL_SAFE_NO_PAD.decode(nonce)?; + let ciphertext = URL_SAFE_NO_PAD.decode(ciphertext)?; + let cipher = Aes256Gcm::new_from_slice(&self.key) + .map_err(|_| CryptoError::InvalidKeyLength)?; + let plaintext = cipher + .decrypt(Nonce::from_slice(&nonce), ciphertext.as_ref()) + .map_err(|_| CryptoError::Decrypt)?; + Ok(SecretString::new(String::from_utf8(plaintext)?)) + } +} +``` + +Modify `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs`: + +```rust +pub mod auth; +pub mod config; +pub mod crypto; +pub mod error; +pub mod state; +pub mod storage; +``` + +- [ ] **Step 4: Verify encryption** + +Run: + +```bash +cargo test secret_box_encrypts_and_decrypts_without_storing_plaintext +``` + +Expected: pass. + +- [ ] **Step 5: Commit secret encryption** + +```bash +git add Cargo.toml src/crypto.rs src/lib.rs tests/crypto_test.rs +git commit -m "feat: encrypt stored upstream secrets" +``` + +--- + +### Task 5: Structured Logging, Event Store, Rotation, and Pagination + +**Files:** +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/pagination.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/logs/mod.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/logs/event.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/logs/repository.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/logs/rotation.rs` +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs` +- Test: `/home/zyy/桌面/Codes/codex-proxy-rs/tests/logs_pagination_test.rs` + +- [ ] **Step 1: Write failing pagination test** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/tests/logs_pagination_test.rs`: + +```rust +use codex_proxy_rs::{ + logs::{event::{EventLevel, EventLog}, repository::EventLogRepository}, + storage::db::connect_sqlite, +}; + +#[tokio::test] +async fn event_logs_are_cursor_paginated() { + let dir = tempfile::tempdir().unwrap(); + let db = dir.path().join("logs.sqlite"); + let url = format!("sqlite://{}", db.display()); + let pool = connect_sqlite(&url).await.unwrap(); + let repo = EventLogRepository::new(pool); + + for idx in 0..3 { + repo.insert(EventLog::new("request", EventLevel::Info, format!("event {idx}"))).await.unwrap(); + } + + let first = repo.list(None, 2).await.unwrap(); + assert_eq!(first.items.len(), 2); + assert!(first.next_cursor.is_some()); + + let second = repo.list(first.next_cursor, 2).await.unwrap(); + assert_eq!(second.items.len(), 1); + assert!(second.next_cursor.is_none()); +} +``` + +- [ ] **Step 2: Run test and verify RED** + +Run: + +```bash +cargo test event_logs_are_cursor_paginated +``` + +Expected: compile failure because logs modules do not exist. + +- [ ] **Step 3: Implement pagination and event models** + +Modify `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs`: + +```rust +pub mod auth; +pub mod config; +pub mod crypto; +pub mod error; +pub mod logs; +pub mod pagination; +pub mod state; +pub mod storage; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/pagination.rs`: + +```rust +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Page { + pub items: Vec, + pub next_cursor: Option, +} + +pub fn encode_cursor(created_at: &str, id: &str) -> String { + URL_SAFE_NO_PAD.encode(format!("{created_at}|{id}")) +} + +pub fn decode_cursor(cursor: &str) -> Option<(String, String)> { + let raw = URL_SAFE_NO_PAD.decode(cursor).ok()?; + let text = String::from_utf8(raw).ok()?; + let (created_at, id) = text.split_once('|')?; + Some((created_at.to_string(), id.to_string())) +} + +pub fn clamp_limit(limit: u32) -> u32 { + limit.clamp(1, 200) +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/logs/mod.rs`: + +```rust +pub mod event; +pub mod repository; +pub mod rotation; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/logs/event.rs`: + +```rust +use chrono::Utc; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub enum EventLevel { + Debug, + Info, + Warn, + Error, +} + +impl EventLevel { + pub fn as_str(self) -> &'static str { + match self { + EventLevel::Debug => "debug", + EventLevel::Info => "info", + EventLevel::Warn => "warn", + EventLevel::Error => "error", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EventLog { + pub id: String, + pub kind: String, + pub level: EventLevel, + pub message: String, + pub created_at: String, +} + +impl EventLog { + pub fn new(kind: impl Into, level: EventLevel, message: impl Into) -> Self { + Self { + id: Uuid::new_v4().to_string(), + kind: kind.into(), + level, + message: message.into(), + created_at: Utc::now().to_rfc3339(), + } + } +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/logs/repository.rs`: + +```rust +use sqlx::{Row, SqlitePool}; + +use crate::{ + logs::event::{EventLevel, EventLog}, + pagination::{clamp_limit, decode_cursor, encode_cursor, Page}, +}; + +#[derive(Clone)] +pub struct EventLogRepository { + pool: SqlitePool, +} + +impl EventLogRepository { + pub fn new(pool: SqlitePool) -> Self { + Self { pool } + } + + pub async fn insert(&self, event: EventLog) -> Result<(), sqlx::Error> { + sqlx::query( + "insert into event_logs (id, kind, level, message, metadata_json, created_at) values (?, ?, ?, ?, '{}', ?)", + ) + .bind(event.id) + .bind(event.kind) + .bind(event.level.as_str()) + .bind(event.message) + .bind(event.created_at) + .execute(&self.pool) + .await?; + Ok(()) + } + + pub async fn list(&self, cursor: Option, limit: u32) -> Result, sqlx::Error> { + let limit = clamp_limit(limit); + let mut rows = if let Some(cursor) = cursor.and_then(|c| decode_cursor(&c)) { + sqlx::query( + "select id, kind, level, message, created_at from event_logs where (created_at < ? or (created_at = ? and id < ?)) order by created_at desc, id desc limit ?", + ) + .bind(&cursor.0) + .bind(&cursor.0) + .bind(&cursor.1) + .bind((limit + 1) as i64) + .fetch_all(&self.pool) + .await? + } else { + sqlx::query("select id, kind, level, message, created_at from event_logs order by created_at desc, id desc limit ?") + .bind((limit + 1) as i64) + .fetch_all(&self.pool) + .await? + }; + + let has_next = rows.len() > limit as usize; + if has_next { + rows.truncate(limit as usize); + } + let items = rows + .into_iter() + .map(|row| EventLog { + id: row.get("id"), + kind: row.get("kind"), + level: match row.get::("level").as_str() { + "debug" => EventLevel::Debug, + "warn" => EventLevel::Warn, + "error" => EventLevel::Error, + _ => EventLevel::Info, + }, + message: row.get("message"), + created_at: row.get("created_at"), + }) + .collect::>(); + let next_cursor = if has_next { + items.last().map(|e| encode_cursor(&e.created_at, &e.id)) + } else { + None + }; + Ok(Page { items, next_cursor }) + } +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/logs/rotation.rs`: + +```rust +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone)] +pub struct RotationConfig { + pub directory: PathBuf, + pub max_file_bytes: u64, + pub retention_days: u64, +} + +impl RotationConfig { + pub fn new(directory: impl AsRef, max_file_bytes: u64, retention_days: u64) -> Self { + Self { + directory: directory.as_ref().to_path_buf(), + max_file_bytes, + retention_days, + } + } +} +``` + +- [ ] **Step 4: Verify event pagination** + +Run: + +```bash +cargo test event_logs_are_cursor_paginated +``` + +Expected: pass. + +- [ ] **Step 5: Commit logging foundation** + +```bash +git add src/logs src/pagination.rs src/lib.rs tests/logs_pagination_test.rs +git commit -m "feat: add paginated event logs" +``` + +--- + +### Task 6: Codex Fingerprint, Header Generation, and TLS Client + +**Files:** +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/fingerprint/mod.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/fingerprint/model.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/codex/mod.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/codex/headers.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/codex/client.rs` +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs` +- Test: `/home/zyy/桌面/Codes/codex-proxy-rs/tests/codex_headers_test.rs` + +- [ ] **Step 1: Write failing header test** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/tests/codex_headers_test.rs`: + +```rust +use codex_proxy_rs::{ + codex::headers::build_codex_headers, + fingerprint::model::Fingerprint, +}; + +#[test] +fn codex_headers_include_desktop_identity_and_turn_state() { + let fp = Fingerprint::default_for_tests(); + let headers = build_codex_headers(&fp, "access-token", Some("acct_123"), Some("turn-state"), "rid_1"); + + assert_eq!(headers.get("originator").unwrap(), "Codex Desktop"); + assert!(headers.get("user-agent").unwrap().contains("Codex")); + assert_eq!(headers.get("authorization").unwrap(), "Bearer access-token"); + assert_eq!(headers.get("chatgpt-account-id").unwrap(), "acct_123"); + assert_eq!(headers.get("x-codex-turn-state").unwrap(), "turn-state"); + assert_eq!(headers.get("x-client-request-id").unwrap(), "rid_1"); +} +``` + +- [ ] **Step 2: Run test and verify RED** + +Run: + +```bash +cargo test codex_headers_include_desktop_identity_and_turn_state +``` + +Expected: compile failure because `codex` and `fingerprint` modules do not exist. + +- [ ] **Step 3: Implement fingerprint and headers** + +Modify `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs`: + +```rust +pub mod auth; +pub mod codex; +pub mod config; +pub mod crypto; +pub mod error; +pub mod fingerprint; +pub mod logs; +pub mod pagination; +pub mod state; +pub mod storage; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/fingerprint/mod.rs`: + +```rust +pub mod model; +pub mod updater; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/fingerprint/model.rs`: + +```rust +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Fingerprint { + pub originator: String, + pub app_version: String, + pub build_number: String, + pub platform: String, + pub arch: String, + pub chromium_version: String, + pub user_agent_template: String, +} + +impl Fingerprint { + pub fn default_for_tests() -> Self { + Self { + originator: "Codex Desktop".to_string(), + app_version: "26.519.81530".to_string(), + build_number: "3178".to_string(), + platform: "darwin".to_string(), + arch: "arm64".to_string(), + chromium_version: "146".to_string(), + user_agent_template: "Codex/{app_version} ({platform}; {arch}) Chromium/{chromium_version}".to_string(), + } + } + + pub fn user_agent(&self) -> String { + self.user_agent_template + .replace("{app_version}", &self.app_version) + .replace("{platform}", &self.platform) + .replace("{arch}", &self.arch) + .replace("{chromium_version}", &self.chromium_version) + } +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/codex/mod.rs`: + +```rust +pub mod client; +pub mod headers; +pub mod sse; +pub mod types; +pub mod usage; +pub mod websocket; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/codex/headers.rs`: + +```rust +use std::collections::BTreeMap; + +use crate::fingerprint::model::Fingerprint; + +pub fn build_codex_headers( + fp: &Fingerprint, + access_token: &str, + account_id: Option<&str>, + turn_state: Option<&str>, + request_id: &str, +) -> BTreeMap { + let mut headers = BTreeMap::new(); + headers.insert("accept".to_string(), "text/event-stream".to_string()); + headers.insert("authorization".to_string(), format!("Bearer {access_token}")); + headers.insert("originator".to_string(), fp.originator.clone()); + headers.insert("user-agent".to_string(), fp.user_agent()); + headers.insert("x-client-request-id".to_string(), request_id.to_string()); + headers.insert("x-openai-internal-codex-residency".to_string(), "global".to_string()); + if let Some(account_id) = account_id { + headers.insert("chatgpt-account-id".to_string(), account_id.to_string()); + } + if let Some(turn_state) = turn_state { + headers.insert("x-codex-turn-state".to_string(), turn_state.to_string()); + } + headers +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/codex/client.rs`: + +```rust +use reqwest::Client; + +pub fn build_reqwest_client(force_http11: bool) -> Result { + let builder = Client::builder() + .use_rustls_tls() + .no_proxy() + .gzip(true) + .brotli(true) + .zstd(true) + .deflate(true); + let builder = if force_http11 { builder.http1_only() } else { builder }; + builder.build() +} +``` + +Create empty module files: + +```bash +touch src/fingerprint/updater.rs src/codex/sse.rs src/codex/types.rs src/codex/usage.rs src/codex/websocket.rs +``` + +- [ ] **Step 4: Verify headers** + +Run: + +```bash +cargo test codex_headers_include_desktop_identity_and_turn_state +cargo tree | rg 'reqwest|rustls' +``` + +Expected: test passes; `cargo tree` shows `reqwest v0.12.28` and `rustls v0.23.36`. + +- [ ] **Step 5: Commit TLS and headers foundation** + +```bash +git add Cargo.toml src/codex src/fingerprint src/lib.rs tests/codex_headers_test.rs +git commit -m "feat: add codex tls client and headers" +``` + +--- + +### Task 7: Account Cookie Persistence + +**Files:** +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/cookies/mod.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/cookies/jar.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/cookies/repository.rs` +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs` +- Test: `/home/zyy/桌面/Codes/codex-proxy-rs/tests/cookie_store_test.rs` + +- [ ] **Step 1: Write failing cookie replay test** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/tests/cookie_store_test.rs`: + +```rust +use codex_proxy_rs::cookies::jar::CookieJar; + +#[test] +fn cookie_jar_captures_and_replays_account_scoped_cookies() { + let mut jar = CookieJar::default(); + jar.capture_set_cookie("acct_a", "cf_clearance=abc; Domain=chatgpt.com; Path=/; HttpOnly"); + jar.capture_set_cookie("acct_b", "cf_clearance=def; Domain=chatgpt.com; Path=/; HttpOnly"); + + assert_eq!(jar.cookie_header("acct_a", "chatgpt.com"), Some("cf_clearance=abc".to_string())); + assert_eq!(jar.cookie_header("acct_b", "chatgpt.com"), Some("cf_clearance=def".to_string())); +} +``` + +- [ ] **Step 2: Run test and verify RED** + +Run: + +```bash +cargo test cookie_jar_captures_and_replays_account_scoped_cookies +``` + +Expected: compile failure because cookies module does not exist. + +- [ ] **Step 3: Implement in-memory cookie jar** + +Modify `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs`: + +```rust +pub mod auth; +pub mod codex; +pub mod config; +pub mod cookies; +pub mod crypto; +pub mod error; +pub mod fingerprint; +pub mod logs; +pub mod pagination; +pub mod state; +pub mod storage; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/cookies/mod.rs`: + +```rust +pub mod jar; +pub mod repository; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/cookies/jar.rs`: + +```rust +use std::collections::HashMap; + +#[derive(Debug, Clone)] +struct StoredCookie { + domain: String, + name: String, + value: String, +} + +#[derive(Debug, Default, Clone)] +pub struct CookieJar { + by_account: HashMap>, +} + +impl CookieJar { + pub fn capture_set_cookie(&mut self, account_id: &str, raw: &str) { + let mut parts = raw.split(';').map(str::trim); + let Some(name_value) = parts.next() else { return }; + let Some((name, value)) = name_value.split_once('=') else { return }; + let mut domain = "chatgpt.com".to_string(); + for part in parts { + if let Some(value) = part.strip_prefix("Domain=") { + domain = value.trim_start_matches('.').to_string(); + } + } + let account = self.by_account.entry(account_id.to_string()).or_default(); + account.retain(|cookie| !(cookie.domain == domain && cookie.name == name)); + account.push(StoredCookie { + domain, + name: name.to_string(), + value: value.to_string(), + }); + } + + pub fn cookie_header(&self, account_id: &str, domain: &str) -> Option { + let cookies = self.by_account.get(account_id)?; + let pairs = cookies + .iter() + .filter(|cookie| domain == cookie.domain || domain.ends_with(&format!(".{}", cookie.domain))) + .map(|cookie| format!("{}={}", cookie.name, cookie.value)) + .collect::>(); + if pairs.is_empty() { None } else { Some(pairs.join("; ")) } + } +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/cookies/repository.rs`: + +```rust +#[derive(Debug, Clone)] +pub struct CookieRepository; +``` + +- [ ] **Step 4: Verify cookie capture** + +Run: + +```bash +cargo test cookie_jar_captures_and_replays_account_scoped_cookies +``` + +Expected: pass. + +- [ ] **Step 5: Commit cookies** + +```bash +git add src/cookies src/lib.rs tests/cookie_store_test.rs +git commit -m "feat: add account scoped cookie jar" +``` + +--- + +### Task 8: Account Model, Pool, and Refresh Scheduler Compatibility + +**Files:** +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/accounts/mod.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/accounts/model.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/accounts/pool.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/accounts/repository.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/accounts/lifecycle.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/auth/refresh.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/auth/token.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/auth/oauth.rs` +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs` +- Test: `/home/zyy/桌面/Codes/codex-proxy-rs/tests/refresh_scheduler_test.rs` + +- [ ] **Step 1: Write failing account acquisition test** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/tests/refresh_scheduler_test.rs`: + +```rust +use codex_proxy_rs::accounts::{ + model::{Account, AccountStatus}, + pool::AccountPool, +}; + +#[test] +fn account_pool_skips_expired_disabled_banned_and_quota_exhausted_accounts() { + let mut pool = AccountPool::default(); + pool.insert(Account::test("active", AccountStatus::Active)); + pool.insert(Account::test("expired", AccountStatus::Expired)); + pool.insert(Account::test("disabled", AccountStatus::Disabled)); + pool.insert(Account::test("banned", AccountStatus::Banned)); + pool.insert(Account::test("quota", AccountStatus::QuotaExhausted)); + + let acquired = pool.acquire("gpt-5.4").unwrap(); + assert_eq!(acquired.id, "active"); +} +``` + +- [ ] **Step 2: Run test and verify RED** + +Run: + +```bash +cargo test account_pool_skips_expired_disabled_banned_and_quota_exhausted_accounts +``` + +Expected: compile failure because accounts module does not exist. + +- [ ] **Step 3: Implement account status and pool** + +Modify `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs`: + +```rust +pub mod accounts; +pub mod auth; +pub mod codex; +pub mod config; +pub mod cookies; +pub mod crypto; +pub mod error; +pub mod fingerprint; +pub mod logs; +pub mod pagination; +pub mod state; +pub mod storage; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/accounts/mod.rs`: + +```rust +pub mod lifecycle; +pub mod model; +pub mod pool; +pub mod repository; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/accounts/model.rs`: + +```rust +use chrono::Utc; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub enum AccountStatus { + Active, + Expired, + QuotaExhausted, + Refreshing, + Disabled, + Banned, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Account { + pub id: String, + pub email: Option, + pub account_id: Option, + pub user_id: Option, + pub label: Option, + pub plan_type: Option, + pub access_token: String, + pub refresh_token: Option, + pub status: AccountStatus, + pub added_at: String, + pub last_used_at: Option, +} + +impl Account { + pub fn test(id: &str, status: AccountStatus) -> Self { + Self { + id: id.to_string(), + email: None, + account_id: None, + user_id: None, + label: None, + plan_type: None, + access_token: format!("token-{id}"), + refresh_token: Some(format!("refresh-{id}")), + status, + added_at: Utc::now().to_rfc3339(), + last_used_at: None, + } + } +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/accounts/pool.rs`: + +```rust +use std::collections::BTreeMap; + +use crate::accounts::model::{Account, AccountStatus}; + +#[derive(Debug, Default)] +pub struct AccountPool { + accounts: BTreeMap, +} + +impl AccountPool { + pub fn insert(&mut self, account: Account) { + self.accounts.insert(account.id.clone(), account); + } + + pub fn acquire(&self, _model: &str) -> Option { + self.accounts + .values() + .filter(|account| account.status == AccountStatus::Active) + .min_by_key(|account| account.last_used_at.clone()) + .cloned() + } +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/accounts/repository.rs`: + +```rust +#[derive(Debug, Clone)] +pub struct AccountRepository; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/accounts/lifecycle.rs`: + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AccountLifecycleEvent { + Added, + Refreshed, + Expired, + QuotaExhausted, + Banned, + Disabled, +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/auth/refresh.rs`: + +```rust +#[derive(Debug, Clone, Copy)] +pub struct RefreshPolicy { + pub refresh_margin_seconds: u64, + pub refresh_concurrency: u32, +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/auth/token.rs`: + +```rust +#[derive(Debug, Clone)] +pub struct TokenPair { + pub access_token: String, + pub refresh_token: Option, +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/auth/oauth.rs`: + +```rust +#[derive(Debug, Clone)] +pub struct OAuthConfig { + pub client_id: String, + pub auth_endpoint: String, + pub token_endpoint: String, +} +``` + +Modify `/home/zyy/桌面/Codes/codex-proxy-rs/src/auth/mod.rs`: + +```rust +pub mod admin_session; +pub mod api_key; +pub mod oauth; +pub mod refresh; +pub mod token; +``` + +- [ ] **Step 4: Verify account pool behavior** + +Run: + +```bash +cargo test account_pool_skips_expired_disabled_banned_and_quota_exhausted_accounts +``` + +Expected: pass. + +- [ ] **Step 5: Commit account foundation** + +```bash +git add src/accounts src/auth src/lib.rs tests/refresh_scheduler_test.rs +git commit -m "feat: add account pool foundation" +``` + +--- + +### Task 9: Translation Layer for OpenAI Chat and Codex Responses + +**Files:** +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/translation/mod.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/translation/schema.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/translation/openai_to_codex.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/translation/codex_to_openai.rs` +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/codex/types.rs` +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs` +- Test: `/home/zyy/桌面/Codes/codex-proxy-rs/tests/routes_chat_test.rs` + +- [ ] **Step 1: Write failing translation test** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/tests/routes_chat_test.rs`: + +```rust +use codex_proxy_rs::translation::openai_to_codex::{translate_chat_to_codex, ChatCompletionRequest, ChatMessage}; + +#[test] +fn chat_completion_translates_to_codex_response_request() { + let req = ChatCompletionRequest { + model: "gpt-5.4".to_string(), + stream: true, + messages: vec![ChatMessage { + role: "user".to_string(), + content: "hello".to_string(), + }], + }; + + let codex = translate_chat_to_codex(req).unwrap(); + assert_eq!(codex.model, "gpt-5.4"); + assert!(codex.stream); + assert_eq!(codex.input[0]["role"], "user"); +} +``` + +- [ ] **Step 2: Run test and verify RED** + +Run: + +```bash +cargo test chat_completion_translates_to_codex_response_request +``` + +Expected: compile failure because translation module does not exist. + +- [ ] **Step 3: Implement minimal translation types** + +Modify `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs`: + +```rust +pub mod accounts; +pub mod auth; +pub mod codex; +pub mod config; +pub mod cookies; +pub mod crypto; +pub mod error; +pub mod fingerprint; +pub mod logs; +pub mod pagination; +pub mod state; +pub mod storage; +pub mod translation; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/translation/mod.rs`: + +```rust +pub mod codex_to_openai; +pub mod openai_to_codex; +pub mod schema; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/codex/types.rs`: + +```rust +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CodexResponsesRequest { + pub model: String, + pub instructions: String, + pub input: Vec, + pub stream: bool, + pub store: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tools: Option>, +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/translation/openai_to_codex.rs`: + +```rust +use serde::{Deserialize, Serialize}; +use serde_json::json; + +use crate::{codex::types::CodexResponsesRequest, error::{AppError, AppResult}}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatCompletionRequest { + pub model: String, + #[serde(default)] + pub stream: bool, + pub messages: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatMessage { + pub role: String, + pub content: String, +} + +pub fn translate_chat_to_codex(req: ChatCompletionRequest) -> AppResult { + if req.messages.is_empty() { + return Err(AppError::BadRequest("messages must not be empty".to_string())); + } + let input = req.messages + .into_iter() + .map(|message| json!({ "role": message.role, "content": message.content })) + .collect(); + Ok(CodexResponsesRequest { + model: req.model, + instructions: String::new(), + input, + stream: req.stream, + store: false, + reasoning: None, + tools: None, + }) +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/translation/codex_to_openai.rs`: + +```rust +use serde_json::{json, Value}; + +pub fn openai_error(message: &str, code: &str) -> Value { + json!({ + "error": { + "message": message, + "type": "server_error", + "param": null, + "code": code + } + }) +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/translation/schema.rs`: + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResponseFormat { + Text, + JsonObject, + JsonSchema, +} +``` + +- [ ] **Step 4: Verify translation** + +Run: + +```bash +cargo test chat_completion_translates_to_codex_response_request +``` + +Expected: pass. + +- [ ] **Step 5: Commit translation layer** + +```bash +git add src/translation src/codex/types.rs src/lib.rs tests/routes_chat_test.rs +git commit -m "feat: add openai to codex translation" +``` + +--- + +### Task 10: HTTP Router and Auth Gates + +**Files:** +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/app.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/http/mod.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/http/middleware.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/http/admin.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/http/auth.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/http/v1.rs` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/src/http/health.rs` +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs` +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/main.rs` +- Test: `/home/zyy/桌面/Codes/codex-proxy-rs/tests/routes_responses_test.rs` + +- [ ] **Step 1: Write failing route boundary test** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/tests/routes_responses_test.rs`: + +```rust +use axum::{body::Body, http::{Request, StatusCode}}; +use tower::ServiceExt; + +use codex_proxy_rs::{app::build_router, config::{AdminConfig, ApiConfig, AppConfig, AuthConfig, DatabaseConfig, LoggingConfig, SecurityConfig, ServerConfig, TlsConfig}, state::AppState}; + +fn test_config() -> AppConfig { + AppConfig { + server: ServerConfig { host: "127.0.0.1".to_string(), port: 0 }, + api: ApiConfig { base_url: "https://chatgpt.com/backend-api".to_string() }, + auth: AuthConfig { refresh_margin_seconds: 300, refresh_enabled: true, refresh_concurrency: 2 }, + database: DatabaseConfig { url: "sqlite://:memory:".to_string() }, + security: SecurityConfig { + master_key_file: "data/master.key".to_string(), + api_key_pepper_file: "data/api-key-pepper.key".to_string(), + }, + tls: TlsConfig { force_http11: false }, + admin: AdminConfig { session_ttl_minutes: 1440 }, + logging: LoggingConfig { directory: "logs".to_string(), max_file_bytes: 10_485_760, retention_days: 14 }, + } +} + +#[tokio::test] +async fn v1_requires_client_api_key_not_admin_cookie() { + let app = build_router(AppState::new(test_config())); + let response = app + .oneshot(Request::builder().method("POST").uri("/v1/responses").body(Body::from("{}")).unwrap()) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); +} +``` + +- [ ] **Step 2: Run test and verify RED** + +Run: + +```bash +cargo test v1_requires_client_api_key_not_admin_cookie +``` + +Expected: compile failure because app/http modules do not exist. + +- [ ] **Step 3: Implement minimal router** + +Modify `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs`: + +```rust +pub mod accounts; +pub mod app; +pub mod auth; +pub mod codex; +pub mod config; +pub mod cookies; +pub mod crypto; +pub mod error; +pub mod fingerprint; +pub mod http; +pub mod logs; +pub mod pagination; +pub mod state; +pub mod storage; +pub mod translation; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/app.rs`: + +```rust +use axum::{routing::{get, post}, Router}; + +use crate::{http::{health::health, v1::responses}, state::AppState}; + +pub fn build_router(state: AppState) -> Router { + Router::new() + .route("/health", get(health)) + .route("/v1/responses", post(responses)) + .route("/v1/chat/completions", post(responses)) + .with_state(state) +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/http/mod.rs`: + +```rust +pub mod admin; +pub mod auth; +pub mod health; +pub mod middleware; +pub mod v1; +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/http/health.rs`: + +```rust +use axum::Json; +use serde_json::{json, Value}; + +pub async fn health() -> Json { + Json(json!({ "status": "ok" })) +} +``` + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/http/v1.rs`: + +```rust +use axum::{http::StatusCode, response::IntoResponse, Json}; + +use crate::translation::codex_to_openai::openai_error; + +pub async fn responses() -> impl IntoResponse { + ( + StatusCode::UNAUTHORIZED, + Json(openai_error("Missing client API key", "invalid_api_key")), + ) +} +``` + +Create empty route module files: + +```bash +touch src/http/admin.rs src/http/auth.rs src/http/middleware.rs +``` + +Modify `/home/zyy/桌面/Codes/codex-proxy-rs/src/main.rs`: + +```rust +use codex_proxy_rs::{app::build_router, config::{AdminConfig, ApiConfig, AppConfig, AuthConfig, DatabaseConfig, LoggingConfig, SecurityConfig, ServerConfig, TlsConfig}, state::AppState}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let config = AppConfig { + server: ServerConfig { host: "127.0.0.1".to_string(), port: 8080 }, + api: ApiConfig { base_url: "https://chatgpt.com/backend-api".to_string() }, + auth: AuthConfig { refresh_margin_seconds: 300, refresh_enabled: true, refresh_concurrency: 2 }, + database: DatabaseConfig { url: "sqlite://data/codex-proxy-rs.sqlite".to_string() }, + security: SecurityConfig { + master_key_file: "data/master.key".to_string(), + api_key_pepper_file: "data/api-key-pepper.key".to_string(), + }, + tls: TlsConfig { force_http11: false }, + admin: AdminConfig { session_ttl_minutes: 1440 }, + logging: LoggingConfig { directory: "logs".to_string(), max_file_bytes: 10_485_760, retention_days: 14 }, + }; + let app = build_router(AppState::new(config.clone())); + let listener = tokio::net::TcpListener::bind((config.server.host.as_str(), config.server.port)).await?; + axum::serve(listener, app).await?; + Ok(()) +} +``` + +- [ ] **Step 4: Verify route boundary** + +Run: + +```bash +cargo test v1_requires_client_api_key_not_admin_cookie +``` + +Expected: pass. + +- [ ] **Step 5: Commit router** + +```bash +git add src/app.rs src/http src/main.rs src/lib.rs tests/routes_responses_test.rs +git commit -m "feat: add http router auth boundaries" +``` + +--- + +### Task 11: Codex Upstream Request Lifecycle + +**Files:** +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/codex/client.rs` +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/codex/sse.rs` +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/http/v1.rs` +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/state.rs` +- Test: `/home/zyy/桌面/Codes/codex-proxy-rs/tests/routes_chat_test.rs` +- Test: `/home/zyy/桌面/Codes/codex-proxy-rs/tests/routes_responses_test.rs` + +- [ ] **Step 1: Add failing upstream mock test** + +Append to `/home/zyy/桌面/Codes/codex-proxy-rs/tests/routes_responses_test.rs`: + +```rust +#[tokio::test] +async fn responses_route_rejects_non_codex_provider_models() { + let app = build_router(AppState::new(test_config())); + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/v1/responses") + .header("authorization", "Bearer cpr_test") + .header("content-type", "application/json") + .body(Body::from(r#"{"model":"claude-3","input":[]}"#)) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::NOT_FOUND); +} +``` + +- [ ] **Step 2: Run test and verify RED** + +Run: + +```bash +cargo test responses_route_rejects_non_codex_provider_models +``` + +Expected: fails with `401 Unauthorized`, because client API key validation and model validation are not implemented. + +- [ ] **Step 3: Implement model allow rule** + +Modify `/home/zyy/桌面/Codes/codex-proxy-rs/src/http/v1.rs`: + +```rust +use axum::{http::{HeaderMap, StatusCode}, response::IntoResponse, Json}; +use serde::Deserialize; + +use crate::translation::codex_to_openai::openai_error; + +#[derive(Deserialize)] +struct ResponsesBody { + model: Option, +} + +pub async fn responses(headers: HeaderMap, Json(body): Json) -> impl IntoResponse { + let auth = headers.get("authorization").and_then(|v| v.to_str().ok()).unwrap_or_default(); + if !auth.starts_with("Bearer cpr_") { + return ( + StatusCode::UNAUTHORIZED, + Json(openai_error("Missing client API key", "invalid_api_key")), + ); + } + let model = body.model.unwrap_or_else(|| "gpt-5.4".to_string()); + if !(model.starts_with("gpt") || model.starts_with("codex") || model.starts_with("o")) { + return ( + StatusCode::NOT_FOUND, + Json(openai_error("Model not found", "model_not_found")), + ); + } + ( + StatusCode::SERVICE_UNAVAILABLE, + Json(openai_error("No available Codex accounts", "no_available_accounts")), + ) +} +``` + +- [ ] **Step 4: Verify model rejection** + +Run: + +```bash +cargo test responses_route_rejects_non_codex_provider_models +``` + +Expected: pass. + +- [ ] **Step 5: Commit upstream lifecycle boundary** + +```bash +git add src/codex src/http/v1.rs src/state.rs tests/routes_chat_test.rs tests/routes_responses_test.rs +git commit -m "feat: enforce codex-only upstream routing" +``` + +--- + +### Task 12: Fingerprint Auto-Update + +**Files:** +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/fingerprint/updater.rs` +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/fingerprint/model.rs` +- Test: `/home/zyy/桌面/Codes/codex-proxy-rs/tests/codex_headers_test.rs` + +- [ ] **Step 1: Add failing updater parse test** + +Append to `/home/zyy/桌面/Codes/codex-proxy-rs/tests/codex_headers_test.rs`: + +```rust +use codex_proxy_rs::fingerprint::updater::parse_update_manifest; + +#[test] +fn update_manifest_updates_app_version_and_build_number() { + let manifest = r#"{"version":"26.600.12345","build_number":"4001"}"#; + let update = parse_update_manifest(manifest).unwrap(); + assert_eq!(update.app_version, "26.600.12345"); + assert_eq!(update.build_number, "4001"); +} +``` + +- [ ] **Step 2: Run test and verify RED** + +Run: + +```bash +cargo test update_manifest_updates_app_version_and_build_number +``` + +Expected: compile failure because `parse_update_manifest` does not exist. + +- [ ] **Step 3: Implement updater parser** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/src/fingerprint/updater.rs`: + +```rust +use serde::Deserialize; +use thiserror::Error; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FingerprintUpdate { + pub app_version: String, + pub build_number: String, +} + +#[derive(Debug, Error)] +pub enum FingerprintError { + #[error("invalid update manifest: {0}")] + InvalidManifest(#[from] serde_json::Error), +} + +#[derive(Deserialize)] +struct Manifest { + version: String, + build_number: String, +} + +pub fn parse_update_manifest(input: &str) -> Result { + let manifest: Manifest = serde_json::from_str(input)?; + Ok(FingerprintUpdate { + app_version: manifest.version, + build_number: manifest.build_number, + }) +} +``` + +- [ ] **Step 4: Verify updater parser** + +Run: + +```bash +cargo test update_manifest_updates_app_version_and_build_number +``` + +Expected: pass. + +- [ ] **Step 5: Commit fingerprint updater parser** + +```bash +git add src/fingerprint/updater.rs tests/codex_headers_test.rs +git commit -m "feat: parse codex desktop fingerprint updates" +``` + +--- + +### Task 13: Documentation and Runtime Verification + +**Files:** +- Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/README.md` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/.env.example` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/Dockerfile` +- Create: `/home/zyy/桌面/Codes/codex-proxy-rs/docker-compose.yml` + +- [ ] **Step 1: Write README with exact scope** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/README.md`: + +```markdown +# codex-proxy-rs + +Rust rewrite of Codex Proxy focused only on ChatGPT/Codex accounts and the Codex backend. + +## Included + +- `/v1/chat/completions` +- `/v1/responses` +- `/v1/models` +- ChatGPT/Codex OAuth and refresh-token based account pool +- Codex Desktop-style TLS, headers, Cookies, and fingerprint updates +- SQLite storage with sqlx migrations +- Structured logs with pagination and file rotation + +## Excluded + +- OpenAI official API Key upstream +- Anthropic, Gemini, custom providers, OpenRouter-style routing +- Ollama bridge +- Per-account proxy assignment +- Electron app + +## Auth Model + +- Admin login uses admin password and HttpOnly session Cookie. +- Client API keys call `/v1/*` and start with `cpr_`. +- Codex account tokens are internal upstream credentials. +``` + +- [ ] **Step 2: Add environment example** + +Create `/home/zyy/桌面/Codes/codex-proxy-rs/.env.example`: + +```dotenv +CPRS_HOST=127.0.0.1 +CPRS_PORT=8080 +CPRS_DATABASE_URL=sqlite://data/codex-proxy-rs.sqlite +CPRS_MASTER_KEY_FILE=data/master.key +CPRS_API_KEY_PEPPER_FILE=data/api-key-pepper.key +RUST_LOG=codex_proxy_rs=info,tower_http=info +``` + +- [ ] **Step 3: Verify full test suite and lint** + +Run: + +```bash +cargo fmt --check +cargo clippy --all-targets --all-features --locked -- -D warnings +cargo test +``` + +Expected: all commands pass. + +- [ ] **Step 4: Run local server smoke test** + +Run: + +```bash +cargo run +curl -s http://127.0.0.1:8080/health +``` + +Expected response: + +```json +{"status":"ok"} +``` + +- [ ] **Step 5: Commit docs and runtime files** + +```bash +git add README.md .env.example Dockerfile docker-compose.yml +git commit -m "docs: document codex-proxy-rs scope" +``` + +--- + +## Acceptance Criteria + +- `codex-proxy-rs` builds as a standalone Rust service. +- `cargo test`, `cargo clippy --all-targets --all-features --locked -- -D warnings`, and `cargo fmt --check` pass. +- `Cargo.toml` contains lint settings for Rust and Clippy; task commits do not silence lint warnings with broad `allow` attributes. +- Non-fingerprint dependencies are resolved from current stable crate releases at implementation time and `Cargo.lock` is committed. +- `reqwest` and `rustls` remain explicitly pinned until a separate TLS fingerprint review approves newer versions. +- Library modules use `thiserror` error enums. `anyhow` is limited to `src/main.rs`, tests, and setup utilities. +- Runtime config loads from `config/default.yaml`, optional local config, and `CPRS_*` environment variables. +- In-memory upstream secrets use `secrecy` types, and persisted access tokens, refresh tokens, Cookies, master key material, and API key pepper material are encrypted or stored outside plaintext database fields. +- `/v1/*` and `/admin/*` response envelopes and status codes match `docs/api.md` and `docs/status-codes.md`. +- `/v1/*` never uses the admin/frontend `code/message/data/requestId` envelope; it keeps OpenAI-compatible response and error bodies. +- `/admin/*` uses accurate HTTP status codes plus body-level frontend codes, with lower camelCase JSON fields and no PascalCase/snake_case JSON keys. +- All responses include `X-Request-Id`; admin/frontend JSON bodies include `requestId` except `204 No Content`. +- Each completed task updates both the plan checkbox state and `docs/implementation-status.md` before commit. +- Any API/status-code change updates `docs/api.md` or `docs/status-codes.md` in the same commit. +- Any dependency change updates `docs/dependency-policy.md` in the same commit. +- No OpenAI official API Key direct upstream exists in code, config, routes, docs, or tests. +- No Anthropic, Gemini, custom provider, Ollama, Electron, or per-account proxy assignment modules exist. +- `/v1/*` accepts only client API keys with `cpr_` shape and never accepts admin session cookies. +- `/admin/*` accepts only admin session cookies and never accepts client API keys. +- Codex upstream request code uses pinned `reqwest = 0.12.28` and `rustls = 0.23.36`. +- Codex headers include Codex Desktop identity, authorization, account id when available, turn-state when available, and request id. +- Cloudflare Cookies are captured from upstream `Set-Cookie`, encrypted at rest, stored per account, and replayed only for the same account. +- Fingerprint update logic can parse a Codex Desktop update manifest and persist app version/build number history. +- SQLite is the only database. Migrations create accounts, client API keys, admin users, sessions, cookies, fingerprints, and event logs. +- Log files rotate by date/size policy, and event query APIs are cursor paginated with a max limit of 200. +- Account refresh behavior preserves the current TypeScript semantics: refresh before expiry, immediate refresh after 401, refresh-token preservation, status transitions for expired/quota/banned/disabled, and bounded concurrency. +- Important protocol, security, and business invariants have sparse Chinese comments; obvious code is left uncommented. + +## Self-Review Notes + +- Spec coverage: the plan covers Rust rewrite, removed channels, TLS fingerprint, headers, Cookies, fingerprint update, auth split, logging, pagination, SQLite storage, API/status-code contracts, frontend body codes, JSON casing, request IDs, Chinese comment policy, dependency policy, documentation completion tracking, and account refresh compatibility. +- Rust best-practices coverage: the revised plan adds lint gates, `thiserror` library errors, binary-only `anyhow`, `secrecy` secret handling, HMAC API key hashing, SQLite URL configuration, and an `Arc` runtime state boundary. +- Open item scan: no open-ended implementation gaps are intentionally left in task steps. +- Type consistency: the early scaffold uses `AppConfig`, `AppState`, `AccountStatus`, `EventLog`, `Fingerprint`, and `CodexResponsesRequest` consistently across later tasks. diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 000000000..73cb934de --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +components = ["rustfmt", "clippy"] diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 000000000..8034c4a17 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,70 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use serde::Serialize; +use thiserror::Error; + +pub type AppResult = Result; + +#[derive(Debug, Error)] +pub enum AppError { + #[error("configuration error: {0}")] + Config(String), + #[error("database error: {0}")] + Database(#[from] sqlx::Error), + #[error("upstream error: {0}")] + Upstream(String), + #[error("unauthorized")] + Unauthorized, + #[error("forbidden")] + Forbidden, + #[error("bad request: {0}")] + BadRequest(String), +} + +#[derive(Serialize)] +struct ErrorBody<'a> { + error: ErrorMessage<'a>, +} + +#[derive(Serialize)] +struct ErrorMessage<'a> { + message: &'a str, + #[serde(rename = "type")] + kind: &'a str, + code: &'a str, +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let (status, code, kind) = match &self { + Self::BadRequest(_) | Self::Config(_) => ( + StatusCode::BAD_REQUEST, + "bad_request", + "invalid_request_error", + ), + Self::Unauthorized => ( + StatusCode::UNAUTHORIZED, + "unauthorized", + "invalid_request_error", + ), + Self::Forbidden => (StatusCode::FORBIDDEN, "forbidden", "invalid_request_error"), + Self::Database(_) | Self::Upstream(_) => { + (StatusCode::BAD_GATEWAY, "upstream_error", "server_error") + } + }; + + let message = self.to_string(); + let body = ErrorBody { + error: ErrorMessage { + message: &message, + kind, + code, + }, + }; + + (status, Json(body)).into_response() + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 000000000..a91e73517 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod error; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 000000000..e469d8f28 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,5 @@ +#[tokio::main] +async fn main() -> anyhow::Result<()> { + println!("codex-proxy-rs bootstrap"); + Ok(()) +}