# Virtual workspace. Every crate shares one version, because they are one # product released together rather than independently useful libraries; see # release.toml, which relies on that. [workspace] resolver = "3" members = ["crates/*"] # tools/ is a workspace of its own: developer tooling that must never enter a # product build or `cargo test --workspace`. See tools/README.md. exclude = ["tools"] [workspace.package] version = "0.1.0" edition = "2021" rust-version = "1.90" license = "MIT" repository = "https://tangled.org/permadeath.com/didbot" publish = false [workspace.dependencies] # The CRC-32/ISO-HDLC every framed file in didbot-pds checks itself with: # the write-ahead log, the heap, the checkpoint and both row logs. One crate, # no transitive dependencies. crc32fast = "1" didbot-config = { version = "0.1.0", path = "crates/didbot-config" } didbot-http = { version = "0.1.0", path = "crates/didbot-http" } didbot-avatar = { version = "0.1.0", path = "crates/didbot-avatar" } didbot-brand = { version = "0.1.0", path = "crates/didbot-brand" } didbot-site-anim = { version = "0.1.0", path = "crates/didbot-site-anim" } didbot-dns = { version = "0.1.0", path = "crates/didbot-dns" } didbot-lookup = { version = "0.1.0", path = "crates/didbot-lookup" } didbot-fsm = { version = "0.1.0", path = "crates/didbot-fsm" } didbot-name = { version = "0.1.0", path = "crates/didbot-name" } didbot-pds = { version = "0.1.0", path = "crates/didbot-pds" } didbot-policy = { version = "0.1.0", path = "crates/didbot-policy" } didbot-policy-regex = { version = "0.1.0", path = "crates/didbot-policy-regex" } didbot-policy-cedar = { version = "0.1.0", path = "crates/didbot-policy-cedar" } didbot-policy-count = { version = "0.1.0", path = "crates/didbot-policy-count" } didbot-policy-records = { version = "0.1.0", path = "crates/didbot-policy-records" } didbot-scope = { version = "0.1.0", path = "crates/didbot-scope" } didbot-serve = { version = "0.1.0", path = "crates/didbot-serve" } didbot-operator = { version = "0.1.0", path = "crates/didbot-operator" } didbot-cli = { version = "0.1.0", path = "crates/didbot-cli" } didbot-swarm = { version = "0.1.0", path = "crates/didbot-swarm" } didbot-identity = { version = "0.1.0", path = "crates/didbot-identity" } didbot-key = { version = "0.1.0", path = "crates/didbot-key" } didbot-lexicon = { version = "0.1.0", path = "crates/didbot-lexicon" } didbot-data = { version = "0.1.0", path = "crates/didbot-data" } didbot-repo = { version = "0.1.0", path = "crates/didbot-repo" } didbot-schema = { version = "0.1.0", path = "crates/didbot-schema" } didbot-tls = { version = "0.1.0", path = "crates/didbot-tls" } didbot-authstore = { version = "0.1.0", path = "crates/didbot-authstore" } didbot-claim-check = { version = "0.1.0", path = "crates/didbot-claim-check" } didbot-onboarding = { version = "0.1.0", path = "crates/didbot-onboarding" } didbot-agentd = { version = "0.1.0", path = "crates/didbot-agentd" } # jacquard is pinned exactly. Its unreleased mainline moves serde_ipld_dagcbor, # reqwest, base64 and sha2 all at once, and a graph carrying two versions of # those ends up with two incompatible copies of the CID types. jacquard-common = { version = "=0.12.1", default-features = false, features = ["std", "crypto", "crypto-k256"] } # The real atproto OAuth 2.1 client -- PAR, PKCE, DPoP and the loopback # callback server -- for `didbot operate`, which authenticates *as* an atproto # account rather than serving one. `didbot-swarm` uses its DPoP proofs and # PKCE to sign synthetic agents in. Pinned to the same release as # `jacquard-common` for the reason given above. # `loopback` is the local-redirect-server flow a one-shot CLI needs; # `browser-open` is the convenience of not making the operator copy a URL by # hand. Neither pulls in `jacquard-lexicon`'s generated scope tables -- # `scope-check` stays off, because the scope this crate requests is built # from `didbot-serve`'s own grammar, not validated against jacquard's copy of # it. jacquard-oauth = { version = "=0.12.1", default-features = false, features = ["loopback", "browser-open"] } # Named only to spell `OAuthClient`'s resolver type parameter, which # `jacquard-oauth` takes from here and does not re-export. jacquard-identity = { version = "=0.12.1", default-features = false } # The string type `jacquard-oauth` spells its scopes and metadata in, and # does not re-export. smol_str = { version = "0.3.6", features = ["serde"] } # The key type `jacquard-oauth` generates for DPoP and does not re-export. jose-jwk = { version = "0.1", default-features = false } # `ws` is not one of axum's default features and `com.atproto.sync.subscribeRepos` # is a WebSocket, so it is named here rather than per-crate: the workspace pins # one axum, and a second feature set would be a second build of it. axum = { version = "0.8", features = ["ws"] } # The `subscribeRepos` frame's encoded bytes are held by the replay buffer and # handed to every subscriber. `Bytes` makes each of those a refcount bump on # one allocation rather than a copy, and it is what `axum`'s # `Message::Binary` already takes, so a frame reaches the socket without one. # `didbot-tls` takes the same type from its ACME HTTP client's body, so the # version is pinned here rather than per-crate. bytes = "1" # `didbot-tls`'s ACME client builds `http::Request`/`Response` directly, and # `didbot-http`'s test suite does the same to construct a response to bound. http = "1" # `Limited`: a body read under a size bound, in `didbot-tls`'s ACME client and # `didbot-serve`'s proxy. http-body-util = "0.1" hex = "0.4" # Reads a blob's type from its magic bytes, for policy on uploads. infer = "0.22" # CIDR containment, for the address blocks `didbot-serve`'s OAuth client # metadata fetch refuses. Already in the graph via `reqwest`. ipnet = "2" # The bounded cache `didbot-lookup` holds its answers in. Eviction order is # the whole of what it adds over a map, and it is the part a hand-rolled # version gets wrong. Its default feature swaps std's map for `hashbrown` # with a `foldhash` hasher, which puts a second `foldhash` major in a graph # that already carries one; the std map is what this cache wants anyway, # since every read of it is behind a mutex rather than in a hot loop. lru = { version = "0.18", default-features = false } futures-core = "0.3" # `stream` is what lets a client read a server-sent-event body as it # arrives rather than waiting for a response that never ends. reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] } sha2 = "0.10" hmac = "0.12" # Verifies OpenID Connect ID tokens against an issuer's JWKS. The RustCrypto # backend, so the graph carries one copy of `p256` and `sha2`; PEM is not # needed because every key arrives as a JWK. jsonwebtoken = { version = "11", default-features = false, features = ["rust_crypto"] } k256 = { version = "0.13", features = ["ecdsa"] } # Verification only, and never signing: see the `secp256r1` module in # `didbot-key` for why this project mints no P-256 key. p256 = { version = "0.13", features = ["ecdsa"] } # Encodes the PNG every account's avatar is stored as, and the brand images. png = "0.18" # Route53's control-plane API speaks XML, and this crate reads and writes it # through serde like everything else here reads JSON. quick-xml = { version = "0.42", features = ["serialize"] } rand = "0.9" rand_core = { version = "0.6", features = ["getrandom"] } serde = { version = "1", features = ["derive"] } serde_json = "1" subtle = "2" thiserror = "2" # Every command-line surface parses through this one crate, via # `didbot-cli`, so help, usage errors and exit statuses read the same # whichever binary answered. clap = { version = "4", features = ["derive"] } tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "signal", "time", "sync", "io-util"] } tower-http = { version = "0.6", features = ["trace", "cors", "timeout"] } tracing = "0.1" unicode-segmentation = "1" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "time"] } time = { version = "0.3", features = ["formatting", "parsing", "macros", "serde-well-known"] } toml = "0.9" tower = { version = "0.5", features = ["util", "limit", "make"] } # Several crates parse URLs directly -- `didbot-tls` for the ACME directory, # `didbot-agentd` for the server it registers with -- so the version is # pinned here rather than per-crate, the way every other shared dependency # is. url = "2" # Two crates hand a browser a wasm module: didbot-site-anim and # didbot-policy-check. scripts/build-wasm.sh checks wasm-bindgen-cli against # the one version Cargo.lock pins for this. wasm-bindgen = "0.2" # PKCE's `S256` challenge is base64url, no padding — RFC 7636 §4.2. base64 = "0.22" # `default-features = false` plus an explicit `ring` here, not the default # `aws-lc-rs`, keeps this workspace on the one crypto backend it already has # via `rustls-webpki` -- see the jacquard comment above for why a second copy # of a crypto crate in the graph is a problem here, not just weight. # # The `hyper-rustls` feature is deliberately left off, even though it is the # obvious way to get instant-acme's built-in HTTP client: that feature always # turns on `rustls-platform-verifier`, which pulls `aws-lc-rs` in regardless # of this `ring` choice -- checked with `cargo tree -i aws-lc-rs` against the # `hyper-rustls` feature before deciding against it. `didbot-tls` supplies its # own `instant_acme::HttpClient` over `reqwest`, which is already on `ring` # via `rustls-tls`, instead. instant-acme = { version = "0.8", default-features = false, features = ["ring", "rcgen", "time"] } rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] } # `tls-rustls-no-provider` gets the hot-reloadable `RustlsConfig` without # pulling `aws-lc-rs` in as axum-server's own default feature would; the # `ring` provider above is what actually gets installed at startup. axum-server = { version = "0.8", default-features = false, features = ["tls-rustls-no-provider"] } # `axum_server::Server::http_builder` hands back hyper's own HTTP/1 builder, # and its `header_read_timeout` needs a timer to measure with. `TokioTimer` is # that timer, and it lives here rather than being reachable through # `axum-server`, which re-exports none of hyper-util. Already in the graph # through axum and axum-server; this names it so `didbot-serve` may say it. hyper-util = { version = "0.1", default-features = false, features = ["tokio"] } # Pure-Rust ASN.1/X.509 parsing, to read a certificate's `notAfter` back out # for the renewal clock and its names back out for a check's report. No # crypto backend of its own. Held at the major `rcgen` asks for, so one copy # of the asn1-rs/der-parser stack builds rather than two. x509-parser = "0.18" # Already in the graph via axum-server's `tls-rustls-no-provider` feature; # named explicitly so the certificate swap can use the same primitive rather # than a second one, per the atomicity requirement in `didbot-tls`. arc-swap = "1" # PEM parsing for the test that drives a real TLS client against `serve_tls`. # `rustls-pemfile` is archived; this is the crate it wrapped, and `rustls` # already re-exports it as `rustls::pki_types`. Named here so the workspace # pins one version of it, the way every other shared dependency is. rustls-pki-types = { version = "1", features = ["std"] } # A TLS client for tests that drive a connection by hand rather than through # `reqwest` -- `didbot-serve`'s slowloris test sends one byte of a request # line and waits for the server to close on it, and `didbot-tls` drives real # handshakes against its own resolver. # # `default-features = false`: tokio-rustls's defaults include `aws_lc_rs`, # which is exactly the second crypto backend this workspace's dependency # choices elsewhere exist to avoid -- caught with `cargo tree -i aws-lc-rs` # after it was first added without it. tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "logging", "tls12"] } # Test-only cert generation for `didbot-tls`'s rotation test. Its default # features already pick `ring`, matching the rest of the workspace. rcgen = "0.14" # The public root store `didbot-onboarding`'s certificate check verifies a # server's chain against, which is the same set a browser or curl would use. # Already in the graph through reqwest's own `rustls-tls`. webpki-roots = "1" # Debug information is most of what a build of this workspace produces, and # almost none of it is used. Nothing here is debugged by stepping: the server # is watched through its log, and a failure arrives as a backtrace. Line tables # are what a backtrace needs — file and line for every frame — and they are a # fraction of the size of the full DWARF a stepping debugger wants. # # Measured by building `didbot-pds` from cold into a target directory of # its own, once with each setting, so that neither answer was polluted by # artefacts of the other: the target directory went from 1.1 GB to 827 MB, the # binary from 106 MB to 43 MB, and the build from 59.6 s to 47.2 s. The # workspace as a whole gains more than that share, because the facade crate's # test binaries link every crate in it and there is one per suite. # # It matters beyond the seconds: four live worktrees have filled this machine's # disk mid-link more than once, and the failure reads as the linker crashing # rather than as a full disk. # # The saving arrives on the next `cargo clean` and not before. Cargo leaves # artefacts it has superseded, so a target directory that has been built in for # a while holds both shapes and briefly grows. On the worktree this was written # in, one that had reached 31 GB rebuilt from clean — the whole workspace, # every test binary included — into 3.4 GB. # # Set `debug = true` here, temporarily, if you do need a stepping debugger. [profile.dev] debug = "line-tables-only" # The same for test binaries, which are the largest artefacts in the tree: the # suites in the facade crate link every crate in the workspace, and there is # one such binary per suite. [profile.test] debug = "line-tables-only" # `lto` and one codegen unit are what actually ship: this is what runs on the # public internet, built once and run for months, so the extra link time is # worth a smaller and faster binary. `strip` drops the symbols that buys. # # `overflow-checks` stays on despite the profile's default of off. The CBOR, # CAR and WAL frame parsers do length and offset arithmetic on bytes read # straight off the network, and a silent wraparound there is a correctness # bug, not a slow path worth optimizing away. [profile.release] lto = "fat" codegen-units = 1 strip = true overflow-checks = true # The profile scripts/build-wasm.sh builds `didbot-policy-check` with: the # policy page's copy of the server's checks, which every visitor to # policy.did.bot downloads before the page can judge anything. Bytes over a # connection nobody here controls are worth more than instructions on a # processor, which is the opposite trade to the server `release` builds for, # hence a profile rather than a change to that one. # # `opt-level = "z"` and `panic = "abort"` are the two settings that differ; # `lto`, one codegen unit and `strip` are inherited and already pull the same # way. Measured on that crate: 5.23 MB down to 3.49 MB, and 1.62 MB down to # 1.05 MB gzipped — a third off both. `policy-site/tests/check-wasm.mjs` holds # the compressed figure to a budget. # # `overflow-checks` stays inherited and on. These crates are the engines the # server runs, compiled a second time for a page, and the page's answers have # to be the server's; a profile that evaluated a policy differently would be a # worse artefact than a large one. # # Binaryen's `wasm-opt` is measured and deliberately not in this path. On this # module `-Oz` takes another 12% off the raw binary but puts 8% back on the # gzipped transfer and 9% on the brotli: it reorders functions and renumbers # locals, which costs more in compressibility than it wins in instructions, and # CloudFront compresses what it serves (`infra/policy-site/main.tf`). The 2 ms # it saves in `WebAssembly.compile` is not worth the 80 KB on the wire. On # `didbot-site-anim` it saves under 1 KB compressed. [profile.wasm-release] inherits = "release" opt-level = "z" panic = "abort" [workspace.lints.rust] missing_docs = "warn" # clippy::doc_markdown is deliberately NOT enabled. The narrative pages under # docs/ are compiled into rustdoc, and that lint wants backticks around every # CamelCase word it sees — which in this project means PreToolUse, # SessionStart, ATProto and every other protocol noun that belongs in prose. # Backticking a proper noun to satisfy a lint makes the sentence worse.