A Rust application to showcase badge awards in the AT Protocol ecosystem.

Initial commit

Nick Gerakines 11f5a3f6

+26
.env.dev
··· 1 + # SQLite Development Environment Configuration 2 + # This file contains environment variables for local development with SQLite 3 + 4 + # Database connection URL for SQLite 5 + DATABASE_URL=sqlite://showcase_dev.db 6 + 7 + # External hostname for the site 8 + EXTERNAL_BASE=http://localhost:8080 9 + 10 + # Semicolon-separated list of trusted badge issuer DIDs 11 + BADGE_ISSUERS=did:plc:test1;did:plc:test2 12 + 13 + # HTTP server port 14 + HTTP_PORT=8080 15 + 16 + # Badge image storage directory 17 + BADGE_IMAGE_STORAGE=./badges 18 + 19 + # PLC server hostname 20 + PLC_HOSTNAME=plc.directory 21 + 22 + # HTTP client timeout 23 + HTTP_CLIENT_TIMEOUT=10s 24 + 25 + # Logging configuration for development 26 + RUST_LOG=showcase=info,debug
+26
.env.test
··· 1 + # PostgreSQL Test Environment Configuration 2 + # This file contains environment variables for running integration tests with PostgreSQL 3 + 4 + # Database connection URL for PostgreSQL 5 + DATABASE_URL=postgresql://showcase:showcase_dev_password@localhost:5433/showcase_test 6 + 7 + # External hostname for the site (used in tests) 8 + EXTERNAL_BASE=http://localhost:8080 9 + 10 + # Semicolon-separated list of trusted badge issuer DIDs for testing 11 + BADGE_ISSUERS=did:plc:test1;did:plc:test2 12 + 13 + # HTTP server port for test instances 14 + HTTP_PORT=8080 15 + 16 + # Badge image storage directory for tests 17 + BADGE_IMAGE_STORAGE=./test_badges 18 + 19 + # PLC server hostname 20 + PLC_HOSTNAME=plc.directory 21 + 22 + # HTTP client timeout 23 + HTTP_CLIENT_TIMEOUT=10s 24 + 25 + # Logging configuration for tests (more verbose for debugging) 26 + RUST_LOG=showcase=debug,info
+38
.gitignore
··· 1 + # Rust build artifacts 2 + /target 3 + 4 + # Database files 5 + *.db 6 + *.db-shm 7 + *.db-wal 8 + showcase_dev.db* 9 + showcase_test.db* 10 + 11 + # Environment files (keep .env.test and .env.dev as examples) 12 + .env 13 + .env.local 14 + .env.production 15 + 16 + # Database backups 17 + /db_backups 18 + 19 + # Test artifacts 20 + /test_badges 21 + /badges 22 + 23 + # Docker volumes 24 + postgres_data/ 25 + pgadmin_data/ 26 + 27 + # IDE and editor files 28 + .vscode/ 29 + .idea/ 30 + *.swp 31 + *.swo 32 + *~ 33 + 34 + # macOS 35 + .DS_Store 36 + 37 + # Logs 38 + *.log
+248
CLAUDE.md
··· 1 + # CLAUDE.md 2 + 3 + This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 + 5 + ## Development Commands 6 + 7 + - **Build**: `cargo build` 8 + - **Run application**: `cargo run --bin showcase` 9 + - **Run tests**: `cargo test` 10 + - **Run single test**: `cargo test <test_name>` 11 + - **Run all tests with databases**: `./scripts/test-all.sh` 12 + - **Run PostgreSQL tests**: `./scripts/test-postgres.sh` 13 + - **Run SQLite tests**: `./scripts/test-sqlite.sh` 14 + - **Check code**: `cargo check` 15 + - **Format code**: `cargo fmt` 16 + - **Lint code**: `cargo clippy` 17 + 18 + ## Project Overview 19 + 20 + This is a Rust application named "showcase" using the 2024 edition. The project is a badge awards showcase application for the AT Protocol community that consumes Jetstream events, validates badge signatures, stores data in multiple database backends (SQLite/PostgreSQL), and provides a web interface to display badge awards. 21 + 22 + ## Architecture 23 + 24 + - `src/bin/showcase.rs`: Main application binary entry point 25 + - `src/lib.rs`: Library crate exposing all modules with `#![warn(missing_docs)]` 26 + - `src/config.rs`: Configuration management via environment variables 27 + - `src/storage/`: Modular storage implementations with multiple backend support 28 + - `src/storage/mod.rs`: Storage trait definitions and common types 29 + - `src/storage/sqlite.rs`: SQLite database implementation 30 + - `src/storage/postgres.rs`: PostgreSQL database implementation 31 + - `src/storage/file_storage.rs`: File storage abstraction (local and S3) 32 + - `src/process.rs`: Badge processing logic, signature validation, and image handling 33 + - `src/consumer.rs`: Jetstream consumer for AT Protocol badge award events 34 + - `src/http.rs`: Axum-based HTTP server with API endpoints and template rendering 35 + - `src/errors.rs`: Comprehensive error handling using `thiserror` 36 + - `src/templates.rs`: Template engine integration 37 + - With `reload` feature: Uses `AutoReloader` for live template reloading in development 38 + - With `embed` feature: Embeds templates at compile time, takes `http_external` and `version` parameters for production 39 + - `build.rs`: Build script for template embedding when using `embed` feature 40 + - `Dockerfile`: Multi-stage Docker build configuration for production deployment 41 + - `templates/`: HTML templates (base.html, index.html, identity.html) 42 + - `static/`: Static assets (Pico CSS, images, badge storage) 43 + 44 + ## Docker Deployment 45 + 46 + The `Dockerfile` provides a multi-stage build configuration for production deployment: 47 + 48 + ### Build Stage 49 + - Uses `rust:1.87-slim` base image with build dependencies (`pkg-config`, `libssl-dev`) 50 + - Configurable build arguments: 51 + - `FEATURES` (default: `embed,postgres,s3`): Cargo features to enable 52 + - `TEMPLATES` (default: `./templates`): Source path for template files 53 + - `STATIC` (default: `./static`): Source path for static assets 54 + - Builds with `--no-default-features` and only specified features for production optimization 55 + - Template embedding is handled at build time when `embed` feature is enabled 56 + 57 + ### Runtime Stage 58 + - Uses minimal `gcr.io/distroless/cc-debian12` image for security and size optimization 59 + - Includes comprehensive OCI metadata labels for container identification 60 + - Exposes port 8080 with configurable environment variables: 61 + - `HTTP_STATIC_PATH=/app/static`: Path to static assets 62 + - `HTTP_PORT=8080`: HTTP server port 63 + - Production-ready configuration with embedded templates and minimal attack surface 64 + 65 + ### Build Commands 66 + - **Build image**: `docker build -t showcase .` 67 + - **Run container**: `docker run -p 8080:8080 showcase` 68 + - **Custom features**: `docker build --build-arg FEATURES=embed,sqlite -t showcase .` 69 + 70 + ## Database Schema 71 + 72 + ### Tables 73 + 74 + #### `badges` 75 + Stores badge definition records from the AT Protocol. 76 + 77 + Key columns: 78 + - `aturi`: AT-URI of the badge definition 79 + - `cid`: Content identifier for the badge record 80 + - `name`: Human-readable badge name 81 + - `image`: Optional image reference (blob CID) 82 + - `record`: Full JSON record of the badge definition (added in migration 002) 83 + - `count`: Number of awards using this badge 84 + - Primary key: `(aturi, cid)` 85 + 86 + #### `awards` 87 + Stores individual badge awards to users. 88 + 89 + Key columns: 90 + - `aturi`: AT-URI of the award record (primary key) 91 + - `cid`: Content identifier for the award record 92 + - `did`: DID of the recipient 93 + - `badge`: AT-URI of the associated badge 94 + - `badge_cid`: Content identifier of the badge record 95 + - `badge_name`: Human-readable badge name 96 + - `validated_issuers`: JSON array of validated issuer DIDs 97 + - `record`: Full JSON record of the award 98 + 99 + #### `identities` 100 + Stores resolved DID documents and identity information. 101 + 102 + Key columns: 103 + - `did`: Decentralized identifier (primary key) 104 + - `handle`: AT Protocol handle 105 + - `record`: Full DID document JSON 106 + 107 + ### Database Type Differences 108 + 109 + - **PostgreSQL**: Uses `JSONB` for JSON columns (better performance) and `TIMESTAMPTZ` for timestamps 110 + - **SQLite**: Uses `JSON` for JSON columns and `TIMESTAMP` for timestamps 111 + 112 + ## Error Handling 113 + 114 + All error strings must use this format: 115 + 116 + error-showcase-<domain>-<number> <message>: <details> 117 + 118 + Example errors: 119 + 120 + * error-showcase-resolve-1 Multiple DIDs resolved for method 121 + * error-showcase-plc-1 HTTP request failed: https://google.com/ Not Found 122 + * error-showcase-key-1 Error decoding key: invalid 123 + 124 + Errors are defined as enums in `src/errors.rs` using the `thiserror` library, organized by domain: 125 + - `ConfigError`: Configuration-related errors 126 + - `ConsumerError`: Jetstream consumer errors 127 + - `HttpError`: HTTP server errors 128 + - `ProcessError`: Badge processing errors 129 + - `StorageError`: Database and file storage errors 130 + 131 + Each error variant follows the naming pattern and includes structured error messages. 132 + 133 + ## Dependencies and Features 134 + 135 + ### Features 136 + - `default = ["reload", "sqlite", "postgres", "s3"]`: Development mode with all features enabled 137 + - `embed`: Production mode with embedded templates via `minijinja-embed` 138 + - `reload`: Development mode with live template reloading via `minijinja-autoreload` 139 + - `sqlite`: SQLite database backend support 140 + - `postgres`: PostgreSQL database backend support 141 + - `s3`: S3-compatible object storage support for file operations 142 + 143 + ### Key Dependencies 144 + - **Web Framework**: `axum` 0.8 with `axum-template` for MiniJinja integration 145 + - **Database**: `sqlx` 0.8 with SQLite, PostgreSQL, JSON, and async support 146 + - **Template Engine**: `minijinja` 2.7 with conditional embed/reload features 147 + - **AT Protocol**: Released versions `atproto-client`, `atproto-identity`, `atproto-record`, `atproto-jetstream` 0.6.0 148 + - **Image Processing**: `image` 0.25 for badge image handling 149 + - **Object Storage**: `minio` 0.3 for S3-compatible storage operations 150 + - **Async Runtime**: `tokio` 1.41 with multi-threaded runtime and signal handling 151 + - **HTTP Client**: `reqwest` 0.12 with TLS and middleware support, `reqwest-chain` 1.0 for middleware chaining 152 + - **Cryptography**: `k256`, `p256`, `ecdsa`, `elliptic-curve` 0.13 for signature validation 153 + - **Serialization**: `serde`, `serde_json`, `serde_ipld_dagcbor` 154 + - **Utilities**: 155 + - `async-trait` 0.1 for async trait implementations 156 + - `base64` 0.22 for encoding/decoding 157 + - `bytes` 1.10 for byte manipulation 158 + - `duration-str` 0.11 for parsing duration strings 159 + - `hickory-resolver` 0.25 for DNS resolution 160 + - `lru` 0.12 for caching 161 + - `multibase` 0.9 for multibase encoding 162 + - `rand` 0.8 for random number generation 163 + - `sha2` 0.10 for SHA-2 hashing 164 + - `tower-http` 0.5 for static file serving 165 + - `ulid` 1.2 for ULID generation 166 + 167 + ## Configuration 168 + 169 + The application is configured via environment variables. Key configuration includes: 170 + 171 + ### Environment Files 172 + - `.env.dev`: SQLite development configuration 173 + - `.env.test`: PostgreSQL test configuration 174 + 175 + ### Required Variables 176 + - `EXTERNAL_BASE`: External hostname for the site 177 + - `BADGE_ISSUERS`: Semicolon-separated list of trusted badge issuer DIDs 178 + 179 + ### Optional Variables (with defaults) 180 + - `HTTP_PORT` (8080): HTTP server port 181 + - `HTTP_STATIC_PATH`: Path to static assets directory 182 + - `HTTP_TEMPLATES_PATH`: Path to templates directory 183 + - `DATABASE_URL` (sqlite://showcase.db): Database connection string (SQLite or PostgreSQL) 184 + - `BADGE_IMAGE_STORAGE` (./badges): Badge image storage location 185 + - Local filesystem: Path to directory (e.g., `./badges`) 186 + - S3-compatible storage: `s3://[key]:[secret]@hostname/bucket[/optional_prefix]` 187 + - `PLC_HOSTNAME` (plc.directory): PLC server hostname 188 + - `HTTP_CLIENT_TIMEOUT` (10s): HTTP client timeout 189 + - `JETSTREAM_CURSOR_PATH`: Optional path for persisting Jetstream cursor state 190 + - `CERTIFICATE_BUNDLES`: Semicolon-separated list of certificate bundle paths 191 + - `USER_AGENT`: Custom user agent string for HTTP requests 192 + - `DNS_NAMESERVERS`: Semicolon-separated list of DNS nameservers 193 + - `RUST_LOG` (showcase=info,info): Logging configuration 194 + 195 + ## Code Style 196 + 197 + ### Error Handling 198 + - Use `ShowcaseError` enum from `src/errors.rs` for all application errors 199 + - Implement `From` traits for automatic error conversion 200 + - Always include detailed error messages following the established format 201 + - Prefer `thiserror` over `anyhow` for structured errors 202 + 203 + ### Result Type 204 + 205 + The codebase defines a type alias in `src/lib.rs`: 206 + ```rust 207 + pub type Result<T> = std::result::Result<T, ShowcaseError>; 208 + ``` 209 + 210 + All functions should use this `showcase::Result<T>` type for consistency, where errors are one of the `ShowcaseError` variants defined in `src/errors.rs`. 211 + 212 + ### Logging 213 + 214 + Use tracing for structured logging. 215 + 216 + All calls to `tracing::error`, `tracing::warn`, `tracing::info`, `tracing::debug`, and `tracing::trace` should be fully qualified. 217 + 218 + Do not use the `println!` macro in library code. 219 + 220 + Async calls should be instrumented using the `.instrument()` that references the `use tracing::Instrument;` trait. 221 + 222 + ### Async Patterns 223 + - Use `tokio::spawn` for concurrent tasks with `TaskTracker` for graceful shutdown 224 + - Implement proper cancellation token handling for all background tasks 225 + - Use `Arc` for shared state across async boundaries 226 + 227 + ### Storage Operations 228 + - Use the `Storage` trait for database operations to support multiple backends 229 + - Implement the `FileStorage` trait for file operations (local and S3) 230 + - Use SQLx with compile-time checked queries where possible 231 + - Implement proper transaction handling for multi-step operations 232 + - Follow the established migration pattern for schema changes 233 + 234 + ### Web Framework 235 + - Use Axum extractors and response types consistently 236 + - Implement proper error handling with `IntoResponse` for `ShowcaseError` 237 + - Use `AppState` pattern for dependency injection 238 + 239 + ## Testing 240 + 241 + The project uses standard Rust testing with `#[cfg(test)]` attributes. Tests should be placed inline with the code they test or in separate test modules within each source file. 242 + 243 + ### Test Scripts 244 + - `scripts/test-all.sh`: Runs tests against both SQLite and PostgreSQL databases 245 + - `scripts/test-postgres.sh`: Runs tests specifically with PostgreSQL backend using Docker 246 + - `scripts/test-sqlite.sh`: Runs tests specifically with SQLite backend 247 + 248 + The PostgreSQL test script automatically starts a PostgreSQL container for testing purposes.
+5112
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "addr2line" 7 + version = "0.24.2" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 + dependencies = [ 11 + "gimli", 12 + ] 13 + 14 + [[package]] 15 + name = "adler2" 16 + version = "2.0.0" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 + 20 + [[package]] 21 + name = "aho-corasick" 22 + version = "1.1.3" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 + dependencies = [ 26 + "memchr", 27 + ] 28 + 29 + [[package]] 30 + name = "aligned-vec" 31 + version = "0.5.0" 32 + source = "registry+https://github.com/rust-lang/crates.io-index" 33 + checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" 34 + 35 + [[package]] 36 + name = "allocator-api2" 37 + version = "0.2.21" 38 + source = "registry+https://github.com/rust-lang/crates.io-index" 39 + checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 40 + 41 + [[package]] 42 + name = "android-tzdata" 43 + version = "0.1.1" 44 + source = "registry+https://github.com/rust-lang/crates.io-index" 45 + checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 46 + 47 + [[package]] 48 + name = "android_system_properties" 49 + version = "0.1.5" 50 + source = "registry+https://github.com/rust-lang/crates.io-index" 51 + checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 52 + dependencies = [ 53 + "libc", 54 + ] 55 + 56 + [[package]] 57 + name = "anstream" 58 + version = "0.6.19" 59 + source = "registry+https://github.com/rust-lang/crates.io-index" 60 + checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" 61 + dependencies = [ 62 + "anstyle", 63 + "anstyle-parse", 64 + "anstyle-query", 65 + "anstyle-wincon", 66 + "colorchoice", 67 + "is_terminal_polyfill", 68 + "utf8parse", 69 + ] 70 + 71 + [[package]] 72 + name = "anstyle" 73 + version = "1.0.11" 74 + source = "registry+https://github.com/rust-lang/crates.io-index" 75 + checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 76 + 77 + [[package]] 78 + name = "anstyle-parse" 79 + version = "0.2.7" 80 + source = "registry+https://github.com/rust-lang/crates.io-index" 81 + checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 82 + dependencies = [ 83 + "utf8parse", 84 + ] 85 + 86 + [[package]] 87 + name = "anstyle-query" 88 + version = "1.1.3" 89 + source = "registry+https://github.com/rust-lang/crates.io-index" 90 + checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" 91 + dependencies = [ 92 + "windows-sys 0.59.0", 93 + ] 94 + 95 + [[package]] 96 + name = "anstyle-wincon" 97 + version = "3.0.9" 98 + source = "registry+https://github.com/rust-lang/crates.io-index" 99 + checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" 100 + dependencies = [ 101 + "anstyle", 102 + "once_cell_polyfill", 103 + "windows-sys 0.59.0", 104 + ] 105 + 106 + [[package]] 107 + name = "anyhow" 108 + version = "1.0.98" 109 + source = "registry+https://github.com/rust-lang/crates.io-index" 110 + checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 111 + 112 + [[package]] 113 + name = "arbitrary" 114 + version = "1.4.1" 115 + source = "registry+https://github.com/rust-lang/crates.io-index" 116 + checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" 117 + 118 + [[package]] 119 + name = "arg_enum_proc_macro" 120 + version = "0.3.4" 121 + source = "registry+https://github.com/rust-lang/crates.io-index" 122 + checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" 123 + dependencies = [ 124 + "proc-macro2", 125 + "quote", 126 + "syn 2.0.101", 127 + ] 128 + 129 + [[package]] 130 + name = "arrayvec" 131 + version = "0.7.6" 132 + source = "registry+https://github.com/rust-lang/crates.io-index" 133 + checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 134 + 135 + [[package]] 136 + name = "async-recursion" 137 + version = "1.1.1" 138 + source = "registry+https://github.com/rust-lang/crates.io-index" 139 + checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" 140 + dependencies = [ 141 + "proc-macro2", 142 + "quote", 143 + "syn 2.0.101", 144 + ] 145 + 146 + [[package]] 147 + name = "async-trait" 148 + version = "0.1.88" 149 + source = "registry+https://github.com/rust-lang/crates.io-index" 150 + checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" 151 + dependencies = [ 152 + "proc-macro2", 153 + "quote", 154 + "syn 2.0.101", 155 + ] 156 + 157 + [[package]] 158 + name = "atoi" 159 + version = "2.0.0" 160 + source = "registry+https://github.com/rust-lang/crates.io-index" 161 + checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" 162 + dependencies = [ 163 + "num-traits", 164 + ] 165 + 166 + [[package]] 167 + name = "atomic-waker" 168 + version = "1.1.2" 169 + source = "registry+https://github.com/rust-lang/crates.io-index" 170 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 171 + 172 + [[package]] 173 + name = "atproto-client" 174 + version = "0.6.0" 175 + source = "registry+https://github.com/rust-lang/crates.io-index" 176 + checksum = "aa0602d75861a85bd30eab325e4aa2f1b940a3fa113f220f9d10c933dcce3793" 177 + dependencies = [ 178 + "anyhow", 179 + "atproto-identity", 180 + "atproto-oauth", 181 + "atproto-record", 182 + "bytes", 183 + "reqwest", 184 + "reqwest-chain", 185 + "reqwest-middleware", 186 + "serde", 187 + "serde_json", 188 + "thiserror 2.0.12", 189 + "tokio", 190 + "tracing", 191 + "urlencoding", 192 + ] 193 + 194 + [[package]] 195 + name = "atproto-identity" 196 + version = "0.6.0" 197 + source = "registry+https://github.com/rust-lang/crates.io-index" 198 + checksum = "e42ad430a638d7732f9306e7dd66eae5a7bd13e8323c68958fa664decdf8618f" 199 + dependencies = [ 200 + "anyhow", 201 + "async-trait", 202 + "axum", 203 + "ecdsa", 204 + "elliptic-curve", 205 + "hickory-resolver", 206 + "http", 207 + "k256", 208 + "lru", 209 + "multibase", 210 + "p256", 211 + "rand 0.8.5", 212 + "reqwest", 213 + "serde", 214 + "serde_ipld_dagcbor", 215 + "serde_json", 216 + "thiserror 2.0.12", 217 + "tokio", 218 + "tracing", 219 + ] 220 + 221 + [[package]] 222 + name = "atproto-jetstream" 223 + version = "0.6.0" 224 + source = "registry+https://github.com/rust-lang/crates.io-index" 225 + checksum = "773b5ab60852dbc4621a8119568b5a894d0e6caf5627f74f4a47144758c5ceef" 226 + dependencies = [ 227 + "anyhow", 228 + "async-trait", 229 + "atproto-identity", 230 + "futures", 231 + "http", 232 + "serde", 233 + "serde_json", 234 + "thiserror 2.0.12", 235 + "tokio", 236 + "tokio-util", 237 + "tokio-websockets", 238 + "tracing", 239 + "tracing-subscriber", 240 + "urlencoding", 241 + "zstd", 242 + ] 243 + 244 + [[package]] 245 + name = "atproto-oauth" 246 + version = "0.6.0" 247 + source = "registry+https://github.com/rust-lang/crates.io-index" 248 + checksum = "c698e8f90e9d8e22eb0fa04dece63be9d0441b9120f511a841aa320622ccb53f" 249 + dependencies = [ 250 + "anyhow", 251 + "async-trait", 252 + "atproto-identity", 253 + "axum", 254 + "base64", 255 + "chrono", 256 + "ecdsa", 257 + "elliptic-curve", 258 + "http", 259 + "k256", 260 + "lru", 261 + "multibase", 262 + "p256", 263 + "rand 0.8.5", 264 + "reqwest", 265 + "reqwest-chain", 266 + "reqwest-middleware", 267 + "serde", 268 + "serde_ipld_dagcbor", 269 + "serde_json", 270 + "sha2", 271 + "thiserror 2.0.12", 272 + "tokio", 273 + "tracing", 274 + "ulid", 275 + ] 276 + 277 + [[package]] 278 + name = "atproto-record" 279 + version = "0.6.0" 280 + source = "registry+https://github.com/rust-lang/crates.io-index" 281 + checksum = "d6aee95d1ccee1e0b39d520e5308e2984780925aa78fdc0c7cd05c66dd22f1fe" 282 + dependencies = [ 283 + "anyhow", 284 + "atproto-identity", 285 + "chrono", 286 + "ecdsa", 287 + "k256", 288 + "multibase", 289 + "p256", 290 + "serde", 291 + "serde_ipld_dagcbor", 292 + "serde_json", 293 + "thiserror 2.0.12", 294 + "tokio", 295 + "tracing", 296 + ] 297 + 298 + [[package]] 299 + name = "autocfg" 300 + version = "1.4.0" 301 + source = "registry+https://github.com/rust-lang/crates.io-index" 302 + checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 303 + 304 + [[package]] 305 + name = "av1-grain" 306 + version = "0.2.4" 307 + source = "registry+https://github.com/rust-lang/crates.io-index" 308 + checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" 309 + dependencies = [ 310 + "anyhow", 311 + "arrayvec", 312 + "log", 313 + "nom", 314 + "num-rational", 315 + "v_frame", 316 + ] 317 + 318 + [[package]] 319 + name = "avif-serialize" 320 + version = "0.8.3" 321 + source = "registry+https://github.com/rust-lang/crates.io-index" 322 + checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" 323 + dependencies = [ 324 + "arrayvec", 325 + ] 326 + 327 + [[package]] 328 + name = "axum" 329 + version = "0.8.4" 330 + source = "registry+https://github.com/rust-lang/crates.io-index" 331 + checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" 332 + dependencies = [ 333 + "axum-core", 334 + "axum-macros", 335 + "bytes", 336 + "form_urlencoded", 337 + "futures-util", 338 + "http", 339 + "http-body", 340 + "http-body-util", 341 + "hyper", 342 + "hyper-util", 343 + "itoa", 344 + "matchit", 345 + "memchr", 346 + "mime", 347 + "percent-encoding", 348 + "pin-project-lite", 349 + "rustversion", 350 + "serde", 351 + "serde_json", 352 + "serde_path_to_error", 353 + "serde_urlencoded", 354 + "sync_wrapper", 355 + "tokio", 356 + "tower", 357 + "tower-layer", 358 + "tower-service", 359 + "tracing", 360 + ] 361 + 362 + [[package]] 363 + name = "axum-core" 364 + version = "0.5.2" 365 + source = "registry+https://github.com/rust-lang/crates.io-index" 366 + checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" 367 + dependencies = [ 368 + "bytes", 369 + "futures-core", 370 + "http", 371 + "http-body", 372 + "http-body-util", 373 + "mime", 374 + "pin-project-lite", 375 + "rustversion", 376 + "sync_wrapper", 377 + "tower-layer", 378 + "tower-service", 379 + "tracing", 380 + ] 381 + 382 + [[package]] 383 + name = "axum-macros" 384 + version = "0.5.0" 385 + source = "registry+https://github.com/rust-lang/crates.io-index" 386 + checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" 387 + dependencies = [ 388 + "proc-macro2", 389 + "quote", 390 + "syn 2.0.101", 391 + ] 392 + 393 + [[package]] 394 + name = "axum-template" 395 + version = "3.0.0" 396 + source = "registry+https://github.com/rust-lang/crates.io-index" 397 + checksum = "3df50f7d669bfc3a8c348f08f536fe37e7acfbeded3cfdffd2ad3d76725fc40c" 398 + dependencies = [ 399 + "axum", 400 + "minijinja", 401 + "minijinja-autoreload", 402 + "serde", 403 + "thiserror 2.0.12", 404 + ] 405 + 406 + [[package]] 407 + name = "backtrace" 408 + version = "0.3.75" 409 + source = "registry+https://github.com/rust-lang/crates.io-index" 410 + checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 411 + dependencies = [ 412 + "addr2line", 413 + "cfg-if", 414 + "libc", 415 + "miniz_oxide", 416 + "object", 417 + "rustc-demangle", 418 + "windows-targets 0.52.6", 419 + ] 420 + 421 + [[package]] 422 + name = "base-x" 423 + version = "0.2.11" 424 + source = "registry+https://github.com/rust-lang/crates.io-index" 425 + checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 426 + 427 + [[package]] 428 + name = "base16ct" 429 + version = "0.2.0" 430 + source = "registry+https://github.com/rust-lang/crates.io-index" 431 + checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" 432 + 433 + [[package]] 434 + name = "base64" 435 + version = "0.22.1" 436 + source = "registry+https://github.com/rust-lang/crates.io-index" 437 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 438 + 439 + [[package]] 440 + name = "base64ct" 441 + version = "1.8.0" 442 + source = "registry+https://github.com/rust-lang/crates.io-index" 443 + checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" 444 + 445 + [[package]] 446 + name = "bit_field" 447 + version = "0.10.2" 448 + source = "registry+https://github.com/rust-lang/crates.io-index" 449 + checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" 450 + 451 + [[package]] 452 + name = "bitflags" 453 + version = "1.3.2" 454 + source = "registry+https://github.com/rust-lang/crates.io-index" 455 + checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 456 + 457 + [[package]] 458 + name = "bitflags" 459 + version = "2.9.1" 460 + source = "registry+https://github.com/rust-lang/crates.io-index" 461 + checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 462 + dependencies = [ 463 + "serde", 464 + ] 465 + 466 + [[package]] 467 + name = "bitstream-io" 468 + version = "2.6.0" 469 + source = "registry+https://github.com/rust-lang/crates.io-index" 470 + checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" 471 + 472 + [[package]] 473 + name = "block-buffer" 474 + version = "0.10.4" 475 + source = "registry+https://github.com/rust-lang/crates.io-index" 476 + checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 477 + dependencies = [ 478 + "generic-array", 479 + ] 480 + 481 + [[package]] 482 + name = "built" 483 + version = "0.7.7" 484 + source = "registry+https://github.com/rust-lang/crates.io-index" 485 + checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" 486 + 487 + [[package]] 488 + name = "bumpalo" 489 + version = "3.18.1" 490 + source = "registry+https://github.com/rust-lang/crates.io-index" 491 + checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" 492 + 493 + [[package]] 494 + name = "bytemuck" 495 + version = "1.23.0" 496 + source = "registry+https://github.com/rust-lang/crates.io-index" 497 + checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" 498 + 499 + [[package]] 500 + name = "byteorder" 501 + version = "1.5.0" 502 + source = "registry+https://github.com/rust-lang/crates.io-index" 503 + checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 504 + 505 + [[package]] 506 + name = "byteorder-lite" 507 + version = "0.1.0" 508 + source = "registry+https://github.com/rust-lang/crates.io-index" 509 + checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" 510 + 511 + [[package]] 512 + name = "bytes" 513 + version = "1.10.1" 514 + source = "registry+https://github.com/rust-lang/crates.io-index" 515 + checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 516 + 517 + [[package]] 518 + name = "cbor4ii" 519 + version = "0.2.14" 520 + source = "registry+https://github.com/rust-lang/crates.io-index" 521 + checksum = "b544cf8c89359205f4f990d0e6f3828db42df85b5dac95d09157a250eb0749c4" 522 + dependencies = [ 523 + "serde", 524 + ] 525 + 526 + [[package]] 527 + name = "cc" 528 + version = "1.2.26" 529 + source = "registry+https://github.com/rust-lang/crates.io-index" 530 + checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" 531 + dependencies = [ 532 + "jobserver", 533 + "libc", 534 + "shlex", 535 + ] 536 + 537 + [[package]] 538 + name = "cfg-expr" 539 + version = "0.15.8" 540 + source = "registry+https://github.com/rust-lang/crates.io-index" 541 + checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" 542 + dependencies = [ 543 + "smallvec", 544 + "target-lexicon", 545 + ] 546 + 547 + [[package]] 548 + name = "cfg-if" 549 + version = "1.0.0" 550 + source = "registry+https://github.com/rust-lang/crates.io-index" 551 + checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 552 + 553 + [[package]] 554 + name = "cfg_aliases" 555 + version = "0.2.1" 556 + source = "registry+https://github.com/rust-lang/crates.io-index" 557 + checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 558 + 559 + [[package]] 560 + name = "chrono" 561 + version = "0.4.41" 562 + source = "registry+https://github.com/rust-lang/crates.io-index" 563 + checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 564 + dependencies = [ 565 + "android-tzdata", 566 + "iana-time-zone", 567 + "js-sys", 568 + "num-traits", 569 + "serde", 570 + "wasm-bindgen", 571 + "windows-link", 572 + ] 573 + 574 + [[package]] 575 + name = "cid" 576 + version = "0.11.1" 577 + source = "registry+https://github.com/rust-lang/crates.io-index" 578 + checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a" 579 + dependencies = [ 580 + "core2", 581 + "multibase", 582 + "multihash", 583 + "serde", 584 + "serde_bytes", 585 + "unsigned-varint", 586 + ] 587 + 588 + [[package]] 589 + name = "color_quant" 590 + version = "1.1.0" 591 + source = "registry+https://github.com/rust-lang/crates.io-index" 592 + checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 593 + 594 + [[package]] 595 + name = "colorchoice" 596 + version = "1.0.4" 597 + source = "registry+https://github.com/rust-lang/crates.io-index" 598 + checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 599 + 600 + [[package]] 601 + name = "concurrent-queue" 602 + version = "2.5.0" 603 + source = "registry+https://github.com/rust-lang/crates.io-index" 604 + checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 605 + dependencies = [ 606 + "crossbeam-utils", 607 + ] 608 + 609 + [[package]] 610 + name = "const-oid" 611 + version = "0.9.6" 612 + source = "registry+https://github.com/rust-lang/crates.io-index" 613 + checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 614 + 615 + [[package]] 616 + name = "core-foundation" 617 + version = "0.9.4" 618 + source = "registry+https://github.com/rust-lang/crates.io-index" 619 + checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 620 + dependencies = [ 621 + "core-foundation-sys", 622 + "libc", 623 + ] 624 + 625 + [[package]] 626 + name = "core-foundation" 627 + version = "0.10.1" 628 + source = "registry+https://github.com/rust-lang/crates.io-index" 629 + checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" 630 + dependencies = [ 631 + "core-foundation-sys", 632 + "libc", 633 + ] 634 + 635 + [[package]] 636 + name = "core-foundation-sys" 637 + version = "0.8.7" 638 + source = "registry+https://github.com/rust-lang/crates.io-index" 639 + checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 640 + 641 + [[package]] 642 + name = "core2" 643 + version = "0.4.0" 644 + source = "registry+https://github.com/rust-lang/crates.io-index" 645 + checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" 646 + dependencies = [ 647 + "memchr", 648 + ] 649 + 650 + [[package]] 651 + name = "cpufeatures" 652 + version = "0.2.17" 653 + source = "registry+https://github.com/rust-lang/crates.io-index" 654 + checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 655 + dependencies = [ 656 + "libc", 657 + ] 658 + 659 + [[package]] 660 + name = "crc" 661 + version = "3.3.0" 662 + source = "registry+https://github.com/rust-lang/crates.io-index" 663 + checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" 664 + dependencies = [ 665 + "crc-catalog", 666 + ] 667 + 668 + [[package]] 669 + name = "crc-catalog" 670 + version = "2.4.0" 671 + source = "registry+https://github.com/rust-lang/crates.io-index" 672 + checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 673 + 674 + [[package]] 675 + name = "crc32fast" 676 + version = "1.4.2" 677 + source = "registry+https://github.com/rust-lang/crates.io-index" 678 + checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 679 + dependencies = [ 680 + "cfg-if", 681 + ] 682 + 683 + [[package]] 684 + name = "critical-section" 685 + version = "1.2.0" 686 + source = "registry+https://github.com/rust-lang/crates.io-index" 687 + checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" 688 + 689 + [[package]] 690 + name = "crossbeam-channel" 691 + version = "0.5.15" 692 + source = "registry+https://github.com/rust-lang/crates.io-index" 693 + checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 694 + dependencies = [ 695 + "crossbeam-utils", 696 + ] 697 + 698 + [[package]] 699 + name = "crossbeam-deque" 700 + version = "0.8.6" 701 + source = "registry+https://github.com/rust-lang/crates.io-index" 702 + checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 703 + dependencies = [ 704 + "crossbeam-epoch", 705 + "crossbeam-utils", 706 + ] 707 + 708 + [[package]] 709 + name = "crossbeam-epoch" 710 + version = "0.9.18" 711 + source = "registry+https://github.com/rust-lang/crates.io-index" 712 + checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 713 + dependencies = [ 714 + "crossbeam-utils", 715 + ] 716 + 717 + [[package]] 718 + name = "crossbeam-queue" 719 + version = "0.3.12" 720 + source = "registry+https://github.com/rust-lang/crates.io-index" 721 + checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" 722 + dependencies = [ 723 + "crossbeam-utils", 724 + ] 725 + 726 + [[package]] 727 + name = "crossbeam-utils" 728 + version = "0.8.21" 729 + source = "registry+https://github.com/rust-lang/crates.io-index" 730 + checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 731 + 732 + [[package]] 733 + name = "crunchy" 734 + version = "0.2.3" 735 + source = "registry+https://github.com/rust-lang/crates.io-index" 736 + checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" 737 + 738 + [[package]] 739 + name = "crypto-bigint" 740 + version = "0.5.5" 741 + source = "registry+https://github.com/rust-lang/crates.io-index" 742 + checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" 743 + dependencies = [ 744 + "generic-array", 745 + "rand_core 0.6.4", 746 + "subtle", 747 + "zeroize", 748 + ] 749 + 750 + [[package]] 751 + name = "crypto-common" 752 + version = "0.1.6" 753 + source = "registry+https://github.com/rust-lang/crates.io-index" 754 + checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 755 + dependencies = [ 756 + "generic-array", 757 + "typenum", 758 + ] 759 + 760 + [[package]] 761 + name = "dashmap" 762 + version = "6.1.0" 763 + source = "registry+https://github.com/rust-lang/crates.io-index" 764 + checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" 765 + dependencies = [ 766 + "cfg-if", 767 + "crossbeam-utils", 768 + "hashbrown 0.14.5", 769 + "lock_api", 770 + "once_cell", 771 + "parking_lot_core", 772 + ] 773 + 774 + [[package]] 775 + name = "data-encoding" 776 + version = "2.9.0" 777 + source = "registry+https://github.com/rust-lang/crates.io-index" 778 + checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 779 + 780 + [[package]] 781 + name = "data-encoding-macro" 782 + version = "0.1.18" 783 + source = "registry+https://github.com/rust-lang/crates.io-index" 784 + checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" 785 + dependencies = [ 786 + "data-encoding", 787 + "data-encoding-macro-internal", 788 + ] 789 + 790 + [[package]] 791 + name = "data-encoding-macro-internal" 792 + version = "0.1.16" 793 + source = "registry+https://github.com/rust-lang/crates.io-index" 794 + checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" 795 + dependencies = [ 796 + "data-encoding", 797 + "syn 2.0.101", 798 + ] 799 + 800 + [[package]] 801 + name = "der" 802 + version = "0.7.10" 803 + source = "registry+https://github.com/rust-lang/crates.io-index" 804 + checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 805 + dependencies = [ 806 + "const-oid", 807 + "pem-rfc7468", 808 + "zeroize", 809 + ] 810 + 811 + [[package]] 812 + name = "deranged" 813 + version = "0.4.0" 814 + source = "registry+https://github.com/rust-lang/crates.io-index" 815 + checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 816 + dependencies = [ 817 + "powerfmt", 818 + ] 819 + 820 + [[package]] 821 + name = "derivative" 822 + version = "2.2.0" 823 + source = "registry+https://github.com/rust-lang/crates.io-index" 824 + checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" 825 + dependencies = [ 826 + "proc-macro2", 827 + "quote", 828 + "syn 1.0.109", 829 + ] 830 + 831 + [[package]] 832 + name = "digest" 833 + version = "0.10.7" 834 + source = "registry+https://github.com/rust-lang/crates.io-index" 835 + checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 836 + dependencies = [ 837 + "block-buffer", 838 + "const-oid", 839 + "crypto-common", 840 + "subtle", 841 + ] 842 + 843 + [[package]] 844 + name = "displaydoc" 845 + version = "0.2.5" 846 + source = "registry+https://github.com/rust-lang/crates.io-index" 847 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 848 + dependencies = [ 849 + "proc-macro2", 850 + "quote", 851 + "syn 2.0.101", 852 + ] 853 + 854 + [[package]] 855 + name = "dotenvy" 856 + version = "0.15.7" 857 + source = "registry+https://github.com/rust-lang/crates.io-index" 858 + checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 859 + 860 + [[package]] 861 + name = "duration-str" 862 + version = "0.11.3" 863 + source = "registry+https://github.com/rust-lang/crates.io-index" 864 + checksum = "f88959de2d447fd3eddcf1909d1f19fe084e27a056a6904203dc5d8b9e771c1e" 865 + dependencies = [ 866 + "chrono", 867 + "rust_decimal", 868 + "serde", 869 + "thiserror 2.0.12", 870 + "time", 871 + "winnow 0.6.26", 872 + ] 873 + 874 + [[package]] 875 + name = "ecdsa" 876 + version = "0.16.9" 877 + source = "registry+https://github.com/rust-lang/crates.io-index" 878 + checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" 879 + dependencies = [ 880 + "der", 881 + "digest", 882 + "elliptic-curve", 883 + "rfc6979", 884 + "serdect", 885 + "signature", 886 + "spki", 887 + ] 888 + 889 + [[package]] 890 + name = "either" 891 + version = "1.15.0" 892 + source = "registry+https://github.com/rust-lang/crates.io-index" 893 + checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 894 + dependencies = [ 895 + "serde", 896 + ] 897 + 898 + [[package]] 899 + name = "elliptic-curve" 900 + version = "0.13.8" 901 + source = "registry+https://github.com/rust-lang/crates.io-index" 902 + checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" 903 + dependencies = [ 904 + "base16ct", 905 + "base64ct", 906 + "crypto-bigint", 907 + "digest", 908 + "ff", 909 + "generic-array", 910 + "group", 911 + "pem-rfc7468", 912 + "pkcs8", 913 + "rand_core 0.6.4", 914 + "sec1", 915 + "serde_json", 916 + "serdect", 917 + "subtle", 918 + "zeroize", 919 + ] 920 + 921 + [[package]] 922 + name = "encoding_rs" 923 + version = "0.8.35" 924 + source = "registry+https://github.com/rust-lang/crates.io-index" 925 + checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 926 + dependencies = [ 927 + "cfg-if", 928 + ] 929 + 930 + [[package]] 931 + name = "enum-as-inner" 932 + version = "0.6.1" 933 + source = "registry+https://github.com/rust-lang/crates.io-index" 934 + checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" 935 + dependencies = [ 936 + "heck", 937 + "proc-macro2", 938 + "quote", 939 + "syn 2.0.101", 940 + ] 941 + 942 + [[package]] 943 + name = "env_filter" 944 + version = "0.1.3" 945 + source = "registry+https://github.com/rust-lang/crates.io-index" 946 + checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 947 + dependencies = [ 948 + "log", 949 + "regex", 950 + ] 951 + 952 + [[package]] 953 + name = "env_logger" 954 + version = "0.11.8" 955 + source = "registry+https://github.com/rust-lang/crates.io-index" 956 + checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 957 + dependencies = [ 958 + "anstream", 959 + "anstyle", 960 + "env_filter", 961 + "jiff", 962 + "log", 963 + ] 964 + 965 + [[package]] 966 + name = "equivalent" 967 + version = "1.0.2" 968 + source = "registry+https://github.com/rust-lang/crates.io-index" 969 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 970 + 971 + [[package]] 972 + name = "errno" 973 + version = "0.3.12" 974 + source = "registry+https://github.com/rust-lang/crates.io-index" 975 + checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" 976 + dependencies = [ 977 + "libc", 978 + "windows-sys 0.59.0", 979 + ] 980 + 981 + [[package]] 982 + name = "etcetera" 983 + version = "0.8.0" 984 + source = "registry+https://github.com/rust-lang/crates.io-index" 985 + checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" 986 + dependencies = [ 987 + "cfg-if", 988 + "home", 989 + "windows-sys 0.48.0", 990 + ] 991 + 992 + [[package]] 993 + name = "event-listener" 994 + version = "5.4.0" 995 + source = "registry+https://github.com/rust-lang/crates.io-index" 996 + checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" 997 + dependencies = [ 998 + "concurrent-queue", 999 + "parking", 1000 + "pin-project-lite", 1001 + ] 1002 + 1003 + [[package]] 1004 + name = "exr" 1005 + version = "1.73.0" 1006 + source = "registry+https://github.com/rust-lang/crates.io-index" 1007 + checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" 1008 + dependencies = [ 1009 + "bit_field", 1010 + "half", 1011 + "lebe", 1012 + "miniz_oxide", 1013 + "rayon-core", 1014 + "smallvec", 1015 + "zune-inflate", 1016 + ] 1017 + 1018 + [[package]] 1019 + name = "fastrand" 1020 + version = "2.3.0" 1021 + source = "registry+https://github.com/rust-lang/crates.io-index" 1022 + checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 1023 + 1024 + [[package]] 1025 + name = "fdeflate" 1026 + version = "0.3.7" 1027 + source = "registry+https://github.com/rust-lang/crates.io-index" 1028 + checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 1029 + dependencies = [ 1030 + "simd-adler32", 1031 + ] 1032 + 1033 + [[package]] 1034 + name = "ff" 1035 + version = "0.13.1" 1036 + source = "registry+https://github.com/rust-lang/crates.io-index" 1037 + checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" 1038 + dependencies = [ 1039 + "rand_core 0.6.4", 1040 + "subtle", 1041 + ] 1042 + 1043 + [[package]] 1044 + name = "filetime" 1045 + version = "0.2.25" 1046 + source = "registry+https://github.com/rust-lang/crates.io-index" 1047 + checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" 1048 + dependencies = [ 1049 + "cfg-if", 1050 + "libc", 1051 + "libredox", 1052 + "windows-sys 0.59.0", 1053 + ] 1054 + 1055 + [[package]] 1056 + name = "flate2" 1057 + version = "1.1.1" 1058 + source = "registry+https://github.com/rust-lang/crates.io-index" 1059 + checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" 1060 + dependencies = [ 1061 + "crc32fast", 1062 + "miniz_oxide", 1063 + ] 1064 + 1065 + [[package]] 1066 + name = "flume" 1067 + version = "0.11.1" 1068 + source = "registry+https://github.com/rust-lang/crates.io-index" 1069 + checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" 1070 + dependencies = [ 1071 + "futures-core", 1072 + "futures-sink", 1073 + "spin", 1074 + ] 1075 + 1076 + [[package]] 1077 + name = "fnv" 1078 + version = "1.0.7" 1079 + source = "registry+https://github.com/rust-lang/crates.io-index" 1080 + checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 1081 + 1082 + [[package]] 1083 + name = "foldhash" 1084 + version = "0.1.5" 1085 + source = "registry+https://github.com/rust-lang/crates.io-index" 1086 + checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 1087 + 1088 + [[package]] 1089 + name = "foreign-types" 1090 + version = "0.3.2" 1091 + source = "registry+https://github.com/rust-lang/crates.io-index" 1092 + checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 1093 + dependencies = [ 1094 + "foreign-types-shared", 1095 + ] 1096 + 1097 + [[package]] 1098 + name = "foreign-types-shared" 1099 + version = "0.1.1" 1100 + source = "registry+https://github.com/rust-lang/crates.io-index" 1101 + checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 1102 + 1103 + [[package]] 1104 + name = "form_urlencoded" 1105 + version = "1.2.1" 1106 + source = "registry+https://github.com/rust-lang/crates.io-index" 1107 + checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 1108 + dependencies = [ 1109 + "percent-encoding", 1110 + ] 1111 + 1112 + [[package]] 1113 + name = "fsevent-sys" 1114 + version = "4.1.0" 1115 + source = "registry+https://github.com/rust-lang/crates.io-index" 1116 + checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" 1117 + dependencies = [ 1118 + "libc", 1119 + ] 1120 + 1121 + [[package]] 1122 + name = "futures" 1123 + version = "0.3.31" 1124 + source = "registry+https://github.com/rust-lang/crates.io-index" 1125 + checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 1126 + dependencies = [ 1127 + "futures-channel", 1128 + "futures-core", 1129 + "futures-executor", 1130 + "futures-io", 1131 + "futures-sink", 1132 + "futures-task", 1133 + "futures-util", 1134 + ] 1135 + 1136 + [[package]] 1137 + name = "futures-channel" 1138 + version = "0.3.31" 1139 + source = "registry+https://github.com/rust-lang/crates.io-index" 1140 + checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 1141 + dependencies = [ 1142 + "futures-core", 1143 + "futures-sink", 1144 + ] 1145 + 1146 + [[package]] 1147 + name = "futures-core" 1148 + version = "0.3.31" 1149 + source = "registry+https://github.com/rust-lang/crates.io-index" 1150 + checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 1151 + 1152 + [[package]] 1153 + name = "futures-executor" 1154 + version = "0.3.31" 1155 + source = "registry+https://github.com/rust-lang/crates.io-index" 1156 + checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 1157 + dependencies = [ 1158 + "futures-core", 1159 + "futures-task", 1160 + "futures-util", 1161 + ] 1162 + 1163 + [[package]] 1164 + name = "futures-intrusive" 1165 + version = "0.5.0" 1166 + source = "registry+https://github.com/rust-lang/crates.io-index" 1167 + checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" 1168 + dependencies = [ 1169 + "futures-core", 1170 + "lock_api", 1171 + "parking_lot", 1172 + ] 1173 + 1174 + [[package]] 1175 + name = "futures-io" 1176 + version = "0.3.31" 1177 + source = "registry+https://github.com/rust-lang/crates.io-index" 1178 + checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 1179 + 1180 + [[package]] 1181 + name = "futures-macro" 1182 + version = "0.3.31" 1183 + source = "registry+https://github.com/rust-lang/crates.io-index" 1184 + checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 1185 + dependencies = [ 1186 + "proc-macro2", 1187 + "quote", 1188 + "syn 2.0.101", 1189 + ] 1190 + 1191 + [[package]] 1192 + name = "futures-sink" 1193 + version = "0.3.31" 1194 + source = "registry+https://github.com/rust-lang/crates.io-index" 1195 + checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 1196 + 1197 + [[package]] 1198 + name = "futures-task" 1199 + version = "0.3.31" 1200 + source = "registry+https://github.com/rust-lang/crates.io-index" 1201 + checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 1202 + 1203 + [[package]] 1204 + name = "futures-util" 1205 + version = "0.3.31" 1206 + source = "registry+https://github.com/rust-lang/crates.io-index" 1207 + checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 1208 + dependencies = [ 1209 + "futures-channel", 1210 + "futures-core", 1211 + "futures-io", 1212 + "futures-macro", 1213 + "futures-sink", 1214 + "futures-task", 1215 + "memchr", 1216 + "pin-project-lite", 1217 + "pin-utils", 1218 + "slab", 1219 + ] 1220 + 1221 + [[package]] 1222 + name = "generator" 1223 + version = "0.8.5" 1224 + source = "registry+https://github.com/rust-lang/crates.io-index" 1225 + checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" 1226 + dependencies = [ 1227 + "cc", 1228 + "cfg-if", 1229 + "libc", 1230 + "log", 1231 + "rustversion", 1232 + "windows", 1233 + ] 1234 + 1235 + [[package]] 1236 + name = "generic-array" 1237 + version = "0.14.7" 1238 + source = "registry+https://github.com/rust-lang/crates.io-index" 1239 + checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 1240 + dependencies = [ 1241 + "typenum", 1242 + "version_check", 1243 + "zeroize", 1244 + ] 1245 + 1246 + [[package]] 1247 + name = "getrandom" 1248 + version = "0.2.16" 1249 + source = "registry+https://github.com/rust-lang/crates.io-index" 1250 + checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 1251 + dependencies = [ 1252 + "cfg-if", 1253 + "js-sys", 1254 + "libc", 1255 + "wasi 0.11.0+wasi-snapshot-preview1", 1256 + "wasm-bindgen", 1257 + ] 1258 + 1259 + [[package]] 1260 + name = "getrandom" 1261 + version = "0.3.3" 1262 + source = "registry+https://github.com/rust-lang/crates.io-index" 1263 + checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 1264 + dependencies = [ 1265 + "cfg-if", 1266 + "js-sys", 1267 + "libc", 1268 + "r-efi", 1269 + "wasi 0.14.2+wasi-0.2.4", 1270 + "wasm-bindgen", 1271 + ] 1272 + 1273 + [[package]] 1274 + name = "gif" 1275 + version = "0.13.1" 1276 + source = "registry+https://github.com/rust-lang/crates.io-index" 1277 + checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" 1278 + dependencies = [ 1279 + "color_quant", 1280 + "weezl", 1281 + ] 1282 + 1283 + [[package]] 1284 + name = "gimli" 1285 + version = "0.31.1" 1286 + source = "registry+https://github.com/rust-lang/crates.io-index" 1287 + checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 1288 + 1289 + [[package]] 1290 + name = "group" 1291 + version = "0.13.0" 1292 + source = "registry+https://github.com/rust-lang/crates.io-index" 1293 + checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" 1294 + dependencies = [ 1295 + "ff", 1296 + "rand_core 0.6.4", 1297 + "subtle", 1298 + ] 1299 + 1300 + [[package]] 1301 + name = "h2" 1302 + version = "0.4.10" 1303 + source = "registry+https://github.com/rust-lang/crates.io-index" 1304 + checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" 1305 + dependencies = [ 1306 + "atomic-waker", 1307 + "bytes", 1308 + "fnv", 1309 + "futures-core", 1310 + "futures-sink", 1311 + "http", 1312 + "indexmap", 1313 + "slab", 1314 + "tokio", 1315 + "tokio-util", 1316 + "tracing", 1317 + ] 1318 + 1319 + [[package]] 1320 + name = "half" 1321 + version = "2.6.0" 1322 + source = "registry+https://github.com/rust-lang/crates.io-index" 1323 + checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" 1324 + dependencies = [ 1325 + "cfg-if", 1326 + "crunchy", 1327 + ] 1328 + 1329 + [[package]] 1330 + name = "hashbrown" 1331 + version = "0.14.5" 1332 + source = "registry+https://github.com/rust-lang/crates.io-index" 1333 + checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 1334 + 1335 + [[package]] 1336 + name = "hashbrown" 1337 + version = "0.15.3" 1338 + source = "registry+https://github.com/rust-lang/crates.io-index" 1339 + checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 1340 + dependencies = [ 1341 + "allocator-api2", 1342 + "equivalent", 1343 + "foldhash", 1344 + ] 1345 + 1346 + [[package]] 1347 + name = "hashlink" 1348 + version = "0.10.0" 1349 + source = "registry+https://github.com/rust-lang/crates.io-index" 1350 + checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 1351 + dependencies = [ 1352 + "hashbrown 0.15.3", 1353 + ] 1354 + 1355 + [[package]] 1356 + name = "heck" 1357 + version = "0.5.0" 1358 + source = "registry+https://github.com/rust-lang/crates.io-index" 1359 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 1360 + 1361 + [[package]] 1362 + name = "hex" 1363 + version = "0.4.3" 1364 + source = "registry+https://github.com/rust-lang/crates.io-index" 1365 + checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 1366 + 1367 + [[package]] 1368 + name = "hickory-proto" 1369 + version = "0.25.2" 1370 + source = "registry+https://github.com/rust-lang/crates.io-index" 1371 + checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" 1372 + dependencies = [ 1373 + "async-trait", 1374 + "cfg-if", 1375 + "data-encoding", 1376 + "enum-as-inner", 1377 + "futures-channel", 1378 + "futures-io", 1379 + "futures-util", 1380 + "idna", 1381 + "ipnet", 1382 + "once_cell", 1383 + "rand 0.9.1", 1384 + "ring", 1385 + "thiserror 2.0.12", 1386 + "tinyvec", 1387 + "tokio", 1388 + "tracing", 1389 + "url", 1390 + ] 1391 + 1392 + [[package]] 1393 + name = "hickory-resolver" 1394 + version = "0.25.2" 1395 + source = "registry+https://github.com/rust-lang/crates.io-index" 1396 + checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" 1397 + dependencies = [ 1398 + "cfg-if", 1399 + "futures-util", 1400 + "hickory-proto", 1401 + "ipconfig", 1402 + "moka", 1403 + "once_cell", 1404 + "parking_lot", 1405 + "rand 0.9.1", 1406 + "resolv-conf", 1407 + "smallvec", 1408 + "thiserror 2.0.12", 1409 + "tokio", 1410 + "tracing", 1411 + ] 1412 + 1413 + [[package]] 1414 + name = "hkdf" 1415 + version = "0.12.4" 1416 + source = "registry+https://github.com/rust-lang/crates.io-index" 1417 + checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" 1418 + dependencies = [ 1419 + "hmac", 1420 + ] 1421 + 1422 + [[package]] 1423 + name = "hmac" 1424 + version = "0.12.1" 1425 + source = "registry+https://github.com/rust-lang/crates.io-index" 1426 + checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 1427 + dependencies = [ 1428 + "digest", 1429 + ] 1430 + 1431 + [[package]] 1432 + name = "home" 1433 + version = "0.5.11" 1434 + source = "registry+https://github.com/rust-lang/crates.io-index" 1435 + checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 1436 + dependencies = [ 1437 + "windows-sys 0.59.0", 1438 + ] 1439 + 1440 + [[package]] 1441 + name = "http" 1442 + version = "1.3.1" 1443 + source = "registry+https://github.com/rust-lang/crates.io-index" 1444 + checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 1445 + dependencies = [ 1446 + "bytes", 1447 + "fnv", 1448 + "itoa", 1449 + ] 1450 + 1451 + [[package]] 1452 + name = "http-body" 1453 + version = "1.0.1" 1454 + source = "registry+https://github.com/rust-lang/crates.io-index" 1455 + checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 1456 + dependencies = [ 1457 + "bytes", 1458 + "http", 1459 + ] 1460 + 1461 + [[package]] 1462 + name = "http-body-util" 1463 + version = "0.1.3" 1464 + source = "registry+https://github.com/rust-lang/crates.io-index" 1465 + checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 1466 + dependencies = [ 1467 + "bytes", 1468 + "futures-core", 1469 + "http", 1470 + "http-body", 1471 + "pin-project-lite", 1472 + ] 1473 + 1474 + [[package]] 1475 + name = "http-range-header" 1476 + version = "0.4.2" 1477 + source = "registry+https://github.com/rust-lang/crates.io-index" 1478 + checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" 1479 + 1480 + [[package]] 1481 + name = "httparse" 1482 + version = "1.10.1" 1483 + source = "registry+https://github.com/rust-lang/crates.io-index" 1484 + checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 1485 + 1486 + [[package]] 1487 + name = "httpdate" 1488 + version = "1.0.3" 1489 + source = "registry+https://github.com/rust-lang/crates.io-index" 1490 + checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 1491 + 1492 + [[package]] 1493 + name = "hyper" 1494 + version = "1.6.0" 1495 + source = "registry+https://github.com/rust-lang/crates.io-index" 1496 + checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 1497 + dependencies = [ 1498 + "bytes", 1499 + "futures-channel", 1500 + "futures-util", 1501 + "h2", 1502 + "http", 1503 + "http-body", 1504 + "httparse", 1505 + "httpdate", 1506 + "itoa", 1507 + "pin-project-lite", 1508 + "smallvec", 1509 + "tokio", 1510 + "want", 1511 + ] 1512 + 1513 + [[package]] 1514 + name = "hyper-rustls" 1515 + version = "0.27.7" 1516 + source = "registry+https://github.com/rust-lang/crates.io-index" 1517 + checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 1518 + dependencies = [ 1519 + "http", 1520 + "hyper", 1521 + "hyper-util", 1522 + "rustls", 1523 + "rustls-pki-types", 1524 + "tokio", 1525 + "tokio-rustls", 1526 + "tower-service", 1527 + "webpki-roots 1.0.0", 1528 + ] 1529 + 1530 + [[package]] 1531 + name = "hyper-tls" 1532 + version = "0.6.0" 1533 + source = "registry+https://github.com/rust-lang/crates.io-index" 1534 + checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 1535 + dependencies = [ 1536 + "bytes", 1537 + "http-body-util", 1538 + "hyper", 1539 + "hyper-util", 1540 + "native-tls", 1541 + "tokio", 1542 + "tokio-native-tls", 1543 + "tower-service", 1544 + ] 1545 + 1546 + [[package]] 1547 + name = "hyper-util" 1548 + version = "0.1.14" 1549 + source = "registry+https://github.com/rust-lang/crates.io-index" 1550 + checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" 1551 + dependencies = [ 1552 + "base64", 1553 + "bytes", 1554 + "futures-channel", 1555 + "futures-core", 1556 + "futures-util", 1557 + "http", 1558 + "http-body", 1559 + "hyper", 1560 + "ipnet", 1561 + "libc", 1562 + "percent-encoding", 1563 + "pin-project-lite", 1564 + "socket2", 1565 + "system-configuration", 1566 + "tokio", 1567 + "tower-service", 1568 + "tracing", 1569 + "windows-registry", 1570 + ] 1571 + 1572 + [[package]] 1573 + name = "iana-time-zone" 1574 + version = "0.1.63" 1575 + source = "registry+https://github.com/rust-lang/crates.io-index" 1576 + checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 1577 + dependencies = [ 1578 + "android_system_properties", 1579 + "core-foundation-sys", 1580 + "iana-time-zone-haiku", 1581 + "js-sys", 1582 + "log", 1583 + "wasm-bindgen", 1584 + "windows-core", 1585 + ] 1586 + 1587 + [[package]] 1588 + name = "iana-time-zone-haiku" 1589 + version = "0.1.2" 1590 + source = "registry+https://github.com/rust-lang/crates.io-index" 1591 + checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 1592 + dependencies = [ 1593 + "cc", 1594 + ] 1595 + 1596 + [[package]] 1597 + name = "icu_collections" 1598 + version = "2.0.0" 1599 + source = "registry+https://github.com/rust-lang/crates.io-index" 1600 + checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 1601 + dependencies = [ 1602 + "displaydoc", 1603 + "potential_utf", 1604 + "yoke", 1605 + "zerofrom", 1606 + "zerovec", 1607 + ] 1608 + 1609 + [[package]] 1610 + name = "icu_locale_core" 1611 + version = "2.0.0" 1612 + source = "registry+https://github.com/rust-lang/crates.io-index" 1613 + checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 1614 + dependencies = [ 1615 + "displaydoc", 1616 + "litemap", 1617 + "tinystr", 1618 + "writeable", 1619 + "zerovec", 1620 + ] 1621 + 1622 + [[package]] 1623 + name = "icu_normalizer" 1624 + version = "2.0.0" 1625 + source = "registry+https://github.com/rust-lang/crates.io-index" 1626 + checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 1627 + dependencies = [ 1628 + "displaydoc", 1629 + "icu_collections", 1630 + "icu_normalizer_data", 1631 + "icu_properties", 1632 + "icu_provider", 1633 + "smallvec", 1634 + "zerovec", 1635 + ] 1636 + 1637 + [[package]] 1638 + name = "icu_normalizer_data" 1639 + version = "2.0.0" 1640 + source = "registry+https://github.com/rust-lang/crates.io-index" 1641 + checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 1642 + 1643 + [[package]] 1644 + name = "icu_properties" 1645 + version = "2.0.1" 1646 + source = "registry+https://github.com/rust-lang/crates.io-index" 1647 + checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 1648 + dependencies = [ 1649 + "displaydoc", 1650 + "icu_collections", 1651 + "icu_locale_core", 1652 + "icu_properties_data", 1653 + "icu_provider", 1654 + "potential_utf", 1655 + "zerotrie", 1656 + "zerovec", 1657 + ] 1658 + 1659 + [[package]] 1660 + name = "icu_properties_data" 1661 + version = "2.0.1" 1662 + source = "registry+https://github.com/rust-lang/crates.io-index" 1663 + checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 1664 + 1665 + [[package]] 1666 + name = "icu_provider" 1667 + version = "2.0.0" 1668 + source = "registry+https://github.com/rust-lang/crates.io-index" 1669 + checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 1670 + dependencies = [ 1671 + "displaydoc", 1672 + "icu_locale_core", 1673 + "stable_deref_trait", 1674 + "tinystr", 1675 + "writeable", 1676 + "yoke", 1677 + "zerofrom", 1678 + "zerotrie", 1679 + "zerovec", 1680 + ] 1681 + 1682 + [[package]] 1683 + name = "idna" 1684 + version = "1.0.3" 1685 + source = "registry+https://github.com/rust-lang/crates.io-index" 1686 + checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 1687 + dependencies = [ 1688 + "idna_adapter", 1689 + "smallvec", 1690 + "utf8_iter", 1691 + ] 1692 + 1693 + [[package]] 1694 + name = "idna_adapter" 1695 + version = "1.2.1" 1696 + source = "registry+https://github.com/rust-lang/crates.io-index" 1697 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 1698 + dependencies = [ 1699 + "icu_normalizer", 1700 + "icu_properties", 1701 + ] 1702 + 1703 + [[package]] 1704 + name = "image" 1705 + version = "0.25.6" 1706 + source = "registry+https://github.com/rust-lang/crates.io-index" 1707 + checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" 1708 + dependencies = [ 1709 + "bytemuck", 1710 + "byteorder-lite", 1711 + "color_quant", 1712 + "exr", 1713 + "gif", 1714 + "image-webp", 1715 + "num-traits", 1716 + "png", 1717 + "qoi", 1718 + "ravif", 1719 + "rayon", 1720 + "rgb", 1721 + "tiff", 1722 + "zune-core", 1723 + "zune-jpeg", 1724 + ] 1725 + 1726 + [[package]] 1727 + name = "image-webp" 1728 + version = "0.2.1" 1729 + source = "registry+https://github.com/rust-lang/crates.io-index" 1730 + checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" 1731 + dependencies = [ 1732 + "byteorder-lite", 1733 + "quick-error", 1734 + ] 1735 + 1736 + [[package]] 1737 + name = "imgref" 1738 + version = "1.11.0" 1739 + source = "registry+https://github.com/rust-lang/crates.io-index" 1740 + checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" 1741 + 1742 + [[package]] 1743 + name = "indexmap" 1744 + version = "2.9.0" 1745 + source = "registry+https://github.com/rust-lang/crates.io-index" 1746 + checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 1747 + dependencies = [ 1748 + "equivalent", 1749 + "hashbrown 0.15.3", 1750 + ] 1751 + 1752 + [[package]] 1753 + name = "inotify" 1754 + version = "0.11.0" 1755 + source = "registry+https://github.com/rust-lang/crates.io-index" 1756 + checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" 1757 + dependencies = [ 1758 + "bitflags 2.9.1", 1759 + "inotify-sys", 1760 + "libc", 1761 + ] 1762 + 1763 + [[package]] 1764 + name = "inotify-sys" 1765 + version = "0.1.5" 1766 + source = "registry+https://github.com/rust-lang/crates.io-index" 1767 + checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" 1768 + dependencies = [ 1769 + "libc", 1770 + ] 1771 + 1772 + [[package]] 1773 + name = "interpolate_name" 1774 + version = "0.2.4" 1775 + source = "registry+https://github.com/rust-lang/crates.io-index" 1776 + checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" 1777 + dependencies = [ 1778 + "proc-macro2", 1779 + "quote", 1780 + "syn 2.0.101", 1781 + ] 1782 + 1783 + [[package]] 1784 + name = "ipconfig" 1785 + version = "0.3.2" 1786 + source = "registry+https://github.com/rust-lang/crates.io-index" 1787 + checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" 1788 + dependencies = [ 1789 + "socket2", 1790 + "widestring", 1791 + "windows-sys 0.48.0", 1792 + "winreg", 1793 + ] 1794 + 1795 + [[package]] 1796 + name = "ipld-core" 1797 + version = "0.4.2" 1798 + source = "registry+https://github.com/rust-lang/crates.io-index" 1799 + checksum = "104718b1cc124d92a6d01ca9c9258a7df311405debb3408c445a36452f9bf8db" 1800 + dependencies = [ 1801 + "cid", 1802 + "serde", 1803 + "serde_bytes", 1804 + ] 1805 + 1806 + [[package]] 1807 + name = "ipnet" 1808 + version = "2.11.0" 1809 + source = "registry+https://github.com/rust-lang/crates.io-index" 1810 + checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 1811 + 1812 + [[package]] 1813 + name = "iri-string" 1814 + version = "0.7.8" 1815 + source = "registry+https://github.com/rust-lang/crates.io-index" 1816 + checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" 1817 + dependencies = [ 1818 + "memchr", 1819 + "serde", 1820 + ] 1821 + 1822 + [[package]] 1823 + name = "is_terminal_polyfill" 1824 + version = "1.70.1" 1825 + source = "registry+https://github.com/rust-lang/crates.io-index" 1826 + checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 1827 + 1828 + [[package]] 1829 + name = "itertools" 1830 + version = "0.12.1" 1831 + source = "registry+https://github.com/rust-lang/crates.io-index" 1832 + checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 1833 + dependencies = [ 1834 + "either", 1835 + ] 1836 + 1837 + [[package]] 1838 + name = "itoa" 1839 + version = "1.0.15" 1840 + source = "registry+https://github.com/rust-lang/crates.io-index" 1841 + checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 1842 + 1843 + [[package]] 1844 + name = "jiff" 1845 + version = "0.2.14" 1846 + source = "registry+https://github.com/rust-lang/crates.io-index" 1847 + checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" 1848 + dependencies = [ 1849 + "jiff-static", 1850 + "log", 1851 + "portable-atomic", 1852 + "portable-atomic-util", 1853 + "serde", 1854 + ] 1855 + 1856 + [[package]] 1857 + name = "jiff-static" 1858 + version = "0.2.14" 1859 + source = "registry+https://github.com/rust-lang/crates.io-index" 1860 + checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" 1861 + dependencies = [ 1862 + "proc-macro2", 1863 + "quote", 1864 + "syn 2.0.101", 1865 + ] 1866 + 1867 + [[package]] 1868 + name = "jobserver" 1869 + version = "0.1.33" 1870 + source = "registry+https://github.com/rust-lang/crates.io-index" 1871 + checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" 1872 + dependencies = [ 1873 + "getrandom 0.3.3", 1874 + "libc", 1875 + ] 1876 + 1877 + [[package]] 1878 + name = "jpeg-decoder" 1879 + version = "0.3.1" 1880 + source = "registry+https://github.com/rust-lang/crates.io-index" 1881 + checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" 1882 + 1883 + [[package]] 1884 + name = "js-sys" 1885 + version = "0.3.77" 1886 + source = "registry+https://github.com/rust-lang/crates.io-index" 1887 + checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 1888 + dependencies = [ 1889 + "once_cell", 1890 + "wasm-bindgen", 1891 + ] 1892 + 1893 + [[package]] 1894 + name = "k256" 1895 + version = "0.13.4" 1896 + source = "registry+https://github.com/rust-lang/crates.io-index" 1897 + checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" 1898 + dependencies = [ 1899 + "cfg-if", 1900 + "ecdsa", 1901 + "elliptic-curve", 1902 + "once_cell", 1903 + "sha2", 1904 + "signature", 1905 + ] 1906 + 1907 + [[package]] 1908 + name = "kqueue" 1909 + version = "1.1.1" 1910 + source = "registry+https://github.com/rust-lang/crates.io-index" 1911 + checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" 1912 + dependencies = [ 1913 + "kqueue-sys", 1914 + "libc", 1915 + ] 1916 + 1917 + [[package]] 1918 + name = "kqueue-sys" 1919 + version = "1.0.4" 1920 + source = "registry+https://github.com/rust-lang/crates.io-index" 1921 + checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" 1922 + dependencies = [ 1923 + "bitflags 1.3.2", 1924 + "libc", 1925 + ] 1926 + 1927 + [[package]] 1928 + name = "lazy_static" 1929 + version = "1.5.0" 1930 + source = "registry+https://github.com/rust-lang/crates.io-index" 1931 + checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1932 + dependencies = [ 1933 + "spin", 1934 + ] 1935 + 1936 + [[package]] 1937 + name = "lebe" 1938 + version = "0.5.2" 1939 + source = "registry+https://github.com/rust-lang/crates.io-index" 1940 + checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" 1941 + 1942 + [[package]] 1943 + name = "libc" 1944 + version = "0.2.172" 1945 + source = "registry+https://github.com/rust-lang/crates.io-index" 1946 + checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 1947 + 1948 + [[package]] 1949 + name = "libfuzzer-sys" 1950 + version = "0.4.9" 1951 + source = "registry+https://github.com/rust-lang/crates.io-index" 1952 + checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" 1953 + dependencies = [ 1954 + "arbitrary", 1955 + "cc", 1956 + ] 1957 + 1958 + [[package]] 1959 + name = "libm" 1960 + version = "0.2.15" 1961 + source = "registry+https://github.com/rust-lang/crates.io-index" 1962 + checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" 1963 + 1964 + [[package]] 1965 + name = "libredox" 1966 + version = "0.1.3" 1967 + source = "registry+https://github.com/rust-lang/crates.io-index" 1968 + checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 1969 + dependencies = [ 1970 + "bitflags 2.9.1", 1971 + "libc", 1972 + "redox_syscall", 1973 + ] 1974 + 1975 + [[package]] 1976 + name = "libsqlite3-sys" 1977 + version = "0.30.1" 1978 + source = "registry+https://github.com/rust-lang/crates.io-index" 1979 + checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" 1980 + dependencies = [ 1981 + "cc", 1982 + "pkg-config", 1983 + "vcpkg", 1984 + ] 1985 + 1986 + [[package]] 1987 + name = "linux-raw-sys" 1988 + version = "0.9.4" 1989 + source = "registry+https://github.com/rust-lang/crates.io-index" 1990 + checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 1991 + 1992 + [[package]] 1993 + name = "litemap" 1994 + version = "0.8.0" 1995 + source = "registry+https://github.com/rust-lang/crates.io-index" 1996 + checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 1997 + 1998 + [[package]] 1999 + name = "lock_api" 2000 + version = "0.4.13" 2001 + source = "registry+https://github.com/rust-lang/crates.io-index" 2002 + checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" 2003 + dependencies = [ 2004 + "autocfg", 2005 + "scopeguard", 2006 + ] 2007 + 2008 + [[package]] 2009 + name = "log" 2010 + version = "0.4.27" 2011 + source = "registry+https://github.com/rust-lang/crates.io-index" 2012 + checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 2013 + 2014 + [[package]] 2015 + name = "loom" 2016 + version = "0.7.2" 2017 + source = "registry+https://github.com/rust-lang/crates.io-index" 2018 + checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" 2019 + dependencies = [ 2020 + "cfg-if", 2021 + "generator", 2022 + "scoped-tls", 2023 + "tracing", 2024 + "tracing-subscriber", 2025 + ] 2026 + 2027 + [[package]] 2028 + name = "loop9" 2029 + version = "0.1.5" 2030 + source = "registry+https://github.com/rust-lang/crates.io-index" 2031 + checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" 2032 + dependencies = [ 2033 + "imgref", 2034 + ] 2035 + 2036 + [[package]] 2037 + name = "lru" 2038 + version = "0.12.5" 2039 + source = "registry+https://github.com/rust-lang/crates.io-index" 2040 + checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 2041 + dependencies = [ 2042 + "hashbrown 0.15.3", 2043 + ] 2044 + 2045 + [[package]] 2046 + name = "lru-slab" 2047 + version = "0.1.2" 2048 + source = "registry+https://github.com/rust-lang/crates.io-index" 2049 + checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 2050 + 2051 + [[package]] 2052 + name = "matchers" 2053 + version = "0.1.0" 2054 + source = "registry+https://github.com/rust-lang/crates.io-index" 2055 + checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 2056 + dependencies = [ 2057 + "regex-automata 0.1.10", 2058 + ] 2059 + 2060 + [[package]] 2061 + name = "matchit" 2062 + version = "0.8.4" 2063 + source = "registry+https://github.com/rust-lang/crates.io-index" 2064 + checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 2065 + 2066 + [[package]] 2067 + name = "maybe-rayon" 2068 + version = "0.1.1" 2069 + source = "registry+https://github.com/rust-lang/crates.io-index" 2070 + checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" 2071 + dependencies = [ 2072 + "cfg-if", 2073 + "rayon", 2074 + ] 2075 + 2076 + [[package]] 2077 + name = "md-5" 2078 + version = "0.10.6" 2079 + source = "registry+https://github.com/rust-lang/crates.io-index" 2080 + checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" 2081 + dependencies = [ 2082 + "cfg-if", 2083 + "digest", 2084 + ] 2085 + 2086 + [[package]] 2087 + name = "md5" 2088 + version = "0.7.0" 2089 + source = "registry+https://github.com/rust-lang/crates.io-index" 2090 + checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" 2091 + 2092 + [[package]] 2093 + name = "memchr" 2094 + version = "2.7.4" 2095 + source = "registry+https://github.com/rust-lang/crates.io-index" 2096 + checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 2097 + 2098 + [[package]] 2099 + name = "memo-map" 2100 + version = "0.3.3" 2101 + source = "registry+https://github.com/rust-lang/crates.io-index" 2102 + checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b" 2103 + 2104 + [[package]] 2105 + name = "mime" 2106 + version = "0.3.17" 2107 + source = "registry+https://github.com/rust-lang/crates.io-index" 2108 + checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 2109 + 2110 + [[package]] 2111 + name = "mime_guess" 2112 + version = "2.0.5" 2113 + source = "registry+https://github.com/rust-lang/crates.io-index" 2114 + checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 2115 + dependencies = [ 2116 + "mime", 2117 + "unicase", 2118 + ] 2119 + 2120 + [[package]] 2121 + name = "minijinja" 2122 + version = "2.10.2" 2123 + source = "registry+https://github.com/rust-lang/crates.io-index" 2124 + checksum = "dd72e8b4e42274540edabec853f607c015c73436159b06c39c7af85a20433155" 2125 + dependencies = [ 2126 + "memo-map", 2127 + "self_cell", 2128 + "serde", 2129 + ] 2130 + 2131 + [[package]] 2132 + name = "minijinja-autoreload" 2133 + version = "2.10.2" 2134 + source = "registry+https://github.com/rust-lang/crates.io-index" 2135 + checksum = "46630543a4c8dce957338cba44df75d06e6e6e38df7646b3a458320fb4522909" 2136 + dependencies = [ 2137 + "minijinja", 2138 + "notify", 2139 + ] 2140 + 2141 + [[package]] 2142 + name = "minijinja-embed" 2143 + version = "2.10.2" 2144 + source = "registry+https://github.com/rust-lang/crates.io-index" 2145 + checksum = "5de0eb34c22c5cfb9b22509599e3942aa1cdfa00884fa76ceccff4163f572c67" 2146 + 2147 + [[package]] 2148 + name = "minimal-lexical" 2149 + version = "0.2.1" 2150 + source = "registry+https://github.com/rust-lang/crates.io-index" 2151 + checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 2152 + 2153 + [[package]] 2154 + name = "minio" 2155 + version = "0.3.0" 2156 + source = "registry+https://github.com/rust-lang/crates.io-index" 2157 + checksum = "3824101357fa899d01c729e4a245776e20a03f2f6645979e86b9d3d5d9c42741" 2158 + dependencies = [ 2159 + "async-recursion", 2160 + "async-trait", 2161 + "base64", 2162 + "byteorder", 2163 + "bytes", 2164 + "chrono", 2165 + "crc", 2166 + "dashmap", 2167 + "derivative", 2168 + "env_logger", 2169 + "futures", 2170 + "futures-util", 2171 + "hex", 2172 + "hmac", 2173 + "http", 2174 + "hyper", 2175 + "lazy_static", 2176 + "log", 2177 + "md5", 2178 + "multimap", 2179 + "percent-encoding", 2180 + "rand 0.8.5", 2181 + "regex", 2182 + "reqwest", 2183 + "serde", 2184 + "serde_json", 2185 + "sha2", 2186 + "tokio", 2187 + "tokio-stream", 2188 + "tokio-util", 2189 + "urlencoding", 2190 + "xmltree", 2191 + ] 2192 + 2193 + [[package]] 2194 + name = "miniz_oxide" 2195 + version = "0.8.8" 2196 + source = "registry+https://github.com/rust-lang/crates.io-index" 2197 + checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 2198 + dependencies = [ 2199 + "adler2", 2200 + "simd-adler32", 2201 + ] 2202 + 2203 + [[package]] 2204 + name = "mio" 2205 + version = "1.0.4" 2206 + source = "registry+https://github.com/rust-lang/crates.io-index" 2207 + checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 2208 + dependencies = [ 2209 + "libc", 2210 + "log", 2211 + "wasi 0.11.0+wasi-snapshot-preview1", 2212 + "windows-sys 0.59.0", 2213 + ] 2214 + 2215 + [[package]] 2216 + name = "moka" 2217 + version = "0.12.10" 2218 + source = "registry+https://github.com/rust-lang/crates.io-index" 2219 + checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" 2220 + dependencies = [ 2221 + "crossbeam-channel", 2222 + "crossbeam-epoch", 2223 + "crossbeam-utils", 2224 + "loom", 2225 + "parking_lot", 2226 + "portable-atomic", 2227 + "rustc_version", 2228 + "smallvec", 2229 + "tagptr", 2230 + "thiserror 1.0.69", 2231 + "uuid", 2232 + ] 2233 + 2234 + [[package]] 2235 + name = "multibase" 2236 + version = "0.9.1" 2237 + source = "registry+https://github.com/rust-lang/crates.io-index" 2238 + checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" 2239 + dependencies = [ 2240 + "base-x", 2241 + "data-encoding", 2242 + "data-encoding-macro", 2243 + ] 2244 + 2245 + [[package]] 2246 + name = "multihash" 2247 + version = "0.19.3" 2248 + source = "registry+https://github.com/rust-lang/crates.io-index" 2249 + checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" 2250 + dependencies = [ 2251 + "core2", 2252 + "serde", 2253 + "unsigned-varint", 2254 + ] 2255 + 2256 + [[package]] 2257 + name = "multimap" 2258 + version = "0.10.1" 2259 + source = "registry+https://github.com/rust-lang/crates.io-index" 2260 + checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" 2261 + dependencies = [ 2262 + "serde", 2263 + ] 2264 + 2265 + [[package]] 2266 + name = "native-tls" 2267 + version = "0.2.14" 2268 + source = "registry+https://github.com/rust-lang/crates.io-index" 2269 + checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 2270 + dependencies = [ 2271 + "libc", 2272 + "log", 2273 + "openssl", 2274 + "openssl-probe", 2275 + "openssl-sys", 2276 + "schannel", 2277 + "security-framework 2.11.1", 2278 + "security-framework-sys", 2279 + "tempfile", 2280 + ] 2281 + 2282 + [[package]] 2283 + name = "new_debug_unreachable" 2284 + version = "1.0.6" 2285 + source = "registry+https://github.com/rust-lang/crates.io-index" 2286 + checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 2287 + 2288 + [[package]] 2289 + name = "nom" 2290 + version = "7.1.3" 2291 + source = "registry+https://github.com/rust-lang/crates.io-index" 2292 + checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 2293 + dependencies = [ 2294 + "memchr", 2295 + "minimal-lexical", 2296 + ] 2297 + 2298 + [[package]] 2299 + name = "noop_proc_macro" 2300 + version = "0.3.0" 2301 + source = "registry+https://github.com/rust-lang/crates.io-index" 2302 + checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" 2303 + 2304 + [[package]] 2305 + name = "notify" 2306 + version = "8.0.0" 2307 + source = "registry+https://github.com/rust-lang/crates.io-index" 2308 + checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" 2309 + dependencies = [ 2310 + "bitflags 2.9.1", 2311 + "filetime", 2312 + "fsevent-sys", 2313 + "inotify", 2314 + "kqueue", 2315 + "libc", 2316 + "log", 2317 + "mio", 2318 + "notify-types", 2319 + "walkdir", 2320 + "windows-sys 0.59.0", 2321 + ] 2322 + 2323 + [[package]] 2324 + name = "notify-types" 2325 + version = "2.0.0" 2326 + source = "registry+https://github.com/rust-lang/crates.io-index" 2327 + checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" 2328 + 2329 + [[package]] 2330 + name = "nu-ansi-term" 2331 + version = "0.46.0" 2332 + source = "registry+https://github.com/rust-lang/crates.io-index" 2333 + checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 2334 + dependencies = [ 2335 + "overload", 2336 + "winapi", 2337 + ] 2338 + 2339 + [[package]] 2340 + name = "num-bigint" 2341 + version = "0.4.6" 2342 + source = "registry+https://github.com/rust-lang/crates.io-index" 2343 + checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 2344 + dependencies = [ 2345 + "num-integer", 2346 + "num-traits", 2347 + ] 2348 + 2349 + [[package]] 2350 + name = "num-bigint-dig" 2351 + version = "0.8.4" 2352 + source = "registry+https://github.com/rust-lang/crates.io-index" 2353 + checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" 2354 + dependencies = [ 2355 + "byteorder", 2356 + "lazy_static", 2357 + "libm", 2358 + "num-integer", 2359 + "num-iter", 2360 + "num-traits", 2361 + "rand 0.8.5", 2362 + "smallvec", 2363 + "zeroize", 2364 + ] 2365 + 2366 + [[package]] 2367 + name = "num-conv" 2368 + version = "0.1.0" 2369 + source = "registry+https://github.com/rust-lang/crates.io-index" 2370 + checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 2371 + 2372 + [[package]] 2373 + name = "num-derive" 2374 + version = "0.4.2" 2375 + source = "registry+https://github.com/rust-lang/crates.io-index" 2376 + checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 2377 + dependencies = [ 2378 + "proc-macro2", 2379 + "quote", 2380 + "syn 2.0.101", 2381 + ] 2382 + 2383 + [[package]] 2384 + name = "num-integer" 2385 + version = "0.1.46" 2386 + source = "registry+https://github.com/rust-lang/crates.io-index" 2387 + checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 2388 + dependencies = [ 2389 + "num-traits", 2390 + ] 2391 + 2392 + [[package]] 2393 + name = "num-iter" 2394 + version = "0.1.45" 2395 + source = "registry+https://github.com/rust-lang/crates.io-index" 2396 + checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 2397 + dependencies = [ 2398 + "autocfg", 2399 + "num-integer", 2400 + "num-traits", 2401 + ] 2402 + 2403 + [[package]] 2404 + name = "num-rational" 2405 + version = "0.4.2" 2406 + source = "registry+https://github.com/rust-lang/crates.io-index" 2407 + checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" 2408 + dependencies = [ 2409 + "num-bigint", 2410 + "num-integer", 2411 + "num-traits", 2412 + ] 2413 + 2414 + [[package]] 2415 + name = "num-traits" 2416 + version = "0.2.19" 2417 + source = "registry+https://github.com/rust-lang/crates.io-index" 2418 + checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 2419 + dependencies = [ 2420 + "autocfg", 2421 + "libm", 2422 + ] 2423 + 2424 + [[package]] 2425 + name = "object" 2426 + version = "0.36.7" 2427 + source = "registry+https://github.com/rust-lang/crates.io-index" 2428 + checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 2429 + dependencies = [ 2430 + "memchr", 2431 + ] 2432 + 2433 + [[package]] 2434 + name = "once_cell" 2435 + version = "1.21.3" 2436 + source = "registry+https://github.com/rust-lang/crates.io-index" 2437 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 2438 + dependencies = [ 2439 + "critical-section", 2440 + "portable-atomic", 2441 + ] 2442 + 2443 + [[package]] 2444 + name = "once_cell_polyfill" 2445 + version = "1.70.1" 2446 + source = "registry+https://github.com/rust-lang/crates.io-index" 2447 + checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 2448 + 2449 + [[package]] 2450 + name = "openssl" 2451 + version = "0.10.73" 2452 + source = "registry+https://github.com/rust-lang/crates.io-index" 2453 + checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" 2454 + dependencies = [ 2455 + "bitflags 2.9.1", 2456 + "cfg-if", 2457 + "foreign-types", 2458 + "libc", 2459 + "once_cell", 2460 + "openssl-macros", 2461 + "openssl-sys", 2462 + ] 2463 + 2464 + [[package]] 2465 + name = "openssl-macros" 2466 + version = "0.1.1" 2467 + source = "registry+https://github.com/rust-lang/crates.io-index" 2468 + checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 2469 + dependencies = [ 2470 + "proc-macro2", 2471 + "quote", 2472 + "syn 2.0.101", 2473 + ] 2474 + 2475 + [[package]] 2476 + name = "openssl-probe" 2477 + version = "0.1.6" 2478 + source = "registry+https://github.com/rust-lang/crates.io-index" 2479 + checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 2480 + 2481 + [[package]] 2482 + name = "openssl-sys" 2483 + version = "0.9.109" 2484 + source = "registry+https://github.com/rust-lang/crates.io-index" 2485 + checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" 2486 + dependencies = [ 2487 + "cc", 2488 + "libc", 2489 + "pkg-config", 2490 + "vcpkg", 2491 + ] 2492 + 2493 + [[package]] 2494 + name = "overload" 2495 + version = "0.1.1" 2496 + source = "registry+https://github.com/rust-lang/crates.io-index" 2497 + checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 2498 + 2499 + [[package]] 2500 + name = "p256" 2501 + version = "0.13.2" 2502 + source = "registry+https://github.com/rust-lang/crates.io-index" 2503 + checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" 2504 + dependencies = [ 2505 + "ecdsa", 2506 + "elliptic-curve", 2507 + "primeorder", 2508 + "serdect", 2509 + "sha2", 2510 + ] 2511 + 2512 + [[package]] 2513 + name = "parking" 2514 + version = "2.2.1" 2515 + source = "registry+https://github.com/rust-lang/crates.io-index" 2516 + checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 2517 + 2518 + [[package]] 2519 + name = "parking_lot" 2520 + version = "0.12.4" 2521 + source = "registry+https://github.com/rust-lang/crates.io-index" 2522 + checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" 2523 + dependencies = [ 2524 + "lock_api", 2525 + "parking_lot_core", 2526 + ] 2527 + 2528 + [[package]] 2529 + name = "parking_lot_core" 2530 + version = "0.9.11" 2531 + source = "registry+https://github.com/rust-lang/crates.io-index" 2532 + checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" 2533 + dependencies = [ 2534 + "cfg-if", 2535 + "libc", 2536 + "redox_syscall", 2537 + "smallvec", 2538 + "windows-targets 0.52.6", 2539 + ] 2540 + 2541 + [[package]] 2542 + name = "paste" 2543 + version = "1.0.15" 2544 + source = "registry+https://github.com/rust-lang/crates.io-index" 2545 + checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 2546 + 2547 + [[package]] 2548 + name = "pem-rfc7468" 2549 + version = "0.7.0" 2550 + source = "registry+https://github.com/rust-lang/crates.io-index" 2551 + checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 2552 + dependencies = [ 2553 + "base64ct", 2554 + ] 2555 + 2556 + [[package]] 2557 + name = "percent-encoding" 2558 + version = "2.3.1" 2559 + source = "registry+https://github.com/rust-lang/crates.io-index" 2560 + checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 2561 + 2562 + [[package]] 2563 + name = "pin-project-lite" 2564 + version = "0.2.16" 2565 + source = "registry+https://github.com/rust-lang/crates.io-index" 2566 + checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 2567 + 2568 + [[package]] 2569 + name = "pin-utils" 2570 + version = "0.1.0" 2571 + source = "registry+https://github.com/rust-lang/crates.io-index" 2572 + checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 2573 + 2574 + [[package]] 2575 + name = "pkcs1" 2576 + version = "0.7.5" 2577 + source = "registry+https://github.com/rust-lang/crates.io-index" 2578 + checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" 2579 + dependencies = [ 2580 + "der", 2581 + "pkcs8", 2582 + "spki", 2583 + ] 2584 + 2585 + [[package]] 2586 + name = "pkcs8" 2587 + version = "0.10.2" 2588 + source = "registry+https://github.com/rust-lang/crates.io-index" 2589 + checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 2590 + dependencies = [ 2591 + "der", 2592 + "spki", 2593 + ] 2594 + 2595 + [[package]] 2596 + name = "pkg-config" 2597 + version = "0.3.32" 2598 + source = "registry+https://github.com/rust-lang/crates.io-index" 2599 + checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 2600 + 2601 + [[package]] 2602 + name = "png" 2603 + version = "0.17.16" 2604 + source = "registry+https://github.com/rust-lang/crates.io-index" 2605 + checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" 2606 + dependencies = [ 2607 + "bitflags 1.3.2", 2608 + "crc32fast", 2609 + "fdeflate", 2610 + "flate2", 2611 + "miniz_oxide", 2612 + ] 2613 + 2614 + [[package]] 2615 + name = "portable-atomic" 2616 + version = "1.11.1" 2617 + source = "registry+https://github.com/rust-lang/crates.io-index" 2618 + checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 2619 + 2620 + [[package]] 2621 + name = "portable-atomic-util" 2622 + version = "0.2.4" 2623 + source = "registry+https://github.com/rust-lang/crates.io-index" 2624 + checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 2625 + dependencies = [ 2626 + "portable-atomic", 2627 + ] 2628 + 2629 + [[package]] 2630 + name = "potential_utf" 2631 + version = "0.1.2" 2632 + source = "registry+https://github.com/rust-lang/crates.io-index" 2633 + checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 2634 + dependencies = [ 2635 + "zerovec", 2636 + ] 2637 + 2638 + [[package]] 2639 + name = "powerfmt" 2640 + version = "0.2.0" 2641 + source = "registry+https://github.com/rust-lang/crates.io-index" 2642 + checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 2643 + 2644 + [[package]] 2645 + name = "ppv-lite86" 2646 + version = "0.2.21" 2647 + source = "registry+https://github.com/rust-lang/crates.io-index" 2648 + checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 2649 + dependencies = [ 2650 + "zerocopy", 2651 + ] 2652 + 2653 + [[package]] 2654 + name = "primeorder" 2655 + version = "0.13.6" 2656 + source = "registry+https://github.com/rust-lang/crates.io-index" 2657 + checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" 2658 + dependencies = [ 2659 + "elliptic-curve", 2660 + "serdect", 2661 + ] 2662 + 2663 + [[package]] 2664 + name = "proc-macro2" 2665 + version = "1.0.95" 2666 + source = "registry+https://github.com/rust-lang/crates.io-index" 2667 + checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 2668 + dependencies = [ 2669 + "unicode-ident", 2670 + ] 2671 + 2672 + [[package]] 2673 + name = "profiling" 2674 + version = "1.0.16" 2675 + source = "registry+https://github.com/rust-lang/crates.io-index" 2676 + checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" 2677 + dependencies = [ 2678 + "profiling-procmacros", 2679 + ] 2680 + 2681 + [[package]] 2682 + name = "profiling-procmacros" 2683 + version = "1.0.16" 2684 + source = "registry+https://github.com/rust-lang/crates.io-index" 2685 + checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" 2686 + dependencies = [ 2687 + "quote", 2688 + "syn 2.0.101", 2689 + ] 2690 + 2691 + [[package]] 2692 + name = "qoi" 2693 + version = "0.4.1" 2694 + source = "registry+https://github.com/rust-lang/crates.io-index" 2695 + checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" 2696 + dependencies = [ 2697 + "bytemuck", 2698 + ] 2699 + 2700 + [[package]] 2701 + name = "quick-error" 2702 + version = "2.0.1" 2703 + source = "registry+https://github.com/rust-lang/crates.io-index" 2704 + checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" 2705 + 2706 + [[package]] 2707 + name = "quinn" 2708 + version = "0.11.8" 2709 + source = "registry+https://github.com/rust-lang/crates.io-index" 2710 + checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" 2711 + dependencies = [ 2712 + "bytes", 2713 + "cfg_aliases", 2714 + "pin-project-lite", 2715 + "quinn-proto", 2716 + "quinn-udp", 2717 + "rustc-hash", 2718 + "rustls", 2719 + "socket2", 2720 + "thiserror 2.0.12", 2721 + "tokio", 2722 + "tracing", 2723 + "web-time", 2724 + ] 2725 + 2726 + [[package]] 2727 + name = "quinn-proto" 2728 + version = "0.11.12" 2729 + source = "registry+https://github.com/rust-lang/crates.io-index" 2730 + checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" 2731 + dependencies = [ 2732 + "bytes", 2733 + "getrandom 0.3.3", 2734 + "lru-slab", 2735 + "rand 0.9.1", 2736 + "ring", 2737 + "rustc-hash", 2738 + "rustls", 2739 + "rustls-pki-types", 2740 + "slab", 2741 + "thiserror 2.0.12", 2742 + "tinyvec", 2743 + "tracing", 2744 + "web-time", 2745 + ] 2746 + 2747 + [[package]] 2748 + name = "quinn-udp" 2749 + version = "0.5.12" 2750 + source = "registry+https://github.com/rust-lang/crates.io-index" 2751 + checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" 2752 + dependencies = [ 2753 + "cfg_aliases", 2754 + "libc", 2755 + "once_cell", 2756 + "socket2", 2757 + "tracing", 2758 + "windows-sys 0.59.0", 2759 + ] 2760 + 2761 + [[package]] 2762 + name = "quote" 2763 + version = "1.0.40" 2764 + source = "registry+https://github.com/rust-lang/crates.io-index" 2765 + checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 2766 + dependencies = [ 2767 + "proc-macro2", 2768 + ] 2769 + 2770 + [[package]] 2771 + name = "r-efi" 2772 + version = "5.2.0" 2773 + source = "registry+https://github.com/rust-lang/crates.io-index" 2774 + checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 2775 + 2776 + [[package]] 2777 + name = "rand" 2778 + version = "0.8.5" 2779 + source = "registry+https://github.com/rust-lang/crates.io-index" 2780 + checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 2781 + dependencies = [ 2782 + "libc", 2783 + "rand_chacha 0.3.1", 2784 + "rand_core 0.6.4", 2785 + ] 2786 + 2787 + [[package]] 2788 + name = "rand" 2789 + version = "0.9.1" 2790 + source = "registry+https://github.com/rust-lang/crates.io-index" 2791 + checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" 2792 + dependencies = [ 2793 + "rand_chacha 0.9.0", 2794 + "rand_core 0.9.3", 2795 + ] 2796 + 2797 + [[package]] 2798 + name = "rand_chacha" 2799 + version = "0.3.1" 2800 + source = "registry+https://github.com/rust-lang/crates.io-index" 2801 + checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 2802 + dependencies = [ 2803 + "ppv-lite86", 2804 + "rand_core 0.6.4", 2805 + ] 2806 + 2807 + [[package]] 2808 + name = "rand_chacha" 2809 + version = "0.9.0" 2810 + source = "registry+https://github.com/rust-lang/crates.io-index" 2811 + checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 2812 + dependencies = [ 2813 + "ppv-lite86", 2814 + "rand_core 0.9.3", 2815 + ] 2816 + 2817 + [[package]] 2818 + name = "rand_core" 2819 + version = "0.6.4" 2820 + source = "registry+https://github.com/rust-lang/crates.io-index" 2821 + checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 2822 + dependencies = [ 2823 + "getrandom 0.2.16", 2824 + ] 2825 + 2826 + [[package]] 2827 + name = "rand_core" 2828 + version = "0.9.3" 2829 + source = "registry+https://github.com/rust-lang/crates.io-index" 2830 + checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 2831 + dependencies = [ 2832 + "getrandom 0.3.3", 2833 + ] 2834 + 2835 + [[package]] 2836 + name = "rav1e" 2837 + version = "0.7.1" 2838 + source = "registry+https://github.com/rust-lang/crates.io-index" 2839 + checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" 2840 + dependencies = [ 2841 + "arbitrary", 2842 + "arg_enum_proc_macro", 2843 + "arrayvec", 2844 + "av1-grain", 2845 + "bitstream-io", 2846 + "built", 2847 + "cfg-if", 2848 + "interpolate_name", 2849 + "itertools", 2850 + "libc", 2851 + "libfuzzer-sys", 2852 + "log", 2853 + "maybe-rayon", 2854 + "new_debug_unreachable", 2855 + "noop_proc_macro", 2856 + "num-derive", 2857 + "num-traits", 2858 + "once_cell", 2859 + "paste", 2860 + "profiling", 2861 + "rand 0.8.5", 2862 + "rand_chacha 0.3.1", 2863 + "simd_helpers", 2864 + "system-deps", 2865 + "thiserror 1.0.69", 2866 + "v_frame", 2867 + "wasm-bindgen", 2868 + ] 2869 + 2870 + [[package]] 2871 + name = "ravif" 2872 + version = "0.11.12" 2873 + source = "registry+https://github.com/rust-lang/crates.io-index" 2874 + checksum = "d6a5f31fcf7500f9401fea858ea4ab5525c99f2322cfcee732c0e6c74208c0c6" 2875 + dependencies = [ 2876 + "avif-serialize", 2877 + "imgref", 2878 + "loop9", 2879 + "quick-error", 2880 + "rav1e", 2881 + "rayon", 2882 + "rgb", 2883 + ] 2884 + 2885 + [[package]] 2886 + name = "rayon" 2887 + version = "1.10.0" 2888 + source = "registry+https://github.com/rust-lang/crates.io-index" 2889 + checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 2890 + dependencies = [ 2891 + "either", 2892 + "rayon-core", 2893 + ] 2894 + 2895 + [[package]] 2896 + name = "rayon-core" 2897 + version = "1.12.1" 2898 + source = "registry+https://github.com/rust-lang/crates.io-index" 2899 + checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 2900 + dependencies = [ 2901 + "crossbeam-deque", 2902 + "crossbeam-utils", 2903 + ] 2904 + 2905 + [[package]] 2906 + name = "redox_syscall" 2907 + version = "0.5.12" 2908 + source = "registry+https://github.com/rust-lang/crates.io-index" 2909 + checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" 2910 + dependencies = [ 2911 + "bitflags 2.9.1", 2912 + ] 2913 + 2914 + [[package]] 2915 + name = "regex" 2916 + version = "1.11.1" 2917 + source = "registry+https://github.com/rust-lang/crates.io-index" 2918 + checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 2919 + dependencies = [ 2920 + "aho-corasick", 2921 + "memchr", 2922 + "regex-automata 0.4.9", 2923 + "regex-syntax 0.8.5", 2924 + ] 2925 + 2926 + [[package]] 2927 + name = "regex-automata" 2928 + version = "0.1.10" 2929 + source = "registry+https://github.com/rust-lang/crates.io-index" 2930 + checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 2931 + dependencies = [ 2932 + "regex-syntax 0.6.29", 2933 + ] 2934 + 2935 + [[package]] 2936 + name = "regex-automata" 2937 + version = "0.4.9" 2938 + source = "registry+https://github.com/rust-lang/crates.io-index" 2939 + checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 2940 + dependencies = [ 2941 + "aho-corasick", 2942 + "memchr", 2943 + "regex-syntax 0.8.5", 2944 + ] 2945 + 2946 + [[package]] 2947 + name = "regex-syntax" 2948 + version = "0.6.29" 2949 + source = "registry+https://github.com/rust-lang/crates.io-index" 2950 + checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 2951 + 2952 + [[package]] 2953 + name = "regex-syntax" 2954 + version = "0.8.5" 2955 + source = "registry+https://github.com/rust-lang/crates.io-index" 2956 + checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 2957 + 2958 + [[package]] 2959 + name = "reqwest" 2960 + version = "0.12.19" 2961 + source = "registry+https://github.com/rust-lang/crates.io-index" 2962 + checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119" 2963 + dependencies = [ 2964 + "base64", 2965 + "bytes", 2966 + "encoding_rs", 2967 + "futures-core", 2968 + "futures-util", 2969 + "h2", 2970 + "http", 2971 + "http-body", 2972 + "http-body-util", 2973 + "hyper", 2974 + "hyper-rustls", 2975 + "hyper-tls", 2976 + "hyper-util", 2977 + "ipnet", 2978 + "js-sys", 2979 + "log", 2980 + "mime", 2981 + "mime_guess", 2982 + "native-tls", 2983 + "once_cell", 2984 + "percent-encoding", 2985 + "pin-project-lite", 2986 + "quinn", 2987 + "rustls", 2988 + "rustls-pki-types", 2989 + "serde", 2990 + "serde_json", 2991 + "serde_urlencoded", 2992 + "sync_wrapper", 2993 + "tokio", 2994 + "tokio-native-tls", 2995 + "tokio-rustls", 2996 + "tokio-util", 2997 + "tower", 2998 + "tower-http 0.6.6", 2999 + "tower-service", 3000 + "url", 3001 + "wasm-bindgen", 3002 + "wasm-bindgen-futures", 3003 + "wasm-streams", 3004 + "web-sys", 3005 + "webpki-roots 1.0.0", 3006 + ] 3007 + 3008 + [[package]] 3009 + name = "reqwest-chain" 3010 + version = "1.0.0" 3011 + source = "registry+https://github.com/rust-lang/crates.io-index" 3012 + checksum = "da5c014fb79a8227db44a0433d748107750d2550b7fca55c59a3d7ee7d2ee2b2" 3013 + dependencies = [ 3014 + "anyhow", 3015 + "async-trait", 3016 + "http", 3017 + "reqwest-middleware", 3018 + ] 3019 + 3020 + [[package]] 3021 + name = "reqwest-middleware" 3022 + version = "0.4.2" 3023 + source = "registry+https://github.com/rust-lang/crates.io-index" 3024 + checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" 3025 + dependencies = [ 3026 + "anyhow", 3027 + "async-trait", 3028 + "http", 3029 + "reqwest", 3030 + "serde", 3031 + "thiserror 1.0.69", 3032 + "tower-service", 3033 + ] 3034 + 3035 + [[package]] 3036 + name = "resolv-conf" 3037 + version = "0.7.4" 3038 + source = "registry+https://github.com/rust-lang/crates.io-index" 3039 + checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" 3040 + 3041 + [[package]] 3042 + name = "rfc6979" 3043 + version = "0.4.0" 3044 + source = "registry+https://github.com/rust-lang/crates.io-index" 3045 + checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" 3046 + dependencies = [ 3047 + "hmac", 3048 + "subtle", 3049 + ] 3050 + 3051 + [[package]] 3052 + name = "rgb" 3053 + version = "0.8.50" 3054 + source = "registry+https://github.com/rust-lang/crates.io-index" 3055 + checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" 3056 + 3057 + [[package]] 3058 + name = "ring" 3059 + version = "0.17.14" 3060 + source = "registry+https://github.com/rust-lang/crates.io-index" 3061 + checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 3062 + dependencies = [ 3063 + "cc", 3064 + "cfg-if", 3065 + "getrandom 0.2.16", 3066 + "libc", 3067 + "untrusted", 3068 + "windows-sys 0.52.0", 3069 + ] 3070 + 3071 + [[package]] 3072 + name = "rsa" 3073 + version = "0.9.8" 3074 + source = "registry+https://github.com/rust-lang/crates.io-index" 3075 + checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" 3076 + dependencies = [ 3077 + "const-oid", 3078 + "digest", 3079 + "num-bigint-dig", 3080 + "num-integer", 3081 + "num-traits", 3082 + "pkcs1", 3083 + "pkcs8", 3084 + "rand_core 0.6.4", 3085 + "signature", 3086 + "spki", 3087 + "subtle", 3088 + "zeroize", 3089 + ] 3090 + 3091 + [[package]] 3092 + name = "rust-embed" 3093 + version = "8.7.2" 3094 + source = "registry+https://github.com/rust-lang/crates.io-index" 3095 + checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" 3096 + dependencies = [ 3097 + "rust-embed-impl", 3098 + "rust-embed-utils", 3099 + "walkdir", 3100 + ] 3101 + 3102 + [[package]] 3103 + name = "rust-embed-impl" 3104 + version = "8.7.2" 3105 + source = "registry+https://github.com/rust-lang/crates.io-index" 3106 + checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" 3107 + dependencies = [ 3108 + "proc-macro2", 3109 + "quote", 3110 + "rust-embed-utils", 3111 + "syn 2.0.101", 3112 + "walkdir", 3113 + ] 3114 + 3115 + [[package]] 3116 + name = "rust-embed-utils" 3117 + version = "8.7.2" 3118 + source = "registry+https://github.com/rust-lang/crates.io-index" 3119 + checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" 3120 + dependencies = [ 3121 + "sha2", 3122 + "walkdir", 3123 + ] 3124 + 3125 + [[package]] 3126 + name = "rust_decimal" 3127 + version = "1.37.1" 3128 + source = "registry+https://github.com/rust-lang/crates.io-index" 3129 + checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50" 3130 + dependencies = [ 3131 + "arrayvec", 3132 + "num-traits", 3133 + ] 3134 + 3135 + [[package]] 3136 + name = "rustc-demangle" 3137 + version = "0.1.24" 3138 + source = "registry+https://github.com/rust-lang/crates.io-index" 3139 + checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 3140 + 3141 + [[package]] 3142 + name = "rustc-hash" 3143 + version = "2.1.1" 3144 + source = "registry+https://github.com/rust-lang/crates.io-index" 3145 + checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 3146 + 3147 + [[package]] 3148 + name = "rustc_version" 3149 + version = "0.4.1" 3150 + source = "registry+https://github.com/rust-lang/crates.io-index" 3151 + checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 3152 + dependencies = [ 3153 + "semver", 3154 + ] 3155 + 3156 + [[package]] 3157 + name = "rustix" 3158 + version = "1.0.7" 3159 + source = "registry+https://github.com/rust-lang/crates.io-index" 3160 + checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 3161 + dependencies = [ 3162 + "bitflags 2.9.1", 3163 + "errno", 3164 + "libc", 3165 + "linux-raw-sys", 3166 + "windows-sys 0.59.0", 3167 + ] 3168 + 3169 + [[package]] 3170 + name = "rustls" 3171 + version = "0.23.27" 3172 + source = "registry+https://github.com/rust-lang/crates.io-index" 3173 + checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" 3174 + dependencies = [ 3175 + "once_cell", 3176 + "ring", 3177 + "rustls-pki-types", 3178 + "rustls-webpki", 3179 + "subtle", 3180 + "zeroize", 3181 + ] 3182 + 3183 + [[package]] 3184 + name = "rustls-native-certs" 3185 + version = "0.8.1" 3186 + source = "registry+https://github.com/rust-lang/crates.io-index" 3187 + checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" 3188 + dependencies = [ 3189 + "openssl-probe", 3190 + "rustls-pki-types", 3191 + "schannel", 3192 + "security-framework 3.2.0", 3193 + ] 3194 + 3195 + [[package]] 3196 + name = "rustls-pki-types" 3197 + version = "1.12.0" 3198 + source = "registry+https://github.com/rust-lang/crates.io-index" 3199 + checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 3200 + dependencies = [ 3201 + "web-time", 3202 + "zeroize", 3203 + ] 3204 + 3205 + [[package]] 3206 + name = "rustls-webpki" 3207 + version = "0.103.3" 3208 + source = "registry+https://github.com/rust-lang/crates.io-index" 3209 + checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" 3210 + dependencies = [ 3211 + "ring", 3212 + "rustls-pki-types", 3213 + "untrusted", 3214 + ] 3215 + 3216 + [[package]] 3217 + name = "rustversion" 3218 + version = "1.0.21" 3219 + source = "registry+https://github.com/rust-lang/crates.io-index" 3220 + checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" 3221 + 3222 + [[package]] 3223 + name = "ryu" 3224 + version = "1.0.20" 3225 + source = "registry+https://github.com/rust-lang/crates.io-index" 3226 + checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 3227 + 3228 + [[package]] 3229 + name = "same-file" 3230 + version = "1.0.6" 3231 + source = "registry+https://github.com/rust-lang/crates.io-index" 3232 + checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 3233 + dependencies = [ 3234 + "winapi-util", 3235 + ] 3236 + 3237 + [[package]] 3238 + name = "schannel" 3239 + version = "0.1.27" 3240 + source = "registry+https://github.com/rust-lang/crates.io-index" 3241 + checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 3242 + dependencies = [ 3243 + "windows-sys 0.59.0", 3244 + ] 3245 + 3246 + [[package]] 3247 + name = "scoped-tls" 3248 + version = "1.0.1" 3249 + source = "registry+https://github.com/rust-lang/crates.io-index" 3250 + checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 3251 + 3252 + [[package]] 3253 + name = "scopeguard" 3254 + version = "1.2.0" 3255 + source = "registry+https://github.com/rust-lang/crates.io-index" 3256 + checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 3257 + 3258 + [[package]] 3259 + name = "sec1" 3260 + version = "0.7.3" 3261 + source = "registry+https://github.com/rust-lang/crates.io-index" 3262 + checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" 3263 + dependencies = [ 3264 + "base16ct", 3265 + "der", 3266 + "generic-array", 3267 + "pkcs8", 3268 + "serdect", 3269 + "subtle", 3270 + "zeroize", 3271 + ] 3272 + 3273 + [[package]] 3274 + name = "security-framework" 3275 + version = "2.11.1" 3276 + source = "registry+https://github.com/rust-lang/crates.io-index" 3277 + checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 3278 + dependencies = [ 3279 + "bitflags 2.9.1", 3280 + "core-foundation 0.9.4", 3281 + "core-foundation-sys", 3282 + "libc", 3283 + "security-framework-sys", 3284 + ] 3285 + 3286 + [[package]] 3287 + name = "security-framework" 3288 + version = "3.2.0" 3289 + source = "registry+https://github.com/rust-lang/crates.io-index" 3290 + checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" 3291 + dependencies = [ 3292 + "bitflags 2.9.1", 3293 + "core-foundation 0.10.1", 3294 + "core-foundation-sys", 3295 + "libc", 3296 + "security-framework-sys", 3297 + ] 3298 + 3299 + [[package]] 3300 + name = "security-framework-sys" 3301 + version = "2.14.0" 3302 + source = "registry+https://github.com/rust-lang/crates.io-index" 3303 + checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 3304 + dependencies = [ 3305 + "core-foundation-sys", 3306 + "libc", 3307 + ] 3308 + 3309 + [[package]] 3310 + name = "self_cell" 3311 + version = "1.2.0" 3312 + source = "registry+https://github.com/rust-lang/crates.io-index" 3313 + checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" 3314 + 3315 + [[package]] 3316 + name = "semver" 3317 + version = "1.0.26" 3318 + source = "registry+https://github.com/rust-lang/crates.io-index" 3319 + checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 3320 + 3321 + [[package]] 3322 + name = "serde" 3323 + version = "1.0.219" 3324 + source = "registry+https://github.com/rust-lang/crates.io-index" 3325 + checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 3326 + dependencies = [ 3327 + "serde_derive", 3328 + ] 3329 + 3330 + [[package]] 3331 + name = "serde_bytes" 3332 + version = "0.11.17" 3333 + source = "registry+https://github.com/rust-lang/crates.io-index" 3334 + checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" 3335 + dependencies = [ 3336 + "serde", 3337 + ] 3338 + 3339 + [[package]] 3340 + name = "serde_derive" 3341 + version = "1.0.219" 3342 + source = "registry+https://github.com/rust-lang/crates.io-index" 3343 + checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 3344 + dependencies = [ 3345 + "proc-macro2", 3346 + "quote", 3347 + "syn 2.0.101", 3348 + ] 3349 + 3350 + [[package]] 3351 + name = "serde_ipld_dagcbor" 3352 + version = "0.6.3" 3353 + source = "registry+https://github.com/rust-lang/crates.io-index" 3354 + checksum = "99600723cf53fb000a66175555098db7e75217c415bdd9a16a65d52a19dcc4fc" 3355 + dependencies = [ 3356 + "cbor4ii", 3357 + "ipld-core", 3358 + "scopeguard", 3359 + "serde", 3360 + ] 3361 + 3362 + [[package]] 3363 + name = "serde_json" 3364 + version = "1.0.140" 3365 + source = "registry+https://github.com/rust-lang/crates.io-index" 3366 + checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 3367 + dependencies = [ 3368 + "itoa", 3369 + "memchr", 3370 + "ryu", 3371 + "serde", 3372 + ] 3373 + 3374 + [[package]] 3375 + name = "serde_path_to_error" 3376 + version = "0.1.17" 3377 + source = "registry+https://github.com/rust-lang/crates.io-index" 3378 + checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" 3379 + dependencies = [ 3380 + "itoa", 3381 + "serde", 3382 + ] 3383 + 3384 + [[package]] 3385 + name = "serde_spanned" 3386 + version = "0.6.9" 3387 + source = "registry+https://github.com/rust-lang/crates.io-index" 3388 + checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" 3389 + dependencies = [ 3390 + "serde", 3391 + ] 3392 + 3393 + [[package]] 3394 + name = "serde_urlencoded" 3395 + version = "0.7.1" 3396 + source = "registry+https://github.com/rust-lang/crates.io-index" 3397 + checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 3398 + dependencies = [ 3399 + "form_urlencoded", 3400 + "itoa", 3401 + "ryu", 3402 + "serde", 3403 + ] 3404 + 3405 + [[package]] 3406 + name = "serdect" 3407 + version = "0.2.0" 3408 + source = "registry+https://github.com/rust-lang/crates.io-index" 3409 + checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" 3410 + dependencies = [ 3411 + "base16ct", 3412 + "serde", 3413 + ] 3414 + 3415 + [[package]] 3416 + name = "sha1" 3417 + version = "0.10.6" 3418 + source = "registry+https://github.com/rust-lang/crates.io-index" 3419 + checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 3420 + dependencies = [ 3421 + "cfg-if", 3422 + "cpufeatures", 3423 + "digest", 3424 + ] 3425 + 3426 + [[package]] 3427 + name = "sha2" 3428 + version = "0.10.9" 3429 + source = "registry+https://github.com/rust-lang/crates.io-index" 3430 + checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 3431 + dependencies = [ 3432 + "cfg-if", 3433 + "cpufeatures", 3434 + "digest", 3435 + ] 3436 + 3437 + [[package]] 3438 + name = "sharded-slab" 3439 + version = "0.1.7" 3440 + source = "registry+https://github.com/rust-lang/crates.io-index" 3441 + checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 3442 + dependencies = [ 3443 + "lazy_static", 3444 + ] 3445 + 3446 + [[package]] 3447 + name = "shlex" 3448 + version = "1.3.0" 3449 + source = "registry+https://github.com/rust-lang/crates.io-index" 3450 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 3451 + 3452 + [[package]] 3453 + name = "showcase" 3454 + version = "0.1.0" 3455 + dependencies = [ 3456 + "anyhow", 3457 + "async-trait", 3458 + "atproto-client", 3459 + "atproto-identity", 3460 + "atproto-jetstream", 3461 + "atproto-record", 3462 + "axum", 3463 + "axum-template", 3464 + "base64", 3465 + "bytes", 3466 + "chrono", 3467 + "duration-str", 3468 + "ecdsa", 3469 + "elliptic-curve", 3470 + "hickory-resolver", 3471 + "image", 3472 + "k256", 3473 + "lru", 3474 + "minijinja", 3475 + "minijinja-autoreload", 3476 + "minijinja-embed", 3477 + "minio", 3478 + "multibase", 3479 + "p256", 3480 + "reqwest", 3481 + "rust-embed", 3482 + "serde", 3483 + "serde_ipld_dagcbor", 3484 + "serde_json", 3485 + "sha2", 3486 + "sqlx", 3487 + "tempfile", 3488 + "thiserror 2.0.12", 3489 + "tokio", 3490 + "tokio-util", 3491 + "tower-http 0.5.2", 3492 + "tracing", 3493 + "tracing-subscriber", 3494 + ] 3495 + 3496 + [[package]] 3497 + name = "signal-hook-registry" 3498 + version = "1.4.5" 3499 + source = "registry+https://github.com/rust-lang/crates.io-index" 3500 + checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" 3501 + dependencies = [ 3502 + "libc", 3503 + ] 3504 + 3505 + [[package]] 3506 + name = "signature" 3507 + version = "2.2.0" 3508 + source = "registry+https://github.com/rust-lang/crates.io-index" 3509 + checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 3510 + dependencies = [ 3511 + "digest", 3512 + "rand_core 0.6.4", 3513 + ] 3514 + 3515 + [[package]] 3516 + name = "simd-adler32" 3517 + version = "0.3.7" 3518 + source = "registry+https://github.com/rust-lang/crates.io-index" 3519 + checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 3520 + 3521 + [[package]] 3522 + name = "simd_helpers" 3523 + version = "0.1.0" 3524 + source = "registry+https://github.com/rust-lang/crates.io-index" 3525 + checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" 3526 + dependencies = [ 3527 + "quote", 3528 + ] 3529 + 3530 + [[package]] 3531 + name = "simdutf8" 3532 + version = "0.1.5" 3533 + source = "registry+https://github.com/rust-lang/crates.io-index" 3534 + checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" 3535 + 3536 + [[package]] 3537 + name = "slab" 3538 + version = "0.4.9" 3539 + source = "registry+https://github.com/rust-lang/crates.io-index" 3540 + checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 3541 + dependencies = [ 3542 + "autocfg", 3543 + ] 3544 + 3545 + [[package]] 3546 + name = "smallvec" 3547 + version = "1.15.1" 3548 + source = "registry+https://github.com/rust-lang/crates.io-index" 3549 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 3550 + dependencies = [ 3551 + "serde", 3552 + ] 3553 + 3554 + [[package]] 3555 + name = "socket2" 3556 + version = "0.5.10" 3557 + source = "registry+https://github.com/rust-lang/crates.io-index" 3558 + checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" 3559 + dependencies = [ 3560 + "libc", 3561 + "windows-sys 0.52.0", 3562 + ] 3563 + 3564 + [[package]] 3565 + name = "spin" 3566 + version = "0.9.8" 3567 + source = "registry+https://github.com/rust-lang/crates.io-index" 3568 + checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 3569 + dependencies = [ 3570 + "lock_api", 3571 + ] 3572 + 3573 + [[package]] 3574 + name = "spki" 3575 + version = "0.7.3" 3576 + source = "registry+https://github.com/rust-lang/crates.io-index" 3577 + checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 3578 + dependencies = [ 3579 + "base64ct", 3580 + "der", 3581 + ] 3582 + 3583 + [[package]] 3584 + name = "sqlx" 3585 + version = "0.8.6" 3586 + source = "registry+https://github.com/rust-lang/crates.io-index" 3587 + checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" 3588 + dependencies = [ 3589 + "sqlx-core", 3590 + "sqlx-macros", 3591 + "sqlx-mysql", 3592 + "sqlx-postgres", 3593 + "sqlx-sqlite", 3594 + ] 3595 + 3596 + [[package]] 3597 + name = "sqlx-core" 3598 + version = "0.8.6" 3599 + source = "registry+https://github.com/rust-lang/crates.io-index" 3600 + checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" 3601 + dependencies = [ 3602 + "base64", 3603 + "bytes", 3604 + "chrono", 3605 + "crc", 3606 + "crossbeam-queue", 3607 + "either", 3608 + "event-listener", 3609 + "futures-core", 3610 + "futures-intrusive", 3611 + "futures-io", 3612 + "futures-util", 3613 + "hashbrown 0.15.3", 3614 + "hashlink", 3615 + "indexmap", 3616 + "log", 3617 + "memchr", 3618 + "once_cell", 3619 + "percent-encoding", 3620 + "rustls", 3621 + "serde", 3622 + "serde_json", 3623 + "sha2", 3624 + "smallvec", 3625 + "thiserror 2.0.12", 3626 + "tokio", 3627 + "tokio-stream", 3628 + "tracing", 3629 + "url", 3630 + "uuid", 3631 + "webpki-roots 0.26.11", 3632 + ] 3633 + 3634 + [[package]] 3635 + name = "sqlx-macros" 3636 + version = "0.8.6" 3637 + source = "registry+https://github.com/rust-lang/crates.io-index" 3638 + checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" 3639 + dependencies = [ 3640 + "proc-macro2", 3641 + "quote", 3642 + "sqlx-core", 3643 + "sqlx-macros-core", 3644 + "syn 2.0.101", 3645 + ] 3646 + 3647 + [[package]] 3648 + name = "sqlx-macros-core" 3649 + version = "0.8.6" 3650 + source = "registry+https://github.com/rust-lang/crates.io-index" 3651 + checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" 3652 + dependencies = [ 3653 + "dotenvy", 3654 + "either", 3655 + "heck", 3656 + "hex", 3657 + "once_cell", 3658 + "proc-macro2", 3659 + "quote", 3660 + "serde", 3661 + "serde_json", 3662 + "sha2", 3663 + "sqlx-core", 3664 + "sqlx-mysql", 3665 + "sqlx-postgres", 3666 + "sqlx-sqlite", 3667 + "syn 2.0.101", 3668 + "tokio", 3669 + "url", 3670 + ] 3671 + 3672 + [[package]] 3673 + name = "sqlx-mysql" 3674 + version = "0.8.6" 3675 + source = "registry+https://github.com/rust-lang/crates.io-index" 3676 + checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" 3677 + dependencies = [ 3678 + "atoi", 3679 + "base64", 3680 + "bitflags 2.9.1", 3681 + "byteorder", 3682 + "bytes", 3683 + "chrono", 3684 + "crc", 3685 + "digest", 3686 + "dotenvy", 3687 + "either", 3688 + "futures-channel", 3689 + "futures-core", 3690 + "futures-io", 3691 + "futures-util", 3692 + "generic-array", 3693 + "hex", 3694 + "hkdf", 3695 + "hmac", 3696 + "itoa", 3697 + "log", 3698 + "md-5", 3699 + "memchr", 3700 + "once_cell", 3701 + "percent-encoding", 3702 + "rand 0.8.5", 3703 + "rsa", 3704 + "serde", 3705 + "sha1", 3706 + "sha2", 3707 + "smallvec", 3708 + "sqlx-core", 3709 + "stringprep", 3710 + "thiserror 2.0.12", 3711 + "tracing", 3712 + "uuid", 3713 + "whoami", 3714 + ] 3715 + 3716 + [[package]] 3717 + name = "sqlx-postgres" 3718 + version = "0.8.6" 3719 + source = "registry+https://github.com/rust-lang/crates.io-index" 3720 + checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" 3721 + dependencies = [ 3722 + "atoi", 3723 + "base64", 3724 + "bitflags 2.9.1", 3725 + "byteorder", 3726 + "chrono", 3727 + "crc", 3728 + "dotenvy", 3729 + "etcetera", 3730 + "futures-channel", 3731 + "futures-core", 3732 + "futures-util", 3733 + "hex", 3734 + "hkdf", 3735 + "hmac", 3736 + "home", 3737 + "itoa", 3738 + "log", 3739 + "md-5", 3740 + "memchr", 3741 + "once_cell", 3742 + "rand 0.8.5", 3743 + "serde", 3744 + "serde_json", 3745 + "sha2", 3746 + "smallvec", 3747 + "sqlx-core", 3748 + "stringprep", 3749 + "thiserror 2.0.12", 3750 + "tracing", 3751 + "uuid", 3752 + "whoami", 3753 + ] 3754 + 3755 + [[package]] 3756 + name = "sqlx-sqlite" 3757 + version = "0.8.6" 3758 + source = "registry+https://github.com/rust-lang/crates.io-index" 3759 + checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" 3760 + dependencies = [ 3761 + "atoi", 3762 + "chrono", 3763 + "flume", 3764 + "futures-channel", 3765 + "futures-core", 3766 + "futures-executor", 3767 + "futures-intrusive", 3768 + "futures-util", 3769 + "libsqlite3-sys", 3770 + "log", 3771 + "percent-encoding", 3772 + "serde", 3773 + "serde_urlencoded", 3774 + "sqlx-core", 3775 + "thiserror 2.0.12", 3776 + "tracing", 3777 + "url", 3778 + "uuid", 3779 + ] 3780 + 3781 + [[package]] 3782 + name = "stable_deref_trait" 3783 + version = "1.2.0" 3784 + source = "registry+https://github.com/rust-lang/crates.io-index" 3785 + checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 3786 + 3787 + [[package]] 3788 + name = "stringprep" 3789 + version = "0.1.5" 3790 + source = "registry+https://github.com/rust-lang/crates.io-index" 3791 + checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" 3792 + dependencies = [ 3793 + "unicode-bidi", 3794 + "unicode-normalization", 3795 + "unicode-properties", 3796 + ] 3797 + 3798 + [[package]] 3799 + name = "subtle" 3800 + version = "2.6.1" 3801 + source = "registry+https://github.com/rust-lang/crates.io-index" 3802 + checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 3803 + 3804 + [[package]] 3805 + name = "syn" 3806 + version = "1.0.109" 3807 + source = "registry+https://github.com/rust-lang/crates.io-index" 3808 + checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 3809 + dependencies = [ 3810 + "proc-macro2", 3811 + "quote", 3812 + "unicode-ident", 3813 + ] 3814 + 3815 + [[package]] 3816 + name = "syn" 3817 + version = "2.0.101" 3818 + source = "registry+https://github.com/rust-lang/crates.io-index" 3819 + checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 3820 + dependencies = [ 3821 + "proc-macro2", 3822 + "quote", 3823 + "unicode-ident", 3824 + ] 3825 + 3826 + [[package]] 3827 + name = "sync_wrapper" 3828 + version = "1.0.2" 3829 + source = "registry+https://github.com/rust-lang/crates.io-index" 3830 + checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 3831 + dependencies = [ 3832 + "futures-core", 3833 + ] 3834 + 3835 + [[package]] 3836 + name = "synstructure" 3837 + version = "0.13.2" 3838 + source = "registry+https://github.com/rust-lang/crates.io-index" 3839 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 3840 + dependencies = [ 3841 + "proc-macro2", 3842 + "quote", 3843 + "syn 2.0.101", 3844 + ] 3845 + 3846 + [[package]] 3847 + name = "system-configuration" 3848 + version = "0.6.1" 3849 + source = "registry+https://github.com/rust-lang/crates.io-index" 3850 + checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 3851 + dependencies = [ 3852 + "bitflags 2.9.1", 3853 + "core-foundation 0.9.4", 3854 + "system-configuration-sys", 3855 + ] 3856 + 3857 + [[package]] 3858 + name = "system-configuration-sys" 3859 + version = "0.6.0" 3860 + source = "registry+https://github.com/rust-lang/crates.io-index" 3861 + checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 3862 + dependencies = [ 3863 + "core-foundation-sys", 3864 + "libc", 3865 + ] 3866 + 3867 + [[package]] 3868 + name = "system-deps" 3869 + version = "6.2.2" 3870 + source = "registry+https://github.com/rust-lang/crates.io-index" 3871 + checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" 3872 + dependencies = [ 3873 + "cfg-expr", 3874 + "heck", 3875 + "pkg-config", 3876 + "toml", 3877 + "version-compare", 3878 + ] 3879 + 3880 + [[package]] 3881 + name = "tagptr" 3882 + version = "0.2.0" 3883 + source = "registry+https://github.com/rust-lang/crates.io-index" 3884 + checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" 3885 + 3886 + [[package]] 3887 + name = "target-lexicon" 3888 + version = "0.12.16" 3889 + source = "registry+https://github.com/rust-lang/crates.io-index" 3890 + checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" 3891 + 3892 + [[package]] 3893 + name = "tempfile" 3894 + version = "3.20.0" 3895 + source = "registry+https://github.com/rust-lang/crates.io-index" 3896 + checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 3897 + dependencies = [ 3898 + "fastrand", 3899 + "getrandom 0.3.3", 3900 + "once_cell", 3901 + "rustix", 3902 + "windows-sys 0.59.0", 3903 + ] 3904 + 3905 + [[package]] 3906 + name = "thiserror" 3907 + version = "1.0.69" 3908 + source = "registry+https://github.com/rust-lang/crates.io-index" 3909 + checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 3910 + dependencies = [ 3911 + "thiserror-impl 1.0.69", 3912 + ] 3913 + 3914 + [[package]] 3915 + name = "thiserror" 3916 + version = "2.0.12" 3917 + source = "registry+https://github.com/rust-lang/crates.io-index" 3918 + checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 3919 + dependencies = [ 3920 + "thiserror-impl 2.0.12", 3921 + ] 3922 + 3923 + [[package]] 3924 + name = "thiserror-impl" 3925 + version = "1.0.69" 3926 + source = "registry+https://github.com/rust-lang/crates.io-index" 3927 + checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 3928 + dependencies = [ 3929 + "proc-macro2", 3930 + "quote", 3931 + "syn 2.0.101", 3932 + ] 3933 + 3934 + [[package]] 3935 + name = "thiserror-impl" 3936 + version = "2.0.12" 3937 + source = "registry+https://github.com/rust-lang/crates.io-index" 3938 + checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 3939 + dependencies = [ 3940 + "proc-macro2", 3941 + "quote", 3942 + "syn 2.0.101", 3943 + ] 3944 + 3945 + [[package]] 3946 + name = "thread_local" 3947 + version = "1.1.8" 3948 + source = "registry+https://github.com/rust-lang/crates.io-index" 3949 + checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 3950 + dependencies = [ 3951 + "cfg-if", 3952 + "once_cell", 3953 + ] 3954 + 3955 + [[package]] 3956 + name = "tiff" 3957 + version = "0.9.1" 3958 + source = "registry+https://github.com/rust-lang/crates.io-index" 3959 + checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" 3960 + dependencies = [ 3961 + "flate2", 3962 + "jpeg-decoder", 3963 + "weezl", 3964 + ] 3965 + 3966 + [[package]] 3967 + name = "time" 3968 + version = "0.3.41" 3969 + source = "registry+https://github.com/rust-lang/crates.io-index" 3970 + checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 3971 + dependencies = [ 3972 + "deranged", 3973 + "num-conv", 3974 + "powerfmt", 3975 + "time-core", 3976 + ] 3977 + 3978 + [[package]] 3979 + name = "time-core" 3980 + version = "0.1.4" 3981 + source = "registry+https://github.com/rust-lang/crates.io-index" 3982 + checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 3983 + 3984 + [[package]] 3985 + name = "tinystr" 3986 + version = "0.8.1" 3987 + source = "registry+https://github.com/rust-lang/crates.io-index" 3988 + checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 3989 + dependencies = [ 3990 + "displaydoc", 3991 + "zerovec", 3992 + ] 3993 + 3994 + [[package]] 3995 + name = "tinyvec" 3996 + version = "1.9.0" 3997 + source = "registry+https://github.com/rust-lang/crates.io-index" 3998 + checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" 3999 + dependencies = [ 4000 + "tinyvec_macros", 4001 + ] 4002 + 4003 + [[package]] 4004 + name = "tinyvec_macros" 4005 + version = "0.1.1" 4006 + source = "registry+https://github.com/rust-lang/crates.io-index" 4007 + checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 4008 + 4009 + [[package]] 4010 + name = "tokio" 4011 + version = "1.45.1" 4012 + source = "registry+https://github.com/rust-lang/crates.io-index" 4013 + checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" 4014 + dependencies = [ 4015 + "backtrace", 4016 + "bytes", 4017 + "libc", 4018 + "mio", 4019 + "parking_lot", 4020 + "pin-project-lite", 4021 + "signal-hook-registry", 4022 + "socket2", 4023 + "tokio-macros", 4024 + "windows-sys 0.52.0", 4025 + ] 4026 + 4027 + [[package]] 4028 + name = "tokio-macros" 4029 + version = "2.5.0" 4030 + source = "registry+https://github.com/rust-lang/crates.io-index" 4031 + checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 4032 + dependencies = [ 4033 + "proc-macro2", 4034 + "quote", 4035 + "syn 2.0.101", 4036 + ] 4037 + 4038 + [[package]] 4039 + name = "tokio-native-tls" 4040 + version = "0.3.1" 4041 + source = "registry+https://github.com/rust-lang/crates.io-index" 4042 + checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 4043 + dependencies = [ 4044 + "native-tls", 4045 + "tokio", 4046 + ] 4047 + 4048 + [[package]] 4049 + name = "tokio-rustls" 4050 + version = "0.26.2" 4051 + source = "registry+https://github.com/rust-lang/crates.io-index" 4052 + checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 4053 + dependencies = [ 4054 + "rustls", 4055 + "tokio", 4056 + ] 4057 + 4058 + [[package]] 4059 + name = "tokio-stream" 4060 + version = "0.1.17" 4061 + source = "registry+https://github.com/rust-lang/crates.io-index" 4062 + checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 4063 + dependencies = [ 4064 + "futures-core", 4065 + "pin-project-lite", 4066 + "tokio", 4067 + ] 4068 + 4069 + [[package]] 4070 + name = "tokio-util" 4071 + version = "0.7.15" 4072 + source = "registry+https://github.com/rust-lang/crates.io-index" 4073 + checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" 4074 + dependencies = [ 4075 + "bytes", 4076 + "futures-core", 4077 + "futures-sink", 4078 + "futures-util", 4079 + "hashbrown 0.15.3", 4080 + "pin-project-lite", 4081 + "tokio", 4082 + ] 4083 + 4084 + [[package]] 4085 + name = "tokio-websockets" 4086 + version = "0.11.4" 4087 + source = "registry+https://github.com/rust-lang/crates.io-index" 4088 + checksum = "9fcaf159b4e7a376b05b5bfd77bfd38f3324f5fce751b4213bfc7eaa47affb4e" 4089 + dependencies = [ 4090 + "base64", 4091 + "bytes", 4092 + "futures-core", 4093 + "futures-sink", 4094 + "http", 4095 + "httparse", 4096 + "rand 0.9.1", 4097 + "ring", 4098 + "rustls-native-certs", 4099 + "rustls-pki-types", 4100 + "simdutf8", 4101 + "tokio", 4102 + "tokio-rustls", 4103 + "tokio-util", 4104 + ] 4105 + 4106 + [[package]] 4107 + name = "toml" 4108 + version = "0.8.23" 4109 + source = "registry+https://github.com/rust-lang/crates.io-index" 4110 + checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" 4111 + dependencies = [ 4112 + "serde", 4113 + "serde_spanned", 4114 + "toml_datetime", 4115 + "toml_edit", 4116 + ] 4117 + 4118 + [[package]] 4119 + name = "toml_datetime" 4120 + version = "0.6.11" 4121 + source = "registry+https://github.com/rust-lang/crates.io-index" 4122 + checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" 4123 + dependencies = [ 4124 + "serde", 4125 + ] 4126 + 4127 + [[package]] 4128 + name = "toml_edit" 4129 + version = "0.22.27" 4130 + source = "registry+https://github.com/rust-lang/crates.io-index" 4131 + checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" 4132 + dependencies = [ 4133 + "indexmap", 4134 + "serde", 4135 + "serde_spanned", 4136 + "toml_datetime", 4137 + "winnow 0.7.10", 4138 + ] 4139 + 4140 + [[package]] 4141 + name = "tower" 4142 + version = "0.5.2" 4143 + source = "registry+https://github.com/rust-lang/crates.io-index" 4144 + checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 4145 + dependencies = [ 4146 + "futures-core", 4147 + "futures-util", 4148 + "pin-project-lite", 4149 + "sync_wrapper", 4150 + "tokio", 4151 + "tower-layer", 4152 + "tower-service", 4153 + "tracing", 4154 + ] 4155 + 4156 + [[package]] 4157 + name = "tower-http" 4158 + version = "0.5.2" 4159 + source = "registry+https://github.com/rust-lang/crates.io-index" 4160 + checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" 4161 + dependencies = [ 4162 + "bitflags 2.9.1", 4163 + "bytes", 4164 + "futures-util", 4165 + "http", 4166 + "http-body", 4167 + "http-body-util", 4168 + "http-range-header", 4169 + "httpdate", 4170 + "mime", 4171 + "mime_guess", 4172 + "percent-encoding", 4173 + "pin-project-lite", 4174 + "tokio", 4175 + "tokio-util", 4176 + "tower-layer", 4177 + "tower-service", 4178 + "tracing", 4179 + ] 4180 + 4181 + [[package]] 4182 + name = "tower-http" 4183 + version = "0.6.6" 4184 + source = "registry+https://github.com/rust-lang/crates.io-index" 4185 + checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 4186 + dependencies = [ 4187 + "bitflags 2.9.1", 4188 + "bytes", 4189 + "futures-util", 4190 + "http", 4191 + "http-body", 4192 + "iri-string", 4193 + "pin-project-lite", 4194 + "tower", 4195 + "tower-layer", 4196 + "tower-service", 4197 + ] 4198 + 4199 + [[package]] 4200 + name = "tower-layer" 4201 + version = "0.3.3" 4202 + source = "registry+https://github.com/rust-lang/crates.io-index" 4203 + checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 4204 + 4205 + [[package]] 4206 + name = "tower-service" 4207 + version = "0.3.3" 4208 + source = "registry+https://github.com/rust-lang/crates.io-index" 4209 + checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 4210 + 4211 + [[package]] 4212 + name = "tracing" 4213 + version = "0.1.41" 4214 + source = "registry+https://github.com/rust-lang/crates.io-index" 4215 + checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 4216 + dependencies = [ 4217 + "log", 4218 + "pin-project-lite", 4219 + "tracing-attributes", 4220 + "tracing-core", 4221 + ] 4222 + 4223 + [[package]] 4224 + name = "tracing-attributes" 4225 + version = "0.1.29" 4226 + source = "registry+https://github.com/rust-lang/crates.io-index" 4227 + checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" 4228 + dependencies = [ 4229 + "proc-macro2", 4230 + "quote", 4231 + "syn 2.0.101", 4232 + ] 4233 + 4234 + [[package]] 4235 + name = "tracing-core" 4236 + version = "0.1.34" 4237 + source = "registry+https://github.com/rust-lang/crates.io-index" 4238 + checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 4239 + dependencies = [ 4240 + "once_cell", 4241 + "valuable", 4242 + ] 4243 + 4244 + [[package]] 4245 + name = "tracing-log" 4246 + version = "0.2.0" 4247 + source = "registry+https://github.com/rust-lang/crates.io-index" 4248 + checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 4249 + dependencies = [ 4250 + "log", 4251 + "once_cell", 4252 + "tracing-core", 4253 + ] 4254 + 4255 + [[package]] 4256 + name = "tracing-subscriber" 4257 + version = "0.3.19" 4258 + source = "registry+https://github.com/rust-lang/crates.io-index" 4259 + checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 4260 + dependencies = [ 4261 + "matchers", 4262 + "nu-ansi-term", 4263 + "once_cell", 4264 + "regex", 4265 + "sharded-slab", 4266 + "smallvec", 4267 + "thread_local", 4268 + "tracing", 4269 + "tracing-core", 4270 + "tracing-log", 4271 + ] 4272 + 4273 + [[package]] 4274 + name = "try-lock" 4275 + version = "0.2.5" 4276 + source = "registry+https://github.com/rust-lang/crates.io-index" 4277 + checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 4278 + 4279 + [[package]] 4280 + name = "typenum" 4281 + version = "1.18.0" 4282 + source = "registry+https://github.com/rust-lang/crates.io-index" 4283 + checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 4284 + 4285 + [[package]] 4286 + name = "ulid" 4287 + version = "1.2.1" 4288 + source = "registry+https://github.com/rust-lang/crates.io-index" 4289 + checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" 4290 + dependencies = [ 4291 + "rand 0.9.1", 4292 + "web-time", 4293 + ] 4294 + 4295 + [[package]] 4296 + name = "unicase" 4297 + version = "2.8.1" 4298 + source = "registry+https://github.com/rust-lang/crates.io-index" 4299 + checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 4300 + 4301 + [[package]] 4302 + name = "unicode-bidi" 4303 + version = "0.3.18" 4304 + source = "registry+https://github.com/rust-lang/crates.io-index" 4305 + checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" 4306 + 4307 + [[package]] 4308 + name = "unicode-ident" 4309 + version = "1.0.18" 4310 + source = "registry+https://github.com/rust-lang/crates.io-index" 4311 + checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 4312 + 4313 + [[package]] 4314 + name = "unicode-normalization" 4315 + version = "0.1.24" 4316 + source = "registry+https://github.com/rust-lang/crates.io-index" 4317 + checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 4318 + dependencies = [ 4319 + "tinyvec", 4320 + ] 4321 + 4322 + [[package]] 4323 + name = "unicode-properties" 4324 + version = "0.1.3" 4325 + source = "registry+https://github.com/rust-lang/crates.io-index" 4326 + checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" 4327 + 4328 + [[package]] 4329 + name = "unsigned-varint" 4330 + version = "0.8.0" 4331 + source = "registry+https://github.com/rust-lang/crates.io-index" 4332 + checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" 4333 + 4334 + [[package]] 4335 + name = "untrusted" 4336 + version = "0.9.0" 4337 + source = "registry+https://github.com/rust-lang/crates.io-index" 4338 + checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 4339 + 4340 + [[package]] 4341 + name = "url" 4342 + version = "2.5.4" 4343 + source = "registry+https://github.com/rust-lang/crates.io-index" 4344 + checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 4345 + dependencies = [ 4346 + "form_urlencoded", 4347 + "idna", 4348 + "percent-encoding", 4349 + ] 4350 + 4351 + [[package]] 4352 + name = "urlencoding" 4353 + version = "2.1.3" 4354 + source = "registry+https://github.com/rust-lang/crates.io-index" 4355 + checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" 4356 + 4357 + [[package]] 4358 + name = "utf8_iter" 4359 + version = "1.0.4" 4360 + source = "registry+https://github.com/rust-lang/crates.io-index" 4361 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 4362 + 4363 + [[package]] 4364 + name = "utf8parse" 4365 + version = "0.2.2" 4366 + source = "registry+https://github.com/rust-lang/crates.io-index" 4367 + checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 4368 + 4369 + [[package]] 4370 + name = "uuid" 4371 + version = "1.17.0" 4372 + source = "registry+https://github.com/rust-lang/crates.io-index" 4373 + checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" 4374 + dependencies = [ 4375 + "getrandom 0.3.3", 4376 + "js-sys", 4377 + "wasm-bindgen", 4378 + ] 4379 + 4380 + [[package]] 4381 + name = "v_frame" 4382 + version = "0.3.8" 4383 + source = "registry+https://github.com/rust-lang/crates.io-index" 4384 + checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" 4385 + dependencies = [ 4386 + "aligned-vec", 4387 + "num-traits", 4388 + "wasm-bindgen", 4389 + ] 4390 + 4391 + [[package]] 4392 + name = "valuable" 4393 + version = "0.1.1" 4394 + source = "registry+https://github.com/rust-lang/crates.io-index" 4395 + checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 4396 + 4397 + [[package]] 4398 + name = "vcpkg" 4399 + version = "0.2.15" 4400 + source = "registry+https://github.com/rust-lang/crates.io-index" 4401 + checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 4402 + 4403 + [[package]] 4404 + name = "version-compare" 4405 + version = "0.2.0" 4406 + source = "registry+https://github.com/rust-lang/crates.io-index" 4407 + checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" 4408 + 4409 + [[package]] 4410 + name = "version_check" 4411 + version = "0.9.5" 4412 + source = "registry+https://github.com/rust-lang/crates.io-index" 4413 + checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 4414 + 4415 + [[package]] 4416 + name = "walkdir" 4417 + version = "2.5.0" 4418 + source = "registry+https://github.com/rust-lang/crates.io-index" 4419 + checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 4420 + dependencies = [ 4421 + "same-file", 4422 + "winapi-util", 4423 + ] 4424 + 4425 + [[package]] 4426 + name = "want" 4427 + version = "0.3.1" 4428 + source = "registry+https://github.com/rust-lang/crates.io-index" 4429 + checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 4430 + dependencies = [ 4431 + "try-lock", 4432 + ] 4433 + 4434 + [[package]] 4435 + name = "wasi" 4436 + version = "0.11.0+wasi-snapshot-preview1" 4437 + source = "registry+https://github.com/rust-lang/crates.io-index" 4438 + checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 4439 + 4440 + [[package]] 4441 + name = "wasi" 4442 + version = "0.14.2+wasi-0.2.4" 4443 + source = "registry+https://github.com/rust-lang/crates.io-index" 4444 + checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 4445 + dependencies = [ 4446 + "wit-bindgen-rt", 4447 + ] 4448 + 4449 + [[package]] 4450 + name = "wasite" 4451 + version = "0.1.0" 4452 + source = "registry+https://github.com/rust-lang/crates.io-index" 4453 + checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" 4454 + 4455 + [[package]] 4456 + name = "wasm-bindgen" 4457 + version = "0.2.100" 4458 + source = "registry+https://github.com/rust-lang/crates.io-index" 4459 + checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 4460 + dependencies = [ 4461 + "cfg-if", 4462 + "once_cell", 4463 + "rustversion", 4464 + "wasm-bindgen-macro", 4465 + ] 4466 + 4467 + [[package]] 4468 + name = "wasm-bindgen-backend" 4469 + version = "0.2.100" 4470 + source = "registry+https://github.com/rust-lang/crates.io-index" 4471 + checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 4472 + dependencies = [ 4473 + "bumpalo", 4474 + "log", 4475 + "proc-macro2", 4476 + "quote", 4477 + "syn 2.0.101", 4478 + "wasm-bindgen-shared", 4479 + ] 4480 + 4481 + [[package]] 4482 + name = "wasm-bindgen-futures" 4483 + version = "0.4.50" 4484 + source = "registry+https://github.com/rust-lang/crates.io-index" 4485 + checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 4486 + dependencies = [ 4487 + "cfg-if", 4488 + "js-sys", 4489 + "once_cell", 4490 + "wasm-bindgen", 4491 + "web-sys", 4492 + ] 4493 + 4494 + [[package]] 4495 + name = "wasm-bindgen-macro" 4496 + version = "0.2.100" 4497 + source = "registry+https://github.com/rust-lang/crates.io-index" 4498 + checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 4499 + dependencies = [ 4500 + "quote", 4501 + "wasm-bindgen-macro-support", 4502 + ] 4503 + 4504 + [[package]] 4505 + name = "wasm-bindgen-macro-support" 4506 + version = "0.2.100" 4507 + source = "registry+https://github.com/rust-lang/crates.io-index" 4508 + checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 4509 + dependencies = [ 4510 + "proc-macro2", 4511 + "quote", 4512 + "syn 2.0.101", 4513 + "wasm-bindgen-backend", 4514 + "wasm-bindgen-shared", 4515 + ] 4516 + 4517 + [[package]] 4518 + name = "wasm-bindgen-shared" 4519 + version = "0.2.100" 4520 + source = "registry+https://github.com/rust-lang/crates.io-index" 4521 + checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 4522 + dependencies = [ 4523 + "unicode-ident", 4524 + ] 4525 + 4526 + [[package]] 4527 + name = "wasm-streams" 4528 + version = "0.4.2" 4529 + source = "registry+https://github.com/rust-lang/crates.io-index" 4530 + checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" 4531 + dependencies = [ 4532 + "futures-util", 4533 + "js-sys", 4534 + "wasm-bindgen", 4535 + "wasm-bindgen-futures", 4536 + "web-sys", 4537 + ] 4538 + 4539 + [[package]] 4540 + name = "web-sys" 4541 + version = "0.3.77" 4542 + source = "registry+https://github.com/rust-lang/crates.io-index" 4543 + checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 4544 + dependencies = [ 4545 + "js-sys", 4546 + "wasm-bindgen", 4547 + ] 4548 + 4549 + [[package]] 4550 + name = "web-time" 4551 + version = "1.1.0" 4552 + source = "registry+https://github.com/rust-lang/crates.io-index" 4553 + checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 4554 + dependencies = [ 4555 + "js-sys", 4556 + "wasm-bindgen", 4557 + ] 4558 + 4559 + [[package]] 4560 + name = "webpki-roots" 4561 + version = "0.26.11" 4562 + source = "registry+https://github.com/rust-lang/crates.io-index" 4563 + checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" 4564 + dependencies = [ 4565 + "webpki-roots 1.0.0", 4566 + ] 4567 + 4568 + [[package]] 4569 + name = "webpki-roots" 4570 + version = "1.0.0" 4571 + source = "registry+https://github.com/rust-lang/crates.io-index" 4572 + checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" 4573 + dependencies = [ 4574 + "rustls-pki-types", 4575 + ] 4576 + 4577 + [[package]] 4578 + name = "weezl" 4579 + version = "0.1.10" 4580 + source = "registry+https://github.com/rust-lang/crates.io-index" 4581 + checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" 4582 + 4583 + [[package]] 4584 + name = "whoami" 4585 + version = "1.6.0" 4586 + source = "registry+https://github.com/rust-lang/crates.io-index" 4587 + checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" 4588 + dependencies = [ 4589 + "redox_syscall", 4590 + "wasite", 4591 + ] 4592 + 4593 + [[package]] 4594 + name = "widestring" 4595 + version = "1.2.0" 4596 + source = "registry+https://github.com/rust-lang/crates.io-index" 4597 + checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" 4598 + 4599 + [[package]] 4600 + name = "winapi" 4601 + version = "0.3.9" 4602 + source = "registry+https://github.com/rust-lang/crates.io-index" 4603 + checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 4604 + dependencies = [ 4605 + "winapi-i686-pc-windows-gnu", 4606 + "winapi-x86_64-pc-windows-gnu", 4607 + ] 4608 + 4609 + [[package]] 4610 + name = "winapi-i686-pc-windows-gnu" 4611 + version = "0.4.0" 4612 + source = "registry+https://github.com/rust-lang/crates.io-index" 4613 + checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 4614 + 4615 + [[package]] 4616 + name = "winapi-util" 4617 + version = "0.1.9" 4618 + source = "registry+https://github.com/rust-lang/crates.io-index" 4619 + checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 4620 + dependencies = [ 4621 + "windows-sys 0.59.0", 4622 + ] 4623 + 4624 + [[package]] 4625 + name = "winapi-x86_64-pc-windows-gnu" 4626 + version = "0.4.0" 4627 + source = "registry+https://github.com/rust-lang/crates.io-index" 4628 + checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 4629 + 4630 + [[package]] 4631 + name = "windows" 4632 + version = "0.61.1" 4633 + source = "registry+https://github.com/rust-lang/crates.io-index" 4634 + checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" 4635 + dependencies = [ 4636 + "windows-collections", 4637 + "windows-core", 4638 + "windows-future", 4639 + "windows-link", 4640 + "windows-numerics", 4641 + ] 4642 + 4643 + [[package]] 4644 + name = "windows-collections" 4645 + version = "0.2.0" 4646 + source = "registry+https://github.com/rust-lang/crates.io-index" 4647 + checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" 4648 + dependencies = [ 4649 + "windows-core", 4650 + ] 4651 + 4652 + [[package]] 4653 + name = "windows-core" 4654 + version = "0.61.2" 4655 + source = "registry+https://github.com/rust-lang/crates.io-index" 4656 + checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 4657 + dependencies = [ 4658 + "windows-implement", 4659 + "windows-interface", 4660 + "windows-link", 4661 + "windows-result", 4662 + "windows-strings", 4663 + ] 4664 + 4665 + [[package]] 4666 + name = "windows-future" 4667 + version = "0.2.1" 4668 + source = "registry+https://github.com/rust-lang/crates.io-index" 4669 + checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" 4670 + dependencies = [ 4671 + "windows-core", 4672 + "windows-link", 4673 + "windows-threading", 4674 + ] 4675 + 4676 + [[package]] 4677 + name = "windows-implement" 4678 + version = "0.60.0" 4679 + source = "registry+https://github.com/rust-lang/crates.io-index" 4680 + checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 4681 + dependencies = [ 4682 + "proc-macro2", 4683 + "quote", 4684 + "syn 2.0.101", 4685 + ] 4686 + 4687 + [[package]] 4688 + name = "windows-interface" 4689 + version = "0.59.1" 4690 + source = "registry+https://github.com/rust-lang/crates.io-index" 4691 + checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 4692 + dependencies = [ 4693 + "proc-macro2", 4694 + "quote", 4695 + "syn 2.0.101", 4696 + ] 4697 + 4698 + [[package]] 4699 + name = "windows-link" 4700 + version = "0.1.1" 4701 + source = "registry+https://github.com/rust-lang/crates.io-index" 4702 + checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 4703 + 4704 + [[package]] 4705 + name = "windows-numerics" 4706 + version = "0.2.0" 4707 + source = "registry+https://github.com/rust-lang/crates.io-index" 4708 + checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" 4709 + dependencies = [ 4710 + "windows-core", 4711 + "windows-link", 4712 + ] 4713 + 4714 + [[package]] 4715 + name = "windows-registry" 4716 + version = "0.5.2" 4717 + source = "registry+https://github.com/rust-lang/crates.io-index" 4718 + checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" 4719 + dependencies = [ 4720 + "windows-link", 4721 + "windows-result", 4722 + "windows-strings", 4723 + ] 4724 + 4725 + [[package]] 4726 + name = "windows-result" 4727 + version = "0.3.4" 4728 + source = "registry+https://github.com/rust-lang/crates.io-index" 4729 + checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 4730 + dependencies = [ 4731 + "windows-link", 4732 + ] 4733 + 4734 + [[package]] 4735 + name = "windows-strings" 4736 + version = "0.4.2" 4737 + source = "registry+https://github.com/rust-lang/crates.io-index" 4738 + checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 4739 + dependencies = [ 4740 + "windows-link", 4741 + ] 4742 + 4743 + [[package]] 4744 + name = "windows-sys" 4745 + version = "0.48.0" 4746 + source = "registry+https://github.com/rust-lang/crates.io-index" 4747 + checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 4748 + dependencies = [ 4749 + "windows-targets 0.48.5", 4750 + ] 4751 + 4752 + [[package]] 4753 + name = "windows-sys" 4754 + version = "0.52.0" 4755 + source = "registry+https://github.com/rust-lang/crates.io-index" 4756 + checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 4757 + dependencies = [ 4758 + "windows-targets 0.52.6", 4759 + ] 4760 + 4761 + [[package]] 4762 + name = "windows-sys" 4763 + version = "0.59.0" 4764 + source = "registry+https://github.com/rust-lang/crates.io-index" 4765 + checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 4766 + dependencies = [ 4767 + "windows-targets 0.52.6", 4768 + ] 4769 + 4770 + [[package]] 4771 + name = "windows-targets" 4772 + version = "0.48.5" 4773 + source = "registry+https://github.com/rust-lang/crates.io-index" 4774 + checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 4775 + dependencies = [ 4776 + "windows_aarch64_gnullvm 0.48.5", 4777 + "windows_aarch64_msvc 0.48.5", 4778 + "windows_i686_gnu 0.48.5", 4779 + "windows_i686_msvc 0.48.5", 4780 + "windows_x86_64_gnu 0.48.5", 4781 + "windows_x86_64_gnullvm 0.48.5", 4782 + "windows_x86_64_msvc 0.48.5", 4783 + ] 4784 + 4785 + [[package]] 4786 + name = "windows-targets" 4787 + version = "0.52.6" 4788 + source = "registry+https://github.com/rust-lang/crates.io-index" 4789 + checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 4790 + dependencies = [ 4791 + "windows_aarch64_gnullvm 0.52.6", 4792 + "windows_aarch64_msvc 0.52.6", 4793 + "windows_i686_gnu 0.52.6", 4794 + "windows_i686_gnullvm", 4795 + "windows_i686_msvc 0.52.6", 4796 + "windows_x86_64_gnu 0.52.6", 4797 + "windows_x86_64_gnullvm 0.52.6", 4798 + "windows_x86_64_msvc 0.52.6", 4799 + ] 4800 + 4801 + [[package]] 4802 + name = "windows-threading" 4803 + version = "0.1.0" 4804 + source = "registry+https://github.com/rust-lang/crates.io-index" 4805 + checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" 4806 + dependencies = [ 4807 + "windows-link", 4808 + ] 4809 + 4810 + [[package]] 4811 + name = "windows_aarch64_gnullvm" 4812 + version = "0.48.5" 4813 + source = "registry+https://github.com/rust-lang/crates.io-index" 4814 + checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 4815 + 4816 + [[package]] 4817 + name = "windows_aarch64_gnullvm" 4818 + version = "0.52.6" 4819 + source = "registry+https://github.com/rust-lang/crates.io-index" 4820 + checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 4821 + 4822 + [[package]] 4823 + name = "windows_aarch64_msvc" 4824 + version = "0.48.5" 4825 + source = "registry+https://github.com/rust-lang/crates.io-index" 4826 + checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 4827 + 4828 + [[package]] 4829 + name = "windows_aarch64_msvc" 4830 + version = "0.52.6" 4831 + source = "registry+https://github.com/rust-lang/crates.io-index" 4832 + checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 4833 + 4834 + [[package]] 4835 + name = "windows_i686_gnu" 4836 + version = "0.48.5" 4837 + source = "registry+https://github.com/rust-lang/crates.io-index" 4838 + checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 4839 + 4840 + [[package]] 4841 + name = "windows_i686_gnu" 4842 + version = "0.52.6" 4843 + source = "registry+https://github.com/rust-lang/crates.io-index" 4844 + checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 4845 + 4846 + [[package]] 4847 + name = "windows_i686_gnullvm" 4848 + version = "0.52.6" 4849 + source = "registry+https://github.com/rust-lang/crates.io-index" 4850 + checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 4851 + 4852 + [[package]] 4853 + name = "windows_i686_msvc" 4854 + version = "0.48.5" 4855 + source = "registry+https://github.com/rust-lang/crates.io-index" 4856 + checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 4857 + 4858 + [[package]] 4859 + name = "windows_i686_msvc" 4860 + version = "0.52.6" 4861 + source = "registry+https://github.com/rust-lang/crates.io-index" 4862 + checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 4863 + 4864 + [[package]] 4865 + name = "windows_x86_64_gnu" 4866 + version = "0.48.5" 4867 + source = "registry+https://github.com/rust-lang/crates.io-index" 4868 + checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 4869 + 4870 + [[package]] 4871 + name = "windows_x86_64_gnu" 4872 + version = "0.52.6" 4873 + source = "registry+https://github.com/rust-lang/crates.io-index" 4874 + checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 4875 + 4876 + [[package]] 4877 + name = "windows_x86_64_gnullvm" 4878 + version = "0.48.5" 4879 + source = "registry+https://github.com/rust-lang/crates.io-index" 4880 + checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 4881 + 4882 + [[package]] 4883 + name = "windows_x86_64_gnullvm" 4884 + version = "0.52.6" 4885 + source = "registry+https://github.com/rust-lang/crates.io-index" 4886 + checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 4887 + 4888 + [[package]] 4889 + name = "windows_x86_64_msvc" 4890 + version = "0.48.5" 4891 + source = "registry+https://github.com/rust-lang/crates.io-index" 4892 + checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 4893 + 4894 + [[package]] 4895 + name = "windows_x86_64_msvc" 4896 + version = "0.52.6" 4897 + source = "registry+https://github.com/rust-lang/crates.io-index" 4898 + checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 4899 + 4900 + [[package]] 4901 + name = "winnow" 4902 + version = "0.6.26" 4903 + source = "registry+https://github.com/rust-lang/crates.io-index" 4904 + checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" 4905 + dependencies = [ 4906 + "memchr", 4907 + ] 4908 + 4909 + [[package]] 4910 + name = "winnow" 4911 + version = "0.7.10" 4912 + source = "registry+https://github.com/rust-lang/crates.io-index" 4913 + checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" 4914 + dependencies = [ 4915 + "memchr", 4916 + ] 4917 + 4918 + [[package]] 4919 + name = "winreg" 4920 + version = "0.50.0" 4921 + source = "registry+https://github.com/rust-lang/crates.io-index" 4922 + checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 4923 + dependencies = [ 4924 + "cfg-if", 4925 + "windows-sys 0.48.0", 4926 + ] 4927 + 4928 + [[package]] 4929 + name = "wit-bindgen-rt" 4930 + version = "0.39.0" 4931 + source = "registry+https://github.com/rust-lang/crates.io-index" 4932 + checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 4933 + dependencies = [ 4934 + "bitflags 2.9.1", 4935 + ] 4936 + 4937 + [[package]] 4938 + name = "writeable" 4939 + version = "0.6.1" 4940 + source = "registry+https://github.com/rust-lang/crates.io-index" 4941 + checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 4942 + 4943 + [[package]] 4944 + name = "xml-rs" 4945 + version = "0.8.26" 4946 + source = "registry+https://github.com/rust-lang/crates.io-index" 4947 + checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" 4948 + 4949 + [[package]] 4950 + name = "xmltree" 4951 + version = "0.11.0" 4952 + source = "registry+https://github.com/rust-lang/crates.io-index" 4953 + checksum = "b619f8c85654798007fb10afa5125590b43b088c225a25fc2fec100a9fad0fc6" 4954 + dependencies = [ 4955 + "xml-rs", 4956 + ] 4957 + 4958 + [[package]] 4959 + name = "yoke" 4960 + version = "0.8.0" 4961 + source = "registry+https://github.com/rust-lang/crates.io-index" 4962 + checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 4963 + dependencies = [ 4964 + "serde", 4965 + "stable_deref_trait", 4966 + "yoke-derive", 4967 + "zerofrom", 4968 + ] 4969 + 4970 + [[package]] 4971 + name = "yoke-derive" 4972 + version = "0.8.0" 4973 + source = "registry+https://github.com/rust-lang/crates.io-index" 4974 + checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 4975 + dependencies = [ 4976 + "proc-macro2", 4977 + "quote", 4978 + "syn 2.0.101", 4979 + "synstructure", 4980 + ] 4981 + 4982 + [[package]] 4983 + name = "zerocopy" 4984 + version = "0.8.25" 4985 + source = "registry+https://github.com/rust-lang/crates.io-index" 4986 + checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" 4987 + dependencies = [ 4988 + "zerocopy-derive", 4989 + ] 4990 + 4991 + [[package]] 4992 + name = "zerocopy-derive" 4993 + version = "0.8.25" 4994 + source = "registry+https://github.com/rust-lang/crates.io-index" 4995 + checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" 4996 + dependencies = [ 4997 + "proc-macro2", 4998 + "quote", 4999 + "syn 2.0.101", 5000 + ] 5001 + 5002 + [[package]] 5003 + name = "zerofrom" 5004 + version = "0.1.6" 5005 + source = "registry+https://github.com/rust-lang/crates.io-index" 5006 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 5007 + dependencies = [ 5008 + "zerofrom-derive", 5009 + ] 5010 + 5011 + [[package]] 5012 + name = "zerofrom-derive" 5013 + version = "0.1.6" 5014 + source = "registry+https://github.com/rust-lang/crates.io-index" 5015 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 5016 + dependencies = [ 5017 + "proc-macro2", 5018 + "quote", 5019 + "syn 2.0.101", 5020 + "synstructure", 5021 + ] 5022 + 5023 + [[package]] 5024 + name = "zeroize" 5025 + version = "1.8.1" 5026 + source = "registry+https://github.com/rust-lang/crates.io-index" 5027 + checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 5028 + 5029 + [[package]] 5030 + name = "zerotrie" 5031 + version = "0.2.2" 5032 + source = "registry+https://github.com/rust-lang/crates.io-index" 5033 + checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 5034 + dependencies = [ 5035 + "displaydoc", 5036 + "yoke", 5037 + "zerofrom", 5038 + ] 5039 + 5040 + [[package]] 5041 + name = "zerovec" 5042 + version = "0.11.2" 5043 + source = "registry+https://github.com/rust-lang/crates.io-index" 5044 + checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" 5045 + dependencies = [ 5046 + "yoke", 5047 + "zerofrom", 5048 + "zerovec-derive", 5049 + ] 5050 + 5051 + [[package]] 5052 + name = "zerovec-derive" 5053 + version = "0.11.1" 5054 + source = "registry+https://github.com/rust-lang/crates.io-index" 5055 + checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 5056 + dependencies = [ 5057 + "proc-macro2", 5058 + "quote", 5059 + "syn 2.0.101", 5060 + ] 5061 + 5062 + [[package]] 5063 + name = "zstd" 5064 + version = "0.13.3" 5065 + source = "registry+https://github.com/rust-lang/crates.io-index" 5066 + checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" 5067 + dependencies = [ 5068 + "zstd-safe", 5069 + ] 5070 + 5071 + [[package]] 5072 + name = "zstd-safe" 5073 + version = "7.2.4" 5074 + source = "registry+https://github.com/rust-lang/crates.io-index" 5075 + checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" 5076 + dependencies = [ 5077 + "zstd-sys", 5078 + ] 5079 + 5080 + [[package]] 5081 + name = "zstd-sys" 5082 + version = "2.0.15+zstd.1.5.7" 5083 + source = "registry+https://github.com/rust-lang/crates.io-index" 5084 + checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" 5085 + dependencies = [ 5086 + "cc", 5087 + "pkg-config", 5088 + ] 5089 + 5090 + [[package]] 5091 + name = "zune-core" 5092 + version = "0.4.12" 5093 + source = "registry+https://github.com/rust-lang/crates.io-index" 5094 + checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" 5095 + 5096 + [[package]] 5097 + name = "zune-inflate" 5098 + version = "0.2.54" 5099 + source = "registry+https://github.com/rust-lang/crates.io-index" 5100 + checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" 5101 + dependencies = [ 5102 + "simd-adler32", 5103 + ] 5104 + 5105 + [[package]] 5106 + name = "zune-jpeg" 5107 + version = "0.4.16" 5108 + source = "registry+https://github.com/rust-lang/crates.io-index" 5109 + checksum = "3e4a518c0ea2576f4da876349d7f67a7be489297cd77c2cf9e04c2e05fcd3974" 5110 + dependencies = [ 5111 + "zune-core", 5112 + ]
+75
Cargo.toml
··· 1 + [package] 2 + name = "showcase" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [features] 7 + default = ["reload", "sqlite", "postgres", "s3"] 8 + embed = ["dep:minijinja-embed", "dep:rust-embed"] 9 + reload = ["dep:minijinja-autoreload", "minijinja/loader", "axum-template/minijinja-autoreload"] 10 + sqlite = ["sqlx/sqlite"] 11 + postgres = ["sqlx/postgres"] 12 + s3 = ["dep:minio"] 13 + 14 + [build-dependencies] 15 + minijinja-embed = {version = "2.7"} 16 + 17 + [dependencies] 18 + # ATProtocol dependencies 19 + # atproto-client = { path = "/Users/nick/development/tangled.sh/smokesignal.events/atproto-identity-rs/crates/atproto-client" } 20 + # atproto-identity = { path = "/Users/nick/development/tangled.sh/smokesignal.events/atproto-identity-rs/crates/atproto-identity" } 21 + # atproto-record = { path = "/Users/nick/development/tangled.sh/smokesignal.events/atproto-identity-rs/crates/atproto-record" } 22 + # atproto-jetstream = { path = "/Users/nick/development/tangled.sh/smokesignal.events/atproto-identity-rs/crates/atproto-jetstream" } 23 + 24 + atproto-client = { version = "0.6.0" } 25 + atproto-identity = { version = "0.6.0" } 26 + atproto-record = { version = "0.6.0" } 27 + atproto-jetstream = { version = "0.6.0" } 28 + 29 + # Web framework 30 + axum = "0.8" 31 + axum-template = { version = "3.0", features = ["minijinja"] } 32 + tower-http = { version = "0.5", features = ["fs"] } 33 + 34 + # Template engine 35 + minijinja = { version = "2.7", features = ["builtins", ] } 36 + minijinja-autoreload = { version = "2.7", optional = true } 37 + minijinja-embed = { version = "2.7", optional = true } 38 + rust-embed = { version = "8.5", optional = true } 39 + 40 + # Database 41 + sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "chrono", "json", "uuid"] } 42 + 43 + # Image processing 44 + image = "0.25" 45 + 46 + # Core dependencies 47 + anyhow = "1.0" 48 + async-trait = "0.1.88" 49 + base64 = "0.22.1" 50 + chrono = {version = "0.4.41", default-features = false, features = ["std", "now", "serde"]} 51 + duration-str = "0.11" 52 + ecdsa = { version = "0.16.9", features = ["std"] } 53 + elliptic-curve = { version = "0.13.8", features = ["jwk", "serde"] } 54 + hickory-resolver = { version = "0.25" } 55 + k256 = "0.13.4" 56 + lru = "0.12" 57 + multibase = "0.9.1" 58 + p256 = "0.13.2" 59 + reqwest = { version = "0.12", features = ["json", "rustls-tls"] } 60 + serde = { version = "1.0", features = ["derive"] } 61 + serde_ipld_dagcbor = "0.6.3" 62 + serde_json = "1.0" 63 + sha2 = "0.10.9" 64 + thiserror = "2.0" 65 + tokio = { version = "1.41", features = ["macros", "rt", "rt-multi-thread", "fs", "signal"] } 66 + tokio-util = { version = "0.7", features = ["rt"] } 67 + tracing = { version = "0.1", features = ["async-await"] } 68 + tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] } 69 + 70 + # Object storage 71 + minio = { version = "0.3", optional = true } 72 + bytes = "1.10.1" 73 + 74 + [dev-dependencies] 75 + tempfile = "3.0"
+807
DEVELOPMENT.md
··· 1 + # Development Guide 2 + 3 + This guide provides instructions for setting up the development environment and running tests for the Showcase application. 4 + 5 + ## Prerequisites 6 + 7 + - [Docker](https://docs.docker.com/get-docker/) installed and running 8 + - [Docker Compose](https://docs.docker.com/compose/install/) (usually included with Docker Desktop) 9 + - [Rust](https://rustup.rs/) 1.70+ with Cargo 10 + - [Git](https://git-scm.com/downloads) 11 + 12 + ## PostgreSQL Development Setup 13 + 14 + The application supports both SQLite (default) and PostgreSQL storage backends. For integration testing with PostgreSQL, you'll need to run a PostgreSQL instance locally. 15 + 16 + ### Option 1: Docker Compose (Recommended) 17 + 18 + Create a `docker-compose.yml` file in the project root: 19 + 20 + ```yaml 21 + version: '3.8' 22 + 23 + services: 24 + postgres: 25 + image: postgres:17-alpine 26 + container_name: showcase_postgres 27 + environment: 28 + POSTGRES_DB: showcase_test 29 + POSTGRES_USER: showcase 30 + POSTGRES_PASSWORD: showcase_dev_password 31 + POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" 32 + ports: 33 + - "5433:5432" # Using 5433 to avoid conflicts with system PostgreSQL 34 + volumes: 35 + - postgres_data:/var/lib/postgresql/data 36 + - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro 37 + healthcheck: 38 + test: ["CMD-SHELL", "pg_isready -U showcase -d showcase_test"] 39 + interval: 5s 40 + timeout: 5s 41 + retries: 5 42 + restart: unless-stopped 43 + 44 + volumes: 45 + postgres_data: 46 + ``` 47 + 48 + Start the PostgreSQL container: 49 + 50 + ```bash 51 + # Start PostgreSQL in background 52 + docker-compose up -d postgres 53 + 54 + # Check logs to ensure it's running properly 55 + docker-compose logs postgres 56 + 57 + # Stop when done 58 + docker-compose down 59 + ``` 60 + 61 + ### Option 2: Docker Run Command 62 + 63 + If you prefer to use Docker directly without Docker Compose: 64 + 65 + ```bash 66 + # Create a Docker network (optional, for better container management) 67 + docker network create showcase-dev 68 + 69 + # Run PostgreSQL 17 container 70 + docker run -d \ 71 + --name showcase_postgres \ 72 + --network showcase-dev \ 73 + -e POSTGRES_DB=showcase_test \ 74 + -e POSTGRES_USER=showcase \ 75 + -e POSTGRES_PASSWORD=showcase_dev_password \ 76 + -e POSTGRES_INITDB_ARGS="--encoding=UTF8 --locale=C" \ 77 + -p 5433:5432 \ 78 + -v showcase_postgres_data:/var/lib/postgresql/data \ 79 + postgres:17-alpine 80 + 81 + # Verify the container is running 82 + docker ps | grep showcase_postgres 83 + 84 + # Check logs 85 + docker logs showcase_postgres 86 + 87 + # Stop and remove container when done 88 + docker stop showcase_postgres 89 + docker rm showcase_postgres 90 + ``` 91 + 92 + ### Database Initialization 93 + 94 + Create an optional `init-db.sql` file for additional database setup: 95 + 96 + ```sql 97 + -- init-db.sql 98 + -- Additional database setup if needed 99 + 100 + -- Create extensions that might be useful for testing 101 + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 102 + CREATE EXTENSION IF NOT EXISTS "pgcrypto"; 103 + 104 + -- Set timezone 105 + SET timezone TO 'UTC'; 106 + 107 + -- Create additional test database for parallel testing 108 + CREATE DATABASE showcase_test_parallel; 109 + GRANT ALL PRIVILEGES ON DATABASE showcase_test_parallel TO showcase; 110 + ``` 111 + 112 + ## MinIO Object Storage Development Setup 113 + 114 + The application supports S3-compatible object storage for badge images using MinIO. This is useful for testing S3 functionality locally and for production deployments that use object storage instead of local file storage. 115 + 116 + ### Setting up MinIO with Docker Compose 117 + 118 + Add MinIO service to your `docker-compose.yml`: 119 + 120 + ```yaml 121 + version: '3.8' 122 + 123 + services: 124 + postgres: 125 + image: postgres:17-alpine 126 + container_name: showcase_postgres 127 + environment: 128 + POSTGRES_DB: showcase_test 129 + POSTGRES_USER: showcase 130 + POSTGRES_PASSWORD: showcase_dev_password 131 + POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" 132 + ports: 133 + - "5433:5432" 134 + volumes: 135 + - postgres_data:/var/lib/postgresql/data 136 + - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro 137 + healthcheck: 138 + test: ["CMD-SHELL", "pg_isready -U showcase -d showcase_test"] 139 + interval: 5s 140 + timeout: 5s 141 + retries: 5 142 + restart: unless-stopped 143 + 144 + minio: 145 + image: minio/minio:latest 146 + container_name: showcase_minio 147 + command: server /data --console-address ":9001" 148 + environment: 149 + MINIO_ROOT_USER: showcase_minio 150 + MINIO_ROOT_PASSWORD: showcase_dev_secret 151 + ports: 152 + - "9000:9000" # MinIO API 153 + - "9001:9001" # MinIO Console 154 + volumes: 155 + - minio_data:/data 156 + healthcheck: 157 + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] 158 + interval: 30s 159 + timeout: 20s 160 + retries: 3 161 + restart: unless-stopped 162 + 163 + volumes: 164 + postgres_data: 165 + minio_data: 166 + ``` 167 + 168 + ### Starting MinIO 169 + 170 + ```bash 171 + # Start MinIO and PostgreSQL 172 + docker-compose up -d 173 + 174 + # Check if MinIO is running 175 + docker-compose logs minio 176 + 177 + # Access MinIO Console at http://localhost:9001 178 + # Username: showcase_minio 179 + # Password: showcase_dev_secret 180 + ``` 181 + 182 + ### MinIO Bucket Setup 183 + 184 + 1. **Via MinIO Console (Web UI):** 185 + - Open http://localhost:9001 in your browser 186 + - Login with `showcase_minio` / `showcase_dev_secret` 187 + - Click "Buckets" → "Create Bucket" 188 + - Create a bucket named `showcase-badges` 189 + - Set bucket policy to allow public read access (for testing) 190 + 191 + 2. **Via MinIO Client (mc):** 192 + ```bash 193 + # Install MinIO client 194 + curl https://dl.min.io/client/mc/release/linux-amd64/mc \ 195 + --create-dirs -o $HOME/minio-binaries/mc 196 + chmod +x $HOME/minio-binaries/mc 197 + export PATH=$PATH:$HOME/minio-binaries/ 198 + 199 + # Configure MinIO client 200 + mc alias set local http://localhost:9000 showcase_minio showcase_dev_secret 201 + 202 + # Create bucket 203 + mc mb local/showcase-badges 204 + 205 + # Set bucket policy for public read (optional, for testing) 206 + mc policy set download local/showcase-badges 207 + 208 + # List buckets 209 + mc ls local 210 + ``` 211 + 212 + 3. **Via Docker:** 213 + ```bash 214 + # Create bucket using MinIO container 215 + docker-compose exec minio mc mb /data/showcase-badges 216 + 217 + # Set bucket policy 218 + docker-compose exec minio mc policy set download /data/showcase-badges 219 + ``` 220 + 221 + ### Alternative MinIO Setup (Docker Run) 222 + 223 + If you prefer not to use Docker Compose: 224 + 225 + ```bash 226 + # Run MinIO container 227 + docker run -d \ 228 + --name showcase_minio \ 229 + -p 9000:9000 \ 230 + -p 9001:9001 \ 231 + -e MINIO_ROOT_USER=showcase_minio \ 232 + -e MINIO_ROOT_PASSWORD=showcase_dev_secret \ 233 + -v showcase_minio_data:/data \ 234 + minio/minio:latest server /data --console-address ":9001" 235 + 236 + # Verify MinIO is running 237 + docker logs showcase_minio 238 + 239 + # Create bucket (after MinIO is started) 240 + docker exec showcase_minio mc mb /data/showcase-badges 241 + 242 + # Stop MinIO when done 243 + docker stop showcase_minio 244 + docker rm showcase_minio 245 + ``` 246 + 247 + ## Environment Configuration 248 + 249 + ### For PostgreSQL Integration Tests 250 + 251 + Create a `.env.test` file for test-specific environment variables: 252 + 253 + ```bash 254 + # .env.test - PostgreSQL test configuration 255 + DATABASE_URL=postgresql://showcase:showcase_dev_password@localhost:5433/showcase_test 256 + EXTERNAL_BASE=http://localhost:8080 257 + BADGE_ISSUERS=did:plc:test1;did:plc:test2 258 + HTTP_PORT=8080 259 + BADGE_IMAGE_STORAGE=./test_badges 260 + PLC_HOSTNAME=plc.directory 261 + HTTP_CLIENT_TIMEOUT=10s 262 + RUST_LOG=showcase=debug,info 263 + ``` 264 + 265 + ### For SQLite Development (Default) 266 + 267 + Create a `.env.dev` file for SQLite development: 268 + 269 + ```bash 270 + # .env.dev - SQLite development configuration 271 + DATABASE_URL=sqlite://showcase_dev.db 272 + EXTERNAL_BASE=http://localhost:8080 273 + BADGE_ISSUERS=did:plc:test1;did:plc:test2 274 + HTTP_PORT=8080 275 + BADGE_IMAGE_STORAGE=./badges 276 + PLC_HOSTNAME=plc.directory 277 + HTTP_CLIENT_TIMEOUT=10s 278 + RUST_LOG=showcase=info,debug 279 + ``` 280 + 281 + ### For S3/MinIO Object Storage 282 + 283 + Create environment files for S3 object storage: 284 + 285 + **For MinIO Local Development (`.env.minio`):** 286 + ```bash 287 + # .env.minio - MinIO development configuration 288 + DATABASE_URL=sqlite://showcase_dev.db 289 + EXTERNAL_BASE=http://localhost:8080 290 + BADGE_ISSUERS=did:plc:test1;did:plc:test2 291 + HTTP_PORT=8080 292 + BADGE_IMAGE_STORAGE=s3://showcase_minio:showcase_dev_secret@localhost:9000/showcase-badges 293 + PLC_HOSTNAME=plc.directory 294 + HTTP_CLIENT_TIMEOUT=10s 295 + RUST_LOG=showcase=info,debug 296 + ``` 297 + 298 + **For AWS S3 Production (`.env.s3`):** 299 + ```bash 300 + # .env.s3 - AWS S3 configuration 301 + DATABASE_URL=postgresql://username:password@hostname:5432/database 302 + EXTERNAL_BASE=https://your-domain.com 303 + BADGE_ISSUERS=did:plc:production1;did:plc:production2 304 + HTTP_PORT=8080 305 + BADGE_IMAGE_STORAGE=s3://access_key:secret_key@s3.amazonaws.com/your-bucket/badges 306 + PLC_HOSTNAME=plc.directory 307 + HTTP_CLIENT_TIMEOUT=10s 308 + RUST_LOG=showcase=info,warn 309 + ``` 310 + 311 + **S3 URL Format:** 312 + The `BADGE_IMAGE_STORAGE` environment variable supports the following format for S3-compatible storage: 313 + 314 + ``` 315 + s3://[access_key]:[secret_key]@[hostname]/[bucket][/optional_prefix] 316 + ``` 317 + 318 + Examples: 319 + - MinIO: `s3://minio_user:minio_pass@localhost:9000/my-bucket/badges` 320 + - AWS S3: `s3://AKIAIOSFODNN7EXAMPLE:wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY@s3.amazonaws.com/my-bucket` 321 + - DigitalOcean Spaces: `s3://access_key:secret_key@nyc3.digitaloceanspaces.com/my-space/images` 322 + 323 + ## Running Tests 324 + 325 + ### Unit Tests (SQLite - Default) 326 + 327 + ```bash 328 + # Run all tests with SQLite (default) 329 + cargo test 330 + 331 + # Run specific test module 332 + cargo test storage::tests 333 + 334 + # Run tests with output 335 + cargo test -- --nocapture 336 + 337 + # Run tests in single thread (useful for database tests) 338 + cargo test -- --test-threads=1 339 + ``` 340 + 341 + ### Integration Tests with PostgreSQL 342 + 343 + 1. **Start PostgreSQL container:** 344 + ```bash 345 + docker-compose up -d postgres 346 + 347 + # Wait for PostgreSQL to be ready 348 + docker-compose exec postgres pg_isready -U showcase -d showcase_test 349 + ``` 350 + 351 + 2. **Set environment variables:** 352 + ```bash 353 + # Load PostgreSQL test environment 354 + export $(cat .env.test | xargs) 355 + 356 + # Or use direnv (if installed) 357 + echo "dotenv .env.test" > .envrc 358 + direnv allow 359 + ``` 360 + 361 + 3. **Run tests with PostgreSQL:** 362 + ```bash 363 + # Run all tests against PostgreSQL 364 + cargo test 365 + 366 + # Run specific storage tests 367 + cargo test --lib storage 368 + 369 + # Run with verbose output 370 + RUST_LOG=debug cargo test -- --nocapture 371 + ``` 372 + 373 + 4. **Clean up:** 374 + ```bash 375 + # Stop PostgreSQL container 376 + docker-compose down 377 + 378 + # Remove test data (optional) 379 + docker-compose down -v 380 + ``` 381 + 382 + ### S3/MinIO Integration Tests 383 + 384 + Test the S3 file storage functionality: 385 + 386 + 1. **Start MinIO container:** 387 + ```bash 388 + docker-compose up -d minio 389 + 390 + # Wait for MinIO to be ready 391 + timeout 30s bash -c 'until curl -f http://localhost:9000/minio/health/live; do sleep 2; done' 392 + ``` 393 + 394 + 2. **Create test bucket:** 395 + ```bash 396 + # Using MinIO client 397 + docker-compose exec minio mc mb /data/showcase-badges 398 + 399 + # Or via API (requires mc client installed) 400 + mc alias set local http://localhost:9000 showcase_minio showcase_dev_secret 401 + mc mb local/showcase-badges 402 + ``` 403 + 404 + 3. **Set environment and run tests:** 405 + ```bash 406 + # Load MinIO test environment 407 + export $(cat .env.minio | xargs) 408 + 409 + # Build with S3 feature 410 + cargo build --features s3 411 + 412 + # Run tests with S3 feature 413 + cargo test --features s3 414 + 415 + # Run specific file storage tests 416 + cargo test --features s3 file_storage 417 + ``` 418 + 419 + 4. **Clean up:** 420 + ```bash 421 + # Stop MinIO container 422 + docker-compose down 423 + 424 + # Remove MinIO data (optional) 425 + docker-compose down -v 426 + ``` 427 + 428 + ### Testing S3 Feature without MinIO 429 + 430 + If you want to test the S3 implementation without running MinIO: 431 + 432 + ```bash 433 + # Build with S3 feature to check compilation 434 + cargo build --features s3 435 + 436 + # Run unit tests that don't require actual S3 connection 437 + cargo test --features s3 test_s3_file_storage_object_key_generation 438 + 439 + # Check that the feature flag works correctly 440 + cargo test --features s3 --lib 441 + ``` 442 + 443 + ### Integration Test Scripts 444 + 445 + Create helper scripts for common test scenarios: 446 + 447 + **`scripts/test-postgres.sh`:** 448 + ```bash 449 + #!/bin/bash 450 + set -e 451 + 452 + echo "Starting PostgreSQL container..." 453 + docker-compose up -d postgres 454 + 455 + echo "Waiting for PostgreSQL to be ready..." 456 + timeout 30s bash -c 'until docker-compose exec postgres pg_isready -U showcase -d showcase_test; do sleep 1; done' 457 + 458 + echo "Loading test environment..." 459 + export $(cat .env.test | xargs) 460 + 461 + echo "Running tests with PostgreSQL..." 462 + cargo test 463 + 464 + echo "Cleaning up..." 465 + docker-compose down 466 + ``` 467 + 468 + **`scripts/test-sqlite.sh`:** 469 + ```bash 470 + #!/bin/bash 471 + set -e 472 + 473 + echo "Loading SQLite test environment..." 474 + export $(cat .env.dev | xargs) 475 + 476 + echo "Running tests with SQLite..." 477 + cargo test 478 + 479 + echo "Cleaning up test databases..." 480 + rm -f showcase_dev.db showcase_test.db 481 + ``` 482 + 483 + **`scripts/test-minio.sh`:** 484 + ```bash 485 + #!/bin/bash 486 + set -e 487 + 488 + echo "Starting MinIO container..." 489 + docker-compose up -d minio 490 + 491 + echo "Waiting for MinIO to be ready..." 492 + timeout 60s bash -c 'until curl -f http://localhost:9000/minio/health/live; do sleep 2; done' 493 + 494 + echo "Creating test bucket..." 495 + docker-compose exec minio mc mb /data/showcase-badges || echo "Bucket may already exist" 496 + 497 + echo "Loading MinIO test environment..." 498 + export $(cat .env.minio | xargs) 499 + 500 + echo "Building with S3 features..." 501 + cargo build --features s3 502 + 503 + echo "Running tests with S3 features..." 504 + cargo test --features s3 505 + 506 + echo "Cleaning up..." 507 + docker-compose down 508 + ``` 509 + 510 + **`scripts/test-all.sh`:** 511 + ```bash 512 + #!/bin/bash 513 + set -e 514 + 515 + echo "Running all test suites..." 516 + 517 + echo "=== SQLite Tests ===" 518 + ./scripts/test-sqlite.sh 519 + 520 + echo "=== PostgreSQL Tests ===" 521 + ./scripts/test-postgres.sh 522 + 523 + echo "=== S3/MinIO Tests ===" 524 + ./scripts/test-minio.sh 525 + 526 + echo "All tests completed successfully!" 527 + ``` 528 + 529 + Make scripts executable: 530 + ```bash 531 + chmod +x scripts/test-postgres.sh scripts/test-sqlite.sh scripts/test-minio.sh scripts/test-all.sh 532 + ``` 533 + 534 + ## Development Workflow 535 + 536 + ### 1. Setting up for Development 537 + 538 + ```bash 539 + # Clone the repository 540 + git clone <repository-url> 541 + cd showcase 542 + 543 + # Install Rust dependencies 544 + cargo build 545 + 546 + # Start PostgreSQL for development 547 + docker-compose up -d postgres 548 + 549 + # Load environment variables 550 + export $(cat .env.test | xargs) 551 + 552 + # Run database migrations 553 + cargo run --bin showcase migrate # If migration command exists 554 + ``` 555 + 556 + ### 2. Running the Application Locally 557 + 558 + **With SQLite (Default):** 559 + ```bash 560 + export $(cat .env.dev | xargs) 561 + cargo run --bin showcase 562 + ``` 563 + 564 + **With PostgreSQL:** 565 + ```bash 566 + # Ensure PostgreSQL is running 567 + docker-compose up -d postgres 568 + 569 + # Load PostgreSQL environment 570 + export $(cat .env.test | xargs) 571 + 572 + # Run the application 573 + cargo run --bin showcase 574 + ``` 575 + 576 + **With MinIO Object Storage:** 577 + ```bash 578 + # Ensure MinIO is running 579 + docker-compose up -d minio 580 + 581 + # Wait for MinIO to be ready 582 + timeout 30s bash -c 'until curl -f http://localhost:9000/minio/health/live; do sleep 2; done' 583 + 584 + # Create bucket if it doesn't exist 585 + docker-compose exec minio mc mb /data/showcase-badges || true 586 + 587 + # Load MinIO environment 588 + export $(cat .env.minio | xargs) 589 + 590 + # Run the application with S3 features 591 + cargo run --features s3 --bin showcase 592 + ``` 593 + 594 + ### 3. Database Management 595 + 596 + **View PostgreSQL logs:** 597 + ```bash 598 + docker-compose logs postgres -f 599 + ``` 600 + 601 + **Connect to PostgreSQL for debugging:** 602 + ```bash 603 + # Using psql in container 604 + docker-compose exec postgres psql -U showcase -d showcase_test 605 + 606 + # Or using psql from host (if installed) 607 + psql postgresql://showcase:showcase_dev_password@localhost:5433/showcase_test 608 + ``` 609 + 610 + **Reset PostgreSQL database:** 611 + ```bash 612 + # Stop and remove with volumes 613 + docker-compose down -v 614 + 615 + # Start fresh 616 + docker-compose up -d postgres 617 + ``` 618 + 619 + **View SQLite database:** 620 + ```bash 621 + # Install sqlite3 if needed 622 + sqlite3 showcase_dev.db 623 + 624 + # View tables 625 + .tables 626 + 627 + # View schema 628 + .schema 629 + ``` 630 + 631 + ## Continuous Integration 632 + 633 + For CI environments, use these commands: 634 + 635 + ```bash 636 + # CI script for PostgreSQL tests 637 + docker run -d \ 638 + --name ci_postgres \ 639 + -e POSTGRES_DB=showcase_test \ 640 + -e POSTGRES_USER=showcase \ 641 + -e POSTGRES_PASSWORD=ci_password \ 642 + -p 5432:5432 \ 643 + postgres:17-alpine 644 + 645 + # Wait for PostgreSQL 646 + timeout 60s bash -c 'until pg_isready -h localhost -p 5432 -U showcase; do sleep 2; done' 647 + 648 + # Set CI environment 649 + export DATABASE_URL=postgresql://showcase:ci_password@localhost:5432/showcase_test 650 + export BADGE_ISSUERS=did:plc:ci1;did:plc:ci2 651 + export EXTERNAL_BASE=http://localhost:8080 652 + 653 + # Run tests 654 + cargo test --all-features 655 + 656 + # Cleanup 657 + docker stop ci_postgres 658 + docker rm ci_postgres 659 + ``` 660 + 661 + ## Troubleshooting 662 + 663 + ### PostgreSQL Connection Issues 664 + 665 + 1. **Port conflicts:** 666 + ```bash 667 + # Check if port 5433 is in use 668 + lsof -i :5433 669 + 670 + # Use different port in docker-compose.yml 671 + ports: 672 + - "5434:5432" 673 + ``` 674 + 675 + 2. **Container not starting:** 676 + ```bash 677 + # Check Docker logs 678 + docker-compose logs postgres 679 + 680 + # Remove and recreate container 681 + docker-compose down -v 682 + docker-compose up -d postgres 683 + ``` 684 + 685 + 3. **Permission issues:** 686 + ```bash 687 + # Fix Docker volume permissions (Linux) 688 + sudo chown -R $(id -u):$(id -g) postgres_data/ 689 + ``` 690 + 691 + ### Test Failures 692 + 693 + 1. **Database schema issues:** 694 + ```bash 695 + # Drop and recreate test database 696 + docker-compose exec postgres psql -U showcase -c "DROP DATABASE IF EXISTS showcase_test;" 697 + docker-compose exec postgres psql -U showcase -c "CREATE DATABASE showcase_test;" 698 + ``` 699 + 700 + 2. **Environment variable issues:** 701 + ```bash 702 + # Verify environment variables are loaded 703 + env | grep DATABASE_URL 704 + env | grep BADGE_ISSUERS 705 + ``` 706 + 707 + 3. **Dependency issues:** 708 + ```bash 709 + # Clean and rebuild 710 + cargo clean 711 + cargo build 712 + ``` 713 + 714 + ### Performance Issues 715 + 716 + 1. **Slow PostgreSQL startup:** 717 + ```bash 718 + # Use smaller PostgreSQL image for faster startup 719 + # In docker-compose.yml, change to: 720 + image: postgres:17-alpine 721 + ``` 722 + 723 + 2. **Test timeouts:** 724 + ```bash 725 + # Increase test timeout 726 + cargo test -- --timeout 60 727 + 728 + # Run tests with single thread for database tests 729 + cargo test -- --test-threads=1 730 + ``` 731 + 732 + ### MinIO/S3 Issues 733 + 734 + 1. **MinIO container not starting:** 735 + ```bash 736 + # Check MinIO logs 737 + docker-compose logs minio 738 + 739 + # Remove and recreate container 740 + docker-compose down -v 741 + docker-compose up -d minio 742 + ``` 743 + 744 + 2. **S3 connection issues:** 745 + ```bash 746 + # Verify MinIO is accessible 747 + curl http://localhost:9000/minio/health/live 748 + 749 + # Check MinIO console 750 + open http://localhost:9001 751 + 752 + # Test bucket access 753 + docker-compose exec minio mc ls /data/ 754 + ``` 755 + 756 + 3. **Bucket permission issues:** 757 + ```bash 758 + # Set bucket policy to public read (for testing) 759 + docker-compose exec minio mc policy set download /data/showcase-badges 760 + 761 + # List bucket contents 762 + docker-compose exec minio mc ls /data/showcase-badges 763 + ``` 764 + 765 + 4. **S3 URL parsing errors:** 766 + ```bash 767 + # Verify environment variable format 768 + echo $BADGE_IMAGE_STORAGE 769 + 770 + # Should match: s3://key:secret@hostname/bucket[/prefix] 771 + # Example: s3://showcase_minio:showcase_dev_secret@localhost:9000/showcase-badges 772 + ``` 773 + 774 + 5. **Feature compilation issues:** 775 + ```bash 776 + # Ensure S3 feature is enabled when needed 777 + cargo build --features s3 778 + 779 + # Check feature flags in code 780 + cargo expand --features s3 | grep -A5 -B5 "cfg.*s3" 781 + ``` 782 + 783 + 6. **Credential issues:** 784 + ```bash 785 + # Test credentials manually 786 + mc alias set test http://localhost:9000 showcase_minio showcase_dev_secret 787 + mc ls test 788 + 789 + # Check if credentials are correctly parsed 790 + echo "URL: s3://showcase_minio:showcase_dev_secret@localhost:9000/showcase-badges" 791 + echo "Should parse to:" 792 + echo " Endpoint: http://localhost:9000" 793 + echo " Access Key: showcase_minio" 794 + echo " Secret Key: showcase_dev_secret" 795 + echo " Bucket: showcase-badges" 796 + ``` 797 + 798 + ## Additional Resources 799 + 800 + - [PostgreSQL 17 Documentation](https://www.postgresql.org/docs/17/) 801 + - [SQLx Documentation](https://docs.rs/sqlx/) 802 + - [Docker Compose Reference](https://docs.docker.com/compose/compose-file/) 803 + - [Rust Testing Guide](https://doc.rust-lang.org/book/ch11-00-testing.html) 804 + - [MinIO Documentation](https://docs.min.io/) 805 + - [MinIO Client (mc) Guide](https://docs.min.io/minio/baremetal/reference/minio-mc.html) 806 + - [AWS S3 API Reference](https://docs.aws.amazon.com/s3/latest/API/Welcome.html) 807 + - [minio-rs Crate Documentation](https://docs.rs/minio-rs/)
+60
Dockerfile
··· 1 + # Build stage 2 + FROM rust:1.87-slim AS builder 3 + 4 + # Install required system dependencies for building 5 + RUN apt-get update && apt-get install -y \ 6 + pkg-config \ 7 + libssl-dev \ 8 + && rm -rf /var/lib/apt/lists/* 9 + 10 + # Set working directory 11 + WORKDIR /app 12 + 13 + # Copy manifests first for better layer caching 14 + COPY Cargo.toml Cargo.lock build.rs ./ 15 + 16 + ARG FEATURES=embed,postgres,s3 17 + ARG TEMPLATES=./templates 18 + ARG STATIC=./static 19 + 20 + # Copy actual source code and assets 21 + COPY src ./src 22 + COPY ${TEMPLATES} ./templates 23 + COPY ${STATIC} ./static 24 + 25 + ENV HTTP_TEMPLATE_PATH=/app/templates/ 26 + 27 + # Build the actual application with embed feature only 28 + RUN cargo build --release --no-default-features --features ${FEATURES} 29 + 30 + # Runtime stage using distroless 31 + FROM gcr.io/distroless/cc-debian12 32 + 33 + # Add OCI labels 34 + LABEL org.opencontainers.image.title="showcase" 35 + LABEL org.opencontainers.image.description="An application showcases awarded badges." 36 + LABEL org.opencontainers.image.version="0.1.0" 37 + LABEL org.opencontainers.image.authors="Nick Gerakines <nick.gerakines@gmail.com>" 38 + LABEL org.opencontainers.image.url="https://badges.smokesignal.events/" 39 + LABEL org.opencontainers.image.source="https://tangled.sh/@smokesignal.events/showcase" 40 + LABEL org.opencontainers.image.licenses="MIT" 41 + LABEL org.opencontainers.image.created="2025-01-06T00:00:00Z" 42 + 43 + # Set working directory 44 + WORKDIR /app 45 + 46 + # Copy the binary from builder stage 47 + COPY --from=builder /app/target/release/showcase /app/showcase 48 + 49 + # Copy static directory 50 + COPY --from=builder /app/static ./static 51 + 52 + # Set environment variables 53 + ENV HTTP_STATIC_PATH=/app/static 54 + ENV HTTP_PORT=8080 55 + 56 + # Expose port 57 + EXPOSE 8080 58 + 59 + # Run the application 60 + ENTRYPOINT ["/app/showcase"]
+9
LICENSE
··· 1 + MIT License 2 + 3 + Copyright (c) 2025 Nick Gerakines 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 + 7 + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 + 9 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+167
README.md
··· 1 + # Showcase 2 + 3 + A Rust crate for badge awards showcase functionality in the AT Protocol ecosystem. 4 + 5 + ## Crate Overview 6 + 7 + The `showcase` crate provides a complete badge awards tracking and display system for AT Protocol communities. It offers both a library API and a standalone server application for processing, validating, and showcasing badge awards in real-time. 8 + 9 + ### Core Functionality 10 + 11 + - **Real-time Event Processing**: Consumes Jetstream events for `community.lexicon.badge.award` records 12 + - **Cryptographic Validation**: Verifies badge issuer signatures using AT Protocol identity resolution 13 + - **Multi-Database Support**: Works with both SQLite and PostgreSQL backends 14 + - **Image Processing**: Downloads, validates, and processes badge images (512x512 PNG format) 15 + - **Web Interface**: Provides REST API and HTML pages for displaying badge awards 16 + - **File Storage**: Supports local filesystem and S3-compatible object storage 17 + 18 + ## Binaries 19 + 20 + ### `showcase` 21 + 22 + The main server application that runs a complete badge awards showcase system. 23 + 24 + **Purpose**: Operates as a web server that consumes AT Protocol Jetstream events, validates badge awards, processes badge images, and serves a web interface for viewing awards. 25 + 26 + **Key Features**: 27 + - Jetstream consumer with automatic reconnection and cursor persistence 28 + - HTTP server with template-rendered pages and REST API endpoints 29 + - Background badge processing with signature validation 30 + - Configurable via environment variables 31 + - Graceful shutdown handling 32 + 33 + **Usage**: 34 + ```bash 35 + cargo run --bin showcase 36 + ``` 37 + 38 + The server starts on the configured HTTP port (default: 8080) and begins consuming Jetstream events immediately. 39 + 40 + ## Features 41 + 42 + - **Jetstream Consumer**: Real-time processing of badge award events 43 + - **Signature Validation**: Verifies badge issuer signatures against configured issuers 44 + - **Web Interface**: Clean, responsive UI using Pico CSS 45 + - **Badge Images**: Downloads and processes badge images 46 + - **Database Storage**: SQLite database with efficient indexing 47 + 48 + ## Architecture 49 + 50 + The application is structured into several modules: 51 + 52 + - `src/config.rs` - Configuration management with environment variables 53 + - `src/storage.rs` - Database operations and data models 54 + - `src/http.rs` - Web server and HTTP handlers 55 + - `src/consumer.rs` - Jetstream consumer and badge processing 56 + - `src/lib.rs` - Main application entry point 57 + 58 + ## Configuration 59 + 60 + Configure the application using environment variables: 61 + 62 + ### HTTP Server 63 + - `HTTP_PORT` (default: `8080`) - HTTP server port 64 + - `HTTP_STATIC_PATH` (default: `${CARGO_MANIFEST_DIR}/static`) - Static files path 65 + - `HTTP_TEMPLATES_PATH` (default: `${CARGO_MANIFEST_DIR}/templates`) - Templates path 66 + - `EXTERNAL_BASE` - External hostname of the site 67 + 68 + ### AT Protocol 69 + - `PLC_HOSTNAME` (default: `plc.directory`) - PLC server hostname 70 + - `DNS_NAMESERVERS` - DNS nameservers for handle resolution 71 + - `USER_AGENT` (default: auto-generated) - HTTP client user agent 72 + 73 + ### Security 74 + - `CERTIFICATE_BUNDLES` - CA certificate file paths (semicolon-separated) 75 + - `HTTP_CLIENT_TIMEOUT` (default: `10s`) - HTTP client timeout 76 + - `BADGE_ISSUERS` - Trusted badge issuer DIDs (semicolon-separated) 77 + 78 + ### Storage 79 + - `DATABASE_URL` (default: `sqlite://showcase.db`) - Database connection string 80 + - `BADGE_IMAGE_STORAGE` (default: `./badges`) - Badge image storage directory 81 + 82 + ### Jetstream 83 + - `JETSTREAM_CURSOR_PATH` - Optional path to cursor file for resuming Jetstream consumption 84 + 85 + ## Quick Start 86 + 87 + 1. **Install dependencies**: 88 + ```bash 89 + cargo build 90 + ``` 91 + 92 + 2. **Set required environment variables**: 93 + ```bash 94 + export EXTERNAL_BASE=example.com 95 + export BADGE_ISSUERS="did:plc:example1;did:plc:example2" 96 + ``` 97 + 98 + 3. **Run the application**: 99 + ```bash 100 + cargo run --bin showcase 101 + ``` 102 + 103 + 4. **Visit the web interface**: 104 + Open http://localhost:8080 in your browser 105 + 106 + ## Development 107 + 108 + ### Build Commands 109 + - `cargo build` - Build the project 110 + - `cargo test` - Run tests 111 + - `cargo run --bin showcase` - Run the application 112 + - `cargo check` - Check code without building 113 + - `cargo fmt` - Format code 114 + - `cargo clippy` - Lint code 115 + 116 + ### Database 117 + 118 + The application uses SQLite with automatic migrations. The database schema includes: 119 + 120 + - `identities` - DID documents and handle mappings 121 + - `awards` - Badge award records with validation status 122 + - `badges` - Badge definitions and metadata 123 + 124 + ### Templates 125 + 126 + HTML templates are located in the `templates/` directory: 127 + - `base.html` - Base template with common layout 128 + - `index.html` - Home page showing recent awards 129 + - `identity.html` - Individual identity badge awards 130 + 131 + ### Static Files 132 + 133 + CSS and other static assets are in the `static/` directory: 134 + - `pico.css` - Main CSS framework 135 + - `pico.colors.css` - Color utilities 136 + - `badges/` - Downloaded badge images 137 + 138 + ## Badge Processing 139 + 140 + When badge awards are received via Jetstream: 141 + 142 + 1. Badge definition is fetched and stored 143 + 2. Badge image is downloaded and processed (512x512 PNG) 144 + 3. Signatures are validated against configured issuers 145 + 4. Award record is stored in database 146 + 5. Badge count is updated 147 + 6. Old awards are trimmed (max 100 per identity) 148 + 149 + ## Jetstream Cursor Support 150 + 151 + The application supports resuming Jetstream consumption from a specific cursor position: 152 + 153 + - Set `JETSTREAM_CURSOR_PATH` to a file path (e.g., `/tmp/jetstream_cursor`) 154 + - On startup, if the file exists and contains a valid cursor value > 1, it will be used 155 + - The cursor (time_us value) is automatically written to the file every 30 seconds 156 + - This allows the application to resume processing from where it left off after restarts 157 + 158 + ## API Endpoints 159 + 160 + - `GET /` - Home page with recent awards 161 + - `GET /badges/:subject` - Identity page (accepts DID or handle) 162 + - `GET /static/*` - Static file serving 163 + 164 + 165 + ## License 166 + 167 + Showcase is open source software released under the [MIT License](LICENSE).
+22
build.rs
··· 1 + fn main() { 2 + #[cfg(all(feature = "embed", feature = "reload"))] 3 + compile_error!("feature \"embed\" and feature \"reload\" cannot be enabled at the same time"); 4 + 5 + #[cfg(not(any(feature = "sqlite", feature = "postgres")))] 6 + compile_error!("one of feature \"sqlite\" or feature \"postgres\" must be enabled"); 7 + 8 + #[cfg(feature = "embed")] 9 + { 10 + use std::env; 11 + use std::path::PathBuf; 12 + let template_path = if let Ok(value) = env::var("HTTP_TEMPLATE_PATH") { 13 + value.to_string() 14 + } else { 15 + PathBuf::from(env!("CARGO_MANIFEST_DIR")) 16 + .join("templates") 17 + .display() 18 + .to_string() 19 + }; 20 + minijinja_embed::embed_templates!(&template_path); 21 + } 22 + }
+83
docker-compose.yml
··· 1 + version: '3.8' 2 + 3 + services: 4 + postgres: 5 + image: postgres:17-alpine 6 + container_name: showcase_postgres 7 + environment: 8 + POSTGRES_DB: showcase_test 9 + POSTGRES_USER: showcase 10 + POSTGRES_PASSWORD: showcase_dev_password 11 + POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" 12 + ports: 13 + - "5433:5432" # Using 5433 to avoid conflicts with system PostgreSQL 14 + volumes: 15 + - postgres_data:/var/lib/postgresql/data 16 + - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro 17 + healthcheck: 18 + test: ["CMD-SHELL", "pg_isready -U showcase -d showcase_test"] 19 + interval: 5s 20 + timeout: 5s 21 + retries: 5 22 + restart: unless-stopped 23 + 24 + # Optional: PostgreSQL admin interface for development 25 + pgadmin: 26 + image: dpage/pgadmin4:latest 27 + container_name: showcase_pgadmin 28 + environment: 29 + PGADMIN_DEFAULT_EMAIL: dev@showcase.local 30 + PGADMIN_DEFAULT_PASSWORD: pgadmin_password 31 + PGADMIN_CONFIG_SERVER_MODE: 'False' 32 + ports: 33 + - "5050:80" 34 + depends_on: 35 + postgres: 36 + condition: service_healthy 37 + volumes: 38 + - pgadmin_data:/var/lib/pgadmin 39 + profiles: 40 + - admin # Only start with: docker-compose --profile admin up 41 + restart: unless-stopped 42 + 43 + minio: 44 + image: minio/minio:latest 45 + container_name: showcase_minio 46 + command: server /data --console-address ":9001" 47 + environment: 48 + MINIO_ROOT_USER: showcase_minio 49 + MINIO_ROOT_PASSWORD: showcase_dev_secret 50 + ports: 51 + - "9000:9000" # MinIO API 52 + - "9001:9001" # MinIO Console 53 + volumes: 54 + - minio_data:/data 55 + healthcheck: 56 + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] 57 + interval: 30s 58 + timeout: 20s 59 + retries: 3 60 + restart: unless-stopped 61 + 62 + createbuckets: 63 + image: minio/mc 64 + depends_on: 65 + - minio 66 + entrypoint: > 67 + /bin/sh -c " 68 + /usr/bin/mc mc alias set local http://localhost:9000 showcase_minio showcase_dev_secret; 69 + /usr/bin/mc mb myminio/showcase-badges; 70 + /usr/bin/mc policy set public myminio/showcase-badges; 71 + exit 0; 72 + " 73 + volumes: 74 + minio_data: 75 + driver: local 76 + postgres_data: 77 + driver: local 78 + pgadmin_data: 79 + driver: local 80 + 81 + networks: 82 + default: 83 + name: showcase_network
+79
init-db.sql
··· 1 + -- init-db.sql 2 + -- Database initialization script for PostgreSQL development and testing 3 + 4 + -- Set timezone to UTC for consistent testing 5 + SET timezone TO 'UTC'; 6 + 7 + -- Create useful extensions for badge storage and testing 8 + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 9 + CREATE EXTENSION IF NOT EXISTS "pgcrypto"; 10 + 11 + -- Create additional test database for parallel testing 12 + CREATE DATABASE showcase_test_parallel; 13 + GRANT ALL PRIVILEGES ON DATABASE showcase_test_parallel TO showcase; 14 + 15 + -- Create database for integration tests 16 + CREATE DATABASE showcase_integration; 17 + GRANT ALL PRIVILEGES ON DATABASE showcase_integration TO showcase; 18 + 19 + -- Set up proper encoding and collation 20 + UPDATE pg_database SET datcollate = 'C', datctype = 'C' WHERE datname = 'showcase_test'; 21 + UPDATE pg_database SET datcollate = 'C', datctype = 'C' WHERE datname = 'showcase_test_parallel'; 22 + UPDATE pg_database SET datcollate = 'C', datctype = 'C' WHERE datname = 'showcase_integration'; 23 + 24 + -- Connect to main test database to set up initial configuration 25 + \c showcase_test; 26 + 27 + -- Set up session parameters for optimal JSONB performance 28 + SET default_statistics_target = 100; 29 + SET random_page_cost = 1.1; 30 + SET effective_cache_size = '256MB'; 31 + 32 + -- Create a function to reset test data (useful for integration tests) 33 + CREATE OR REPLACE FUNCTION reset_test_data() 34 + RETURNS void AS $$ 35 + BEGIN 36 + -- Drop all tables if they exist (for clean test runs) 37 + DROP TABLE IF EXISTS awards CASCADE; 38 + DROP TABLE IF EXISTS badges CASCADE; 39 + DROP TABLE IF EXISTS identities CASCADE; 40 + 41 + RAISE NOTICE 'Test data reset completed'; 42 + END; 43 + $$ LANGUAGE plpgsql; 44 + 45 + -- Create a function to get database statistics (useful for debugging) 46 + CREATE OR REPLACE FUNCTION get_table_stats() 47 + RETURNS TABLE( 48 + table_name text, 49 + row_count bigint, 50 + table_size text, 51 + index_size text 52 + ) AS $$ 53 + BEGIN 54 + RETURN QUERY 55 + SELECT 56 + schemaname||'.'||tablename as table_name, 57 + n_tup_ins - n_tup_del as row_count, 58 + pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as table_size, 59 + pg_size_pretty(pg_indexes_size(schemaname||'.'||tablename)) as index_size 60 + FROM pg_stat_user_tables 61 + WHERE schemaname = 'public' 62 + ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC; 63 + END; 64 + $$ LANGUAGE plpgsql; 65 + 66 + -- Insert some test data for development (will be ignored if tables don't exist) 67 + DO $$ 68 + BEGIN 69 + -- This block will only execute if we're in a development environment 70 + -- Production migrations should be handled by the Rust application 71 + IF current_setting('server_version_num')::int >= 170000 THEN 72 + RAISE NOTICE 'PostgreSQL 17+ detected - ready for JSONB optimizations'; 73 + END IF; 74 + 75 + RAISE NOTICE 'Database initialization completed successfully'; 76 + RAISE NOTICE 'Available databases: showcase_test, showcase_test_parallel, showcase_integration'; 77 + RAISE NOTICE 'Available functions: reset_test_data(), get_table_stats()'; 78 + END; 79 + $$;
+376
scripts/test-all.sh
··· 1 + #!/bin/bash 2 + # Comprehensive Test Runner 3 + # This script runs tests against both SQLite and PostgreSQL backends 4 + 5 + set -e 6 + 7 + # Colors for output 8 + RED='\033[0;31m' 9 + GREEN='\033[0;32m' 10 + YELLOW='\033[1;33m' 11 + BLUE='\033[0;34m' 12 + CYAN='\033[0;36m' 13 + NC='\033[0m' # No Color 14 + 15 + # Configuration 16 + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 17 + PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" 18 + 19 + # Test results tracking 20 + SQLITE_RESULT=0 21 + POSTGRES_RESULT=0 22 + TOTAL_START_TIME=$(date +%s) 23 + 24 + # Function to print colored output 25 + print_info() { 26 + echo -e "${BLUE}[INFO]${NC} $1" 27 + } 28 + 29 + print_success() { 30 + echo -e "${GREEN}[SUCCESS]${NC} $1" 31 + } 32 + 33 + print_warning() { 34 + echo -e "${YELLOW}[WARNING]${NC} $1" 35 + } 36 + 37 + print_error() { 38 + echo -e "${RED}[ERROR]${NC} $1" 39 + } 40 + 41 + print_header() { 42 + echo -e "${CYAN}============================================${NC}" 43 + echo -e "${CYAN} $1${NC}" 44 + echo -e "${CYAN}============================================${NC}" 45 + } 46 + 47 + # Function to format time duration 48 + format_duration() { 49 + local duration=$1 50 + local minutes=$((duration / 60)) 51 + local seconds=$((duration % 60)) 52 + 53 + if [[ $minutes -gt 0 ]]; then 54 + echo "${minutes}m ${seconds}s" 55 + else 56 + echo "${seconds}s" 57 + fi 58 + } 59 + 60 + # Function to show usage 61 + usage() { 62 + echo "Usage: $0 [OPTIONS] [TEST_ARGS]" 63 + echo "" 64 + echo "Options:" 65 + echo " -h, --help Show this help message" 66 + echo " -s, --sqlite Run only SQLite tests" 67 + echo " -p, --postgres Run only PostgreSQL tests" 68 + echo " -c, --clean Clean start for all backends" 69 + echo " -k, --keep Keep PostgreSQL container running" 70 + echo " -f, --fail-fast Stop on first failure" 71 + echo " -v, --verbose Enable verbose output" 72 + echo " --stats Show database statistics" 73 + echo " --parallel Run tests in parallel (experimental)" 74 + echo "" 75 + echo "Examples:" 76 + echo " $0 # Run tests on both backends" 77 + echo " $0 --sqlite # Run only SQLite tests" 78 + echo " $0 --postgres --keep # Run PostgreSQL tests, keep container" 79 + echo " $0 --clean storage::tests # Clean start, test only storage module" 80 + echo " $0 --fail-fast # Stop on first backend failure" 81 + } 82 + 83 + # Function to run SQLite tests 84 + run_sqlite_tests() { 85 + print_header "RUNNING SQLITE TESTS" 86 + 87 + local start_time=$(date +%s) 88 + local args=() 89 + 90 + if [[ "$CLEAN_START" == "true" ]]; then 91 + args+=("--clean") 92 + fi 93 + 94 + if [[ "$SHOW_STATS" == "true" ]]; then 95 + args+=("--stats" "--optimize") 96 + fi 97 + 98 + if [[ "$VERBOSE" == "true" ]]; then 99 + args+=("--verbose") 100 + fi 101 + 102 + # Add test arguments 103 + if [[ ${#TEST_ARGS[@]} -gt 0 ]]; then 104 + args+=("--" "${TEST_ARGS[@]}") 105 + fi 106 + 107 + # Run SQLite tests 108 + if "$SCRIPT_DIR/test-sqlite.sh" "${args[@]}"; then 109 + local end_time=$(date +%s) 110 + local duration=$((end_time - start_time)) 111 + print_success "SQLite tests completed in $(format_duration $duration)" 112 + SQLITE_RESULT=0 113 + else 114 + local end_time=$(date +%s) 115 + local duration=$((end_time - start_time)) 116 + print_error "SQLite tests failed after $(format_duration $duration)" 117 + SQLITE_RESULT=1 118 + 119 + if [[ "$FAIL_FAST" == "true" ]]; then 120 + print_error "Fail-fast enabled, stopping execution" 121 + exit 1 122 + fi 123 + fi 124 + } 125 + 126 + # Function to run PostgreSQL tests 127 + run_postgres_tests() { 128 + print_header "RUNNING POSTGRESQL TESTS" 129 + 130 + local start_time=$(date +%s) 131 + local args=() 132 + 133 + if [[ "$CLEAN_START" == "true" ]]; then 134 + args+=("--clean") 135 + fi 136 + 137 + if [[ "$KEEP_CONTAINER" == "true" ]]; then 138 + args+=("--keep") 139 + fi 140 + 141 + if [[ "$VERBOSE" == "true" ]]; then 142 + args+=("--verbose") 143 + fi 144 + 145 + # Add test arguments 146 + if [[ ${#TEST_ARGS[@]} -gt 0 ]]; then 147 + args+=("--" "${TEST_ARGS[@]}") 148 + fi 149 + 150 + # Run PostgreSQL tests 151 + if "$SCRIPT_DIR/test-postgres.sh" "${args[@]}"; then 152 + local end_time=$(date +%s) 153 + local duration=$((end_time - start_time)) 154 + print_success "PostgreSQL tests completed in $(format_duration $duration)" 155 + POSTGRES_RESULT=0 156 + else 157 + local end_time=$(date +%s) 158 + local duration=$((end_time - start_time)) 159 + print_error "PostgreSQL tests failed after $(format_duration $duration)" 160 + POSTGRES_RESULT=1 161 + 162 + if [[ "$FAIL_FAST" == "true" ]]; then 163 + print_error "Fail-fast enabled, stopping execution" 164 + exit 1 165 + fi 166 + fi 167 + } 168 + 169 + # Function to run tests in parallel 170 + run_parallel_tests() { 171 + print_header "RUNNING TESTS IN PARALLEL" 172 + print_warning "Parallel testing is experimental and may cause resource conflicts" 173 + 174 + local sqlite_pid="" 175 + local postgres_pid="" 176 + 177 + # Start SQLite tests in background 178 + if [[ "$RUN_SQLITE" == "true" ]]; then 179 + print_info "Starting SQLite tests in background..." 180 + run_sqlite_tests & 181 + sqlite_pid=$! 182 + fi 183 + 184 + # Start PostgreSQL tests in background 185 + if [[ "$RUN_POSTGRES" == "true" ]]; then 186 + print_info "Starting PostgreSQL tests in background..." 187 + run_postgres_tests & 188 + postgres_pid=$! 189 + fi 190 + 191 + # Wait for both to complete 192 + if [[ -n "$sqlite_pid" ]]; then 193 + wait $sqlite_pid 194 + SQLITE_RESULT=$? 195 + fi 196 + 197 + if [[ -n "$postgres_pid" ]]; then 198 + wait $postgres_pid 199 + POSTGRES_RESULT=$? 200 + fi 201 + } 202 + 203 + # Function to show test summary 204 + show_summary() { 205 + local total_end_time=$(date +%s) 206 + local total_duration=$((total_end_time - TOTAL_START_TIME)) 207 + 208 + print_header "TEST SUMMARY" 209 + 210 + echo -e "Total execution time: $(format_duration $total_duration)" 211 + echo "" 212 + 213 + if [[ "$RUN_SQLITE" == "true" ]]; then 214 + if [[ $SQLITE_RESULT -eq 0 ]]; then 215 + echo -e "SQLite tests: ${GREEN}✓ PASSED${NC}" 216 + else 217 + echo -e "SQLite tests: ${RED}✗ FAILED${NC}" 218 + fi 219 + fi 220 + 221 + if [[ "$RUN_POSTGRES" == "true" ]]; then 222 + if [[ $POSTGRES_RESULT -eq 0 ]]; then 223 + echo -e "PostgreSQL tests: ${GREEN}✓ PASSED${NC}" 224 + else 225 + echo -e "PostgreSQL tests: ${RED}✗ FAILED${NC}" 226 + fi 227 + fi 228 + 229 + echo "" 230 + 231 + local total_failures=$((SQLITE_RESULT + POSTGRES_RESULT)) 232 + if [[ $total_failures -eq 0 ]]; then 233 + print_success "All tests passed!" 234 + return 0 235 + else 236 + print_error "$total_failures backend(s) failed" 237 + return 1 238 + fi 239 + } 240 + 241 + # Function to check prerequisites 242 + check_prerequisites() { 243 + # Check if we're in the right directory 244 + if [[ ! -f "Cargo.toml" ]] || [[ ! -d "src" ]]; then 245 + print_error "This script must be run from the project root directory" 246 + exit 1 247 + fi 248 + 249 + # Check if test scripts exist 250 + if [[ ! -f "$SCRIPT_DIR/test-sqlite.sh" ]]; then 251 + print_error "SQLite test script not found: $SCRIPT_DIR/test-sqlite.sh" 252 + exit 1 253 + fi 254 + 255 + if [[ ! -f "$SCRIPT_DIR/test-postgres.sh" ]]; then 256 + print_error "PostgreSQL test script not found: $SCRIPT_DIR/test-postgres.sh" 257 + exit 1 258 + fi 259 + } 260 + 261 + # Parse command line arguments 262 + RUN_SQLITE=true 263 + RUN_POSTGRES=true 264 + CLEAN_START=false 265 + KEEP_CONTAINER=false 266 + FAIL_FAST=false 267 + VERBOSE=false 268 + SHOW_STATS=false 269 + PARALLEL=false 270 + TEST_ARGS=() 271 + 272 + while [[ $# -gt 0 ]]; do 273 + case $1 in 274 + -h|--help) 275 + usage 276 + exit 0 277 + ;; 278 + -s|--sqlite) 279 + RUN_SQLITE=true 280 + RUN_POSTGRES=false 281 + shift 282 + ;; 283 + -p|--postgres) 284 + RUN_SQLITE=false 285 + RUN_POSTGRES=true 286 + shift 287 + ;; 288 + -c|--clean) 289 + CLEAN_START=true 290 + shift 291 + ;; 292 + -k|--keep) 293 + KEEP_CONTAINER=true 294 + shift 295 + ;; 296 + -f|--fail-fast) 297 + FAIL_FAST=true 298 + shift 299 + ;; 300 + -v|--verbose) 301 + VERBOSE=true 302 + shift 303 + ;; 304 + --stats) 305 + SHOW_STATS=true 306 + shift 307 + ;; 308 + --parallel) 309 + PARALLEL=true 310 + shift 311 + ;; 312 + --) 313 + shift 314 + TEST_ARGS+=("$@") 315 + break 316 + ;; 317 + *) 318 + TEST_ARGS+=("$1") 319 + shift 320 + ;; 321 + esac 322 + done 323 + 324 + # Enable verbose output if requested 325 + if [[ "$VERBOSE" == "true" ]]; then 326 + set -x 327 + fi 328 + 329 + # Main execution 330 + main() { 331 + print_header "SHOWCASE TEST RUNNER" 332 + 333 + # Pre-flight checks 334 + check_prerequisites 335 + 336 + # Show configuration 337 + print_info "Test configuration:" 338 + echo " SQLite tests: $(if [[ "$RUN_SQLITE" == "true" ]]; then echo "enabled"; else echo "disabled"; fi)" 339 + echo " PostgreSQL tests: $(if [[ "$RUN_POSTGRES" == "true" ]]; then echo "enabled"; else echo "disabled"; fi)" 340 + echo " Clean start: $(if [[ "$CLEAN_START" == "true" ]]; then echo "yes"; else echo "no"; fi)" 341 + echo " Fail fast: $(if [[ "$FAIL_FAST" == "true" ]]; then echo "yes"; else echo "no"; fi)" 342 + echo " Parallel mode: $(if [[ "$PARALLEL" == "true" ]]; then echo "yes"; else echo "no"; fi)" 343 + 344 + if [[ ${#TEST_ARGS[@]} -gt 0 ]]; then 345 + echo " Test arguments: ${TEST_ARGS[*]}" 346 + fi 347 + 348 + echo "" 349 + 350 + # Run tests 351 + if [[ "$PARALLEL" == "true" ]]; then 352 + run_parallel_tests 353 + else 354 + # Sequential execution 355 + if [[ "$RUN_SQLITE" == "true" ]]; then 356 + run_sqlite_tests 357 + fi 358 + 359 + if [[ "$RUN_POSTGRES" == "true" ]]; then 360 + run_postgres_tests 361 + fi 362 + fi 363 + 364 + # Show summary and exit with appropriate code 365 + if show_summary; then 366 + exit 0 367 + else 368 + exit 1 369 + fi 370 + } 371 + 372 + # Change to project root directory 373 + cd "$PROJECT_ROOT" 374 + 375 + # Run main function 376 + main "$@"
+288
scripts/test-postgres.sh
··· 1 + #!/bin/bash 2 + # PostgreSQL Integration Test Script 3 + # This script sets up PostgreSQL in Docker and runs the test suite 4 + 5 + set -e 6 + 7 + # Colors for output 8 + RED='\033[0;31m' 9 + GREEN='\033[0;32m' 10 + YELLOW='\033[1;33m' 11 + BLUE='\033[0;34m' 12 + NC='\033[0m' # No Color 13 + 14 + # Configuration 15 + POSTGRES_CONTAINER="showcase_postgres" 16 + TEST_DATABASE="showcase_test" 17 + POSTGRES_PORT="5433" 18 + MAX_WAIT_TIME=60 19 + 20 + # Function to print colored output 21 + print_info() { 22 + echo -e "${BLUE}[INFO]${NC} $1" 23 + } 24 + 25 + print_success() { 26 + echo -e "${GREEN}[SUCCESS]${NC} $1" 27 + } 28 + 29 + print_warning() { 30 + echo -e "${YELLOW}[WARNING]${NC} $1" 31 + } 32 + 33 + print_error() { 34 + echo -e "${RED}[ERROR]${NC} $1" 35 + } 36 + 37 + # Function to check if Docker is running 38 + check_docker() { 39 + if ! docker info > /dev/null 2>&1; then 40 + print_error "Docker is not running. Please start Docker and try again." 41 + exit 1 42 + fi 43 + print_success "Docker is running" 44 + } 45 + 46 + # Function to check if .env.test exists 47 + check_env_file() { 48 + if [[ ! -f ".env.test" ]]; then 49 + print_warning ".env.test not found. Creating default test environment file..." 50 + cat > .env.test << EOF 51 + DATABASE_URL=postgresql://showcase:showcase_dev_password@localhost:5433/showcase_test 52 + EXTERNAL_BASE=http://localhost:8080 53 + BADGE_ISSUERS=did:plc:test1;did:plc:test2 54 + HTTP_PORT=8080 55 + BADGE_IMAGE_STORAGE=./test_badges 56 + PLC_HOSTNAME=plc.directory 57 + HTTP_CLIENT_TIMEOUT=10s 58 + RUST_LOG=showcase=debug,info 59 + EOF 60 + print_success "Created .env.test file" 61 + fi 62 + } 63 + 64 + # Function to clean up existing container 65 + cleanup_container() { 66 + if docker ps -a --format 'table {{.Names}}' | grep -q "^${POSTGRES_CONTAINER}$"; then 67 + print_info "Stopping and removing existing PostgreSQL container..." 68 + docker stop $POSTGRES_CONTAINER > /dev/null 2>&1 || true 69 + docker rm $POSTGRES_CONTAINER > /dev/null 2>&1 || true 70 + print_success "Cleaned up existing container" 71 + fi 72 + } 73 + 74 + # Function to start PostgreSQL container 75 + start_postgres() { 76 + print_info "Starting PostgreSQL container..." 77 + 78 + if command -v docker-compose &> /dev/null && [[ -f "docker-compose.yml" ]]; then 79 + # Use docker-compose if available 80 + print_info "Using docker-compose to start PostgreSQL..." 81 + docker-compose up -d postgres 82 + else 83 + # Use docker run as fallback 84 + print_info "Using docker run to start PostgreSQL..." 85 + docker run -d \ 86 + --name $POSTGRES_CONTAINER \ 87 + -e POSTGRES_DB=$TEST_DATABASE \ 88 + -e POSTGRES_USER=showcase \ 89 + -e POSTGRES_PASSWORD=showcase_dev_password \ 90 + -e POSTGRES_INITDB_ARGS="--encoding=UTF8 --locale=C" \ 91 + -p $POSTGRES_PORT:5432 \ 92 + -v showcase_postgres_data:/var/lib/postgresql/data \ 93 + postgres:17-alpine > /dev/null 94 + fi 95 + 96 + print_success "PostgreSQL container started" 97 + } 98 + 99 + # Function to wait for PostgreSQL to be ready 100 + wait_for_postgres() { 101 + print_info "Waiting for PostgreSQL to be ready..." 102 + 103 + local count=0 104 + while [ $count -lt $MAX_WAIT_TIME ]; do 105 + if docker exec $POSTGRES_CONTAINER pg_isready -U showcase -d $TEST_DATABASE > /dev/null 2>&1; then 106 + print_success "PostgreSQL is ready!" 107 + return 0 108 + fi 109 + 110 + printf "." 111 + sleep 2 112 + count=$((count + 2)) 113 + done 114 + 115 + print_error "PostgreSQL failed to start within $MAX_WAIT_TIME seconds" 116 + print_info "Container logs:" 117 + docker logs $POSTGRES_CONTAINER 118 + exit 1 119 + } 120 + 121 + # Function to load environment variables 122 + load_environment() { 123 + print_info "Loading test environment variables..." 124 + 125 + if [[ -f ".env.test" ]]; then 126 + export $(cat .env.test | grep -v '^#' | xargs) 127 + print_success "Environment variables loaded" 128 + else 129 + print_error ".env.test file not found" 130 + exit 1 131 + fi 132 + } 133 + 134 + # Function to verify database connection 135 + verify_connection() { 136 + print_info "Verifying database connection..." 137 + 138 + if docker exec $POSTGRES_CONTAINER psql -U showcase -d $TEST_DATABASE -c "SELECT version();" > /dev/null 2>&1; then 139 + local pg_version=$(docker exec $POSTGRES_CONTAINER psql -U showcase -d $TEST_DATABASE -t -c "SELECT version();" | xargs) 140 + print_success "Database connection verified: $pg_version" 141 + else 142 + print_error "Failed to connect to database" 143 + exit 1 144 + fi 145 + } 146 + 147 + # Function to run tests 148 + run_tests() { 149 + print_info "Running tests with PostgreSQL backend..." 150 + 151 + # Create test directories if they don't exist 152 + mkdir -p ./test_badges 153 + 154 + # Run the tests 155 + if cargo test "$@"; then 156 + print_success "All tests passed!" 157 + else 158 + print_error "Some tests failed" 159 + return 1 160 + fi 161 + } 162 + 163 + # Function to cleanup 164 + cleanup() { 165 + print_info "Cleaning up..." 166 + 167 + if command -v docker-compose &> /dev/null && [[ -f "docker-compose.yml" ]]; then 168 + docker-compose down > /dev/null 2>&1 || true 169 + else 170 + docker stop $POSTGRES_CONTAINER > /dev/null 2>&1 || true 171 + docker rm $POSTGRES_CONTAINER > /dev/null 2>&1 || true 172 + fi 173 + 174 + # Clean up test artifacts 175 + rm -rf ./test_badges 176 + 177 + print_success "Cleanup completed" 178 + } 179 + 180 + # Function to show usage 181 + usage() { 182 + echo "Usage: $0 [OPTIONS] [TEST_ARGS]" 183 + echo "" 184 + echo "Options:" 185 + echo " -h, --help Show this help message" 186 + echo " -k, --keep Keep PostgreSQL container running after tests" 187 + echo " -c, --clean Clean up containers and volumes before starting" 188 + echo " -v, --verbose Enable verbose output" 189 + echo "" 190 + echo "Examples:" 191 + echo " $0 # Run all tests" 192 + echo " $0 storage::tests # Run only storage tests" 193 + echo " $0 --keep # Run tests and keep container running" 194 + echo " $0 --clean --verbose # Clean start with verbose output" 195 + echo " $0 -- --nocapture # Pass --nocapture to cargo test" 196 + } 197 + 198 + # Parse command line arguments 199 + KEEP_CONTAINER=false 200 + CLEAN_START=false 201 + VERBOSE=false 202 + TEST_ARGS=() 203 + 204 + while [[ $# -gt 0 ]]; do 205 + case $1 in 206 + -h|--help) 207 + usage 208 + exit 0 209 + ;; 210 + -k|--keep) 211 + KEEP_CONTAINER=true 212 + shift 213 + ;; 214 + -c|--clean) 215 + CLEAN_START=true 216 + shift 217 + ;; 218 + -v|--verbose) 219 + VERBOSE=true 220 + shift 221 + ;; 222 + --) 223 + shift 224 + TEST_ARGS+=("$@") 225 + break 226 + ;; 227 + *) 228 + TEST_ARGS+=("$1") 229 + shift 230 + ;; 231 + esac 232 + done 233 + 234 + # Enable verbose output if requested 235 + if [[ "$VERBOSE" == "true" ]]; then 236 + set -x 237 + fi 238 + 239 + # Main execution 240 + main() { 241 + print_info "Starting PostgreSQL integration tests..." 242 + 243 + # Pre-flight checks 244 + check_docker 245 + check_env_file 246 + 247 + # Clean up if requested 248 + if [[ "$CLEAN_START" == "true" ]]; then 249 + print_info "Performing clean start..." 250 + cleanup_container 251 + if command -v docker-compose &> /dev/null && [[ -f "docker-compose.yml" ]]; then 252 + docker-compose down -v > /dev/null 2>&1 || true 253 + fi 254 + fi 255 + 256 + # Setup 257 + cleanup_container 258 + start_postgres 259 + wait_for_postgres 260 + load_environment 261 + verify_connection 262 + 263 + # Run tests 264 + local test_result=0 265 + run_tests "${TEST_ARGS[@]}" || test_result=$? 266 + 267 + # Cleanup unless keeping container 268 + if [[ "$KEEP_CONTAINER" == "false" ]]; then 269 + cleanup 270 + else 271 + print_info "PostgreSQL container is still running for debugging" 272 + print_info "Connect with: docker exec -it $POSTGRES_CONTAINER psql -U showcase -d $TEST_DATABASE" 273 + print_info "Stop with: docker stop $POSTGRES_CONTAINER" 274 + fi 275 + 276 + if [[ $test_result -eq 0 ]]; then 277 + print_success "PostgreSQL integration tests completed successfully!" 278 + else 279 + print_error "PostgreSQL integration tests failed!" 280 + exit $test_result 281 + fi 282 + } 283 + 284 + # Trap to ensure cleanup on script exit 285 + trap 'if [[ "$KEEP_CONTAINER" == "false" ]]; then cleanup; fi' EXIT INT TERM 286 + 287 + # Run main function 288 + main "$@"
+309
scripts/test-sqlite.sh
··· 1 + #!/bin/bash 2 + # SQLite Test Script 3 + # This script sets up SQLite environment and runs the test suite 4 + 5 + set -e 6 + 7 + # Colors for output 8 + RED='\033[0;31m' 9 + GREEN='\033[0;32m' 10 + YELLOW='\033[1;33m' 11 + BLUE='\033[0;34m' 12 + NC='\033[0m' # No Color 13 + 14 + # Configuration 15 + SQLITE_DB_DEV="showcase_dev.db" 16 + SQLITE_DB_TEST="showcase_test.db" 17 + BACKUP_DIR="./db_backups" 18 + 19 + # Function to print colored output 20 + print_info() { 21 + echo -e "${BLUE}[INFO]${NC} $1" 22 + } 23 + 24 + print_success() { 25 + echo -e "${GREEN}[SUCCESS]${NC} $1" 26 + } 27 + 28 + print_warning() { 29 + echo -e "${YELLOW}[WARNING]${NC} $1" 30 + } 31 + 32 + print_error() { 33 + echo -e "${RED}[ERROR]${NC} $1" 34 + } 35 + 36 + # Function to check if .env.dev exists 37 + check_env_file() { 38 + if [[ ! -f ".env.dev" ]]; then 39 + print_warning ".env.dev not found. Creating default development environment file..." 40 + cat > .env.dev << EOF 41 + DATABASE_URL=sqlite://showcase_dev.db 42 + EXTERNAL_BASE=http://localhost:8080 43 + BADGE_ISSUERS=did:plc:test1;did:plc:test2 44 + HTTP_PORT=8080 45 + BADGE_IMAGE_STORAGE=./badges 46 + PLC_HOSTNAME=plc.directory 47 + HTTP_CLIENT_TIMEOUT=10s 48 + RUST_LOG=showcase=info,debug 49 + EOF 50 + print_success "Created .env.dev file" 51 + fi 52 + } 53 + 54 + # Function to backup existing databases 55 + backup_databases() { 56 + local backup_needed=false 57 + 58 + if [[ -f "$SQLITE_DB_DEV" ]] || [[ -f "$SQLITE_DB_TEST" ]]; then 59 + backup_needed=true 60 + fi 61 + 62 + if [[ "$backup_needed" == "true" ]]; then 63 + print_info "Backing up existing databases..." 64 + mkdir -p "$BACKUP_DIR" 65 + 66 + local timestamp=$(date +"%Y%m%d_%H%M%S") 67 + 68 + if [[ -f "$SQLITE_DB_DEV" ]]; then 69 + cp "$SQLITE_DB_DEV" "$BACKUP_DIR/showcase_dev_${timestamp}.db" 70 + print_success "Backed up $SQLITE_DB_DEV" 71 + fi 72 + 73 + if [[ -f "$SQLITE_DB_TEST" ]]; then 74 + cp "$SQLITE_DB_TEST" "$BACKUP_DIR/showcase_test_${timestamp}.db" 75 + print_success "Backed up $SQLITE_DB_TEST" 76 + fi 77 + fi 78 + } 79 + 80 + # Function to clean up test databases 81 + cleanup_databases() { 82 + local force_clean=$1 83 + 84 + if [[ "$force_clean" == "true" ]] || [[ "$CLEAN_START" == "true" ]]; then 85 + print_info "Cleaning up test databases..." 86 + 87 + # Remove test databases 88 + rm -f "$SQLITE_DB_DEV" "$SQLITE_DB_TEST" 89 + 90 + # Remove test directories 91 + rm -rf ./badges ./test_badges 92 + 93 + print_success "Database cleanup completed" 94 + fi 95 + } 96 + 97 + # Function to load environment variables 98 + load_environment() { 99 + print_info "Loading SQLite environment variables..." 100 + 101 + if [[ -f ".env.dev" ]]; then 102 + export $(cat .env.dev | grep -v '^#' | xargs) 103 + print_success "Environment variables loaded" 104 + 105 + # Create badge storage directory 106 + mkdir -p "$(dirname "${BADGE_IMAGE_STORAGE:-./badges}")" 107 + else 108 + print_error ".env.dev file not found" 109 + exit 1 110 + fi 111 + } 112 + 113 + # Function to verify SQLite setup 114 + verify_sqlite() { 115 + print_info "Verifying SQLite setup..." 116 + 117 + # Check if sqlite3 is available (optional) 118 + if command -v sqlite3 &> /dev/null; then 119 + local sqlite_version=$(sqlite3 --version | cut -d' ' -f1) 120 + print_success "SQLite3 CLI available: version $sqlite_version" 121 + else 122 + print_info "SQLite3 CLI not available (not required for tests)" 123 + fi 124 + 125 + # Verify we can create a test database 126 + if cargo check > /dev/null 2>&1; then 127 + print_success "Cargo build verification passed" 128 + else 129 + print_error "Cargo build verification failed" 130 + return 1 131 + fi 132 + } 133 + 134 + # Function to run tests 135 + run_tests() { 136 + print_info "Running tests with SQLite backend..." 137 + 138 + # Ensure badge storage directory exists 139 + mkdir -p "${BADGE_IMAGE_STORAGE:-./badges}" 140 + 141 + # Run the tests 142 + if cargo test "$@"; then 143 + print_success "All tests passed!" 144 + return 0 145 + else 146 + print_error "Some tests failed" 147 + return 1 148 + fi 149 + } 150 + 151 + # Function to show database statistics 152 + show_db_stats() { 153 + if [[ -f "$SQLITE_DB_DEV" ]] && command -v sqlite3 &> /dev/null; then 154 + print_info "Database statistics:" 155 + 156 + local db_size=$(du -h "$SQLITE_DB_DEV" | cut -f1) 157 + print_info "Database file size: $db_size" 158 + 159 + # Show table information if database exists and has tables 160 + local table_count=$(sqlite3 "$SQLITE_DB_DEV" "SELECT COUNT(*) FROM sqlite_master WHERE type='table';" 2>/dev/null || echo "0") 161 + if [[ "$table_count" -gt 0 ]]; then 162 + print_info "Number of tables: $table_count" 163 + 164 + # Show table names and row counts 165 + sqlite3 "$SQLITE_DB_DEV" ".tables" 2>/dev/null | while read table; do 166 + if [[ -n "$table" ]]; then 167 + local row_count=$(sqlite3 "$SQLITE_DB_DEV" "SELECT COUNT(*) FROM $table;" 2>/dev/null || echo "N/A") 168 + print_info " Table '$table': $row_count rows" 169 + fi 170 + done 171 + else 172 + print_info "No tables found in database" 173 + fi 174 + fi 175 + } 176 + 177 + # Function to optimize SQLite database 178 + optimize_database() { 179 + if [[ -f "$SQLITE_DB_DEV" ]] && command -v sqlite3 &> /dev/null; then 180 + print_info "Optimizing SQLite database..." 181 + 182 + # Run VACUUM to optimize database 183 + sqlite3 "$SQLITE_DB_DEV" "VACUUM;" 2>/dev/null || print_warning "Could not vacuum database" 184 + 185 + # Analyze database for query optimizer 186 + sqlite3 "$SQLITE_DB_DEV" "ANALYZE;" 2>/dev/null || print_warning "Could not analyze database" 187 + 188 + print_success "Database optimization completed" 189 + fi 190 + } 191 + 192 + # Function to show usage 193 + usage() { 194 + echo "Usage: $0 [OPTIONS] [TEST_ARGS]" 195 + echo "" 196 + echo "Options:" 197 + echo " -h, --help Show this help message" 198 + echo " -c, --clean Clean up databases before starting" 199 + echo " -b, --backup Backup existing databases before cleaning" 200 + echo " -s, --stats Show database statistics after tests" 201 + echo " -o, --optimize Optimize database after tests" 202 + echo " -v, --verbose Enable verbose output" 203 + echo "" 204 + echo "Examples:" 205 + echo " $0 # Run all tests" 206 + echo " $0 storage::tests # Run only storage tests" 207 + echo " $0 --clean --stats # Clean start and show stats" 208 + echo " $0 --backup --clean # Backup before clean start" 209 + echo " $0 -- --nocapture # Pass --nocapture to cargo test" 210 + } 211 + 212 + # Parse command line arguments 213 + CLEAN_START=false 214 + BACKUP_FIRST=false 215 + SHOW_STATS=false 216 + OPTIMIZE_DB=false 217 + VERBOSE=false 218 + TEST_ARGS=() 219 + 220 + while [[ $# -gt 0 ]]; do 221 + case $1 in 222 + -h|--help) 223 + usage 224 + exit 0 225 + ;; 226 + -c|--clean) 227 + CLEAN_START=true 228 + shift 229 + ;; 230 + -b|--backup) 231 + BACKUP_FIRST=true 232 + shift 233 + ;; 234 + -s|--stats) 235 + SHOW_STATS=true 236 + shift 237 + ;; 238 + -o|--optimize) 239 + OPTIMIZE_DB=true 240 + shift 241 + ;; 242 + -v|--verbose) 243 + VERBOSE=true 244 + shift 245 + ;; 246 + --) 247 + shift 248 + TEST_ARGS+=("$@") 249 + break 250 + ;; 251 + *) 252 + TEST_ARGS+=("$1") 253 + shift 254 + ;; 255 + esac 256 + done 257 + 258 + # Enable verbose output if requested 259 + if [[ "$VERBOSE" == "true" ]]; then 260 + set -x 261 + fi 262 + 263 + # Main execution 264 + main() { 265 + print_info "Starting SQLite tests..." 266 + 267 + # Pre-flight checks 268 + check_env_file 269 + 270 + # Backup if requested 271 + if [[ "$BACKUP_FIRST" == "true" ]]; then 272 + backup_databases 273 + fi 274 + 275 + # Clean up if requested 276 + if [[ "$CLEAN_START" == "true" ]]; then 277 + cleanup_databases true 278 + fi 279 + 280 + # Setup 281 + load_environment 282 + verify_sqlite 283 + 284 + # Run tests 285 + local test_result=0 286 + run_tests "${TEST_ARGS[@]}" || test_result=$? 287 + 288 + # Post-test operations 289 + if [[ "$SHOW_STATS" == "true" ]]; then 290 + show_db_stats 291 + fi 292 + 293 + if [[ "$OPTIMIZE_DB" == "true" ]]; then 294 + optimize_database 295 + fi 296 + 297 + # Final cleanup of test artifacts 298 + rm -f "$SQLITE_DB_TEST" 299 + 300 + if [[ $test_result -eq 0 ]]; then 301 + print_success "SQLite tests completed successfully!" 302 + else 303 + print_error "SQLite tests failed!" 304 + exit $test_result 305 + fi 306 + } 307 + 308 + # Run main function 309 + main "$@"
+431
src/bin/showcase.rs
··· 1 + use atproto_identity::resolve::{IdentityResolver, InnerIdentityResolver, create_resolver}; 2 + use atproto_jetstream::{CancellationToken, Consumer as JetstreamConsumer, ConsumerTaskConfig}; 3 + use showcase::errors::Result; 4 + use showcase::http::AppEngine; 5 + #[cfg(feature = "s3")] 6 + use showcase::storage::S3FileStorage; 7 + #[cfg(feature = "s3")] 8 + use showcase::storage::file_storage::parse_s3_url; 9 + #[cfg(feature = "postgres")] 10 + use showcase::storage::{PostgresStorage, PostgresStorageDidDocumentStorage}; 11 + #[cfg(feature = "sqlite")] 12 + use showcase::storage::{SqliteStorage, SqliteStorageDidDocumentStorage}; 13 + use showcase::{ 14 + config::Config, 15 + consumer::Consumer, 16 + http::{AppState, create_router}, 17 + process::BadgeProcessor, 18 + storage::{FileStorage, LocalFileStorage, Storage}, 19 + }; 20 + #[cfg(feature = "sqlite")] 21 + use sqlx::SqlitePool; 22 + #[cfg(feature = "postgres")] 23 + use sqlx::postgres::PgPool; 24 + use std::{env, sync::Arc}; 25 + use tokio::net::TcpListener; 26 + use tokio::signal; 27 + use tokio_util::task::TaskTracker; 28 + use tracing::{error, info}; 29 + use tracing_subscriber::prelude::*; 30 + 31 + #[cfg(feature = "embed")] 32 + use showcase::templates::build_env; 33 + 34 + #[cfg(feature = "reload")] 35 + use showcase::templates::build_env; 36 + 37 + 38 + /// Create the appropriate FileStorage implementation based on the storage configuration 39 + fn create_file_storage(storage_config: &str) -> Result<Arc<dyn FileStorage>> { 40 + if storage_config.starts_with("s3://") { 41 + #[cfg(feature = "s3")] 42 + { 43 + tracing::warn!("object storage used"); 44 + 45 + let (endpoint, access_key, secret_key, bucket, prefix) = parse_s3_url(storage_config)?; 46 + let s3_storage = S3FileStorage::new(endpoint, access_key, secret_key, bucket, prefix)?; 47 + Ok(Arc::new(s3_storage)) 48 + } 49 + #[cfg(not(feature = "s3"))] 50 + { 51 + Err(showcase::errors::ShowcaseError::ConfigFeatureNotEnabled { 52 + feature: "S3 storage requested but s3 feature is not enabled".to_string(), 53 + }) 54 + } 55 + } else { 56 + tracing::warn!("file storage used"); 57 + // Use local file storage for non-S3 configurations 58 + Ok(Arc::new(LocalFileStorage::new(storage_config.to_string()))) 59 + } 60 + } 61 + 62 + #[tokio::main] 63 + async fn main() -> Result<()> { 64 + // Initialize logging 65 + tracing_subscriber::registry() 66 + .with(tracing_subscriber::EnvFilter::new( 67 + std::env::var("RUST_LOG").unwrap_or_else(|_| "showcase=info,info".into()), 68 + )) 69 + .with(tracing_subscriber::fmt::layer().pretty()) 70 + .init(); 71 + 72 + // Handle version flag 73 + env::args().for_each(|arg| { 74 + if arg == "--version" { 75 + println!("showcase {}", env!("CARGO_PKG_VERSION")); 76 + std::process::exit(0); 77 + } 78 + }); 79 + 80 + // Load configuration 81 + let config = Arc::new(Config::from_env()?); 82 + info!("Starting Showcase with config"); 83 + 84 + // Setup HTTP client 85 + let mut client_builder = reqwest::Client::builder(); 86 + for ca_certificate in &config.certificate_bundles { 87 + info!("Loading CA certificate: {:?}", ca_certificate); 88 + let cert = std::fs::read(ca_certificate)?; 89 + let cert = reqwest::Certificate::from_pem(&cert)?; 90 + client_builder = client_builder.add_root_certificate(cert); 91 + } 92 + 93 + let http_client = client_builder 94 + .user_agent(config.user_agent.clone()) 95 + .timeout(config.http_client_timeout) 96 + .build()?; 97 + 98 + // Setup database based on the database URL and available features 99 + let (storage, document_storage): ( 100 + Arc<dyn Storage>, 101 + Arc<dyn atproto_identity::storage::DidDocumentStorage + Send + Sync>, 102 + ) = { 103 + #[cfg(all(feature = "sqlite", not(feature = "postgres")))] 104 + { 105 + let pool = SqlitePool::connect(&config.database_url).await?; 106 + let storage = Arc::new(SqliteStorage::new(pool)); 107 + let document_storage = Arc::new(SqliteStorageDidDocumentStorage::new(storage.clone())); 108 + (storage, document_storage) 109 + } 110 + 111 + #[cfg(all(feature = "postgres", not(feature = "sqlite")))] 112 + { 113 + let pool = PgPool::connect(&config.database_url).await?; 114 + let storage = Arc::new(PostgresStorage::new(pool)); 115 + let document_storage = 116 + Arc::new(PostgresStorageDidDocumentStorage::new(storage.clone())); 117 + (storage, document_storage) 118 + } 119 + 120 + #[cfg(all(feature = "sqlite", feature = "postgres"))] 121 + { 122 + // When both features are enabled, determine based on the database URL 123 + if config.database_url.starts_with("postgres://") 124 + || config.database_url.starts_with("postgresql://") 125 + { 126 + let pool = PgPool::connect(&config.database_url).await?; 127 + let storage = Arc::new(PostgresStorage::new(pool)); 128 + let document_storage = 129 + Arc::new(PostgresStorageDidDocumentStorage::new(storage.clone())); 130 + (storage, document_storage) 131 + } else { 132 + let pool = SqlitePool::connect(&config.database_url).await?; 133 + let storage = Arc::new(SqliteStorage::new(pool)); 134 + let document_storage = 135 + Arc::new(SqliteStorageDidDocumentStorage::new(storage.clone())); 136 + (storage, document_storage) 137 + } 138 + } 139 + }; 140 + 141 + // Run migrations 142 + storage.migrate().await?; 143 + info!("Database migrations completed"); 144 + 145 + // Initialize DNS resolver 146 + let nameserver_ips: Vec<std::net::IpAddr> = config 147 + .dns_nameservers 148 + .iter() 149 + .filter_map(|s| s.parse().ok()) 150 + .collect(); 151 + let dns_resolver = create_resolver(&nameserver_ips); 152 + 153 + // Initialize identity resolver 154 + let identity_resolver = IdentityResolver(Arc::new(InnerIdentityResolver { 155 + dns_resolver, 156 + http_client: http_client.clone(), 157 + plc_hostname: config.plc_hostname.clone(), 158 + })); 159 + 160 + // Setup template engine 161 + let template_env = { 162 + #[cfg(feature = "embed")] 163 + { 164 + AppEngine::from(build_env( 165 + config.external_base.clone(), 166 + env!("CARGO_PKG_VERSION").to_string(), 167 + )) 168 + } 169 + 170 + #[cfg(feature = "reload")] 171 + { 172 + AppEngine::from(build_env()) 173 + } 174 + 175 + #[cfg(not(any(feature = "reload", feature = "embed")))] 176 + { 177 + use minijinja::Environment; 178 + let mut env = Environment::new(); 179 + // Add a simple template for the minimal case 180 + env.add_template( 181 + "index.html", 182 + "<!DOCTYPE html><html><body>Showcase</body></html>", 183 + ) 184 + .unwrap(); 185 + env.add_template( 186 + "identity.html", 187 + "<!DOCTYPE html><html><body>Identity</body></html>", 188 + ) 189 + .unwrap(); 190 + AppEngine::from(env) 191 + } 192 + }; 193 + 194 + // Create file storage for badge images 195 + let file_storage = create_file_storage(&config.badge_image_storage)?; 196 + 197 + // Create application state for HTTP server 198 + let app_state = AppState { 199 + storage: storage.clone(), 200 + config: config.clone(), 201 + document_storage: document_storage.clone(), 202 + identity_resolver: identity_resolver.clone(), 203 + template_env, 204 + file_storage: file_storage.clone(), 205 + }; 206 + 207 + // Create HTTP router 208 + let app = create_router(app_state); 209 + 210 + // Setup task tracking and cancellation 211 + let tracker = TaskTracker::new(); 212 + let token = CancellationToken::new(); 213 + 214 + // Setup signal handling 215 + { 216 + let tracker = tracker.clone(); 217 + let inner_token = token.clone(); 218 + 219 + let ctrl_c = async { 220 + signal::ctrl_c() 221 + .await 222 + .expect("failed to install Ctrl+C handler"); 223 + }; 224 + 225 + #[cfg(unix)] 226 + let terminate = async { 227 + signal::unix::signal(signal::unix::SignalKind::terminate()) 228 + .expect("failed to install signal handler") 229 + .recv() 230 + .await; 231 + }; 232 + 233 + #[cfg(not(unix))] 234 + let terminate = std::future::pending::<()>(); 235 + 236 + tokio::spawn(async move { 237 + tokio::select! { 238 + () = inner_token.cancelled() => { }, 239 + _ = terminate => { 240 + info!("Received SIGTERM, shutting down"); 241 + }, 242 + _ = ctrl_c => { 243 + info!("Received Ctrl+C, shutting down"); 244 + }, 245 + } 246 + 247 + tracker.close(); 248 + inner_token.cancel(); 249 + }); 250 + } 251 + 252 + // Start HTTP server 253 + { 254 + let inner_config = config.clone(); 255 + let http_port = inner_config.http.port; 256 + let inner_token = token.clone(); 257 + tracker.spawn(async move { 258 + let bind_address = format!("0.0.0.0:{}", http_port); 259 + info!("Starting HTTP server on {}", bind_address); 260 + let listener = TcpListener::bind(&bind_address).await.unwrap(); 261 + 262 + let shutdown_token = inner_token.clone(); 263 + let result = axum::serve(listener, app) 264 + .with_graceful_shutdown(async move { 265 + tokio::select! { 266 + () = shutdown_token.cancelled() => { } 267 + } 268 + info!("HTTP server graceful shutdown complete"); 269 + }) 270 + .await; 271 + 272 + if let Err(err) = result { 273 + error!("error-showcase-runtime-1 HTTP server task failed: {}", err); 274 + } 275 + 276 + inner_token.cancel(); 277 + }); 278 + } 279 + 280 + // Start badge processor 281 + { 282 + let consumer = Consumer {}; 283 + let (badge_handler, event_receiver) = consumer.create_badge_handler(); 284 + 285 + let badge_processor = BadgeProcessor::new( 286 + storage.clone(), 287 + config.clone(), 288 + identity_resolver.clone(), 289 + document_storage.clone(), 290 + http_client.clone(), 291 + file_storage.clone(), 292 + ); 293 + 294 + let inner_token = token.clone(); 295 + tracker.spawn(async move { 296 + tokio::select! { 297 + result = badge_processor.start_processing(event_receiver) => { 298 + if let Err(err) = result { 299 + error!("error-showcase-runtime-2 Badge processor failed: {}", err); 300 + } 301 + } 302 + () = inner_token.cancelled() => { 303 + info!("Badge processor cancelled"); 304 + } 305 + } 306 + }); 307 + 308 + // Read cursor from file if configured 309 + let cursor = if let Some(cursor_path) = &config.jetstream_cursor_path { 310 + match tokio::fs::read_to_string(cursor_path).await { 311 + Ok(contents) => match contents.trim().parse::<u64>() { 312 + Ok(cursor_value) if cursor_value > 1 => { 313 + info!("Loaded cursor from {}: {}", cursor_path, cursor_value); 314 + Some(cursor_value as i64) 315 + } 316 + _ => { 317 + info!( 318 + "Invalid or low cursor value in {}, starting fresh", 319 + cursor_path 320 + ); 321 + None 322 + } 323 + }, 324 + Err(err) => { 325 + info!( 326 + "Could not read cursor file {}: {}, starting fresh", 327 + cursor_path, err 328 + ); 329 + None 330 + } 331 + } 332 + } else { 333 + None 334 + }; 335 + 336 + // Start Jetstream consumer with reconnect logic 337 + let inner_token = token.clone(); 338 + let inner_config = config.clone(); 339 + tracker.spawn(async move { 340 + let mut disconnect_times = Vec::new(); 341 + let disconnect_window = std::time::Duration::from_secs(60); // 1 minute window 342 + let max_disconnects_per_minute = 1; 343 + let reconnect_delay = std::time::Duration::from_secs(5); 344 + 345 + loop { 346 + // Create new consumer for each connection attempt 347 + let jetstream_config = ConsumerTaskConfig { 348 + user_agent: inner_config.user_agent.clone(), 349 + compression: false, 350 + zstd_dictionary_location: String::new(), 351 + jetstream_hostname: "jetstream2.us-east.bsky.network".to_string(), 352 + collections: vec!["community.lexicon.badge.award".to_string()], 353 + dids: vec![], 354 + max_message_size_bytes: Some(10 * 1024 * 1024), // 10MB 355 + cursor, 356 + require_hello: true, 357 + }; 358 + 359 + let jetstream_consumer = JetstreamConsumer::new(jetstream_config); 360 + 361 + // Register badge handler 362 + if let Err(err) = jetstream_consumer.register_handler(badge_handler.clone()).await { 363 + error!("Failed to register badge handler: {}", err); 364 + inner_token.cancel(); 365 + break; 366 + } 367 + 368 + // Register cursor writer if configured 369 + if let Some(cursor_path) = inner_config.jetstream_cursor_path.clone() { 370 + let cursor_writer = consumer.create_cursor_writer_handler(cursor_path); 371 + if let Err(err) = jetstream_consumer.register_handler(cursor_writer).await { 372 + error!("Failed to register cursor writer: {}", err); 373 + inner_token.cancel(); 374 + break; 375 + } 376 + } 377 + 378 + tokio::select! { 379 + result = jetstream_consumer.run_background(inner_token.clone()) => { 380 + if let Err(err) = result { 381 + let now = std::time::Instant::now(); 382 + disconnect_times.push(now); 383 + 384 + // Remove disconnect times older than the window 385 + disconnect_times.retain(|&t| now.duration_since(t) <= disconnect_window); 386 + 387 + if disconnect_times.len() > max_disconnects_per_minute { 388 + error!( 389 + "error-showcase-consumer-3 Jetstream disconnect rate exceeded: {} disconnects in 1 minute, exiting", 390 + disconnect_times.len() 391 + ); 392 + inner_token.cancel(); 393 + break; 394 + } 395 + 396 + error!("error-showcase-consumer-2 Jetstream disconnected: {}, reconnecting in {:?}", err, reconnect_delay); 397 + 398 + // Wait before reconnecting 399 + tokio::select! { 400 + () = tokio::time::sleep(reconnect_delay) => {}, 401 + () = inner_token.cancelled() => { 402 + info!("Jetstream consumer cancelled during reconnect delay"); 403 + break; 404 + } 405 + } 406 + 407 + // Continue the loop to reconnect 408 + continue; 409 + } 410 + } 411 + () = inner_token.cancelled() => { 412 + info!("Jetstream consumer cancelled"); 413 + break; 414 + } 415 + } 416 + 417 + // If we reach here, the consumer exited without error (unlikely) 418 + info!("Jetstream consumer exited normally"); 419 + break; 420 + } 421 + }); 422 + } 423 + 424 + info!("All services started successfully"); 425 + 426 + // Wait for all tasks to complete 427 + tracker.wait().await; 428 + 429 + info!("Showcase shutting down"); 430 + Ok(()) 431 + }
+181
src/config.rs
··· 1 + //! Configuration management for environment variables and application settings. 2 + //! 3 + //! Loads configuration from environment variables with sensible defaults 4 + //! for HTTP server, AT Protocol, and storage settings. 5 + 6 + use serde::{Deserialize, Serialize}; 7 + use std::time::Duration; 8 + 9 + use crate::errors::{Result, ShowcaseError}; 10 + 11 + /// Application configuration loaded from environment variables. 12 + #[derive(Debug, Clone, Serialize, Deserialize)] 13 + pub struct Config { 14 + /// HTTP server configuration. 15 + pub http: HttpConfig, 16 + /// External base URL for the application. 17 + pub external_base: String, 18 + /// Certificate bundles for TLS verification. 19 + pub certificate_bundles: Vec<String>, 20 + /// User agent string for HTTP requests. 21 + pub user_agent: String, 22 + /// PLC server hostname for identity resolution. 23 + pub plc_hostname: String, 24 + /// DNS nameservers for domain resolution. 25 + pub dns_nameservers: Vec<String>, 26 + /// HTTP client timeout duration. 27 + pub http_client_timeout: Duration, 28 + /// List of trusted badge issuer DIDs. 29 + pub badge_issuers: Vec<String>, 30 + /// Directory path for badge image storage. 31 + pub badge_image_storage: String, 32 + /// Database connection URL. 33 + pub database_url: String, 34 + /// Optional path for persisting Jetstream cursor state. 35 + pub jetstream_cursor_path: Option<String>, 36 + } 37 + 38 + /// HTTP server configuration. 39 + #[derive(Debug, Clone, Serialize, Deserialize)] 40 + pub struct HttpConfig { 41 + /// HTTP server port. 42 + pub port: u16, 43 + /// Path to static assets directory. 44 + pub static_path: String, 45 + /// Path to templates directory. 46 + pub templates_path: String, 47 + } 48 + 49 + impl Default for Config { 50 + fn default() -> Self { 51 + Self { 52 + http: HttpConfig::default(), 53 + external_base: "http://localhost:8080".to_string(), 54 + certificate_bundles: Vec::new(), 55 + user_agent: format!( 56 + "showcase/{} (+https://tangled.sh/@smokesignal.events/showcase)", 57 + env!("CARGO_PKG_VERSION") 58 + ), 59 + plc_hostname: "plc.directory".to_string(), 60 + dns_nameservers: Vec::new(), 61 + http_client_timeout: Duration::from_secs(10), 62 + badge_issuers: Vec::new(), 63 + badge_image_storage: "./badges".to_string(), 64 + database_url: "sqlite://showcase.db".to_string(), 65 + jetstream_cursor_path: None, 66 + } 67 + } 68 + } 69 + 70 + impl Default for HttpConfig { 71 + fn default() -> Self { 72 + Self { 73 + port: 8080, 74 + static_path: format!("{}/static", env!("CARGO_MANIFEST_DIR")), 75 + templates_path: format!("{}/templates", env!("CARGO_MANIFEST_DIR")), 76 + } 77 + } 78 + } 79 + 80 + impl Config { 81 + /// Create configuration from environment variables. 82 + pub fn from_env() -> Result<Self> { 83 + let mut config = Self::default(); 84 + 85 + if let Ok(port) = std::env::var("HTTP_PORT") { 86 + config.http.port = port 87 + .parse() 88 + .map_err(|_| ShowcaseError::ConfigHttpPortInvalid { port: port.clone() })?; 89 + } 90 + 91 + if let Ok(static_path) = std::env::var("HTTP_STATIC_PATH") { 92 + config.http.static_path = static_path; 93 + } 94 + 95 + if let Ok(templates_path) = std::env::var("HTTP_TEMPLATES_PATH") { 96 + config.http.templates_path = templates_path; 97 + } 98 + 99 + if let Ok(external_base) = std::env::var("EXTERNAL_BASE") { 100 + config.external_base = external_base; 101 + } 102 + 103 + if let Ok(cert_bundles) = std::env::var("CERTIFICATE_BUNDLES") { 104 + config.certificate_bundles = cert_bundles 105 + .split(';') 106 + .map(|s| s.trim().to_string()) 107 + .filter(|s| !s.is_empty()) 108 + .collect(); 109 + } 110 + 111 + if let Ok(user_agent) = std::env::var("USER_AGENT") { 112 + config.user_agent = user_agent; 113 + } 114 + 115 + if let Ok(plc_hostname) = std::env::var("PLC_HOSTNAME") { 116 + config.plc_hostname = plc_hostname; 117 + } 118 + 119 + if let Ok(dns_nameservers) = std::env::var("DNS_NAMESERVERS") { 120 + config.dns_nameservers = dns_nameservers 121 + .split(';') 122 + .map(|s| s.trim().to_string()) 123 + .filter(|s| !s.is_empty()) 124 + .collect(); 125 + } 126 + 127 + if let Ok(timeout) = std::env::var("HTTP_CLIENT_TIMEOUT") { 128 + config.http_client_timeout = duration_str::parse(&timeout).map_err(|e| { 129 + ShowcaseError::ConfigHttpTimeoutInvalid { 130 + details: e.to_string(), 131 + } 132 + })?; 133 + } 134 + 135 + if let Ok(badge_issuers) = std::env::var("BADGE_ISSUERS") { 136 + config.badge_issuers = badge_issuers 137 + .split(';') 138 + .map(|s| s.trim().to_string()) 139 + .filter(|s| !s.is_empty()) 140 + .collect(); 141 + } 142 + 143 + if let Ok(badge_image_storage) = std::env::var("BADGE_IMAGE_STORAGE") { 144 + config.badge_image_storage = badge_image_storage; 145 + } 146 + 147 + if let Ok(database_url) = std::env::var("DATABASE_URL") { 148 + config.database_url = database_url; 149 + } 150 + 151 + if let Ok(jetstream_cursor_path) = std::env::var("JETSTREAM_CURSOR_PATH") { 152 + config.jetstream_cursor_path = Some(jetstream_cursor_path); 153 + } 154 + 155 + Ok(config) 156 + } 157 + } 158 + 159 + #[cfg(test)] 160 + mod tests { 161 + use super::*; 162 + 163 + #[test] 164 + fn test_parse_duration() { 165 + assert_eq!(duration_str::parse("10s").unwrap(), Duration::from_secs(10)); 166 + assert_eq!( 167 + duration_str::parse("500ms").unwrap(), 168 + Duration::from_millis(500) 169 + ); 170 + assert_eq!(duration_str::parse("2m").unwrap(), Duration::from_secs(120)); 171 + assert_eq!(duration_str::parse("30").unwrap(), Duration::from_secs(30)); 172 + } 173 + 174 + #[test] 175 + fn test_default_config() { 176 + let config = Config::default(); 177 + assert_eq!(config.http.port, 8080); 178 + assert_eq!(config.plc_hostname, "plc.directory"); 179 + assert_eq!(config.http_client_timeout, Duration::from_secs(10)); 180 + } 181 + }
+184
src/consumer.rs
··· 1 + //! Jetstream consumer for AT Protocol badge award events. 2 + //! 3 + //! Handles real-time processing of badge award events from Jetstream, 4 + //! with event queuing and cursor management for reliable consumption. 5 + 6 + use std::sync::Arc; 7 + use std::sync::atomic::{AtomicU64, Ordering}; 8 + use std::time::{Duration, Instant}; 9 + 10 + use crate::errors::ShowcaseError; 11 + use anyhow::Result; 12 + use async_trait::async_trait; 13 + use atproto_jetstream::{EventHandler, JetstreamEvent}; 14 + use tokio::sync::Mutex; 15 + use tokio::sync::mpsc; 16 + 17 + /// Type alias for badge event receiver to hide implementation details 18 + pub type BadgeEventReceiver = mpsc::UnboundedReceiver<AwardEvent>; 19 + 20 + /// Badge event types from Jetstream 21 + #[derive(Debug, Clone)] 22 + pub enum AwardEvent { 23 + /// Record commit event for a badge award. 24 + Commit { 25 + /// DID of the record owner. 26 + did: String, 27 + /// Record key within the collection. 28 + rkey: String, 29 + /// Content identifier of the record. 30 + cid: String, 31 + /// The complete record data. 32 + record: serde_json::Value, 33 + }, 34 + /// Record deletion event for a badge award. 35 + Delete { 36 + /// DID of the record owner. 37 + did: String, 38 + /// Record key within the collection. 39 + rkey: String, 40 + }, 41 + } 42 + 43 + /// Event handler that publishes badge events to an in-memory queue 44 + pub struct AwardEventHandler { 45 + id: String, 46 + event_sender: mpsc::UnboundedSender<AwardEvent>, 47 + } 48 + 49 + impl AwardEventHandler { 50 + fn new(id: String, event_sender: mpsc::UnboundedSender<AwardEvent>) -> Self { 51 + Self { id, event_sender } 52 + } 53 + } 54 + 55 + #[async_trait] 56 + impl EventHandler for AwardEventHandler { 57 + async fn handle_event(&self, event: JetstreamEvent) -> Result<()> { 58 + let award_event = match event { 59 + JetstreamEvent::Commit { did, commit, .. } => { 60 + if commit.collection != "community.lexicon.badge.award" { 61 + return Ok(()); 62 + } 63 + 64 + AwardEvent::Commit { 65 + did, 66 + rkey: commit.rkey, 67 + cid: commit.cid, 68 + record: commit.record, 69 + } 70 + } 71 + JetstreamEvent::Delete { did, commit, .. } => { 72 + if commit.collection != "community.lexicon.badge.award" { 73 + return Ok(()); 74 + } 75 + AwardEvent::Delete { 76 + did, 77 + rkey: commit.rkey, 78 + } 79 + } 80 + JetstreamEvent::Identity { .. } | JetstreamEvent::Account { .. } => { 81 + return Ok(()); 82 + } 83 + }; 84 + 85 + if let Err(err) = self.event_sender.send(award_event) { 86 + let showcase_error = ShowcaseError::ConsumerQueueSendFailed { 87 + details: err.to_string(), 88 + }; 89 + tracing::error!(?showcase_error); 90 + } 91 + 92 + Ok(()) 93 + } 94 + 95 + fn handler_id(&self) -> String { 96 + self.id.clone() 97 + } 98 + } 99 + 100 + /// Cursor writer handler that periodically writes the latest time_us to a file 101 + pub struct CursorWriterHandler { 102 + id: String, 103 + cursor_path: String, 104 + last_time_us: Arc<AtomicU64>, 105 + last_write: Arc<Mutex<Instant>>, 106 + write_interval: Duration, 107 + } 108 + 109 + impl CursorWriterHandler { 110 + fn new(id: String, cursor_path: String) -> Self { 111 + Self { 112 + id, 113 + cursor_path, 114 + last_time_us: Arc::new(AtomicU64::new(0)), 115 + last_write: Arc::new(Mutex::new(Instant::now())), 116 + write_interval: Duration::from_secs(30), 117 + } 118 + } 119 + 120 + async fn maybe_write_cursor(&self) -> Result<()> { 121 + let current_time_us = self.last_time_us.load(Ordering::Relaxed); 122 + if current_time_us == 0 { 123 + return Ok(()); 124 + } 125 + 126 + let mut last_write = self.last_write.lock().await; 127 + if last_write.elapsed() >= self.write_interval { 128 + tokio::fs::write(&self.cursor_path, current_time_us.to_string()).await?; 129 + *last_write = Instant::now(); 130 + tracing::debug!("Wrote cursor to {}: {}", self.cursor_path, current_time_us); 131 + } 132 + Ok(()) 133 + } 134 + } 135 + 136 + #[async_trait] 137 + impl EventHandler for CursorWriterHandler { 138 + async fn handle_event(&self, event: JetstreamEvent) -> Result<()> { 139 + // Extract time_us from any event type 140 + let time_us = match &event { 141 + JetstreamEvent::Commit { time_us, .. } => *time_us, 142 + JetstreamEvent::Delete { time_us, .. } => *time_us, 143 + JetstreamEvent::Identity { time_us, .. } => *time_us, 144 + JetstreamEvent::Account { time_us, .. } => *time_us, 145 + }; 146 + 147 + // Update the latest time_us 148 + self.last_time_us.store(time_us, Ordering::Relaxed); 149 + 150 + // Try to write the cursor periodically 151 + if let Err(err) = self.maybe_write_cursor().await { 152 + tracing::warn!("Failed to write cursor: {}", err); 153 + } 154 + 155 + Ok(()) 156 + } 157 + 158 + fn handler_id(&self) -> String { 159 + self.id.clone() 160 + } 161 + } 162 + 163 + /// Consumer factory for creating event handlers and queue receivers 164 + pub struct Consumer {} 165 + 166 + impl Consumer { 167 + /// Create a badge event handler and return the receiver for processing 168 + pub fn create_badge_handler(&self) -> (Arc<AwardEventHandler>, BadgeEventReceiver) { 169 + let (sender, receiver) = mpsc::unbounded_channel(); 170 + let handler = Arc::new(AwardEventHandler::new( 171 + "badge-processor".to_string(), 172 + sender, 173 + )); 174 + (handler, receiver) 175 + } 176 + 177 + /// Create a cursor writer handler for the given cursor path 178 + pub fn create_cursor_writer_handler(&self, cursor_path: String) -> Arc<CursorWriterHandler> { 179 + Arc::new(CursorWriterHandler::new( 180 + "cursor-writer".to_string(), 181 + cursor_path, 182 + )) 183 + } 184 + }
+307
src/errors.rs
··· 1 + use axum::response::{IntoResponse, Response}; 2 + use reqwest::StatusCode; 3 + use thiserror::Error; 4 + 5 + /// Main error type for the showcase application 6 + /// 7 + /// All errors follow the format: error-showcase-<domain>-<number> <message>: <details> 8 + /// Domains are organized alphabetically: config, consumer, http, process, storage 9 + #[derive(Error, Debug)] 10 + pub enum ShowcaseError { 11 + // Config domain - Configuration-related errors 12 + #[error("error-showcase-config-1 Failed to parse HTTP client timeout: {details}")] 13 + /// Failed to parse HTTP client timeout from environment variable. 14 + ConfigHttpTimeoutInvalid { 15 + /// Details about the parsing error. 16 + details: String, 17 + }, 18 + 19 + #[error("error-showcase-config-2 Failed to parse HTTP port: {port}")] 20 + /// Failed to parse HTTP port from environment variable. 21 + ConfigHttpPortInvalid { 22 + /// The invalid port value. 23 + port: String, 24 + }, 25 + 26 + #[error("error-showcase-config-3 Invalid S3 URL format: {details}")] 27 + /// Failed to parse S3 URL format from environment variable. 28 + ConfigS3UrlInvalid { 29 + /// Details about the S3 URL parsing error. 30 + details: String, 31 + }, 32 + 33 + #[error("error-showcase-config-4 Required feature not enabled: {feature}")] 34 + /// Required feature is not enabled in the build. 35 + ConfigFeatureNotEnabled { 36 + /// The feature that is required but not enabled. 37 + feature: String, 38 + }, 39 + 40 + // Consumer domain - Jetstream consumer errors 41 + #[error("error-showcase-consumer-1 Failed to send badge event to queue: {details}")] 42 + /// Failed to send badge event to the processing queue. 43 + ConsumerQueueSendFailed { 44 + /// Details about the send failure. 45 + details: String, 46 + }, 47 + 48 + #[error("error-showcase-consumer-2 Jetstream disconnected: {details}")] 49 + /// Jetstream consumer disconnected. 50 + ConsumerDisconnected { 51 + /// Details about the disconnection. 52 + details: String, 53 + }, 54 + 55 + #[error( 56 + "error-showcase-consumer-3 Jetstream disconnect rate exceeded: {disconnect_count} disconnects in {duration_mins} minutes" 57 + )] 58 + /// Jetstream consumer disconnect rate exceeded the allowable limit. 59 + ConsumerDisconnectRateExceeded { 60 + /// Number of disconnects in the time period. 61 + disconnect_count: usize, 62 + /// Duration in minutes for the disconnect rate calculation. 63 + duration_mins: u64, 64 + }, 65 + 66 + // HTTP domain - HTTP server and template errors 67 + #[error("error-showcase-http-1 Template rendering failed: {template}")] 68 + /// Template rendering failed in HTTP response. 69 + HttpTemplateRenderFailed { 70 + /// The template that failed to render. 71 + template: String, 72 + }, 73 + 74 + #[error("error-showcase-http-2 Internal server error")] 75 + /// Generic internal server error for HTTP responses. 76 + HttpInternalServerError, 77 + 78 + // Process domain - Badge processing errors 79 + #[error("error-showcase-process-1 Invalid AT-URI format: {uri}")] 80 + /// Invalid AT-URI format encountered during processing. 81 + ProcessInvalidAturi { 82 + /// The invalid URI string. 83 + uri: String, 84 + }, 85 + 86 + #[error("error-showcase-process-2 Failed to resolve identity for {did}: {details}")] 87 + /// Identity resolution failed with detailed error information. 88 + ProcessIdentityResolutionFailed { 89 + /// The DID that could not be resolved. 90 + did: String, 91 + /// Detailed error information. 92 + details: String, 93 + }, 94 + 95 + #[error("error-showcase-process-3 Failed to fetch badge record: {uri}")] 96 + /// Failed to fetch badge record from AT Protocol. 97 + ProcessBadgeFetchFailed { 98 + /// The badge URI that could not be fetched. 99 + uri: String, 100 + }, 101 + 102 + #[error("error-showcase-process-4 Failed to fetch badge {uri}: {details}")] 103 + /// Badge record fetch failed with detailed error information. 104 + ProcessBadgeRecordFetchFailed { 105 + /// The badge URI that could not be fetched. 106 + uri: String, 107 + /// Detailed error information. 108 + details: String, 109 + }, 110 + 111 + #[error("error-showcase-process-5 Failed to download badge image: {image_ref}")] 112 + /// Failed to download badge image. 113 + ProcessImageDownloadFailed { 114 + /// Reference to the image that failed to download. 115 + image_ref: String, 116 + }, 117 + 118 + #[error("error-showcase-process-6 Failed to process badge event: {event_type}")] 119 + /// Failed to process a badge event from Jetstream. 120 + ProcessEventHandlingFailed { 121 + /// Type of event that failed to process. 122 + event_type: String, 123 + }, 124 + 125 + #[error("error-showcase-process-7 Image file too large: {size} bytes exceeds 3MB limit")] 126 + /// Badge image file exceeds maximum allowed size. 127 + ProcessImageTooLarge { 128 + /// The actual size of the image in bytes. 129 + size: usize, 130 + }, 131 + 132 + #[error("error-showcase-process-8 Failed to decode image: {details}")] 133 + /// Failed to decode badge image data. 134 + ProcessImageDecodeFailed { 135 + /// Details about the decode error. 136 + details: String, 137 + }, 138 + 139 + #[error("error-showcase-process-9 Unsupported image format: {format}")] 140 + /// Badge image is in an unsupported format. 141 + ProcessUnsupportedImageFormat { 142 + /// The unsupported image format. 143 + format: String, 144 + }, 145 + 146 + #[error( 147 + "error-showcase-process-10 Image dimensions too small: {width}x{height}, minimum is 512x512" 148 + )] 149 + /// Badge image dimensions are below minimum requirements. 150 + ProcessImageTooSmall { 151 + /// The actual width of the image. 152 + width: u32, 153 + /// The actual height of the image. 154 + height: u32, 155 + }, 156 + 157 + #[error( 158 + "error-showcase-process-11 Image width too small after resize: {width}, minimum is 512" 159 + )] 160 + /// Badge image width is below minimum after resize. 161 + ProcessImageWidthTooSmall { 162 + /// The actual width after resize. 163 + width: u32, 164 + }, 165 + 166 + #[error("error-showcase-process-12 No signatures field found in record")] 167 + /// Badge record is missing required signatures field. 168 + ProcessNoSignaturesField, 169 + 170 + #[error("error-showcase-process-13 Missing issuer field in signature")] 171 + /// Signature is missing required issuer field. 172 + ProcessMissingIssuerField, 173 + 174 + #[error("error-showcase-process-14 Missing signature field in signature")] 175 + /// Signature object is missing required signature field. 176 + ProcessMissingSignatureField, 177 + 178 + #[error("error-showcase-process-15 Record serialization failed: {details}")] 179 + /// Failed to serialize record for signature verification. 180 + ProcessRecordSerializationFailed { 181 + /// Details about the serialization error. 182 + details: String, 183 + }, 184 + 185 + #[error("error-showcase-process-16 Signature decoding failed: {details}")] 186 + /// Failed to decode signature bytes. 187 + ProcessSignatureDecodingFailed { 188 + /// Details about the decoding error. 189 + details: String, 190 + }, 191 + 192 + #[error( 193 + "error-showcase-process-17 Cryptographic validation failed for issuer {issuer}: {details}" 194 + )] 195 + /// Cryptographic signature validation failed. 196 + ProcessCryptographicValidationFailed { 197 + /// The issuer DID whose signature validation failed. 198 + issuer: String, 199 + /// Detailed error information. 200 + details: String, 201 + }, 202 + 203 + // Storage domain - Database and storage errors 204 + #[error("error-showcase-storage-1 Database operation failed: {operation}")] 205 + /// Database operation failed. 206 + StorageDatabaseFailed { 207 + /// Description of the failed operation. 208 + operation: String, 209 + }, 210 + 211 + #[error("error-showcase-storage-2 File storage operation failed: {operation}")] 212 + /// File storage operation failed. 213 + StorageFileOperationFailed { 214 + /// Description of the failed file operation. 215 + operation: String, 216 + }, 217 + } 218 + 219 + /// Result type alias for convenience 220 + pub type Result<T> = std::result::Result<T, ShowcaseError>; 221 + 222 + impl From<sqlx::Error> for ShowcaseError { 223 + fn from(err: sqlx::Error) -> Self { 224 + ShowcaseError::StorageDatabaseFailed { 225 + operation: err.to_string(), 226 + } 227 + } 228 + } 229 + 230 + impl From<serde_json::Error> for ShowcaseError { 231 + fn from(err: serde_json::Error) -> Self { 232 + ShowcaseError::ProcessEventHandlingFailed { 233 + event_type: err.to_string(), 234 + } 235 + } 236 + } 237 + 238 + impl From<reqwest::Error> for ShowcaseError { 239 + fn from(err: reqwest::Error) -> Self { 240 + ShowcaseError::ProcessBadgeFetchFailed { 241 + uri: err.to_string(), 242 + } 243 + } 244 + } 245 + 246 + impl From<image::ImageError> for ShowcaseError { 247 + fn from(err: image::ImageError) -> Self { 248 + ShowcaseError::ProcessImageDownloadFailed { 249 + image_ref: err.to_string(), 250 + } 251 + } 252 + } 253 + 254 + impl From<tokio::sync::mpsc::error::SendError<crate::consumer::AwardEvent>> for ShowcaseError { 255 + fn from(err: tokio::sync::mpsc::error::SendError<crate::consumer::AwardEvent>) -> Self { 256 + ShowcaseError::ConsumerQueueSendFailed { 257 + details: err.to_string(), 258 + } 259 + } 260 + } 261 + 262 + impl From<minijinja::Error> for ShowcaseError { 263 + fn from(err: minijinja::Error) -> Self { 264 + ShowcaseError::HttpTemplateRenderFailed { 265 + template: err.to_string(), 266 + } 267 + } 268 + } 269 + 270 + impl From<std::io::Error> for ShowcaseError { 271 + fn from(err: std::io::Error) -> Self { 272 + ShowcaseError::ProcessImageDownloadFailed { 273 + image_ref: err.to_string(), 274 + } 275 + } 276 + } 277 + 278 + impl From<std::num::ParseIntError> for ShowcaseError { 279 + fn from(err: std::num::ParseIntError) -> Self { 280 + ShowcaseError::ConfigHttpPortInvalid { 281 + port: err.to_string(), 282 + } 283 + } 284 + } 285 + 286 + impl From<anyhow::Error> for ShowcaseError { 287 + fn from(err: anyhow::Error) -> Self { 288 + ShowcaseError::ProcessEventHandlingFailed { 289 + event_type: err.to_string(), 290 + } 291 + } 292 + } 293 + 294 + impl From<atproto_record::errors::AturiError> for ShowcaseError { 295 + fn from(err: atproto_record::errors::AturiError) -> Self { 296 + ShowcaseError::ProcessInvalidAturi { 297 + uri: err.to_string(), 298 + } 299 + } 300 + } 301 + 302 + impl IntoResponse for ShowcaseError { 303 + fn into_response(self) -> Response { 304 + tracing::error!(error = ?self, "internal server error"); 305 + (StatusCode::INTERNAL_SERVER_ERROR).into_response() 306 + } 307 + }
+375
src/http.rs
··· 1 + //! HTTP server implementation with Axum web framework. 2 + //! 3 + //! Provides REST API endpoints and template-rendered pages for displaying 4 + //! badge awards, with identity resolution and static file serving. 5 + 6 + use std::sync::Arc; 7 + 8 + use atproto_identity::{ 9 + axum::state::DidDocumentStorageExtractor, 10 + resolve::{IdentityResolver, InputType, parse_input}, 11 + storage::DidDocumentStorage, 12 + }; 13 + use axum::{ 14 + Router, 15 + body::Body, 16 + extract::{FromRef, Path, State}, 17 + http::{StatusCode, header}, 18 + response::{IntoResponse, Response}, 19 + routing::get, 20 + }; 21 + use axum_template::RenderHtml; 22 + use axum_template::engine::Engine; 23 + use minijinja::context; 24 + use serde::{Deserialize, Serialize}; 25 + use tower_http::services::ServeDir; 26 + 27 + use crate::{ 28 + config::Config, 29 + errors::{Result, ShowcaseError}, 30 + storage::{FileStorage, Storage}, 31 + }; 32 + 33 + #[cfg(feature = "reload")] 34 + use minijinja_autoreload::AutoReloader; 35 + 36 + #[cfg(feature = "reload")] 37 + /// Template engine with auto-reloading support for development. 38 + pub type AppEngine = Engine<AutoReloader>; 39 + 40 + #[cfg(feature = "embed")] 41 + use minijinja::Environment; 42 + 43 + #[cfg(feature = "embed")] 44 + pub type AppEngine = Engine<Environment<'static>>; 45 + 46 + #[cfg(not(any(feature = "reload", feature = "embed")))] 47 + pub type AppEngine = Engine<minijinja::Environment<'static>>; 48 + 49 + /// Application state shared across HTTP handlers. 50 + #[derive(Clone)] 51 + pub struct AppState { 52 + /// Database storage for badges and awards. 53 + pub storage: Arc<dyn Storage>, 54 + /// Storage for DID documents. 55 + pub document_storage: Arc<dyn DidDocumentStorage + Send + Sync>, 56 + /// Identity resolver for DID resolution. 57 + pub identity_resolver: IdentityResolver, 58 + /// Application configuration. 59 + pub config: Arc<Config>, 60 + /// Template engine for rendering HTML responses. 61 + pub template_env: AppEngine, 62 + /// File storage for badge images. 63 + pub file_storage: Arc<dyn FileStorage>, 64 + } 65 + 66 + impl FromRef<AppState> for DidDocumentStorageExtractor { 67 + fn from_ref(context: &AppState) -> Self { 68 + atproto_identity::axum::state::DidDocumentStorageExtractor(context.document_storage.clone()) 69 + } 70 + } 71 + 72 + impl FromRef<AppState> for IdentityResolver { 73 + fn from_ref(context: &AppState) -> Self { 74 + context.identity_resolver.clone() 75 + } 76 + } 77 + 78 + #[derive(Debug, Serialize, Deserialize)] 79 + struct AwardDisplay { 80 + pub did: String, 81 + pub handle: String, 82 + pub badge_name: String, 83 + pub badge_image: Option<String>, 84 + pub signers: Vec<String>, 85 + pub created_at: String, 86 + } 87 + 88 + /// Create the main application router with all HTTP routes. 89 + pub fn create_router(state: AppState) -> Router { 90 + Router::new() 91 + .route("/", get(handle_index)) 92 + .route("/badges/{subject}", get(handle_identity)) 93 + .route("/badge/{cid}", get(handle_badge_image)) 94 + .nest_service("/static", ServeDir::new(&state.config.http.static_path)) 95 + .with_state(state) 96 + } 97 + 98 + async fn handle_index(State(state): State<AppState>) -> Result<impl IntoResponse> { 99 + match get_recent_awards(&state).await { 100 + Ok(awards) => Ok(RenderHtml( 101 + "index.html", 102 + state.template_env.clone(), 103 + context! { 104 + title => "Recent Badge Awards", 105 + awards => awards, 106 + }, 107 + )), 108 + Err(_) => Err(ShowcaseError::HttpInternalServerError), 109 + } 110 + } 111 + 112 + async fn handle_identity( 113 + Path(subject): Path<String>, 114 + State(state): State<AppState>, 115 + ) -> Result<impl IntoResponse> { 116 + let did = match parse_input(&subject) { 117 + Ok(InputType::Plc(did)) | Ok(InputType::Web(did)) => did, 118 + Ok(InputType::Handle(_)) => match state.storage.get_identity_by_handle(&subject).await { 119 + Ok(Some(identity)) => identity.did, 120 + Ok(None) => return Ok((StatusCode::NOT_FOUND).into_response()), 121 + Err(_) => return Ok((StatusCode::NOT_FOUND).into_response()), 122 + }, 123 + Err(_) => return Ok((StatusCode::NOT_FOUND).into_response()), 124 + }; 125 + 126 + match get_awards_for_identity(&state, &did).await { 127 + Ok((awards, identity_handle)) => Ok(RenderHtml( 128 + "identity.html", 129 + state.template_env.clone(), 130 + context! { 131 + title => format!("Badge Awards for @{}", identity_handle), 132 + subject => identity_handle, 133 + awards => awards, 134 + }, 135 + ) 136 + .into_response()), 137 + Err(_) => Err(ShowcaseError::HttpInternalServerError), 138 + } 139 + } 140 + 141 + async fn handle_badge_image( 142 + Path(cid): Path<String>, 143 + State(state): State<AppState>, 144 + ) -> impl IntoResponse { 145 + // Validate that the CID ends with ".png" 146 + if !cid.ends_with(".png") { 147 + tracing::warn!(?cid, "file not png"); 148 + return (StatusCode::NOT_FOUND).into_response(); 149 + } 150 + 151 + // Read the file using FileStorage 152 + let file_data = match state.file_storage.read_file(&cid).await { 153 + Ok(data) => data, 154 + Err(err) => { 155 + tracing::warn!(?cid, ?err, "file_storage read_file error"); 156 + return (StatusCode::NOT_FOUND).into_response(); 157 + } 158 + }; 159 + 160 + // Check file size (must be less than 1MB) 161 + const MAX_FILE_SIZE: usize = 1024 * 1024; // 1MB 162 + if file_data.len() > MAX_FILE_SIZE { 163 + tracing::warn!(?cid, len = file_data.len(), "file too big"); 164 + return (StatusCode::NOT_FOUND).into_response(); 165 + } 166 + 167 + // Validate that it's actually a PNG image 168 + if !is_valid_png(&file_data) { 169 + tracing::warn!(?cid, "file not png"); 170 + return (StatusCode::NOT_FOUND).into_response(); 171 + } 172 + 173 + // Return the image with appropriate content type 174 + Response::builder() 175 + .status(StatusCode::OK) 176 + .header(header::CONTENT_TYPE, "image/png") 177 + .header(header::CACHE_CONTROL, "public, max-age=86400") // Cache for 1 day 178 + .body(Body::from(file_data)) 179 + .unwrap() 180 + .into_response() 181 + } 182 + 183 + /// Validate that the provided bytes represent a valid PNG image 184 + fn is_valid_png(data: &[u8]) -> bool { 185 + // PNG signature: 89 50 4E 47 0D 0A 1A 0A 186 + const PNG_SIGNATURE: &[u8] = &[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; 187 + 188 + data.len() >= PNG_SIGNATURE.len() && data.starts_with(PNG_SIGNATURE) 189 + } 190 + 191 + async fn get_recent_awards(state: &AppState) -> Result<Vec<AwardDisplay>> { 192 + let awards_with_details = state.storage.get_recent_awards(50).await?; 193 + 194 + let mut result = Vec::new(); 195 + for item in awards_with_details { 196 + let handle = item 197 + .identity 198 + .as_ref() 199 + .map(|i| i.handle.clone()) 200 + .unwrap_or_else(|| "unknown.handle".to_string()); 201 + 202 + let badge_image = item 203 + .badge 204 + .as_ref() 205 + .and_then(|b| b.image.clone()) 206 + .map(|img| format!("{}.png", img)); 207 + 208 + let signers = format_signers(&item.signer_identities); 209 + 210 + result.push(AwardDisplay { 211 + did: item.award.did, 212 + handle, 213 + badge_name: item.award.badge_name.clone(), 214 + badge_image, 215 + signers, 216 + created_at: item 217 + .award 218 + .created_at 219 + .format("%Y-%m-%d %H:%M UTC") 220 + .to_string(), 221 + }); 222 + } 223 + 224 + Ok(result) 225 + } 226 + 227 + async fn get_awards_for_identity( 228 + state: &AppState, 229 + did: &str, 230 + ) -> Result<(Vec<AwardDisplay>, String)> { 231 + let awards_with_details = state.storage.get_awards_for_did(did, 50).await?; 232 + 233 + let identity_handle = state 234 + .storage 235 + .get_identity_by_did(did) 236 + .await? 237 + .map(|i| i.handle) 238 + .unwrap_or_else(|| "unknown.handle".to_string()); 239 + 240 + let mut result = Vec::new(); 241 + for item in awards_with_details { 242 + let handle = item 243 + .identity 244 + .as_ref() 245 + .map(|i| i.handle.clone()) 246 + .unwrap_or_else(|| "unknown.handle".to_string()); 247 + 248 + let badge_image = item 249 + .badge 250 + .as_ref() 251 + .and_then(|b| b.image.clone()) 252 + .map(|img| format!("{}.png", img)); 253 + 254 + let signers = format_signers(&item.signer_identities); 255 + 256 + result.push(AwardDisplay { 257 + did: item.award.did, 258 + handle, 259 + badge_name: item.award.badge_name.clone(), 260 + badge_image, 261 + signers, 262 + created_at: item 263 + .award 264 + .created_at 265 + .format("%Y-%m-%d %H:%M UTC") 266 + .to_string(), 267 + }); 268 + } 269 + 270 + Ok((result, identity_handle)) 271 + } 272 + 273 + fn format_signers(signer_identities: &[crate::storage::Identity]) -> Vec<String> { 274 + let handles: Vec<String> = signer_identities 275 + .iter() 276 + .map(|i| format!("{}", i.handle)) 277 + .collect(); 278 + 279 + if handles.len() <= 3 { 280 + handles 281 + } else { 282 + let mut result = handles[..2].to_vec(); 283 + result.push(format!("and {} others", handles.len() - 2)); 284 + result 285 + } 286 + } 287 + 288 + #[cfg(test)] 289 + mod tests { 290 + use super::*; 291 + use crate::storage::Identity; 292 + 293 + #[test] 294 + fn test_format_signers() { 295 + let identities = vec![ 296 + Identity { 297 + did: "did:plc:1".to_string(), 298 + handle: "alice.bsky.social".to_string(), 299 + record: serde_json::Value::Null, 300 + created_at: chrono::Utc::now(), 301 + updated_at: chrono::Utc::now(), 302 + }, 303 + Identity { 304 + did: "did:plc:2".to_string(), 305 + handle: "bob.bsky.social".to_string(), 306 + record: serde_json::Value::Null, 307 + created_at: chrono::Utc::now(), 308 + updated_at: chrono::Utc::now(), 309 + }, 310 + ]; 311 + 312 + let result = format_signers(&identities); 313 + assert_eq!(result, vec!["@alice.bsky.social", "@bob.bsky.social"]); 314 + 315 + let many_identities = vec![ 316 + Identity { 317 + did: "did:plc:1".to_string(), 318 + handle: "alice.bsky.social".to_string(), 319 + record: serde_json::Value::Null, 320 + created_at: chrono::Utc::now(), 321 + updated_at: chrono::Utc::now(), 322 + }, 323 + Identity { 324 + did: "did:plc:2".to_string(), 325 + handle: "bob.bsky.social".to_string(), 326 + record: serde_json::Value::Null, 327 + created_at: chrono::Utc::now(), 328 + updated_at: chrono::Utc::now(), 329 + }, 330 + Identity { 331 + did: "did:plc:3".to_string(), 332 + handle: "charlie.bsky.social".to_string(), 333 + record: serde_json::Value::Null, 334 + created_at: chrono::Utc::now(), 335 + updated_at: chrono::Utc::now(), 336 + }, 337 + Identity { 338 + did: "did:plc:4".to_string(), 339 + handle: "dave.bsky.social".to_string(), 340 + record: serde_json::Value::Null, 341 + created_at: chrono::Utc::now(), 342 + updated_at: chrono::Utc::now(), 343 + }, 344 + ]; 345 + 346 + let result = format_signers(&many_identities); 347 + assert_eq!( 348 + result, 349 + vec!["@alice.bsky.social", "@bob.bsky.social", "and 2 others"] 350 + ); 351 + } 352 + 353 + #[test] 354 + fn test_is_valid_png() { 355 + // Valid PNG signature 356 + let valid_png = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00]; 357 + assert!(is_valid_png(&valid_png)); 358 + 359 + // Invalid signature 360 + let invalid_png = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0B]; 361 + assert!(!is_valid_png(&invalid_png)); 362 + 363 + // Too short 364 + let too_short = vec![0x89, 0x50]; 365 + assert!(!is_valid_png(&too_short)); 366 + 367 + // Empty 368 + let empty = vec![]; 369 + assert!(!is_valid_png(&empty)); 370 + 371 + // JPEG signature (should fail) 372 + let jpeg = vec![0xFF, 0xD8, 0xFF, 0xE0]; 373 + assert!(!is_valid_png(&jpeg)); 374 + } 375 + }
+21
src/lib.rs
··· 1 + //! Badge awards showcase application for the AT Protocol community. 2 + //! 3 + //! This application consumes Jetstream events, validates badge signatures, 4 + //! stores data in a database, and provides a web interface to display badge awards. 5 + 6 + #![warn(missing_docs)] 7 + 8 + /// Configuration management via environment variables. 9 + pub mod config; 10 + /// Jetstream consumer for AT Protocol badge award events. 11 + pub mod consumer; 12 + /// Comprehensive error handling using `thiserror`. 13 + pub mod errors; 14 + /// Axum-based HTTP server with API endpoints and template rendering. 15 + pub mod http; 16 + /// Badge processing logic, signature validation, and image handling. 17 + pub mod process; 18 + /// Database models and operations using SQLx. 19 + pub mod storage; 20 + /// Template engine integration with MiniJinja. 21 + pub mod templates;
+884
src/process.rs
··· 1 + //! Badge processing logic with signature validation and image handling. 2 + //! 3 + //! Processes badge award events by validating cryptographic signatures, 4 + //! downloading and processing badge images, and storing validated awards. 5 + 6 + use std::str::FromStr; 7 + use std::sync::Arc; 8 + 9 + use crate::errors::{Result, ShowcaseError}; 10 + use atproto_client::com::atproto::repo::{get_blob, get_record}; 11 + use atproto_identity::{ 12 + key::{KeyData, identify_key, validate}, 13 + model::Document, 14 + resolve::IdentityResolver, 15 + storage::DidDocumentStorage, 16 + }; 17 + use atproto_record::aturi::ATURI; 18 + use chrono::Utc; 19 + use image::{GenericImageView, ImageFormat}; 20 + use serde::{Deserialize, Serialize}; 21 + use serde_json::{Value, json}; 22 + use tracing::{error, info, warn}; 23 + 24 + use crate::{ 25 + config::Config, 26 + consumer::{AwardEvent, BadgeEventReceiver}, 27 + storage::{Award, Badge, FileStorage, Storage}, 28 + }; 29 + 30 + /// Badge award record structure from AT Protocol 31 + #[derive(Debug, Deserialize, Serialize)] 32 + struct AwardRecord { 33 + pub did: String, 34 + pub badge: StrongRef, 35 + pub issued: String, 36 + pub signatures: Vec<Signature>, 37 + } 38 + 39 + /// Strong reference to another record 40 + #[derive(Debug, Deserialize, Serialize)] 41 + struct StrongRef { 42 + #[serde(rename = "$type")] 43 + pub type_: String, 44 + pub uri: String, 45 + pub cid: String, 46 + } 47 + 48 + /// Signature from badge issuer 49 + #[derive(Debug, Deserialize, Serialize)] 50 + struct Signature { 51 + pub issuer: String, 52 + #[serde(rename = "issuedAt")] 53 + pub issued_at: String, 54 + pub signature: String, 55 + } 56 + 57 + /// Badge definition record 58 + #[derive(Debug, Deserialize, Serialize)] 59 + struct BadgeRecord { 60 + pub name: String, 61 + pub description: String, 62 + pub image: Option<BlobRef>, 63 + } 64 + 65 + /// Blob reference for badge images 66 + #[derive(Debug, Deserialize, Serialize)] 67 + struct BlobRef { 68 + #[serde(rename = "$type")] 69 + pub type_: String, 70 + #[serde(rename = "ref")] 71 + pub ref_: Option<LinkRef>, 72 + #[serde(rename = "mimeType")] 73 + pub mime_type: String, 74 + pub size: u64, 75 + } 76 + 77 + /// Link reference in blob 78 + #[derive(Debug, Deserialize, Serialize)] 79 + struct LinkRef { 80 + #[serde(rename = "$link")] 81 + pub link: String, 82 + } 83 + 84 + impl BlobRef { 85 + fn get_ref(&self) -> Option<String> { 86 + self.ref_.as_ref().map(|r| r.link.clone()) 87 + } 88 + } 89 + 90 + /// Background processor for badge events 91 + pub struct BadgeProcessor { 92 + storage: Arc<dyn Storage>, 93 + config: Arc<Config>, 94 + identity_resolver: IdentityResolver, 95 + document_storage: Arc<dyn DidDocumentStorage + Send + Sync>, 96 + http_client: reqwest::Client, 97 + file_storage: Arc<dyn FileStorage>, 98 + } 99 + 100 + impl BadgeProcessor { 101 + /// Create a new badge processor with the required dependencies. 102 + pub fn new( 103 + storage: Arc<dyn Storage>, 104 + config: Arc<Config>, 105 + identity_resolver: IdentityResolver, 106 + document_storage: Arc<dyn DidDocumentStorage + Send + Sync>, 107 + http_client: reqwest::Client, 108 + file_storage: Arc<dyn FileStorage>, 109 + ) -> Self { 110 + Self { 111 + storage, 112 + config, 113 + identity_resolver, 114 + document_storage, 115 + http_client, 116 + file_storage, 117 + } 118 + } 119 + 120 + /// Start processing badge events from the queue 121 + pub async fn start_processing(&self, mut event_receiver: BadgeEventReceiver) -> Result<()> { 122 + info!("Badge processor started"); 123 + 124 + while let Some(event) = event_receiver.recv().await { 125 + match &event { 126 + AwardEvent::Commit { 127 + did, 128 + cid, 129 + record, 130 + rkey, 131 + .. 132 + } => { 133 + if let Err(e) = self.handle_commit(did, rkey, cid, record).await { 134 + error!( 135 + "error-showcase-process-6 Failed to process create event: {}", 136 + e 137 + ); 138 + } 139 + } 140 + AwardEvent::Delete { did, rkey, .. } => { 141 + if let Err(e) = self.handle_delete(did, rkey).await { 142 + error!( 143 + "error-showcase-process-8 Failed to process delete event: {}", 144 + e 145 + ); 146 + } 147 + } 148 + } 149 + } 150 + 151 + info!("Badge processor finished"); 152 + Ok(()) 153 + } 154 + 155 + async fn handle_commit(&self, did: &str, rkey: &str, cid: &str, record: &Value) -> Result<()> { 156 + info!("Processing award: {} for {}", rkey, did); 157 + 158 + let aturi = format!("at://{did}/community.lexicon.badge.award/{rkey}"); 159 + 160 + // Parse the award record 161 + let award_record: AwardRecord = serde_json::from_value(record.clone())?; 162 + tracing::debug!(?award_record, "processing award"); 163 + 164 + let badge_from_issuer = self.config.badge_issuers.iter().any(|value| { 165 + award_record 166 + .badge 167 + .uri 168 + .starts_with(&format!("at://{value}/")) 169 + }); 170 + if !badge_from_issuer { 171 + return Ok(()); 172 + } 173 + 174 + // Ensure identity is stored 175 + let document = self.ensure_identity_stored(did).await?; 176 + tracing::debug!(?document, "resolved award DID"); 177 + 178 + // Get or create badge 179 + let badge = self.get_or_create_badge(&award_record.badge).await?; 180 + 181 + let badge_aturi = ATURI::from_str(&award_record.badge.uri)?; 182 + 183 + tracing::debug!(?badge, "processing badge"); 184 + 185 + let badge_isser_document = self.ensure_identity_stored(&badge_aturi.authority).await?; 186 + 187 + let issuer_pds_endpoint = { 188 + badge_isser_document 189 + .pds_endpoints() 190 + .first() 191 + .ok_or_else(|| ShowcaseError::ProcessIdentityResolutionFailed { 192 + did: did.to_string(), 193 + details: "No PDS endpoint found in DID document".to_string(), 194 + }) 195 + .cloned()? 196 + }; 197 + 198 + self.download_badge_image(issuer_pds_endpoint, &badge_isser_document.id, &badge) 199 + .await?; 200 + 201 + // Validate signatures 202 + let validated_issuers = self 203 + .validate_signatures(record, &document.id, "community.lexicon.badge.award") 204 + .await?; 205 + 206 + // Create award record 207 + let award = Award { 208 + aturi: aturi.to_string(), 209 + cid: cid.to_string(), 210 + did: document.id.clone(), 211 + badge: award_record.badge.uri.clone(), 212 + badge_cid: award_record.badge.cid.clone(), 213 + badge_name: badge.name, 214 + validated_issuers: serde_json::to_value(&validated_issuers)?, 215 + created_at: Utc::now(), 216 + updated_at: Utc::now(), 217 + record: record.clone(), 218 + }; 219 + 220 + // Store award 221 + let is_new = self.storage.upsert_award(&award).await?; 222 + 223 + // Update badge count if this is a new award 224 + if is_new { 225 + self.storage 226 + .increment_badge_count(&award.badge, &award.badge_cid) 227 + .await?; 228 + } 229 + 230 + // Trim awards for this DID to max 100 231 + self.storage.trim_awards_for_did(did, 100).await?; 232 + 233 + info!("Successfully processed award: {}", aturi); 234 + Ok(()) 235 + } 236 + 237 + async fn handle_delete(&self, did: &str, rkey: &str) -> Result<()> { 238 + let aturi = format!("at://{did}/community.lexicon.badge.award/{rkey}"); 239 + 240 + if let Some(award) = self.storage.delete_award(&aturi).await? { 241 + // Decrement badge count 242 + self.storage 243 + .decrement_badge_count(&award.badge, &award.badge_cid) 244 + .await?; 245 + info!("Successfully deleted award: {}", aturi); 246 + } 247 + 248 + Ok(()) 249 + } 250 + 251 + async fn ensure_identity_stored(&self, did: &str) -> Result<Document> { 252 + // Check if we already have this identity 253 + if let Some(document) = self.document_storage.get_document_by_did(did).await? { 254 + return Ok(document); 255 + } 256 + 257 + let document = self.identity_resolver.resolve(did).await?; 258 + self.document_storage 259 + .store_document(document.clone()) 260 + .await?; 261 + Ok(document) 262 + } 263 + 264 + async fn get_or_create_badge(&self, badge_ref: &StrongRef) -> Result<BadgeRecord> { 265 + // Check if we already have this badge 266 + if let Some(existing) = self 267 + .storage 268 + .get_badge(&badge_ref.uri, &badge_ref.cid) 269 + .await? 270 + { 271 + let badge_record = serde_json::from_value::<BadgeRecord>(existing.record.clone())?; 272 + return Ok(badge_record); 273 + } 274 + 275 + // Parse AT-URI to get DID and record key 276 + let parts: Vec<&str> = badge_ref 277 + .uri 278 + .strip_prefix("at://") 279 + .unwrap_or(&badge_ref.uri) 280 + .split('/') 281 + .collect(); 282 + if parts.len() != 3 { 283 + return Err(ShowcaseError::ProcessInvalidAturi { 284 + uri: badge_ref.uri.clone(), 285 + }); 286 + } 287 + 288 + let repo = parts[0]; 289 + let rkey = parts[2]; 290 + 291 + // Get the DID document to find PDS endpoint 292 + let document = self.ensure_identity_stored(repo).await?; 293 + let pds_endpoints = document.pds_endpoints(); 294 + let pds_endpoint = 295 + pds_endpoints 296 + .first() 297 + .ok_or_else(|| ShowcaseError::ProcessBadgeFetchFailed { 298 + uri: format!("No PDS endpoint found for DID: {}", repo), 299 + })?; 300 + 301 + let badge_record = self 302 + .fetch_badge_record(pds_endpoint, repo, rkey, &badge_ref.cid) 303 + .await?; 304 + let badge = Badge { 305 + aturi: badge_ref.uri.clone(), 306 + cid: badge_ref.cid.clone(), 307 + name: badge_record.name.clone(), 308 + image: badge_record.image.as_ref().and_then(|img| img.get_ref()), 309 + created_at: Utc::now(), 310 + updated_at: Utc::now(), 311 + count: 0, 312 + record: serde_json::to_value(&badge_record)?, 313 + }; 314 + 315 + self.storage.upsert_badge(&badge).await?; 316 + Ok(badge_record) 317 + } 318 + 319 + async fn fetch_badge_record( 320 + &self, 321 + pds: &str, 322 + did: &str, 323 + rkey: &str, 324 + cid: &str, 325 + ) -> Result<BadgeRecord> { 326 + let get_record_response = get_record( 327 + &self.http_client, 328 + None, 329 + pds, 330 + did, 331 + "community.lexicon.badge.definition", 332 + rkey, 333 + Some(cid), 334 + ) 335 + .await?; 336 + 337 + match get_record_response { 338 + atproto_client::com::atproto::repo::GetRecordResponse::Record { value, .. } => { 339 + serde_json::from_value(value.clone()).map_err(|e| { 340 + ShowcaseError::ProcessBadgeRecordFetchFailed { 341 + uri: format!("at://{}/community.lexicon.badge.definition/{}", did, rkey), 342 + details: format!("Failed to deserialize record: {}", e), 343 + } 344 + }) 345 + } 346 + atproto_client::com::atproto::repo::GetRecordResponse::Error(simple_error) => { 347 + Err(ShowcaseError::ProcessBadgeRecordFetchFailed { 348 + uri: format!("at://{}/community.lexicon.badge.definition/{}", did, rkey), 349 + details: format!("Get record returned an error: {:?}", simple_error), 350 + }) 351 + } 352 + } 353 + } 354 + 355 + async fn download_badge_image(&self, pds: &str, did: &str, badge: &BadgeRecord) -> Result<()> { 356 + if let Some(ref image_blob) = badge.image { 357 + let image_ref = match image_blob.get_ref() { 358 + Some(ref_val) => ref_val, 359 + None => return Ok(()), // No image reference 360 + }; 361 + 362 + let image_path = format!("{}.png", image_ref); 363 + 364 + // Check if image already exists 365 + if self.file_storage.file_exists(&image_path).await? { 366 + return Ok(()); 367 + } 368 + 369 + // Download and process image 370 + match self 371 + .download_and_process_image(pds, did, &image_ref, &image_path) 372 + .await 373 + { 374 + Ok(()) => { 375 + info!("Downloaded badge image: {}", image_ref); 376 + } 377 + Err(e) => { 378 + warn!( 379 + "error-showcase-process-11 Failed to download badge image {}: {}", 380 + image_ref, e 381 + ); 382 + } 383 + } 384 + } 385 + 386 + Ok(()) 387 + } 388 + 389 + async fn download_and_process_image( 390 + &self, 391 + pds: &str, 392 + did: &str, 393 + blob_ref: &str, 394 + output_path: &str, 395 + ) -> Result<()> { 396 + // Fetch the blob from PDS 397 + let image_bytes = get_blob(&self.http_client, pds, did, blob_ref).await?; 398 + 399 + // 1. Check size limit (3MB = 3 * 1024 * 1024 bytes) 400 + const MAX_SIZE: usize = 3 * 1024 * 1024; 401 + if image_bytes.len() > MAX_SIZE { 402 + return Err(ShowcaseError::ProcessImageTooLarge { 403 + size: image_bytes.len(), 404 + }); 405 + } 406 + 407 + // 2. Try to load and detect image format 408 + let img = image::load_from_memory(&image_bytes).map_err(|e| { 409 + ShowcaseError::ProcessImageDecodeFailed { 410 + details: e.to_string(), 411 + } 412 + })?; 413 + 414 + // Check if format is supported (JPG, PNG, WebP) 415 + let format = image::guess_format(&image_bytes).map_err(|e| { 416 + ShowcaseError::ProcessImageDecodeFailed { 417 + details: format!("Could not detect image format: {}", e), 418 + } 419 + })?; 420 + 421 + match format { 422 + ImageFormat::Jpeg | ImageFormat::Png | ImageFormat::WebP => { 423 + // Supported formats 424 + } 425 + _ => { 426 + return Err(ShowcaseError::ProcessUnsupportedImageFormat { 427 + format: format!("{:?}", format), 428 + }); 429 + } 430 + } 431 + 432 + // 3. Check original dimensions (minimum 512x512) 433 + let (original_width, original_height) = img.dimensions(); 434 + if original_width < 512 || original_height < 512 { 435 + return Err(ShowcaseError::ProcessImageTooSmall { 436 + width: original_width, 437 + height: original_height, 438 + }); 439 + } 440 + 441 + // 4. Resize to height 512 while preserving aspect ratio 442 + let aspect_ratio = original_width as f32 / original_height as f32; 443 + let new_height = 512; 444 + let new_width = (new_height as f32 * aspect_ratio) as u32; 445 + 446 + // 5. Check that width after resize is >= 512 447 + if new_width < 512 { 448 + return Err(ShowcaseError::ProcessImageWidthTooSmall { width: new_width }); 449 + } 450 + 451 + // Perform the resize 452 + let mut resized_img = 453 + img.resize_exact(new_width, new_height, image::imageops::FilterType::Lanczos3); 454 + 455 + // 6. Center crop the width to exactly 512 pixels if needed 456 + let final_img = if new_width > 512 { 457 + let crop_x = (new_width - 512) / 2; 458 + resized_img.crop(crop_x, 0, 512, 512) 459 + } else { 460 + resized_img 461 + }; 462 + 463 + // Save as PNG to byte buffer 464 + let mut png_buffer = std::io::Cursor::new(Vec::new()); 465 + final_img.write_to(&mut png_buffer, ImageFormat::Png)?; 466 + let png_bytes = png_buffer.into_inner(); 467 + 468 + // Write the processed image using file storage 469 + self.file_storage 470 + .write_file(output_path, &png_bytes) 471 + .await?; 472 + 473 + info!( 474 + "Processed badge image {}: {}x{} -> 512x512", 475 + blob_ref, original_width, original_height 476 + ); 477 + Ok(()) 478 + } 479 + 480 + async fn validate_signatures( 481 + &self, 482 + record: &serde_json::Value, 483 + repository: &str, 484 + collection: &str, 485 + ) -> Result<Vec<String>> { 486 + let mut validated_issuers = Vec::new(); 487 + 488 + // Extract signatures from the record 489 + let signatures = record 490 + .get("signatures") 491 + .and_then(|v| v.as_array()) 492 + .ok_or(ShowcaseError::ProcessNoSignaturesField)?; 493 + 494 + for sig_obj in signatures { 495 + // Extract the issuer from the signature object 496 + let signature_issuer = sig_obj 497 + .get("issuer") 498 + .and_then(|v| v.as_str()) 499 + .ok_or(ShowcaseError::ProcessMissingIssuerField)?; 500 + 501 + // Retrieve the DID document for the issuer 502 + let did_document = match self 503 + .document_storage 504 + .get_document_by_did(signature_issuer) 505 + .await 506 + { 507 + Ok(Some(doc)) => doc, 508 + Ok(None) => { 509 + warn!( 510 + "error-showcase-process-16 Failed to retrieve DID document for issuer {}: not found", 511 + signature_issuer 512 + ); 513 + continue; 514 + } 515 + Err(e) => { 516 + warn!( 517 + "error-showcase-process-16 Failed to retrieve DID document for issuer {}: {}", 518 + signature_issuer, e 519 + ); 520 + continue; 521 + } 522 + }; 523 + 524 + // Iterate over all keys available in the DID document 525 + for key_string in did_document.did_keys() { 526 + // Decode the key using identify_key 527 + let key_data = match identify_key(key_string) { 528 + Ok(key_data) => key_data, 529 + Err(e) => { 530 + warn!( 531 + "error-showcase-process-17 Failed to decode key {} for issuer {}: {}", 532 + key_string, signature_issuer, e 533 + ); 534 + continue; 535 + } 536 + }; 537 + 538 + // Attempt to verify the signature with this key 539 + match self 540 + .verify_signature( 541 + signature_issuer, 542 + &key_data, 543 + record, 544 + repository, 545 + collection, 546 + sig_obj, 547 + ) 548 + .await 549 + { 550 + Ok(()) => { 551 + validated_issuers.push(signature_issuer.to_string()); 552 + info!( 553 + "Validated signature from trusted issuer {} using key: {}", 554 + signature_issuer, key_string 555 + ); 556 + break; // Stop trying other keys once we find one that works 557 + } 558 + Err(_) => { 559 + // Continue trying other keys - don't warn on each failure as this is expected 560 + continue; 561 + } 562 + } 563 + } 564 + } 565 + 566 + Ok(validated_issuers) 567 + } 568 + 569 + async fn verify_signature( 570 + &self, 571 + issuer: &str, 572 + key_data: &KeyData, 573 + record: &serde_json::Value, 574 + repository: &str, 575 + collection: &str, 576 + sig_obj: &serde_json::Value, 577 + ) -> Result<()> { 578 + // Reconstruct the $sig object as per the reference implementation 579 + let mut sig_variable = sig_obj.clone(); 580 + 581 + if let Some(sig_map) = sig_variable.as_object_mut() { 582 + sig_map.remove("signature"); 583 + sig_map.insert("repository".to_string(), json!(repository)); 584 + sig_map.insert("collection".to_string(), json!(collection)); 585 + } 586 + 587 + // Create the signed record for verification 588 + let mut signed_record = record.clone(); 589 + if let Some(record_map) = signed_record.as_object_mut() { 590 + record_map.remove("signatures"); 591 + record_map.insert("$sig".to_string(), sig_variable); 592 + } 593 + 594 + // Serialize the record using IPLD DAG-CBOR 595 + let serialized_record = serde_ipld_dagcbor::to_vec(&signed_record).map_err(|e| { 596 + ShowcaseError::ProcessRecordSerializationFailed { 597 + details: e.to_string(), 598 + } 599 + })?; 600 + 601 + // Get the signature value and decode it 602 + let signature_value = sig_obj 603 + .get("signature") 604 + .and_then(|v| v.as_str()) 605 + .ok_or(ShowcaseError::ProcessMissingSignatureField)?; 606 + 607 + let (_, signature_bytes) = multibase::decode(signature_value).map_err(|e| { 608 + ShowcaseError::ProcessSignatureDecodingFailed { 609 + details: e.to_string(), 610 + } 611 + })?; 612 + 613 + // Validate the signature 614 + validate(key_data, &signature_bytes, &serialized_record).map_err(|e| { 615 + ShowcaseError::ProcessCryptographicValidationFailed { 616 + issuer: issuer.to_string(), 617 + details: e.to_string(), 618 + } 619 + })?; 620 + 621 + Ok(()) 622 + } 623 + } 624 + 625 + #[cfg(test)] 626 + mod tests { 627 + use super::*; 628 + #[cfg(feature = "sqlite")] 629 + use crate::storage::{LocalFileStorage, SqliteStorage, SqliteStorageDidDocumentStorage}; 630 + use atproto_identity::model::Document; 631 + use serde_json::json; 632 + use std::collections::HashMap; 633 + 634 + #[cfg(feature = "sqlite")] 635 + #[tokio::test] 636 + async fn test_validate_signatures_no_signatures_field() { 637 + let config = Arc::new(Config::default()); 638 + let storage = Arc::new(SqliteStorage::new( 639 + sqlx::SqlitePool::connect(":memory:").await.unwrap(), 640 + )); 641 + let identity_resolver = create_mock_identity_resolver(); 642 + let document_storage = Arc::new(SqliteStorageDidDocumentStorage::new(storage.clone())); 643 + let http_client = reqwest::Client::new(); 644 + 645 + let processor = BadgeProcessor::new( 646 + storage as Arc<dyn Storage>, 647 + config, 648 + identity_resolver, 649 + document_storage as Arc<dyn DidDocumentStorage + Send + Sync>, 650 + http_client, 651 + create_test_file_storage(), 652 + ); 653 + 654 + // Record without signatures field 655 + let record = json!({ 656 + "did": "did:plc:test", 657 + "badge": { 658 + "$type": "strongRef", 659 + "uri": "at://did:plc:issuer/community.lexicon.badge.definition/test", 660 + "cid": "bafyreiabc123" 661 + }, 662 + "issued": "2023-01-01T00:00:00Z" 663 + }); 664 + 665 + let result = processor 666 + .validate_signatures(&record, "did:plc:test", "community.lexicon.badge.award") 667 + .await; 668 + 669 + // Should return error because no signatures field 670 + assert!(result.is_err()); 671 + if let Err(ShowcaseError::ProcessNoSignaturesField) = result { 672 + // Expected error 673 + } else { 674 + panic!("Expected ProcessNoSignaturesField error"); 675 + } 676 + } 677 + 678 + #[cfg(feature = "sqlite")] 679 + #[tokio::test] 680 + async fn test_validate_signatures_untrusted_issuer() { 681 + let mut config = Config::default(); 682 + config.badge_issuers = vec!["did:plc:trusted".to_string()]; 683 + let config = Arc::new(config); 684 + 685 + let storage = Arc::new(SqliteStorage::new( 686 + sqlx::SqlitePool::connect(":memory:").await.unwrap(), 687 + )); 688 + let identity_resolver = create_mock_identity_resolver(); 689 + let document_storage = Arc::new(SqliteStorageDidDocumentStorage::new(storage.clone())); 690 + let http_client = reqwest::Client::new(); 691 + 692 + let processor = BadgeProcessor::new( 693 + storage as Arc<dyn Storage>, 694 + config, 695 + identity_resolver, 696 + document_storage as Arc<dyn DidDocumentStorage + Send + Sync>, 697 + http_client, 698 + create_test_file_storage(), 699 + ); 700 + 701 + // Record with signature from untrusted issuer 702 + let record = json!({ 703 + "did": "did:plc:test", 704 + "badge": { 705 + "$type": "strongRef", 706 + "uri": "at://did:plc:issuer/community.lexicon.badge.definition/test", 707 + "cid": "bafyreiabc123" 708 + }, 709 + "issued": "2023-01-01T00:00:00Z", 710 + "signatures": [{ 711 + "issuer": "did:plc:untrusted", 712 + "issuedAt": "2023-01-01T00:00:00Z", 713 + "signature": "mEiDqZ4..." 714 + }] 715 + }); 716 + 717 + let result = processor 718 + .validate_signatures(&record, "did:plc:test", "community.lexicon.badge.award") 719 + .await; 720 + 721 + // Should succeed but return empty list (untrusted issuer ignored) 722 + assert!(result.is_ok()); 723 + assert_eq!(result.unwrap().len(), 0); 724 + } 725 + 726 + #[cfg(feature = "sqlite")] 727 + #[tokio::test] 728 + async fn test_validate_signatures_no_did_keys() { 729 + let mut config = Config::default(); 730 + config.badge_issuers = vec!["did:plc:trusted".to_string()]; 731 + let config = Arc::new(config); 732 + 733 + let storage = Arc::new(SqliteStorage::new( 734 + sqlx::SqlitePool::connect(":memory:").await.unwrap(), 735 + )); 736 + storage.migrate().await.unwrap(); // Initialize database tables 737 + 738 + let identity_resolver = create_mock_identity_resolver(); 739 + let document_storage = Arc::new(SqliteStorageDidDocumentStorage::new(storage.clone())); 740 + let http_client = reqwest::Client::new(); 741 + 742 + // Create a mock DID document with no keys (using a Document that has an empty did_keys() result) 743 + let document = Document { 744 + id: "did:plc:trusted".to_string(), 745 + also_known_as: vec![], 746 + service: vec![], 747 + verification_method: vec![], // Empty verification methods means no keys 748 + extra: HashMap::new(), 749 + }; 750 + 751 + // Store the document in our storage 752 + document_storage.store_document(document).await.unwrap(); 753 + 754 + let processor = BadgeProcessor::new( 755 + storage as Arc<dyn Storage>, 756 + config, 757 + identity_resolver, 758 + document_storage as Arc<dyn DidDocumentStorage + Send + Sync>, 759 + http_client, 760 + create_test_file_storage(), 761 + ); 762 + 763 + // Record with signature from trusted issuer but no valid keys in DID document 764 + let record = json!({ 765 + "did": "did:plc:test", 766 + "badge": { 767 + "$type": "strongRef", 768 + "uri": "at://did:plc:issuer/community.lexicon.badge.definition/test", 769 + "cid": "bafyreiabc123" 770 + }, 771 + "issued": "2023-01-01T00:00:00Z", 772 + "signatures": [{ 773 + "issuer": "did:plc:trusted", 774 + "issuedAt": "2023-01-01T00:00:00Z", 775 + "signature": "mEiDqZ4..." 776 + }] 777 + }); 778 + 779 + let result = processor 780 + .validate_signatures(&record, "did:plc:test", "community.lexicon.badge.award") 781 + .await; 782 + 783 + // Should succeed but return empty list (no valid keys to verify with) 784 + assert!(result.is_ok()); 785 + assert_eq!(result.unwrap().len(), 0); 786 + } 787 + 788 + #[cfg(feature = "sqlite")] 789 + #[tokio::test] 790 + async fn test_image_validation_too_large() { 791 + let config = Arc::new(Config::default()); 792 + let storage = Arc::new(SqliteStorage::new( 793 + sqlx::SqlitePool::connect(":memory:").await.unwrap(), 794 + )); 795 + let identity_resolver = create_mock_identity_resolver(); 796 + let document_storage = Arc::new(SqliteStorageDidDocumentStorage::new(storage.clone())); 797 + let http_client = reqwest::Client::new(); 798 + 799 + let _processor = BadgeProcessor::new( 800 + storage, 801 + config, 802 + identity_resolver, 803 + document_storage, 804 + http_client, 805 + create_test_file_storage(), 806 + ); 807 + 808 + // Create test image bytes larger than 3MB 809 + let large_image_bytes = vec![0u8; 4 * 1024 * 1024]; // 4MB 810 + 811 + // We can't easily test this without mocking get_blob, but we can test the size check logic directly 812 + assert!(large_image_bytes.len() > 3 * 1024 * 1024); 813 + } 814 + 815 + #[tokio::test] 816 + async fn test_image_validation_unsupported_format() { 817 + // This would require creating actual image bytes of unsupported format 818 + // For now, we can verify the error types exist and compile correctly 819 + let error = ShowcaseError::ProcessUnsupportedImageFormat { 820 + format: "BMP".to_string(), 821 + }; 822 + assert!(error.to_string().contains("Unsupported image format")); 823 + } 824 + 825 + #[tokio::test] 826 + async fn test_image_validation_too_small() { 827 + let error = ShowcaseError::ProcessImageTooSmall { 828 + width: 256, 829 + height: 256, 830 + }; 831 + assert!(error.to_string().contains("256x256")); 832 + assert!(error.to_string().contains("minimum is 512x512")); 833 + } 834 + 835 + #[tokio::test] 836 + async fn test_image_validation_width_too_small_after_resize() { 837 + let error = ShowcaseError::ProcessImageWidthTooSmall { width: 300 }; 838 + assert!(error.to_string().contains("300")); 839 + assert!(error.to_string().contains("minimum is 512")); 840 + } 841 + 842 + #[tokio::test] 843 + async fn test_center_crop_logic() { 844 + // Test the center crop calculation logic 845 + // If we have an image that's 1024x512 after resize, it should be center cropped to 512x512 846 + let original_width = 1024u32; 847 + let target_width = 512u32; 848 + 849 + // This is the same calculation used in download_and_process_image 850 + let crop_x = (original_width - target_width) / 2; 851 + 852 + // Should crop from x=256 to get the center 512 pixels 853 + assert_eq!(crop_x, 256); 854 + 855 + // Test with different widths 856 + let wide_width = 768u32; 857 + let crop_x_wide = (wide_width - target_width) / 2; 858 + assert_eq!(crop_x_wide, 128); // Should crop 128 pixels from each side 859 + } 860 + 861 + fn create_mock_identity_resolver() -> IdentityResolver { 862 + // Create a mock resolver - in a real test, you'd want to properly mock this 863 + // For now, we'll create one with default DNS resolver and HTTP client 864 + use atproto_identity::resolve::{InnerIdentityResolver, create_resolver}; 865 + 866 + let dns_resolver = create_resolver(&[]); 867 + let http_client = reqwest::Client::new(); 868 + 869 + IdentityResolver(Arc::new(InnerIdentityResolver { 870 + dns_resolver, 871 + http_client, 872 + plc_hostname: "plc.directory".to_string(), 873 + })) 874 + } 875 + 876 + #[cfg(feature = "sqlite")] 877 + fn create_test_file_storage() -> Arc<dyn FileStorage> { 878 + // Create a temporary directory for test file storage 879 + // Note: In a real test, you'd want to ensure this gets cleaned up properly 880 + // For simplicity, we'll use a simple path that gets cleaned up by the OS 881 + let temp_dir = "/tmp/showcase_test_storage"; 882 + Arc::new(LocalFileStorage::new(temp_dir.to_string())) 883 + } 884 + }
+440
src/storage/file_storage.rs
··· 1 + use crate::errors::Result; 2 + use async_trait::async_trait; 3 + use std::path::Path; 4 + 5 + #[cfg(feature = "s3")] 6 + use minio::s3::{Client as MinioClient, creds::StaticProvider, http::BaseUrl, types::S3Api}; 7 + 8 + #[cfg(feature = "s3")] 9 + use tracing::{debug, error}; 10 + 11 + /// Parse an S3 URL in the format: s3://[key]:[secret]@hostname/bucket[/optional_prefix] 12 + /// Returns (endpoint, access_key, secret_key, bucket, prefix) 13 + #[cfg(feature = "s3")] 14 + pub fn parse_s3_url(url: &str) -> Result<(String, String, String, String, Option<String>)> { 15 + if !url.starts_with("s3://") { 16 + return Err(crate::errors::ShowcaseError::ConfigS3UrlInvalid { 17 + details: format!("Invalid S3 URL format: {}", url), 18 + }); 19 + } 20 + 21 + let url_without_scheme = &url[5..]; // Remove "s3://" 22 + 23 + // Split by '@' to separate credentials from hostname/path 24 + let parts: Vec<&str> = url_without_scheme.splitn(2, '@').collect(); 25 + if parts.len() != 2 { 26 + return Err(crate::errors::ShowcaseError::ConfigS3UrlInvalid { 27 + details: format!("Invalid S3 URL format - missing @ separator: {}", url), 28 + }); 29 + } 30 + 31 + let credentials = parts[0]; 32 + let hostname_and_path = parts[1]; 33 + 34 + // Parse credentials: key:secret 35 + let cred_parts: Vec<&str> = credentials.splitn(2, ':').collect(); 36 + if cred_parts.len() != 2 { 37 + return Err(crate::errors::ShowcaseError::ConfigS3UrlInvalid { 38 + details: format!( 39 + "Invalid S3 URL format - credentials must be key:secret: {}", 40 + url 41 + ), 42 + }); 43 + } 44 + 45 + let access_key = cred_parts[0].to_string(); 46 + let secret_key = cred_parts[1].to_string(); 47 + 48 + // Parse hostname and path: hostname/bucket[/prefix] 49 + let path_parts: Vec<&str> = hostname_and_path.splitn(2, '/').collect(); 50 + if path_parts.len() != 2 { 51 + return Err(crate::errors::ShowcaseError::ConfigS3UrlInvalid { 52 + details: format!("Invalid S3 URL format - must include bucket: {}", url), 53 + }); 54 + } 55 + 56 + let hostname = path_parts[0].to_string(); 57 + let bucket_and_prefix = path_parts[1]; 58 + 59 + // Split bucket from optional prefix 60 + let bucket_parts: Vec<&str> = bucket_and_prefix.splitn(2, '/').collect(); 61 + let bucket = bucket_parts[0].to_string(); 62 + let prefix = if bucket_parts.len() > 1 && !bucket_parts[1].is_empty() { 63 + Some(bucket_parts[1].to_string()) 64 + } else { 65 + None 66 + }; 67 + 68 + let endpoint = if hostname.starts_with("http://") || hostname.starts_with("https://") { 69 + hostname 70 + } else { 71 + format!("https://{}", hostname) 72 + }; 73 + 74 + tracing::debug!(?endpoint, ?access_key, ?secret_key, ?bucket, ?prefix, "parsed s3 url"); 75 + 76 + Ok((endpoint, access_key, secret_key, bucket, prefix)) 77 + } 78 + 79 + /// File storage trait defining async file operations 80 + /// 81 + /// This trait provides basic file operations for storing and retrieving files 82 + /// in an async and thread-safe manner. 83 + #[async_trait] 84 + pub trait FileStorage: Send + Sync { 85 + /// Check if a file exists at the given path 86 + async fn file_exists(&self, path: &str) -> Result<bool>; 87 + 88 + /// Write bytes to a file at the given path 89 + /// 90 + /// Creates parent directories if they don't exist 91 + async fn write_file(&self, path: &str, data: &[u8]) -> Result<()>; 92 + 93 + /// Read bytes from a file at the given path 94 + async fn read_file(&self, path: &str) -> Result<Vec<u8>>; 95 + 96 + /// Create directories for the given path if they don't exist 97 + async fn create_directories(&self, path: &str) -> Result<()>; 98 + } 99 + 100 + /// Local file system implementation of FileStorage 101 + /// 102 + /// Uses a base directory for all file operations and provides thread-safe 103 + /// access to the local file system. 104 + pub struct LocalFileStorage { 105 + base_dir: String, 106 + } 107 + 108 + impl LocalFileStorage { 109 + /// Create a new LocalFileStorage with the given base directory 110 + pub fn new(base_dir: String) -> Self { 111 + Self { base_dir } 112 + } 113 + 114 + /// Get the full path by combining base directory with relative path 115 + fn get_full_path(&self, path: &str) -> String { 116 + if path.starts_with('/') { 117 + // Absolute path - use as-is for security (don't escape base_dir) 118 + format!("{}{}", self.base_dir, path) 119 + } else { 120 + // Relative path - combine with base_dir 121 + format!("{}/{}", self.base_dir, path) 122 + } 123 + } 124 + } 125 + 126 + #[async_trait] 127 + impl FileStorage for LocalFileStorage { 128 + async fn file_exists(&self, path: &str) -> Result<bool> { 129 + let full_path = self.get_full_path(path); 130 + Ok(Path::new(&full_path).exists()) 131 + } 132 + 133 + async fn write_file(&self, path: &str, data: &[u8]) -> Result<()> { 134 + let full_path = self.get_full_path(path); 135 + 136 + // Create parent directories if they don't exist 137 + if let Some(parent) = Path::new(&full_path).parent() { 138 + tokio::fs::create_dir_all(parent).await?; 139 + } 140 + 141 + tokio::fs::write(&full_path, data).await?; 142 + Ok(()) 143 + } 144 + 145 + async fn read_file(&self, path: &str) -> Result<Vec<u8>> { 146 + let full_path = self.get_full_path(path); 147 + let data = tokio::fs::read(&full_path).await?; 148 + Ok(data) 149 + } 150 + 151 + async fn create_directories(&self, path: &str) -> Result<()> { 152 + let full_path = self.get_full_path(path); 153 + if let Some(parent) = Path::new(&full_path).parent() { 154 + tokio::fs::create_dir_all(parent).await?; 155 + } 156 + Ok(()) 157 + } 158 + } 159 + 160 + /// S3-compatible object storage implementation of FileStorage 161 + /// 162 + /// Uses MinIO client to provide file storage operations against S3-compatible 163 + /// object storage endpoints like MinIO, AWS S3, etc. 164 + #[cfg(feature = "s3")] 165 + pub struct S3FileStorage { 166 + client: MinioClient, 167 + bucket: String, 168 + prefix: Option<String>, 169 + } 170 + 171 + #[cfg(feature = "s3")] 172 + impl S3FileStorage { 173 + /// Create a new S3FileStorage with the given credentials and bucket information 174 + pub fn new( 175 + endpoint: String, 176 + access_key: String, 177 + secret_key: String, 178 + bucket: String, 179 + prefix: Option<String>, 180 + ) -> Result<Self> { 181 + let base_url: BaseUrl = endpoint.parse().unwrap(); 182 + tracing::debug!(?base_url, "s3 file storage base url"); 183 + 184 + let static_provider = StaticProvider::new(&access_key, &secret_key, None); 185 + tracing::debug!(?static_provider, "s3 file storage static provider"); 186 + 187 + let client = MinioClient::new(base_url, Some(Box::new(static_provider)), None, None) 188 + .map_err( 189 + |e| crate::errors::ShowcaseError::StorageFileOperationFailed { 190 + operation: format!("Failed to create S3 client: {}", e), 191 + }, 192 + )?; 193 + 194 + Ok(Self { 195 + client, 196 + bucket, 197 + prefix, 198 + }) 199 + } 200 + 201 + /// Get the full object key by combining prefix with path 202 + fn get_object_key(&self, path: &str) -> String { 203 + match &self.prefix { 204 + Some(prefix) => { 205 + if path.starts_with('/') { 206 + format!("/{prefix}{path}") 207 + } else { 208 + format!("/{prefix}/{path}") 209 + } 210 + } 211 + None => { 212 + if path.starts_with('/') { 213 + path.to_string() 214 + } else { 215 + format!("/{path}") 216 + } 217 + } 218 + } 219 + } 220 + } 221 + 222 + #[cfg(feature = "s3")] 223 + #[async_trait] 224 + impl FileStorage for S3FileStorage { 225 + async fn file_exists(&self, path: &str) -> Result<bool> { 226 + use minio::s3::error::ErrorCode; 227 + 228 + let object_key = self.get_object_key(path); 229 + debug!( 230 + "Checking if S3 object exists: {object_key}" 231 + ); 232 + 233 + match self 234 + .client 235 + .stat_object(&self.bucket, &object_key) 236 + .send() 237 + .await 238 + { 239 + Ok(_) => Ok(true), 240 + Err(minio::s3::error::Error::S3Error(ref s3_err)) 241 + if s3_err.code == ErrorCode::NoSuchKey => 242 + { 243 + Ok(false) 244 + } 245 + Err(e) => { 246 + error!("Failed to check S3 object existence: {}", e); 247 + Err(crate::errors::ShowcaseError::StorageFileOperationFailed { 248 + operation: format!("Failed to check if S3 object exists: {}", e), 249 + }) 250 + } 251 + } 252 + } 253 + 254 + async fn write_file(&self, path: &str, data: &[u8]) -> Result<()> { 255 + use minio::s3::segmented_bytes::SegmentedBytes; 256 + 257 + let object_key = self.get_object_key(path); 258 + debug!( 259 + "Writing S3 object: {}/{} ({} bytes)", 260 + self.bucket, 261 + object_key, 262 + data.len() 263 + ); 264 + 265 + let put_data = SegmentedBytes::from(bytes::Bytes::copy_from_slice(data)); 266 + 267 + self.client 268 + .put_object(&self.bucket, &object_key, put_data) 269 + .send() 270 + .await 271 + .map_err( 272 + |e| crate::errors::ShowcaseError::StorageFileOperationFailed { 273 + operation: format!("Failed to write S3 object: {}", e), 274 + }, 275 + )?; 276 + 277 + debug!( 278 + "Successfully wrote S3 object: {}/{}", 279 + self.bucket, object_key 280 + ); 281 + Ok(()) 282 + } 283 + 284 + async fn read_file(&self, path: &str) -> Result<Vec<u8>> { 285 + let object_key = self.get_object_key(path); 286 + debug!("Reading S3 object: {}/{}", self.bucket, object_key); 287 + 288 + let response = self 289 + .client 290 + .get_object(&self.bucket, &object_key) 291 + .send() 292 + .await 293 + .map_err( 294 + |e| crate::errors::ShowcaseError::StorageFileOperationFailed { 295 + operation: format!("Failed to read S3 object: {}", e), 296 + }, 297 + )?; 298 + 299 + let data = response 300 + .content 301 + .to_segmented_bytes() 302 + .await 303 + .map_err( 304 + |e| crate::errors::ShowcaseError::StorageFileOperationFailed { 305 + operation: format!("Failed to read S3 object data: {}", e), 306 + }, 307 + )? 308 + .to_bytes(); 309 + 310 + debug!( 311 + "Successfully read S3 object: {}/{} ({} bytes)", 312 + self.bucket, 313 + object_key, 314 + data.len() 315 + ); 316 + Ok(data.to_vec()) 317 + } 318 + 319 + async fn create_directories(&self, _path: &str) -> Result<()> { 320 + // Object storage doesn't require directory creation 321 + // Directories are implicit based on object keys 322 + Ok(()) 323 + } 324 + } 325 + 326 + #[cfg(test)] 327 + mod tests { 328 + use super::*; 329 + use tempfile::TempDir; 330 + 331 + #[tokio::test] 332 + async fn test_local_file_storage_write_and_read() { 333 + let temp_dir = TempDir::new().unwrap(); 334 + let base_dir = temp_dir.path().to_str().unwrap().to_string(); 335 + let storage = LocalFileStorage::new(base_dir); 336 + 337 + let test_data = b"Hello, World!"; 338 + let test_path = "test/file.txt"; 339 + 340 + // Test write 341 + storage.write_file(test_path, test_data).await.unwrap(); 342 + 343 + // Test file exists 344 + assert!(storage.file_exists(test_path).await.unwrap()); 345 + 346 + // Test read 347 + let read_data = storage.read_file(test_path).await.unwrap(); 348 + assert_eq!(read_data, test_data); 349 + } 350 + 351 + #[tokio::test] 352 + async fn test_local_file_storage_create_directories() { 353 + let temp_dir = TempDir::new().unwrap(); 354 + let base_dir = temp_dir.path().to_str().unwrap().to_string(); 355 + let storage = LocalFileStorage::new(base_dir); 356 + 357 + let nested_path = "deeply/nested/directory/structure/file.txt"; 358 + 359 + // This should create all necessary directories 360 + storage.create_directories(nested_path).await.unwrap(); 361 + 362 + // Verify the parent directory was created 363 + let full_path = storage.get_full_path(nested_path); 364 + let parent_path = std::path::Path::new(&full_path).parent().unwrap(); 365 + assert!(parent_path.exists()); 366 + } 367 + 368 + #[tokio::test] 369 + async fn test_local_file_storage_file_not_exists() { 370 + let temp_dir = TempDir::new().unwrap(); 371 + let base_dir = temp_dir.path().to_str().unwrap().to_string(); 372 + let storage = LocalFileStorage::new(base_dir); 373 + 374 + let non_existent_path = "does/not/exist.txt"; 375 + assert!(!storage.file_exists(non_existent_path).await.unwrap()); 376 + } 377 + 378 + #[tokio::test] 379 + async fn test_local_file_storage_absolute_path_handling() { 380 + let temp_dir = TempDir::new().unwrap(); 381 + let base_dir = temp_dir.path().to_str().unwrap().to_string(); 382 + let storage = LocalFileStorage::new(base_dir.clone()); 383 + 384 + let relative_path = "relative/path.txt"; 385 + let absolute_path = "/absolute/path.txt"; 386 + 387 + let relative_full = storage.get_full_path(relative_path); 388 + let absolute_full = storage.get_full_path(absolute_path); 389 + 390 + assert!(relative_full.starts_with(&base_dir)); 391 + assert!(!relative_full.starts_with("//")); 392 + 393 + assert!(absolute_full.starts_with(&base_dir)); 394 + assert!(absolute_full.contains("/absolute/path.txt")); 395 + } 396 + 397 + #[cfg(feature = "s3")] 398 + #[tokio::test] 399 + async fn test_s3_file_storage_object_key_generation() { 400 + // Test prefix handling for object keys 401 + let base_url: BaseUrl = "http://localhost:9000".parse().expect("base_url parses"); 402 + let storage_with_prefix = S3FileStorage { 403 + client: MinioClient::new(base_url.clone(), None, None, None).unwrap(), 404 + bucket: "test-bucket".to_string(), 405 + prefix: Some("badges".to_string()), 406 + }; 407 + 408 + assert_eq!( 409 + storage_with_prefix.get_object_key("test.png"), 410 + "/badges/test.png" 411 + ); 412 + assert_eq!( 413 + storage_with_prefix.get_object_key("/test.png"), 414 + "/badges/test.png" 415 + ); 416 + 417 + let storage_no_prefix = S3FileStorage { 418 + client: MinioClient::new(base_url, None, None, None).unwrap(), 419 + bucket: "test-bucket".to_string(), 420 + prefix: None, 421 + }; 422 + 423 + assert_eq!(storage_no_prefix.get_object_key("test.png"), "/test.png"); 424 + assert_eq!(storage_no_prefix.get_object_key("/test.png"), "/test.png"); 425 + } 426 + 427 + #[cfg(feature = "s3")] 428 + #[test] 429 + fn test_parse_s3_url() { 430 + // Test parsing the specific URL provided in the request 431 + let url = "s3://the_key:the_secret@example.com/showcase-badges"; 432 + let result = parse_s3_url(url).unwrap(); 433 + 434 + assert_eq!(result.0, "http://example.com"); // endpoint 435 + assert_eq!(result.1, "the_key"); // access_key 436 + assert_eq!(result.2, "the_secret"); // secret_key 437 + assert_eq!(result.3, "showcase-badges"); // bucket 438 + assert_eq!(result.4, None); // prefix 439 + } 440 + }
+215
src/storage/mod.rs
··· 1 + //! Database storage implementations for badges, awards, and identities. 2 + //! 3 + //! Provides trait-based storage abstraction with SQLite and PostgreSQL backends, 4 + //! plus file storage for badge images with local and S3 support. 5 + 6 + use crate::errors::Result; 7 + use async_trait::async_trait; 8 + use chrono::{DateTime, Utc}; 9 + use serde::{Deserialize, Serialize}; 10 + use serde_json::Value; 11 + 12 + // Re-export storage implementations 13 + /// File storage implementations for badge images. 14 + pub mod file_storage; 15 + #[cfg(feature = "postgres")] 16 + /// PostgreSQL storage implementation. 17 + pub mod postgres; 18 + #[cfg(feature = "sqlite")] 19 + /// SQLite storage implementation. 20 + pub mod sqlite; 21 + 22 + #[cfg(feature = "s3")] 23 + pub use file_storage::S3FileStorage; 24 + pub use file_storage::{FileStorage, LocalFileStorage}; 25 + #[cfg(feature = "postgres")] 26 + pub use postgres::*; 27 + #[cfg(feature = "sqlite")] 28 + pub use sqlite::*; 29 + 30 + /// Identity information for a DID. 31 + #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] 32 + pub struct Identity { 33 + /// Decentralized identifier. 34 + pub did: String, 35 + /// AT Protocol handle. 36 + pub handle: String, 37 + /// Full DID document JSON. 38 + pub record: Value, 39 + /// Record creation timestamp. 40 + pub created_at: DateTime<Utc>, 41 + /// Record last update timestamp. 42 + pub updated_at: DateTime<Utc>, 43 + } 44 + 45 + /// Badge award record. 46 + #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] 47 + pub struct Award { 48 + /// AT-URI of the award record. 49 + pub aturi: String, 50 + /// Content identifier for the award record. 51 + pub cid: String, 52 + /// DID of the award recipient. 53 + pub did: String, 54 + /// AT-URI of the associated badge. 55 + pub badge: String, 56 + /// Content identifier of the badge record. 57 + pub badge_cid: String, 58 + /// Human-readable badge name. 59 + pub badge_name: String, 60 + /// JSON array of validated issuer DIDs. 61 + pub validated_issuers: Value, 62 + /// Record creation timestamp. 63 + pub created_at: DateTime<Utc>, 64 + /// Record last update timestamp. 65 + pub updated_at: DateTime<Utc>, 66 + /// Full JSON record of the award. 67 + pub record: Value, 68 + } 69 + 70 + /// Badge definition record. 71 + #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] 72 + pub struct Badge { 73 + /// AT-URI of the badge definition. 74 + pub aturi: String, 75 + /// Content identifier for the badge record. 76 + pub cid: String, 77 + /// Human-readable badge name. 78 + pub name: String, 79 + /// Optional image reference (blob CID). 80 + pub image: Option<String>, 81 + /// Record creation timestamp. 82 + pub created_at: DateTime<Utc>, 83 + /// Record last update timestamp. 84 + pub updated_at: DateTime<Utc>, 85 + /// Number of awards using this badge. 86 + pub count: i64, 87 + /// Full JSON record of the badge definition. 88 + pub record: Value, 89 + } 90 + 91 + /// Award with enriched badge and identity information. 92 + #[derive(Debug, Clone)] 93 + pub struct AwardWithBadge { 94 + /// The award record. 95 + pub award: Award, 96 + /// Associated badge information if available. 97 + pub badge: Option<Badge>, 98 + /// Recipient identity information if available. 99 + pub identity: Option<Identity>, 100 + /// Identity information for award signers. 101 + pub signer_identities: Vec<Identity>, 102 + } 103 + 104 + /// Storage trait defining the interface for badge storage operations 105 + #[async_trait] 106 + pub trait Storage: Send + Sync { 107 + /// Run database migrations 108 + async fn migrate(&self) -> Result<()>; 109 + 110 + /// Insert or update an identity 111 + async fn upsert_identity(&self, identity: &Identity) -> Result<()>; 112 + 113 + /// Get an identity by DID 114 + async fn get_identity_by_did(&self, did: &str) -> Result<Option<Identity>>; 115 + 116 + /// Get an identity by handle 117 + async fn get_identity_by_handle(&self, handle: &str) -> Result<Option<Identity>>; 118 + 119 + /// Insert or update a badge 120 + async fn upsert_badge(&self, badge: &Badge) -> Result<()>; 121 + 122 + /// Get a badge by AT-URI and CID 123 + async fn get_badge(&self, aturi: &str, cid: &str) -> Result<Option<Badge>>; 124 + 125 + /// Increment the count for a badge 126 + async fn increment_badge_count(&self, aturi: &str, cid: &str) -> Result<()>; 127 + 128 + /// Decrement the count for a badge 129 + async fn decrement_badge_count(&self, aturi: &str, cid: &str) -> Result<()>; 130 + 131 + /// Insert or update an award, returns true if it's a new award 132 + async fn upsert_award(&self, award: &Award) -> Result<bool>; 133 + 134 + /// Get an award by AT-URI 135 + async fn get_award(&self, aturi: &str) -> Result<Option<Award>>; 136 + 137 + /// Delete an award by AT-URI 138 + async fn delete_award(&self, aturi: &str) -> Result<Option<Award>>; 139 + 140 + /// Trim awards for a DID to keep only the most recent ones 141 + async fn trim_awards_for_did(&self, did: &str, max_count: i64) -> Result<()>; 142 + 143 + /// Get recent awards with enriched badge and identity information 144 + async fn get_recent_awards(&self, limit: i64) -> Result<Vec<AwardWithBadge>>; 145 + 146 + /// Get awards for a specific DID with enriched information 147 + async fn get_awards_for_did(&self, did: &str, limit: i64) -> Result<Vec<AwardWithBadge>>; 148 + } 149 + 150 + #[cfg(test)] 151 + mod tests { 152 + use super::*; 153 + use chrono::Utc; 154 + use serde_json::json; 155 + 156 + #[test] 157 + fn test_badge_with_record() { 158 + let record = json!({ 159 + "name": "Test Badge", 160 + "description": "A test badge for unit testing", 161 + "image": { 162 + "$type": "blob", 163 + "ref": {"$link": "bafkreiabc123"}, 164 + "mimeType": "image/png", 165 + "size": 1024 166 + } 167 + }); 168 + 169 + let badge = Badge { 170 + aturi: "at://did:plc:test/community.lexicon.badge.definition/test".to_string(), 171 + cid: "bafyreiabc123".to_string(), 172 + name: "Test Badge".to_string(), 173 + image: Some("bafkreiabc123".to_string()), 174 + created_at: Utc::now(), 175 + updated_at: Utc::now(), 176 + count: 0, 177 + record: record.clone(), 178 + }; 179 + 180 + // Verify the badge contains the record 181 + assert_eq!(badge.record, record); 182 + assert_eq!(badge.name, "Test Badge"); 183 + assert_eq!(badge.record["name"], "Test Badge"); 184 + assert_eq!(badge.record["description"], "A test badge for unit testing"); 185 + } 186 + 187 + #[test] 188 + fn test_badge_record_serialization() { 189 + let record = json!({ 190 + "name": "Serialization Test", 191 + "description": "Testing JSON serialization", 192 + "issuer": "did:plc:issuer" 193 + }); 194 + 195 + let badge = Badge { 196 + aturi: "at://did:plc:test/community.lexicon.badge.definition/serial".to_string(), 197 + cid: "bafyreiserial".to_string(), 198 + name: "Serialization Test".to_string(), 199 + image: None, 200 + created_at: Utc::now(), 201 + updated_at: Utc::now(), 202 + count: 5, 203 + record: record.clone(), 204 + }; 205 + 206 + // Test that we can serialize and deserialize the badge 207 + let serialized = serde_json::to_string(&badge).expect("Failed to serialize badge"); 208 + let deserialized: Badge = 209 + serde_json::from_str(&serialized).expect("Failed to deserialize badge"); 210 + 211 + assert_eq!(deserialized.record, record); 212 + assert_eq!(deserialized.name, badge.name); 213 + assert_eq!(deserialized.count, badge.count); 214 + } 215 + }
+770
src/storage/postgres.rs
··· 1 + use std::sync::Arc; 2 + 3 + use crate::errors::Result; 4 + use async_trait::async_trait; 5 + use atproto_identity::model::Document; 6 + use atproto_identity::storage::DidDocumentStorage; 7 + use chrono::Utc; 8 + use sqlx::postgres::PgPool; 9 + 10 + use super::{Award, AwardWithBadge, Badge, Identity, Storage}; 11 + 12 + /// PostgreSQL storage implementation for badge storage operations 13 + #[derive(Debug, Clone)] 14 + pub struct PostgresStorage { 15 + pool: PgPool, 16 + } 17 + 18 + impl PostgresStorage { 19 + /// Create a new PostgreSQL storage instance with the given connection pool. 20 + pub fn new(pool: PgPool) -> Self { 21 + Self { pool } 22 + } 23 + 24 + async fn migrate(&self) -> Result<()> { 25 + // Create identities table with JSONB for better JSON performance 26 + sqlx::query( 27 + r#" 28 + CREATE TABLE IF NOT EXISTS identities ( 29 + did TEXT PRIMARY KEY, 30 + handle TEXT NOT NULL, 31 + record JSONB NOT NULL, 32 + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 33 + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 34 + ); 35 + "#, 36 + ) 37 + .execute(&self.pool) 38 + .await?; 39 + 40 + // Create indexes for identities table 41 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_identities_handle ON identities(handle)") 42 + .execute(&self.pool) 43 + .await?; 44 + 45 + sqlx::query( 46 + "CREATE INDEX IF NOT EXISTS idx_identities_created_at ON identities(created_at)", 47 + ) 48 + .execute(&self.pool) 49 + .await?; 50 + 51 + sqlx::query( 52 + "CREATE INDEX IF NOT EXISTS idx_identities_updated_at ON identities(updated_at)", 53 + ) 54 + .execute(&self.pool) 55 + .await?; 56 + 57 + // Create awards table with JSONB 58 + sqlx::query( 59 + r#" 60 + CREATE TABLE IF NOT EXISTS awards ( 61 + aturi TEXT PRIMARY KEY, 62 + cid TEXT NOT NULL, 63 + did TEXT NOT NULL, 64 + badge TEXT NOT NULL, 65 + badge_cid TEXT NOT NULL, 66 + badge_name TEXT NOT NULL, 67 + validated_issuers JSONB NOT NULL, 68 + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 69 + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 70 + record JSONB NOT NULL 71 + ); 72 + "#, 73 + ) 74 + .execute(&self.pool) 75 + .await?; 76 + 77 + // Create indexes for awards table 78 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_awards_did ON awards(did)") 79 + .execute(&self.pool) 80 + .await?; 81 + 82 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_awards_badge ON awards(badge)") 83 + .execute(&self.pool) 84 + .await?; 85 + 86 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_awards_badge_cid ON awards(badge_cid)") 87 + .execute(&self.pool) 88 + .await?; 89 + 90 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_awards_created_at ON awards(created_at)") 91 + .execute(&self.pool) 92 + .await?; 93 + 94 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_awards_updated_at ON awards(updated_at)") 95 + .execute(&self.pool) 96 + .await?; 97 + 98 + // Create badges table with JSONB and composite primary key 99 + sqlx::query( 100 + r#" 101 + CREATE TABLE IF NOT EXISTS badges ( 102 + aturi TEXT NOT NULL, 103 + cid TEXT NOT NULL, 104 + name TEXT NOT NULL, 105 + image TEXT, 106 + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 107 + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 108 + count BIGINT NOT NULL DEFAULT 0, 109 + record JSONB NOT NULL DEFAULT '{}'::jsonb, 110 + PRIMARY KEY (aturi, cid) 111 + ); 112 + "#, 113 + ) 114 + .execute(&self.pool) 115 + .await?; 116 + 117 + // Create indexes for badges table with JSONB path expressions 118 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_badges_aturi ON badges(aturi)") 119 + .execute(&self.pool) 120 + .await?; 121 + 122 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_badges_cid ON badges(cid)") 123 + .execute(&self.pool) 124 + .await?; 125 + 126 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_badges_created_at ON badges(created_at)") 127 + .execute(&self.pool) 128 + .await?; 129 + 130 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_badges_updated_at ON badges(updated_at)") 131 + .execute(&self.pool) 132 + .await?; 133 + 134 + sqlx::query( 135 + "CREATE INDEX IF NOT EXISTS idx_badges_record_name ON badges((record->>'name'))", 136 + ) 137 + .execute(&self.pool) 138 + .await?; 139 + 140 + Ok(()) 141 + } 142 + 143 + /// Insert or update an identity record in the database. 144 + pub async fn upsert_identity(&self, identity: &Identity) -> Result<()> { 145 + sqlx::query( 146 + r#" 147 + INSERT INTO identities (did, handle, record, created_at, updated_at) 148 + VALUES ($1, $2, $3, $4, $5) 149 + ON CONFLICT(did) DO UPDATE SET 150 + handle = EXCLUDED.handle, 151 + record = EXCLUDED.record, 152 + updated_at = EXCLUDED.updated_at 153 + "#, 154 + ) 155 + .bind(&identity.did) 156 + .bind(&identity.handle) 157 + .bind(&identity.record) 158 + .bind(identity.created_at) 159 + .bind(identity.updated_at) 160 + .execute(&self.pool) 161 + .await?; 162 + 163 + Ok(()) 164 + } 165 + 166 + async fn get_identity_by_did(&self, did: &str) -> Result<Option<Identity>> { 167 + let row = sqlx::query_as::<_, Identity>("SELECT * FROM identities WHERE did = $1") 168 + .bind(did) 169 + .fetch_optional(&self.pool) 170 + .await?; 171 + 172 + Ok(row) 173 + } 174 + 175 + async fn get_identity_by_handle(&self, handle: &str) -> Result<Option<Identity>> { 176 + let row = sqlx::query_as::<_, Identity>("SELECT * FROM identities WHERE handle = $1") 177 + .bind(handle) 178 + .fetch_optional(&self.pool) 179 + .await?; 180 + 181 + Ok(row) 182 + } 183 + 184 + async fn upsert_badge(&self, badge: &Badge) -> Result<()> { 185 + sqlx::query( 186 + r#" 187 + INSERT INTO badges (aturi, cid, name, image, created_at, updated_at, count, record) 188 + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) 189 + ON CONFLICT(aturi, cid) DO UPDATE SET 190 + name = EXCLUDED.name, 191 + image = EXCLUDED.image, 192 + updated_at = EXCLUDED.updated_at, 193 + record = EXCLUDED.record 194 + "#, 195 + ) 196 + .bind(&badge.aturi) 197 + .bind(&badge.cid) 198 + .bind(&badge.name) 199 + .bind(&badge.image) 200 + .bind(badge.created_at) 201 + .bind(badge.updated_at) 202 + .bind(badge.count) 203 + .bind(&badge.record) 204 + .execute(&self.pool) 205 + .await?; 206 + 207 + Ok(()) 208 + } 209 + 210 + async fn get_badge(&self, aturi: &str, cid: &str) -> Result<Option<Badge>> { 211 + let row = sqlx::query_as::<_, Badge>("SELECT * FROM badges WHERE aturi = $1 AND cid = $2") 212 + .bind(aturi) 213 + .bind(cid) 214 + .fetch_optional(&self.pool) 215 + .await?; 216 + 217 + Ok(row) 218 + } 219 + 220 + async fn increment_badge_count(&self, aturi: &str, cid: &str) -> Result<()> { 221 + sqlx::query( 222 + r#" 223 + UPDATE badges 224 + SET count = count + 1, updated_at = NOW() 225 + WHERE aturi = $1 AND cid = $2 226 + "#, 227 + ) 228 + .bind(aturi) 229 + .bind(cid) 230 + .execute(&self.pool) 231 + .await?; 232 + 233 + Ok(()) 234 + } 235 + 236 + async fn decrement_badge_count(&self, aturi: &str, cid: &str) -> Result<()> { 237 + sqlx::query( 238 + r#" 239 + UPDATE badges 240 + SET count = GREATEST(0, count - 1), updated_at = NOW() 241 + WHERE aturi = $1 AND cid = $2 242 + "#, 243 + ) 244 + .bind(aturi) 245 + .bind(cid) 246 + .execute(&self.pool) 247 + .await?; 248 + 249 + Ok(()) 250 + } 251 + 252 + async fn upsert_award(&self, award: &Award) -> Result<bool> { 253 + let existing = self.get_award(&award.aturi).await?; 254 + let is_new = existing.is_none(); 255 + 256 + sqlx::query( 257 + r#" 258 + INSERT INTO awards (aturi, cid, did, badge, badge_cid, badge_name, validated_issuers, created_at, updated_at, record) 259 + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) 260 + ON CONFLICT(aturi) DO UPDATE SET 261 + cid = EXCLUDED.cid, 262 + badge = EXCLUDED.badge, 263 + badge_cid = EXCLUDED.badge_cid, 264 + badge_name = EXCLUDED.badge_name, 265 + validated_issuers = EXCLUDED.validated_issuers, 266 + updated_at = EXCLUDED.updated_at, 267 + record = EXCLUDED.record 268 + "#, 269 + ) 270 + .bind(&award.aturi) 271 + .bind(&award.cid) 272 + .bind(&award.did) 273 + .bind(&award.badge) 274 + .bind(&award.badge_cid) 275 + .bind(&award.badge_name) 276 + .bind(&award.validated_issuers) 277 + .bind(award.created_at) 278 + .bind(award.updated_at) 279 + .bind(&award.record) 280 + .execute(&self.pool) 281 + .await?; 282 + 283 + Ok(is_new) 284 + } 285 + 286 + async fn get_award(&self, aturi: &str) -> Result<Option<Award>> { 287 + let row = sqlx::query_as::<_, Award>("SELECT * FROM awards WHERE aturi = $1") 288 + .bind(aturi) 289 + .fetch_optional(&self.pool) 290 + .await?; 291 + 292 + Ok(row) 293 + } 294 + 295 + async fn delete_award(&self, aturi: &str) -> Result<Option<Award>> { 296 + let award = self.get_award(aturi).await?; 297 + 298 + if award.is_some() { 299 + sqlx::query("DELETE FROM awards WHERE aturi = $1") 300 + .bind(aturi) 301 + .execute(&self.pool) 302 + .await?; 303 + } 304 + 305 + Ok(award) 306 + } 307 + 308 + async fn trim_awards_for_did(&self, did: &str, max_count: i64) -> Result<()> { 309 + sqlx::query( 310 + r#" 311 + DELETE FROM awards 312 + WHERE aturi IN ( 313 + SELECT aturi FROM awards 314 + WHERE did = $1 315 + ORDER BY updated_at DESC 316 + OFFSET $2 317 + ) 318 + "#, 319 + ) 320 + .bind(did) 321 + .bind(max_count) 322 + .execute(&self.pool) 323 + .await?; 324 + 325 + Ok(()) 326 + } 327 + 328 + async fn get_recent_awards(&self, limit: i64) -> Result<Vec<AwardWithBadge>> { 329 + let awards = 330 + sqlx::query_as::<_, Award>("SELECT * FROM awards ORDER BY updated_at DESC LIMIT $1") 331 + .bind(limit) 332 + .fetch_all(&self.pool) 333 + .await?; 334 + 335 + self.enrich_awards_with_details(awards).await 336 + } 337 + 338 + async fn get_awards_for_did(&self, did: &str, limit: i64) -> Result<Vec<AwardWithBadge>> { 339 + let awards = sqlx::query_as::<_, Award>( 340 + "SELECT * FROM awards WHERE did = $1 ORDER BY updated_at DESC LIMIT $2", 341 + ) 342 + .bind(did) 343 + .bind(limit) 344 + .fetch_all(&self.pool) 345 + .await?; 346 + 347 + self.enrich_awards_with_details(awards).await 348 + } 349 + 350 + async fn enrich_awards_with_details(&self, awards: Vec<Award>) -> Result<Vec<AwardWithBadge>> { 351 + let mut result = Vec::new(); 352 + 353 + for award in awards { 354 + let badge = self.get_badge(&award.badge, &award.badge_cid).await?; 355 + let identity = self.get_identity_by_did(&award.did).await?; 356 + 357 + let validated_issuers: Vec<String> = 358 + serde_json::from_value(award.validated_issuers.clone()).unwrap_or_default(); 359 + 360 + let mut signer_identities = Vec::new(); 361 + for issuer in validated_issuers { 362 + if let Ok(Some(signer_identity)) = self.get_identity_by_did(&issuer).await { 363 + signer_identities.push(signer_identity); 364 + } 365 + } 366 + 367 + result.push(AwardWithBadge { 368 + award, 369 + badge, 370 + identity, 371 + signer_identities, 372 + }); 373 + } 374 + 375 + Ok(result) 376 + } 377 + } 378 + 379 + #[async_trait] 380 + impl Storage for PostgresStorage { 381 + async fn migrate(&self) -> Result<()> { 382 + self.migrate().await 383 + } 384 + 385 + async fn upsert_identity(&self, identity: &Identity) -> Result<()> { 386 + self.upsert_identity(identity).await 387 + } 388 + 389 + async fn get_identity_by_did(&self, did: &str) -> Result<Option<Identity>> { 390 + self.get_identity_by_did(did).await 391 + } 392 + 393 + async fn get_identity_by_handle(&self, handle: &str) -> Result<Option<Identity>> { 394 + self.get_identity_by_handle(handle).await 395 + } 396 + 397 + async fn upsert_badge(&self, badge: &Badge) -> Result<()> { 398 + self.upsert_badge(badge).await 399 + } 400 + 401 + async fn get_badge(&self, aturi: &str, cid: &str) -> Result<Option<Badge>> { 402 + self.get_badge(aturi, cid).await 403 + } 404 + 405 + async fn increment_badge_count(&self, aturi: &str, cid: &str) -> Result<()> { 406 + self.increment_badge_count(aturi, cid).await 407 + } 408 + 409 + async fn decrement_badge_count(&self, aturi: &str, cid: &str) -> Result<()> { 410 + self.decrement_badge_count(aturi, cid).await 411 + } 412 + 413 + async fn upsert_award(&self, award: &Award) -> Result<bool> { 414 + self.upsert_award(award).await 415 + } 416 + 417 + async fn get_award(&self, aturi: &str) -> Result<Option<Award>> { 418 + self.get_award(aturi).await 419 + } 420 + 421 + async fn delete_award(&self, aturi: &str) -> Result<Option<Award>> { 422 + self.delete_award(aturi).await 423 + } 424 + 425 + async fn trim_awards_for_did(&self, did: &str, max_count: i64) -> Result<()> { 426 + self.trim_awards_for_did(did, max_count).await 427 + } 428 + 429 + async fn get_recent_awards(&self, limit: i64) -> Result<Vec<AwardWithBadge>> { 430 + self.get_recent_awards(limit).await 431 + } 432 + 433 + async fn get_awards_for_did(&self, did: &str, limit: i64) -> Result<Vec<AwardWithBadge>> { 434 + self.get_awards_for_did(did, limit).await 435 + } 436 + } 437 + 438 + /// PostgreSQL-specific DID document storage adapter 439 + pub struct PostgresStorageDidDocumentStorage { 440 + storage: Arc<PostgresStorage>, 441 + } 442 + 443 + impl PostgresStorageDidDocumentStorage { 444 + /// Create a new DID document storage instance backed by PostgreSQL. 445 + pub fn new(storage: Arc<PostgresStorage>) -> Self { 446 + Self { storage } 447 + } 448 + } 449 + 450 + #[async_trait] 451 + impl DidDocumentStorage for PostgresStorageDidDocumentStorage { 452 + async fn get_document_by_did(&self, did: &str) -> anyhow::Result<Option<Document>> { 453 + if let Some(identity) = self 454 + .storage 455 + .get_identity_by_did(did) 456 + .await 457 + .map_err(anyhow::Error::new)? 458 + { 459 + let document: Document = serde_json::from_value(identity.record)?; 460 + Ok(Some(document)) 461 + } else { 462 + Ok(None) 463 + } 464 + } 465 + 466 + async fn store_document(&self, doc: Document) -> anyhow::Result<()> { 467 + let handle = doc 468 + .also_known_as 469 + .first() 470 + .and_then(|aka| aka.strip_prefix("at://")) 471 + .unwrap_or("unknown.handle") 472 + .to_string(); 473 + 474 + // Create a simple JSON representation of the document 475 + let record = serde_json::json!(doc); 476 + 477 + let identity = Identity { 478 + did: doc.id.clone(), 479 + handle, 480 + record, 481 + created_at: Utc::now(), 482 + updated_at: Utc::now(), 483 + }; 484 + 485 + self.storage 486 + .upsert_identity(&identity) 487 + .await 488 + .map_err(anyhow::Error::new) 489 + } 490 + 491 + async fn delete_document_by_did(&self, _did: &str) -> anyhow::Result<()> { 492 + Ok(()) 493 + } 494 + } 495 + 496 + // PostgreSQL Migration Tests 497 + #[cfg(test)] 498 + mod postgres_migration_tests { 499 + use super::*; 500 + use sqlx::Row; 501 + use sqlx::postgres::{PgPoolOptions, PgRow}; 502 + use std::collections::HashMap; 503 + 504 + // Helper function to get PostgreSQL test database URL 505 + fn get_test_database_url() -> String { 506 + std::env::var("DATABASE_URL").unwrap_or_else(|_| { 507 + "postgresql://showcase:showcase_dev_password@localhost:5433/showcase_test".to_string() 508 + }) 509 + } 510 + 511 + // Helper function to create a test PostgreSQL storage instance 512 + async fn create_test_postgres_storage() -> Result<PostgresStorage> { 513 + let database_url = get_test_database_url(); 514 + let pool = PgPoolOptions::new() 515 + .max_connections(5) 516 + .connect(&database_url) 517 + .await 518 + .map_err(|e| crate::errors::ShowcaseError::StorageDatabaseFailed { 519 + operation: format!("Failed to connect to test database: {}", e), 520 + })?; 521 + 522 + Ok(PostgresStorage::new(pool)) 523 + } 524 + 525 + // Helper function to drop all test tables 526 + async fn drop_test_tables(storage: &PostgresStorage) -> Result<()> { 527 + sqlx::query("DROP TABLE IF EXISTS awards CASCADE") 528 + .execute(&storage.pool) 529 + .await?; 530 + sqlx::query("DROP TABLE IF EXISTS badges CASCADE") 531 + .execute(&storage.pool) 532 + .await?; 533 + sqlx::query("DROP TABLE IF EXISTS identities CASCADE") 534 + .execute(&storage.pool) 535 + .await?; 536 + Ok(()) 537 + } 538 + 539 + // Helper function to check if a table exists 540 + async fn table_exists(storage: &PostgresStorage, table_name: &str) -> Result<bool> { 541 + let row: (bool,) = sqlx::query_as( 542 + "SELECT EXISTS ( 543 + SELECT FROM information_schema.tables 544 + WHERE table_schema = 'public' 545 + AND table_name = $1 546 + )", 547 + ) 548 + .bind(table_name) 549 + .fetch_one(&storage.pool) 550 + .await?; 551 + 552 + Ok(row.0) 553 + } 554 + 555 + // Helper function to get table column information 556 + async fn get_table_columns( 557 + storage: &PostgresStorage, 558 + table_name: &str, 559 + ) -> Result<HashMap<String, TableColumnInfo>> { 560 + let rows: Vec<PgRow> = sqlx::query( 561 + "SELECT 562 + column_name, 563 + data_type, 564 + is_nullable, 565 + column_default, 566 + character_maximum_length, 567 + numeric_precision, 568 + numeric_scale 569 + FROM information_schema.columns 570 + WHERE table_schema = 'public' 571 + AND table_name = $1 572 + ORDER BY ordinal_position", 573 + ) 574 + .bind(table_name) 575 + .fetch_all(&storage.pool) 576 + .await?; 577 + 578 + let mut columns = HashMap::new(); 579 + for row in rows { 580 + let column_name: String = row.get("column_name"); 581 + let info = TableColumnInfo { 582 + data_type: row.get("data_type"), 583 + is_nullable: row.get::<String, _>("is_nullable") == "YES", 584 + column_default: row.get("column_default"), 585 + character_maximum_length: row.get("character_maximum_length"), 586 + }; 587 + columns.insert(column_name, info); 588 + } 589 + Ok(columns) 590 + } 591 + 592 + // Helper function to check if an index exists 593 + #[allow(dead_code)] 594 + async fn index_exists(storage: &PostgresStorage, index_name: &str) -> Result<bool> { 595 + let row: (bool,) = sqlx::query_as( 596 + "SELECT EXISTS ( 597 + SELECT FROM pg_indexes 598 + WHERE schemaname = 'public' 599 + AND indexname = $1 600 + )", 601 + ) 602 + .bind(index_name) 603 + .fetch_one(&storage.pool) 604 + .await?; 605 + 606 + Ok(row.0) 607 + } 608 + 609 + // Helper function to get primary key information 610 + async fn get_primary_key_columns( 611 + storage: &PostgresStorage, 612 + table_name: &str, 613 + ) -> Result<Vec<String>> { 614 + let rows: Vec<PgRow> = sqlx::query( 615 + "SELECT a.attname 616 + FROM pg_index i 617 + JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) 618 + WHERE i.indrelid = $1::regclass AND i.indisprimary 619 + ORDER BY a.attnum", 620 + ) 621 + .bind(table_name) 622 + .fetch_all(&storage.pool) 623 + .await?; 624 + 625 + Ok(rows 626 + .into_iter() 627 + .map(|row| row.get::<String, _>("attname")) 628 + .collect()) 629 + } 630 + 631 + #[derive(Debug)] 632 + struct TableColumnInfo { 633 + data_type: String, 634 + is_nullable: bool, 635 + column_default: Option<String>, 636 + #[allow(dead_code)] 637 + character_maximum_length: Option<i32>, 638 + } 639 + 640 + #[tokio::test] 641 + #[ignore] // Run only when PostgreSQL is available 642 + async fn test_postgres_migration_creates_all_tables() { 643 + let storage = create_test_postgres_storage().await.unwrap(); 644 + 645 + // Clean up any existing tables 646 + drop_test_tables(&storage).await.unwrap(); 647 + 648 + // Run migration 649 + storage.migrate().await.unwrap(); 650 + 651 + // Verify all tables were created 652 + assert!( 653 + table_exists(&storage, "identities").await.unwrap(), 654 + "identities table should exist" 655 + ); 656 + assert!( 657 + table_exists(&storage, "awards").await.unwrap(), 658 + "awards table should exist" 659 + ); 660 + assert!( 661 + table_exists(&storage, "badges").await.unwrap(), 662 + "badges table should exist" 663 + ); 664 + 665 + // Cleanup 666 + drop_test_tables(&storage).await.unwrap(); 667 + } 668 + 669 + #[tokio::test] 670 + #[ignore] // Run only when PostgreSQL is available 671 + async fn test_postgres_identities_table_schema() { 672 + let storage = create_test_postgres_storage().await.unwrap(); 673 + drop_test_tables(&storage).await.unwrap(); 674 + storage.migrate().await.unwrap(); 675 + 676 + let columns = get_table_columns(&storage, "identities").await.unwrap(); 677 + 678 + // Check primary key column 679 + let did_col = columns.get("did").expect("did column should exist"); 680 + assert_eq!(did_col.data_type, "text"); 681 + assert!(!did_col.is_nullable, "did should be NOT NULL"); 682 + 683 + // Check handle column 684 + let handle_col = columns.get("handle").expect("handle column should exist"); 685 + assert_eq!(handle_col.data_type, "text"); 686 + assert!(!handle_col.is_nullable, "handle should be NOT NULL"); 687 + 688 + // Check JSONB record column 689 + let record_col = columns.get("record").expect("record column should exist"); 690 + assert_eq!(record_col.data_type, "jsonb", "record should be JSONB type"); 691 + assert!(!record_col.is_nullable, "record should be NOT NULL"); 692 + 693 + // Check timestamp columns 694 + let created_at_col = columns 695 + .get("created_at") 696 + .expect("created_at column should exist"); 697 + assert_eq!( 698 + created_at_col.data_type, "timestamp with time zone", 699 + "created_at should be TIMESTAMPTZ" 700 + ); 701 + assert!(!created_at_col.is_nullable, "created_at should be NOT NULL"); 702 + assert!( 703 + created_at_col.column_default.is_some(), 704 + "created_at should have DEFAULT" 705 + ); 706 + 707 + let updated_at_col = columns 708 + .get("updated_at") 709 + .expect("updated_at column should exist"); 710 + assert_eq!( 711 + updated_at_col.data_type, "timestamp with time zone", 712 + "updated_at should be TIMESTAMPTZ" 713 + ); 714 + assert!(!updated_at_col.is_nullable, "updated_at should be NOT NULL"); 715 + assert!( 716 + updated_at_col.column_default.is_some(), 717 + "updated_at should have DEFAULT" 718 + ); 719 + 720 + // Check primary key 721 + let pk_columns = get_primary_key_columns(&storage, "identities") 722 + .await 723 + .unwrap(); 724 + assert_eq!( 725 + pk_columns, 726 + vec!["did"], 727 + "Primary key should be on did column" 728 + ); 729 + 730 + drop_test_tables(&storage).await.unwrap(); 731 + } 732 + 733 + #[tokio::test] 734 + #[ignore] // Run only when PostgreSQL is available 735 + async fn test_postgres_migration_idempotency() { 736 + let storage = create_test_postgres_storage().await.unwrap(); 737 + drop_test_tables(&storage).await.unwrap(); 738 + 739 + // Run migration first time 740 + storage.migrate().await.unwrap(); 741 + 742 + // Verify tables exist 743 + assert!(table_exists(&storage, "identities").await.unwrap()); 744 + assert!(table_exists(&storage, "awards").await.unwrap()); 745 + assert!(table_exists(&storage, "badges").await.unwrap()); 746 + 747 + // Run migration second time - should not fail 748 + let result = storage.migrate().await; 749 + assert!( 750 + result.is_ok(), 751 + "Second migration should not fail: {:?}", 752 + result 753 + ); 754 + 755 + // Verify tables still exist 756 + assert!(table_exists(&storage, "identities").await.unwrap()); 757 + assert!(table_exists(&storage, "awards").await.unwrap()); 758 + assert!(table_exists(&storage, "badges").await.unwrap()); 759 + 760 + // Run migration third time - should still not fail 761 + let result = storage.migrate().await; 762 + assert!( 763 + result.is_ok(), 764 + "Third migration should not fail: {:?}", 765 + result 766 + ); 767 + 768 + drop_test_tables(&storage).await.unwrap(); 769 + } 770 + }
+473
src/storage/sqlite.rs
··· 1 + use std::sync::Arc; 2 + 3 + use crate::errors::Result; 4 + use async_trait::async_trait; 5 + use atproto_identity::model::Document; 6 + use atproto_identity::storage::DidDocumentStorage; 7 + use chrono::Utc; 8 + use sqlx::sqlite::SqlitePool; 9 + 10 + use super::{Award, AwardWithBadge, Badge, Identity, Storage}; 11 + 12 + /// SQLite storage implementation for badges and awards. 13 + #[derive(Debug, Clone)] 14 + pub struct SqliteStorage { 15 + pool: SqlitePool, 16 + } 17 + 18 + impl SqliteStorage { 19 + /// Create a new SQLite storage instance with the given connection pool. 20 + pub fn new(pool: SqlitePool) -> Self { 21 + Self { pool } 22 + } 23 + 24 + async fn migrate(&self) -> Result<()> { 25 + sqlx::query( 26 + r#" 27 + CREATE TABLE IF NOT EXISTS identities ( 28 + did TEXT PRIMARY KEY, 29 + handle TEXT NOT NULL, 30 + record JSON NOT NULL, 31 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 32 + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 33 + ); 34 + "#, 35 + ) 36 + .execute(&self.pool) 37 + .await?; 38 + 39 + sqlx::query( 40 + r#" 41 + CREATE INDEX IF NOT EXISTS idx_identities_handle ON identities(handle); 42 + CREATE INDEX IF NOT EXISTS idx_identities_created_at ON identities(created_at); 43 + CREATE INDEX IF NOT EXISTS idx_identities_updated_at ON identities(updated_at); 44 + "#, 45 + ) 46 + .execute(&self.pool) 47 + .await?; 48 + 49 + sqlx::query( 50 + r#" 51 + CREATE TABLE IF NOT EXISTS awards ( 52 + aturi TEXT PRIMARY KEY, 53 + cid TEXT NOT NULL, 54 + did TEXT NOT NULL, 55 + badge TEXT NOT NULL, 56 + badge_cid TEXT NOT NULL, 57 + badge_name TEXT NOT NULL, 58 + validated_issuers JSON NOT NULL, 59 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 60 + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 61 + record JSON NOT NULL 62 + ); 63 + "#, 64 + ) 65 + .execute(&self.pool) 66 + .await?; 67 + 68 + sqlx::query( 69 + r#" 70 + CREATE INDEX IF NOT EXISTS idx_awards_did ON awards(did); 71 + CREATE INDEX IF NOT EXISTS idx_awards_badge ON awards(badge); 72 + CREATE INDEX IF NOT EXISTS idx_awards_badge_cid ON awards(badge_cid); 73 + CREATE INDEX IF NOT EXISTS idx_awards_created_at ON awards(created_at); 74 + CREATE INDEX IF NOT EXISTS idx_awards_updated_at ON awards(updated_at); 75 + "#, 76 + ) 77 + .execute(&self.pool) 78 + .await?; 79 + 80 + sqlx::query( 81 + r#" 82 + CREATE TABLE IF NOT EXISTS badges ( 83 + aturi TEXT NOT NULL, 84 + cid TEXT NOT NULL, 85 + name TEXT NOT NULL, 86 + image TEXT, 87 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 88 + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 89 + count INTEGER NOT NULL DEFAULT 0, 90 + record JSON NOT NULL DEFAULT '{}', 91 + PRIMARY KEY (aturi, cid) 92 + ); 93 + "#, 94 + ) 95 + .execute(&self.pool) 96 + .await?; 97 + 98 + // Add record column to existing badges table if it doesn't exist 99 + sqlx::query( 100 + r#" 101 + ALTER TABLE badges ADD COLUMN record JSON NOT NULL DEFAULT '{}'; 102 + "#, 103 + ) 104 + .execute(&self.pool) 105 + .await 106 + .ok(); // Ignore error if column already exists 107 + 108 + sqlx::query( 109 + r#" 110 + CREATE INDEX IF NOT EXISTS idx_badges_aturi ON badges(aturi); 111 + CREATE INDEX IF NOT EXISTS idx_badges_cid ON badges(cid); 112 + CREATE INDEX IF NOT EXISTS idx_badges_created_at ON badges(created_at); 113 + CREATE INDEX IF NOT EXISTS idx_badges_updated_at ON badges(updated_at); 114 + CREATE INDEX IF NOT EXISTS idx_badges_record ON badges(json_extract(record, '$.name')); 115 + "#, 116 + ) 117 + .execute(&self.pool) 118 + .await?; 119 + 120 + Ok(()) 121 + } 122 + 123 + async fn upsert_identity(&self, identity: &Identity) -> Result<()> { 124 + sqlx::query( 125 + r#" 126 + INSERT INTO identities (did, handle, record, created_at, updated_at) 127 + VALUES ($1, $2, $3, $4, $5) 128 + ON CONFLICT(did) DO UPDATE SET 129 + handle = EXCLUDED.handle, 130 + record = EXCLUDED.record, 131 + updated_at = EXCLUDED.updated_at 132 + "#, 133 + ) 134 + .bind(&identity.did) 135 + .bind(&identity.handle) 136 + .bind(&identity.record) 137 + .bind(identity.created_at) 138 + .bind(identity.updated_at) 139 + .execute(&self.pool) 140 + .await?; 141 + 142 + Ok(()) 143 + } 144 + 145 + async fn get_identity_by_did(&self, did: &str) -> Result<Option<Identity>> { 146 + let row = sqlx::query_as::<_, Identity>("SELECT * FROM identities WHERE did = $1") 147 + .bind(did) 148 + .fetch_optional(&self.pool) 149 + .await?; 150 + 151 + Ok(row) 152 + } 153 + 154 + async fn get_identity_by_handle(&self, handle: &str) -> Result<Option<Identity>> { 155 + let row = sqlx::query_as::<_, Identity>("SELECT * FROM identities WHERE handle = $1") 156 + .bind(handle) 157 + .fetch_optional(&self.pool) 158 + .await?; 159 + 160 + Ok(row) 161 + } 162 + 163 + async fn upsert_badge(&self, badge: &Badge) -> Result<()> { 164 + sqlx::query( 165 + r#" 166 + INSERT INTO badges (aturi, cid, name, image, created_at, updated_at, count, record) 167 + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) 168 + ON CONFLICT(aturi, cid) DO UPDATE SET 169 + name = EXCLUDED.name, 170 + image = EXCLUDED.image, 171 + updated_at = EXCLUDED.updated_at, 172 + record = EXCLUDED.record 173 + "#, 174 + ) 175 + .bind(&badge.aturi) 176 + .bind(&badge.cid) 177 + .bind(&badge.name) 178 + .bind(&badge.image) 179 + .bind(badge.created_at) 180 + .bind(badge.updated_at) 181 + .bind(badge.count) 182 + .bind(&badge.record) 183 + .execute(&self.pool) 184 + .await?; 185 + 186 + Ok(()) 187 + } 188 + 189 + async fn get_badge(&self, aturi: &str, cid: &str) -> Result<Option<Badge>> { 190 + let row = sqlx::query_as::<_, Badge>("SELECT * FROM badges WHERE aturi = $1 AND cid = $2") 191 + .bind(aturi) 192 + .bind(cid) 193 + .fetch_optional(&self.pool) 194 + .await?; 195 + 196 + Ok(row) 197 + } 198 + 199 + async fn increment_badge_count(&self, aturi: &str, cid: &str) -> Result<()> { 200 + sqlx::query( 201 + r#" 202 + UPDATE badges 203 + SET count = count + 1, updated_at = CURRENT_TIMESTAMP 204 + WHERE aturi = $1 AND cid = $2 205 + "#, 206 + ) 207 + .bind(aturi) 208 + .bind(cid) 209 + .execute(&self.pool) 210 + .await?; 211 + 212 + Ok(()) 213 + } 214 + 215 + async fn decrement_badge_count(&self, aturi: &str, cid: &str) -> Result<()> { 216 + sqlx::query( 217 + r#" 218 + UPDATE badges 219 + SET count = GREATEST(0, count - 1), updated_at = CURRENT_TIMESTAMP 220 + WHERE aturi = $1 AND cid = $2 221 + "#, 222 + ) 223 + .bind(aturi) 224 + .bind(cid) 225 + .execute(&self.pool) 226 + .await?; 227 + 228 + Ok(()) 229 + } 230 + 231 + async fn upsert_award(&self, award: &Award) -> Result<bool> { 232 + let existing = self.get_award(&award.aturi).await?; 233 + let is_new = existing.is_none(); 234 + 235 + sqlx::query( 236 + r#" 237 + INSERT INTO awards (aturi, cid, did, badge, badge_cid, badge_name, validated_issuers, created_at, updated_at, record) 238 + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) 239 + ON CONFLICT(aturi) DO UPDATE SET 240 + cid = EXCLUDED.cid, 241 + badge = EXCLUDED.badge, 242 + badge_cid = EXCLUDED.badge_cid, 243 + badge_name = EXCLUDED.badge_name, 244 + validated_issuers = EXCLUDED.validated_issuers, 245 + updated_at = EXCLUDED.updated_at, 246 + record = EXCLUDED.record 247 + "#, 248 + ) 249 + .bind(&award.aturi) 250 + .bind(&award.cid) 251 + .bind(&award.did) 252 + .bind(&award.badge) 253 + .bind(&award.badge_cid) 254 + .bind(&award.badge_name) 255 + .bind(&award.validated_issuers) 256 + .bind(award.created_at) 257 + .bind(award.updated_at) 258 + .bind(&award.record) 259 + .execute(&self.pool) 260 + .await?; 261 + 262 + Ok(is_new) 263 + } 264 + 265 + async fn get_award(&self, aturi: &str) -> Result<Option<Award>> { 266 + let row = sqlx::query_as::<_, Award>("SELECT * FROM awards WHERE aturi = $1") 267 + .bind(aturi) 268 + .fetch_optional(&self.pool) 269 + .await?; 270 + 271 + Ok(row) 272 + } 273 + 274 + async fn delete_award(&self, aturi: &str) -> Result<Option<Award>> { 275 + let award = self.get_award(aturi).await?; 276 + 277 + if award.is_some() { 278 + sqlx::query("DELETE FROM awards WHERE aturi = $1") 279 + .bind(aturi) 280 + .execute(&self.pool) 281 + .await?; 282 + } 283 + 284 + Ok(award) 285 + } 286 + 287 + async fn trim_awards_for_did(&self, did: &str, max_count: i64) -> Result<()> { 288 + sqlx::query( 289 + r#" 290 + DELETE FROM awards 291 + WHERE aturi IN ( 292 + SELECT aturi FROM awards 293 + WHERE did = $1 294 + ORDER BY updated_at DESC 295 + LIMIT -1 OFFSET $2 296 + ) 297 + "#, 298 + ) 299 + .bind(did) 300 + .bind(max_count) 301 + .execute(&self.pool) 302 + .await?; 303 + 304 + Ok(()) 305 + } 306 + 307 + async fn get_recent_awards(&self, limit: i64) -> Result<Vec<AwardWithBadge>> { 308 + let awards = 309 + sqlx::query_as::<_, Award>("SELECT * FROM awards ORDER BY updated_at DESC LIMIT $1") 310 + .bind(limit) 311 + .fetch_all(&self.pool) 312 + .await?; 313 + 314 + self.enrich_awards_with_details(awards).await 315 + } 316 + 317 + async fn get_awards_for_did(&self, did: &str, limit: i64) -> Result<Vec<AwardWithBadge>> { 318 + let awards = sqlx::query_as::<_, Award>( 319 + "SELECT * FROM awards WHERE did = $1 ORDER BY updated_at DESC LIMIT $2", 320 + ) 321 + .bind(did) 322 + .bind(limit) 323 + .fetch_all(&self.pool) 324 + .await?; 325 + 326 + self.enrich_awards_with_details(awards).await 327 + } 328 + 329 + async fn enrich_awards_with_details(&self, awards: Vec<Award>) -> Result<Vec<AwardWithBadge>> { 330 + let mut result = Vec::new(); 331 + 332 + for award in awards { 333 + let badge = self.get_badge(&award.badge, &award.badge_cid).await?; 334 + let identity = self.get_identity_by_did(&award.did).await?; 335 + 336 + let validated_issuers: Vec<String> = 337 + serde_json::from_value(award.validated_issuers.clone()).unwrap_or_default(); 338 + 339 + let mut signer_identities = Vec::new(); 340 + for issuer in validated_issuers { 341 + if let Ok(Some(signer_identity)) = self.get_identity_by_did(&issuer).await { 342 + signer_identities.push(signer_identity); 343 + } 344 + } 345 + 346 + result.push(AwardWithBadge { 347 + award, 348 + badge, 349 + identity, 350 + signer_identities, 351 + }); 352 + } 353 + 354 + Ok(result) 355 + } 356 + } 357 + 358 + #[async_trait] 359 + impl Storage for SqliteStorage { 360 + async fn migrate(&self) -> Result<()> { 361 + self.migrate().await 362 + } 363 + 364 + async fn upsert_identity(&self, identity: &Identity) -> Result<()> { 365 + self.upsert_identity(identity).await 366 + } 367 + 368 + async fn get_identity_by_did(&self, did: &str) -> Result<Option<Identity>> { 369 + self.get_identity_by_did(did).await 370 + } 371 + 372 + async fn get_identity_by_handle(&self, handle: &str) -> Result<Option<Identity>> { 373 + self.get_identity_by_handle(handle).await 374 + } 375 + 376 + async fn upsert_badge(&self, badge: &Badge) -> Result<()> { 377 + self.upsert_badge(badge).await 378 + } 379 + 380 + async fn get_badge(&self, aturi: &str, cid: &str) -> Result<Option<Badge>> { 381 + self.get_badge(aturi, cid).await 382 + } 383 + 384 + async fn increment_badge_count(&self, aturi: &str, cid: &str) -> Result<()> { 385 + self.increment_badge_count(aturi, cid).await 386 + } 387 + 388 + async fn decrement_badge_count(&self, aturi: &str, cid: &str) -> Result<()> { 389 + self.decrement_badge_count(aturi, cid).await 390 + } 391 + 392 + async fn upsert_award(&self, award: &Award) -> Result<bool> { 393 + self.upsert_award(award).await 394 + } 395 + 396 + async fn get_award(&self, aturi: &str) -> Result<Option<Award>> { 397 + self.get_award(aturi).await 398 + } 399 + 400 + async fn delete_award(&self, aturi: &str) -> Result<Option<Award>> { 401 + self.delete_award(aturi).await 402 + } 403 + 404 + async fn trim_awards_for_did(&self, did: &str, max_count: i64) -> Result<()> { 405 + self.trim_awards_for_did(did, max_count).await 406 + } 407 + 408 + async fn get_recent_awards(&self, limit: i64) -> Result<Vec<AwardWithBadge>> { 409 + self.get_recent_awards(limit).await 410 + } 411 + 412 + async fn get_awards_for_did(&self, did: &str, limit: i64) -> Result<Vec<AwardWithBadge>> { 413 + self.get_awards_for_did(did, limit).await 414 + } 415 + } 416 + 417 + /// DID document storage implementation using SQLite. 418 + pub struct SqliteStorageDidDocumentStorage { 419 + storage: Arc<SqliteStorage>, 420 + } 421 + 422 + impl SqliteStorageDidDocumentStorage { 423 + /// Create a new DID document storage instance backed by SQLite. 424 + pub fn new(storage: Arc<SqliteStorage>) -> Self { 425 + Self { storage } 426 + } 427 + } 428 + 429 + #[async_trait] 430 + impl DidDocumentStorage for SqliteStorageDidDocumentStorage { 431 + async fn get_document_by_did(&self, did: &str) -> anyhow::Result<Option<Document>> { 432 + if let Some(identity) = self 433 + .storage 434 + .get_identity_by_did(did) 435 + .await 436 + .map_err(anyhow::Error::new)? 437 + { 438 + let document: Document = serde_json::from_value(identity.record)?; 439 + Ok(Some(document)) 440 + } else { 441 + Ok(None) 442 + } 443 + } 444 + 445 + async fn store_document(&self, doc: Document) -> anyhow::Result<()> { 446 + let handle = doc 447 + .also_known_as 448 + .first() 449 + .and_then(|aka| aka.strip_prefix("at://")) 450 + .unwrap_or("unknown.handle") 451 + .to_string(); 452 + 453 + // Create a simple JSON representation of the document 454 + let record = serde_json::json!(doc); 455 + 456 + let identity = Identity { 457 + did: doc.id.clone(), 458 + handle, 459 + record, 460 + created_at: Utc::now(), 461 + updated_at: Utc::now(), 462 + }; 463 + 464 + self.storage 465 + .upsert_identity(&identity) 466 + .await 467 + .map_err(anyhow::Error::new) 468 + } 469 + 470 + async fn delete_document_by_did(&self, _did: &str) -> anyhow::Result<()> { 471 + Ok(()) 472 + } 473 + }
+58
src/templates.rs
··· 1 + //! Template engine configuration for embedded and reloadable templates. 2 + //! 3 + //! Provides template engines with embedded assets (production) or 4 + //! filesystem auto-reloading (development). Feature-flag controlled. 5 + 6 + #[cfg(feature = "reload")] 7 + use minijinja_autoreload::AutoReloader; 8 + 9 + #[cfg(feature = "embed")] 10 + use minijinja::Environment; 11 + 12 + #[cfg(feature = "reload")] 13 + /// Build template environment with auto-reloading for development 14 + pub fn build_env() -> AutoReloader { 15 + reload_env::build_env() 16 + } 17 + 18 + #[cfg(feature = "embed")] 19 + /// Build template environment with embedded templates for production 20 + pub fn build_env(http_external: String, version: String) -> Environment<'static> { 21 + embed_env::build_env(http_external, version) 22 + } 23 + 24 + #[cfg(feature = "reload")] 25 + mod reload_env { 26 + use std::path::PathBuf; 27 + 28 + use minijinja::{Environment, path_loader}; 29 + use minijinja_autoreload::AutoReloader; 30 + 31 + pub fn build_env() -> AutoReloader { 32 + AutoReloader::new(move |notifier| { 33 + let template_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("templates"); 34 + let mut env = Environment::new(); 35 + env.set_trim_blocks(true); 36 + env.set_lstrip_blocks(true); 37 + env.set_loader(path_loader(&template_path)); 38 + notifier.set_fast_reload(true); 39 + notifier.watch_path(&template_path, true); 40 + Ok(env) 41 + }) 42 + } 43 + } 44 + 45 + #[cfg(feature = "embed")] 46 + mod embed_env { 47 + use minijinja::Environment; 48 + 49 + pub fn build_env(http_external: String, version: String) -> Environment<'static> { 50 + let mut env = Environment::new(); 51 + env.set_trim_blocks(true); 52 + env.set_lstrip_blocks(true); 53 + env.add_global("base", format!("https://{}", http_external)); 54 + env.add_global("version", version.clone()); 55 + minijinja_embed::load_templates!(&mut env); 56 + env 57 + } 58 + }
static/.DS_Store

This is a binary file and will not be displayed.

static/badges/bafkreidjzewhcu2rk2537dkhzuhu54gcoae4tvmukcu7gmczx3qofwg2xa.png

This is a binary file and will not be displayed.

static/badges/bafkreigb3vxlgckp66o2bffnwapagshpt2xdcgdltuzjymapobvgunxmfi.png

This is a binary file and will not be displayed.

+4
static/pico.colors.css
··· 1 + @charset "UTF-8";/*! 2 + * Pico CSS ✨ v2.1.1 (https://picocss.com) 3 + * Copyright 2019-2025 - Licensed under MIT 4 + */:host,:root{--pico-color-red-950:#1c0d06;--pico-color-red-900:#30130a;--pico-color-red-850:#45150c;--pico-color-red-800:#5c160d;--pico-color-red-750:#72170f;--pico-color-red-700:#861d13;--pico-color-red-650:#9b2318;--pico-color-red-600:#af291d;--pico-color-red-550:#c52f21;--pico-color-red-500:#d93526;--pico-color-red-450:#ee402e;--pico-color-red-400:#f06048;--pico-color-red-350:#f17961;--pico-color-red-300:#f38f79;--pico-color-red-250:#f5a390;--pico-color-red-200:#f5b7a8;--pico-color-red-150:#f6cabf;--pico-color-red-100:#f8dcd6;--pico-color-red-50:#faeeeb;--pico-color-red:#c52f21;--pico-color-pink-950:#25060c;--pico-color-pink-900:#380916;--pico-color-pink-850:#4b0c1f;--pico-color-pink-800:#5f0e28;--pico-color-pink-750:#740f31;--pico-color-pink-700:#88143b;--pico-color-pink-650:#9d1945;--pico-color-pink-600:#b21e4f;--pico-color-pink-550:#c72259;--pico-color-pink-500:#d92662;--pico-color-pink-450:#f42c6f;--pico-color-pink-400:#f6547e;--pico-color-pink-350:#f7708e;--pico-color-pink-300:#f8889e;--pico-color-pink-250:#f99eae;--pico-color-pink-200:#f9b4be;--pico-color-pink-150:#f9c8ce;--pico-color-pink-100:#f9dbdf;--pico-color-pink-50:#fbedef;--pico-color-pink:#d92662;--pico-color-fuchsia-950:#230518;--pico-color-fuchsia-900:#360925;--pico-color-fuchsia-850:#480b33;--pico-color-fuchsia-800:#5c0d41;--pico-color-fuchsia-750:#700e4f;--pico-color-fuchsia-700:#84135e;--pico-color-fuchsia-650:#98176d;--pico-color-fuchsia-600:#ac1c7c;--pico-color-fuchsia-550:#c1208b;--pico-color-fuchsia-500:#d9269d;--pico-color-fuchsia-450:#ed2aac;--pico-color-fuchsia-400:#f748b7;--pico-color-fuchsia-350:#f869bf;--pico-color-fuchsia-300:#f983c7;--pico-color-fuchsia-250:#fa9acf;--pico-color-fuchsia-200:#f9b1d8;--pico-color-fuchsia-150:#f9c6e1;--pico-color-fuchsia-100:#f9daea;--pico-color-fuchsia-50:#fbedf4;--pico-color-fuchsia:#c1208b;--pico-color-purple-950:#1e0820;--pico-color-purple-900:#2d0f33;--pico-color-purple-850:#3d1545;--pico-color-purple-800:#4d1a57;--pico-color-purple-750:#5e206b;--pico-color-purple-700:#6f277d;--pico-color-purple-650:#802e90;--pico-color-purple-600:#9236a4;--pico-color-purple-550:#aa40bf;--pico-color-purple-500:#b645cd;--pico-color-purple-450:#c652dc;--pico-color-purple-400:#cd68e0;--pico-color-purple-350:#d47de4;--pico-color-purple-300:#db90e8;--pico-color-purple-250:#e2a3eb;--pico-color-purple-200:#e7b6ee;--pico-color-purple-150:#edc9f1;--pico-color-purple-100:#f2dcf4;--pico-color-purple-50:#f8eef9;--pico-color-purple:#9236a4;--pico-color-violet-950:#190928;--pico-color-violet-900:#251140;--pico-color-violet-850:#321856;--pico-color-violet-800:#3f1e6d;--pico-color-violet-750:#4d2585;--pico-color-violet-700:#5b2d9c;--pico-color-violet-650:#6935b3;--pico-color-violet-600:#7540bf;--pico-color-violet-550:#8352c5;--pico-color-violet-500:#9062ca;--pico-color-violet-450:#9b71cf;--pico-color-violet-400:#a780d4;--pico-color-violet-350:#b290d9;--pico-color-violet-300:#bd9fdf;--pico-color-violet-250:#c9afe4;--pico-color-violet-200:#d3bfe8;--pico-color-violet-150:#decfed;--pico-color-violet-100:#e8dff2;--pico-color-violet-50:#f3eff7;--pico-color-violet:#7540bf;--pico-color-indigo-950:#110b31;--pico-color-indigo-900:#181546;--pico-color-indigo-850:#1f1e5e;--pico-color-indigo-800:#272678;--pico-color-indigo-750:#2f2f92;--pico-color-indigo-700:#3838ab;--pico-color-indigo-650:#4040bf;--pico-color-indigo-600:#524ed2;--pico-color-indigo-550:#655cd6;--pico-color-indigo-500:#7569da;--pico-color-indigo-450:#8577dd;--pico-color-indigo-400:#9486e1;--pico-color-indigo-350:#a294e5;--pico-color-indigo-300:#b0a3e8;--pico-color-indigo-250:#bdb2ec;--pico-color-indigo-200:#cac1ee;--pico-color-indigo-150:#d8d0f1;--pico-color-indigo-100:#e5e0f4;--pico-color-indigo-50:#f2f0f9;--pico-color-indigo:#524ed2;--pico-color-blue-950:#080f2d;--pico-color-blue-900:#0c1a41;--pico-color-blue-850:#0e2358;--pico-color-blue-800:#0f2d70;--pico-color-blue-750:#0f3888;--pico-color-blue-700:#1343a0;--pico-color-blue-650:#184eb8;--pico-color-blue-600:#1d59d0;--pico-color-blue-550:#2060df;--pico-color-blue-500:#3c71f7;--pico-color-blue-450:#5c7ef8;--pico-color-blue-400:#748bf8;--pico-color-blue-350:#8999f9;--pico-color-blue-300:#9ca7fa;--pico-color-blue-250:#aeb5fb;--pico-color-blue-200:#bfc3fa;--pico-color-blue-150:#d0d2fa;--pico-color-blue-100:#e0e1fa;--pico-color-blue-50:#f0f0fb;--pico-color-blue:#2060df;--pico-color-azure-950:#04121d;--pico-color-azure-900:#061e2f;--pico-color-azure-850:#052940;--pico-color-azure-800:#033452;--pico-color-azure-750:#014063;--pico-color-azure-700:#014c75;--pico-color-azure-650:#015887;--pico-color-azure-600:#02659a;--pico-color-azure-550:#0172ad;--pico-color-azure-500:#017fc0;--pico-color-azure-450:#018cd4;--pico-color-azure-400:#029ae8;--pico-color-azure-350:#01aaff;--pico-color-azure-300:#51b4ff;--pico-color-azure-250:#79c0ff;--pico-color-azure-200:#9bccfd;--pico-color-azure-150:#b7d9fc;--pico-color-azure-100:#d1e5fb;--pico-color-azure-50:#e9f2fc;--pico-color-azure:#0172ad;--pico-color-cyan-950:#041413;--pico-color-cyan-900:#051f1f;--pico-color-cyan-850:#052b2b;--pico-color-cyan-800:#043737;--pico-color-cyan-750:#014343;--pico-color-cyan-700:#015050;--pico-color-cyan-650:#025d5d;--pico-color-cyan-600:#046a6a;--pico-color-cyan-550:#047878;--pico-color-cyan-500:#058686;--pico-color-cyan-450:#059494;--pico-color-cyan-400:#05a2a2;--pico-color-cyan-350:#0ab1b1;--pico-color-cyan-300:#0ac2c2;--pico-color-cyan-250:#0ccece;--pico-color-cyan-200:#25dddd;--pico-color-cyan-150:#3deceb;--pico-color-cyan-100:#58faf9;--pico-color-cyan-50:#c3fcfa;--pico-color-cyan:#047878;--pico-color-jade-950:#04140c;--pico-color-jade-900:#052014;--pico-color-jade-850:#042c1b;--pico-color-jade-800:#033823;--pico-color-jade-750:#00452b;--pico-color-jade-700:#015234;--pico-color-jade-650:#005f3d;--pico-color-jade-600:#006d46;--pico-color-jade-550:#007a50;--pico-color-jade-500:#00895a;--pico-color-jade-450:#029764;--pico-color-jade-400:#00a66e;--pico-color-jade-350:#00b478;--pico-color-jade-300:#00c482;--pico-color-jade-250:#00cc88;--pico-color-jade-200:#21e299;--pico-color-jade-150:#39f1a6;--pico-color-jade-100:#70fcba;--pico-color-jade-50:#cbfce1;--pico-color-jade:#007a50;--pico-color-green-950:#0b1305;--pico-color-green-900:#131f07;--pico-color-green-850:#152b07;--pico-color-green-800:#173806;--pico-color-green-750:#1a4405;--pico-color-green-700:#205107;--pico-color-green-650:#265e09;--pico-color-green-600:#2c6c0c;--pico-color-green-550:#33790f;--pico-color-green-500:#398712;--pico-color-green-450:#409614;--pico-color-green-400:#47a417;--pico-color-green-350:#4eb31b;--pico-color-green-300:#55c21e;--pico-color-green-250:#5dd121;--pico-color-green-200:#62d926;--pico-color-green-150:#77ef3d;--pico-color-green-100:#95fb62;--pico-color-green-50:#d7fbc1;--pico-color-green:#398712;--pico-color-lime-950:#101203;--pico-color-lime-900:#191d03;--pico-color-lime-850:#202902;--pico-color-lime-800:#273500;--pico-color-lime-750:#304100;--pico-color-lime-700:#394d00;--pico-color-lime-650:#435a00;--pico-color-lime-600:#4d6600;--pico-color-lime-550:#577400;--pico-color-lime-500:#628100;--pico-color-lime-450:#6c8f00;--pico-color-lime-400:#779c00;--pico-color-lime-350:#82ab00;--pico-color-lime-300:#8eb901;--pico-color-lime-250:#99c801;--pico-color-lime-200:#a5d601;--pico-color-lime-150:#b2e51a;--pico-color-lime-100:#c1f335;--pico-color-lime-50:#defc85;--pico-color-lime:#a5d601;--pico-color-yellow-950:#141103;--pico-color-yellow-900:#1f1c02;--pico-color-yellow-850:#2b2600;--pico-color-yellow-800:#363100;--pico-color-yellow-750:#423c00;--pico-color-yellow-700:#4e4700;--pico-color-yellow-650:#5b5300;--pico-color-yellow-600:#685f00;--pico-color-yellow-550:#756b00;--pico-color-yellow-500:#827800;--pico-color-yellow-450:#908501;--pico-color-yellow-400:#9e9200;--pico-color-yellow-350:#ad9f00;--pico-color-yellow-300:#bbac00;--pico-color-yellow-250:#caba01;--pico-color-yellow-200:#d9c800;--pico-color-yellow-150:#e8d600;--pico-color-yellow-100:#f2df0d;--pico-color-yellow-50:#fdf1b4;--pico-color-yellow:#f2df0d;--pico-color-amber-950:#161003;--pico-color-amber-900:#231a03;--pico-color-amber-850:#312302;--pico-color-amber-800:#3f2d00;--pico-color-amber-750:#4d3700;--pico-color-amber-700:#5b4200;--pico-color-amber-650:#694d00;--pico-color-amber-600:#785800;--pico-color-amber-550:#876400;--pico-color-amber-500:#977000;--pico-color-amber-450:#a77c00;--pico-color-amber-400:#b78800;--pico-color-amber-350:#c79400;--pico-color-amber-300:#d8a100;--pico-color-amber-250:#e8ae01;--pico-color-amber-200:#ffbf00;--pico-color-amber-150:#fecc63;--pico-color-amber-100:#fddea6;--pico-color-amber-50:#fcefd9;--pico-color-amber:#ffbf00;--pico-color-pumpkin-950:#180f04;--pico-color-pumpkin-900:#271805;--pico-color-pumpkin-850:#372004;--pico-color-pumpkin-800:#482802;--pico-color-pumpkin-750:#593100;--pico-color-pumpkin-700:#693a00;--pico-color-pumpkin-650:#7a4400;--pico-color-pumpkin-600:#8b4f00;--pico-color-pumpkin-550:#9c5900;--pico-color-pumpkin-500:#ad6400;--pico-color-pumpkin-450:#bf6e00;--pico-color-pumpkin-400:#d27a01;--pico-color-pumpkin-350:#e48500;--pico-color-pumpkin-300:#ff9500;--pico-color-pumpkin-250:#ffa23a;--pico-color-pumpkin-200:#feb670;--pico-color-pumpkin-150:#fcca9b;--pico-color-pumpkin-100:#fcdcc1;--pico-color-pumpkin-50:#fceee3;--pico-color-pumpkin:#ff9500;--pico-color-orange-950:#1b0d06;--pico-color-orange-900:#2d1509;--pico-color-orange-850:#411a0a;--pico-color-orange-800:#561e0a;--pico-color-orange-750:#6b220a;--pico-color-orange-700:#7f270b;--pico-color-orange-650:#942d0d;--pico-color-orange-600:#a83410;--pico-color-orange-550:#bd3c13;--pico-color-orange-500:#d24317;--pico-color-orange-450:#e74b1a;--pico-color-orange-400:#f45d2c;--pico-color-orange-350:#f56b3d;--pico-color-orange-300:#f68e68;--pico-color-orange-250:#f8a283;--pico-color-orange-200:#f8b79f;--pico-color-orange-150:#f8cab9;--pico-color-orange-100:#f9dcd2;--pico-color-orange-50:#faeeea;--pico-color-orange:#d24317;--pico-color-sand-950:#111110;--pico-color-sand-900:#1c1b19;--pico-color-sand-850:#272622;--pico-color-sand-800:#32302b;--pico-color-sand-750:#3d3b35;--pico-color-sand-700:#49463f;--pico-color-sand-650:#55524a;--pico-color-sand-600:#615e55;--pico-color-sand-550:#6e6a60;--pico-color-sand-500:#7b776b;--pico-color-sand-450:#888377;--pico-color-sand-400:#959082;--pico-color-sand-350:#a39e8f;--pico-color-sand-300:#b0ab9b;--pico-color-sand-250:#beb8a7;--pico-color-sand-200:#ccc6b4;--pico-color-sand-150:#dad4c2;--pico-color-sand-100:#e8e2d2;--pico-color-sand-50:#f2f0ec;--pico-color-sand:#ccc6b4;--pico-color-grey-950:#111111;--pico-color-grey-900:#1b1b1b;--pico-color-grey-850:#262626;--pico-color-grey-800:#303030;--pico-color-grey-750:#3b3b3b;--pico-color-grey-700:#474747;--pico-color-grey-650:#525252;--pico-color-grey-600:#5e5e5e;--pico-color-grey-550:#6a6a6a;--pico-color-grey-500:#777777;--pico-color-grey-450:#808080;--pico-color-grey-400:#919191;--pico-color-grey-350:#9e9e9e;--pico-color-grey-300:#ababab;--pico-color-grey-250:#b9b9b9;--pico-color-grey-200:#c6c6c6;--pico-color-grey-150:#d4d4d4;--pico-color-grey-100:#e2e2e2;--pico-color-grey-50:#f1f1f1;--pico-color-grey:#ababab;--pico-color-zinc-950:#0f1114;--pico-color-zinc-900:#191c20;--pico-color-zinc-850:#23262c;--pico-color-zinc-800:#2d3138;--pico-color-zinc-750:#373c44;--pico-color-zinc-700:#424751;--pico-color-zinc-650:#4d535e;--pico-color-zinc-600:#5c6370;--pico-color-zinc-550:#646b79;--pico-color-zinc-500:#6f7887;--pico-color-zinc-450:#7b8495;--pico-color-zinc-400:#8891a4;--pico-color-zinc-350:#969eaf;--pico-color-zinc-300:#a4acba;--pico-color-zinc-250:#b3b9c5;--pico-color-zinc-200:#c2c7d0;--pico-color-zinc-150:#d1d5db;--pico-color-zinc-100:#e0e3e7;--pico-color-zinc-50:#f0f1f3;--pico-color-zinc:#646b79;--pico-color-slate-950:#0e1118;--pico-color-slate-900:#181c25;--pico-color-slate-850:#202632;--pico-color-slate-800:#2a3140;--pico-color-slate-750:#333c4e;--pico-color-slate-700:#3d475c;--pico-color-slate-650:#48536b;--pico-color-slate-600:#525f7a;--pico-color-slate-550:#5d6b89;--pico-color-slate-500:#687899;--pico-color-slate-450:#7385a9;--pico-color-slate-400:#8191b5;--pico-color-slate-350:#909ebe;--pico-color-slate-300:#a0acc7;--pico-color-slate-250:#b0b9d0;--pico-color-slate-200:#bfc7d9;--pico-color-slate-150:#cfd5e2;--pico-color-slate-100:#dfe3eb;--pico-color-slate-50:#eff1f4;--pico-color-slate:#525f7a;--pico-color-light:#fff;--pico-color-dark:#000}.pico-color-red-950{color:var(--pico-color-red-950)}.pico-color-red-900{color:var(--pico-color-red-900)}.pico-color-red-850{color:var(--pico-color-red-850)}.pico-color-red-800{color:var(--pico-color-red-800)}.pico-color-red-750{color:var(--pico-color-red-750)}.pico-color-red-700{color:var(--pico-color-red-700)}.pico-color-red-650{color:var(--pico-color-red-650)}.pico-color-red-600{color:var(--pico-color-red-600)}.pico-color-red-550{color:var(--pico-color-red-550)}.pico-color-red-500{color:var(--pico-color-red-500)}.pico-color-red-450{color:var(--pico-color-red-450)}.pico-color-red-400{color:var(--pico-color-red-400)}.pico-color-red-350{color:var(--pico-color-red-350)}.pico-color-red-300{color:var(--pico-color-red-300)}.pico-color-red-250{color:var(--pico-color-red-250)}.pico-color-red-200{color:var(--pico-color-red-200)}.pico-color-red-150{color:var(--pico-color-red-150)}.pico-color-red-100{color:var(--pico-color-red-100)}.pico-color-red-50{color:var(--pico-color-red-50)}.pico-color-red{color:var(--pico-color-red)}.pico-color-pink-950{color:var(--pico-color-pink-950)}.pico-color-pink-900{color:var(--pico-color-pink-900)}.pico-color-pink-850{color:var(--pico-color-pink-850)}.pico-color-pink-800{color:var(--pico-color-pink-800)}.pico-color-pink-750{color:var(--pico-color-pink-750)}.pico-color-pink-700{color:var(--pico-color-pink-700)}.pico-color-pink-650{color:var(--pico-color-pink-650)}.pico-color-pink-600{color:var(--pico-color-pink-600)}.pico-color-pink-550{color:var(--pico-color-pink-550)}.pico-color-pink-500{color:var(--pico-color-pink-500)}.pico-color-pink-450{color:var(--pico-color-pink-450)}.pico-color-pink-400{color:var(--pico-color-pink-400)}.pico-color-pink-350{color:var(--pico-color-pink-350)}.pico-color-pink-300{color:var(--pico-color-pink-300)}.pico-color-pink-250{color:var(--pico-color-pink-250)}.pico-color-pink-200{color:var(--pico-color-pink-200)}.pico-color-pink-150{color:var(--pico-color-pink-150)}.pico-color-pink-100{color:var(--pico-color-pink-100)}.pico-color-pink-50{color:var(--pico-color-pink-50)}.pico-color-pink{color:var(--pico-color-pink)}.pico-color-fuchsia-950{color:var(--pico-color-fuchsia-950)}.pico-color-fuchsia-900{color:var(--pico-color-fuchsia-900)}.pico-color-fuchsia-850{color:var(--pico-color-fuchsia-850)}.pico-color-fuchsia-800{color:var(--pico-color-fuchsia-800)}.pico-color-fuchsia-750{color:var(--pico-color-fuchsia-750)}.pico-color-fuchsia-700{color:var(--pico-color-fuchsia-700)}.pico-color-fuchsia-650{color:var(--pico-color-fuchsia-650)}.pico-color-fuchsia-600{color:var(--pico-color-fuchsia-600)}.pico-color-fuchsia-550{color:var(--pico-color-fuchsia-550)}.pico-color-fuchsia-500{color:var(--pico-color-fuchsia-500)}.pico-color-fuchsia-450{color:var(--pico-color-fuchsia-450)}.pico-color-fuchsia-400{color:var(--pico-color-fuchsia-400)}.pico-color-fuchsia-350{color:var(--pico-color-fuchsia-350)}.pico-color-fuchsia-300{color:var(--pico-color-fuchsia-300)}.pico-color-fuchsia-250{color:var(--pico-color-fuchsia-250)}.pico-color-fuchsia-200{color:var(--pico-color-fuchsia-200)}.pico-color-fuchsia-150{color:var(--pico-color-fuchsia-150)}.pico-color-fuchsia-100{color:var(--pico-color-fuchsia-100)}.pico-color-fuchsia-50{color:var(--pico-color-fuchsia-50)}.pico-color-fuchsia{color:var(--pico-color-fuchsia)}.pico-color-purple-950{color:var(--pico-color-purple-950)}.pico-color-purple-900{color:var(--pico-color-purple-900)}.pico-color-purple-850{color:var(--pico-color-purple-850)}.pico-color-purple-800{color:var(--pico-color-purple-800)}.pico-color-purple-750{color:var(--pico-color-purple-750)}.pico-color-purple-700{color:var(--pico-color-purple-700)}.pico-color-purple-650{color:var(--pico-color-purple-650)}.pico-color-purple-600{color:var(--pico-color-purple-600)}.pico-color-purple-550{color:var(--pico-color-purple-550)}.pico-color-purple-500{color:var(--pico-color-purple-500)}.pico-color-purple-450{color:var(--pico-color-purple-450)}.pico-color-purple-400{color:var(--pico-color-purple-400)}.pico-color-purple-350{color:var(--pico-color-purple-350)}.pico-color-purple-300{color:var(--pico-color-purple-300)}.pico-color-purple-250{color:var(--pico-color-purple-250)}.pico-color-purple-200{color:var(--pico-color-purple-200)}.pico-color-purple-150{color:var(--pico-color-purple-150)}.pico-color-purple-100{color:var(--pico-color-purple-100)}.pico-color-purple-50{color:var(--pico-color-purple-50)}.pico-color-purple{color:var(--pico-color-purple)}.pico-color-violet-950{color:var(--pico-color-violet-950)}.pico-color-violet-900{color:var(--pico-color-violet-900)}.pico-color-violet-850{color:var(--pico-color-violet-850)}.pico-color-violet-800{color:var(--pico-color-violet-800)}.pico-color-violet-750{color:var(--pico-color-violet-750)}.pico-color-violet-700{color:var(--pico-color-violet-700)}.pico-color-violet-650{color:var(--pico-color-violet-650)}.pico-color-violet-600{color:var(--pico-color-violet-600)}.pico-color-violet-550{color:var(--pico-color-violet-550)}.pico-color-violet-500{color:var(--pico-color-violet-500)}.pico-color-violet-450{color:var(--pico-color-violet-450)}.pico-color-violet-400{color:var(--pico-color-violet-400)}.pico-color-violet-350{color:var(--pico-color-violet-350)}.pico-color-violet-300{color:var(--pico-color-violet-300)}.pico-color-violet-250{color:var(--pico-color-violet-250)}.pico-color-violet-200{color:var(--pico-color-violet-200)}.pico-color-violet-150{color:var(--pico-color-violet-150)}.pico-color-violet-100{color:var(--pico-color-violet-100)}.pico-color-violet-50{color:var(--pico-color-violet-50)}.pico-color-violet{color:var(--pico-color-violet)}.pico-color-indigo-950{color:var(--pico-color-indigo-950)}.pico-color-indigo-900{color:var(--pico-color-indigo-900)}.pico-color-indigo-850{color:var(--pico-color-indigo-850)}.pico-color-indigo-800{color:var(--pico-color-indigo-800)}.pico-color-indigo-750{color:var(--pico-color-indigo-750)}.pico-color-indigo-700{color:var(--pico-color-indigo-700)}.pico-color-indigo-650{color:var(--pico-color-indigo-650)}.pico-color-indigo-600{color:var(--pico-color-indigo-600)}.pico-color-indigo-550{color:var(--pico-color-indigo-550)}.pico-color-indigo-500{color:var(--pico-color-indigo-500)}.pico-color-indigo-450{color:var(--pico-color-indigo-450)}.pico-color-indigo-400{color:var(--pico-color-indigo-400)}.pico-color-indigo-350{color:var(--pico-color-indigo-350)}.pico-color-indigo-300{color:var(--pico-color-indigo-300)}.pico-color-indigo-250{color:var(--pico-color-indigo-250)}.pico-color-indigo-200{color:var(--pico-color-indigo-200)}.pico-color-indigo-150{color:var(--pico-color-indigo-150)}.pico-color-indigo-100{color:var(--pico-color-indigo-100)}.pico-color-indigo-50{color:var(--pico-color-indigo-50)}.pico-color-indigo{color:var(--pico-color-indigo)}.pico-color-blue-950{color:var(--pico-color-blue-950)}.pico-color-blue-900{color:var(--pico-color-blue-900)}.pico-color-blue-850{color:var(--pico-color-blue-850)}.pico-color-blue-800{color:var(--pico-color-blue-800)}.pico-color-blue-750{color:var(--pico-color-blue-750)}.pico-color-blue-700{color:var(--pico-color-blue-700)}.pico-color-blue-650{color:var(--pico-color-blue-650)}.pico-color-blue-600{color:var(--pico-color-blue-600)}.pico-color-blue-550{color:var(--pico-color-blue-550)}.pico-color-blue-500{color:var(--pico-color-blue-500)}.pico-color-blue-450{color:var(--pico-color-blue-450)}.pico-color-blue-400{color:var(--pico-color-blue-400)}.pico-color-blue-350{color:var(--pico-color-blue-350)}.pico-color-blue-300{color:var(--pico-color-blue-300)}.pico-color-blue-250{color:var(--pico-color-blue-250)}.pico-color-blue-200{color:var(--pico-color-blue-200)}.pico-color-blue-150{color:var(--pico-color-blue-150)}.pico-color-blue-100{color:var(--pico-color-blue-100)}.pico-color-blue-50{color:var(--pico-color-blue-50)}.pico-color-blue{color:var(--pico-color-blue)}.pico-color-azure-950{color:var(--pico-color-azure-950)}.pico-color-azure-900{color:var(--pico-color-azure-900)}.pico-color-azure-850{color:var(--pico-color-azure-850)}.pico-color-azure-800{color:var(--pico-color-azure-800)}.pico-color-azure-750{color:var(--pico-color-azure-750)}.pico-color-azure-700{color:var(--pico-color-azure-700)}.pico-color-azure-650{color:var(--pico-color-azure-650)}.pico-color-azure-600{color:var(--pico-color-azure-600)}.pico-color-azure-550{color:var(--pico-color-azure-550)}.pico-color-azure-500{color:var(--pico-color-azure-500)}.pico-color-azure-450{color:var(--pico-color-azure-450)}.pico-color-azure-400{color:var(--pico-color-azure-400)}.pico-color-azure-350{color:var(--pico-color-azure-350)}.pico-color-azure-300{color:var(--pico-color-azure-300)}.pico-color-azure-250{color:var(--pico-color-azure-250)}.pico-color-azure-200{color:var(--pico-color-azure-200)}.pico-color-azure-150{color:var(--pico-color-azure-150)}.pico-color-azure-100{color:var(--pico-color-azure-100)}.pico-color-azure-50{color:var(--pico-color-azure-50)}.pico-color-azure{color:var(--pico-color-azure)}.pico-color-cyan-950{color:var(--pico-color-cyan-950)}.pico-color-cyan-900{color:var(--pico-color-cyan-900)}.pico-color-cyan-850{color:var(--pico-color-cyan-850)}.pico-color-cyan-800{color:var(--pico-color-cyan-800)}.pico-color-cyan-750{color:var(--pico-color-cyan-750)}.pico-color-cyan-700{color:var(--pico-color-cyan-700)}.pico-color-cyan-650{color:var(--pico-color-cyan-650)}.pico-color-cyan-600{color:var(--pico-color-cyan-600)}.pico-color-cyan-550{color:var(--pico-color-cyan-550)}.pico-color-cyan-500{color:var(--pico-color-cyan-500)}.pico-color-cyan-450{color:var(--pico-color-cyan-450)}.pico-color-cyan-400{color:var(--pico-color-cyan-400)}.pico-color-cyan-350{color:var(--pico-color-cyan-350)}.pico-color-cyan-300{color:var(--pico-color-cyan-300)}.pico-color-cyan-250{color:var(--pico-color-cyan-250)}.pico-color-cyan-200{color:var(--pico-color-cyan-200)}.pico-color-cyan-150{color:var(--pico-color-cyan-150)}.pico-color-cyan-100{color:var(--pico-color-cyan-100)}.pico-color-cyan-50{color:var(--pico-color-cyan-50)}.pico-color-cyan{color:var(--pico-color-cyan)}.pico-color-jade-950{color:var(--pico-color-jade-950)}.pico-color-jade-900{color:var(--pico-color-jade-900)}.pico-color-jade-850{color:var(--pico-color-jade-850)}.pico-color-jade-800{color:var(--pico-color-jade-800)}.pico-color-jade-750{color:var(--pico-color-jade-750)}.pico-color-jade-700{color:var(--pico-color-jade-700)}.pico-color-jade-650{color:var(--pico-color-jade-650)}.pico-color-jade-600{color:var(--pico-color-jade-600)}.pico-color-jade-550{color:var(--pico-color-jade-550)}.pico-color-jade-500{color:var(--pico-color-jade-500)}.pico-color-jade-450{color:var(--pico-color-jade-450)}.pico-color-jade-400{color:var(--pico-color-jade-400)}.pico-color-jade-350{color:var(--pico-color-jade-350)}.pico-color-jade-300{color:var(--pico-color-jade-300)}.pico-color-jade-250{color:var(--pico-color-jade-250)}.pico-color-jade-200{color:var(--pico-color-jade-200)}.pico-color-jade-150{color:var(--pico-color-jade-150)}.pico-color-jade-100{color:var(--pico-color-jade-100)}.pico-color-jade-50{color:var(--pico-color-jade-50)}.pico-color-jade{color:var(--pico-color-jade)}.pico-color-green-950{color:var(--pico-color-green-950)}.pico-color-green-900{color:var(--pico-color-green-900)}.pico-color-green-850{color:var(--pico-color-green-850)}.pico-color-green-800{color:var(--pico-color-green-800)}.pico-color-green-750{color:var(--pico-color-green-750)}.pico-color-green-700{color:var(--pico-color-green-700)}.pico-color-green-650{color:var(--pico-color-green-650)}.pico-color-green-600{color:var(--pico-color-green-600)}.pico-color-green-550{color:var(--pico-color-green-550)}.pico-color-green-500{color:var(--pico-color-green-500)}.pico-color-green-450{color:var(--pico-color-green-450)}.pico-color-green-400{color:var(--pico-color-green-400)}.pico-color-green-350{color:var(--pico-color-green-350)}.pico-color-green-300{color:var(--pico-color-green-300)}.pico-color-green-250{color:var(--pico-color-green-250)}.pico-color-green-200{color:var(--pico-color-green-200)}.pico-color-green-150{color:var(--pico-color-green-150)}.pico-color-green-100{color:var(--pico-color-green-100)}.pico-color-green-50{color:var(--pico-color-green-50)}.pico-color-green{color:var(--pico-color-green)}.pico-color-lime-950{color:var(--pico-color-lime-950)}.pico-color-lime-900{color:var(--pico-color-lime-900)}.pico-color-lime-850{color:var(--pico-color-lime-850)}.pico-color-lime-800{color:var(--pico-color-lime-800)}.pico-color-lime-750{color:var(--pico-color-lime-750)}.pico-color-lime-700{color:var(--pico-color-lime-700)}.pico-color-lime-650{color:var(--pico-color-lime-650)}.pico-color-lime-600{color:var(--pico-color-lime-600)}.pico-color-lime-550{color:var(--pico-color-lime-550)}.pico-color-lime-500{color:var(--pico-color-lime-500)}.pico-color-lime-450{color:var(--pico-color-lime-450)}.pico-color-lime-400{color:var(--pico-color-lime-400)}.pico-color-lime-350{color:var(--pico-color-lime-350)}.pico-color-lime-300{color:var(--pico-color-lime-300)}.pico-color-lime-250{color:var(--pico-color-lime-250)}.pico-color-lime-200{color:var(--pico-color-lime-200)}.pico-color-lime-150{color:var(--pico-color-lime-150)}.pico-color-lime-100{color:var(--pico-color-lime-100)}.pico-color-lime-50{color:var(--pico-color-lime-50)}.pico-color-lime{color:var(--pico-color-lime)}.pico-color-yellow-950{color:var(--pico-color-yellow-950)}.pico-color-yellow-900{color:var(--pico-color-yellow-900)}.pico-color-yellow-850{color:var(--pico-color-yellow-850)}.pico-color-yellow-800{color:var(--pico-color-yellow-800)}.pico-color-yellow-750{color:var(--pico-color-yellow-750)}.pico-color-yellow-700{color:var(--pico-color-yellow-700)}.pico-color-yellow-650{color:var(--pico-color-yellow-650)}.pico-color-yellow-600{color:var(--pico-color-yellow-600)}.pico-color-yellow-550{color:var(--pico-color-yellow-550)}.pico-color-yellow-500{color:var(--pico-color-yellow-500)}.pico-color-yellow-450{color:var(--pico-color-yellow-450)}.pico-color-yellow-400{color:var(--pico-color-yellow-400)}.pico-color-yellow-350{color:var(--pico-color-yellow-350)}.pico-color-yellow-300{color:var(--pico-color-yellow-300)}.pico-color-yellow-250{color:var(--pico-color-yellow-250)}.pico-color-yellow-200{color:var(--pico-color-yellow-200)}.pico-color-yellow-150{color:var(--pico-color-yellow-150)}.pico-color-yellow-100{color:var(--pico-color-yellow-100)}.pico-color-yellow-50{color:var(--pico-color-yellow-50)}.pico-color-yellow{color:var(--pico-color-yellow)}.pico-color-amber-950{color:var(--pico-color-amber-950)}.pico-color-amber-900{color:var(--pico-color-amber-900)}.pico-color-amber-850{color:var(--pico-color-amber-850)}.pico-color-amber-800{color:var(--pico-color-amber-800)}.pico-color-amber-750{color:var(--pico-color-amber-750)}.pico-color-amber-700{color:var(--pico-color-amber-700)}.pico-color-amber-650{color:var(--pico-color-amber-650)}.pico-color-amber-600{color:var(--pico-color-amber-600)}.pico-color-amber-550{color:var(--pico-color-amber-550)}.pico-color-amber-500{color:var(--pico-color-amber-500)}.pico-color-amber-450{color:var(--pico-color-amber-450)}.pico-color-amber-400{color:var(--pico-color-amber-400)}.pico-color-amber-350{color:var(--pico-color-amber-350)}.pico-color-amber-300{color:var(--pico-color-amber-300)}.pico-color-amber-250{color:var(--pico-color-amber-250)}.pico-color-amber-200{color:var(--pico-color-amber-200)}.pico-color-amber-150{color:var(--pico-color-amber-150)}.pico-color-amber-100{color:var(--pico-color-amber-100)}.pico-color-amber-50{color:var(--pico-color-amber-50)}.pico-color-amber{color:var(--pico-color-amber)}.pico-color-pumpkin-950{color:var(--pico-color-pumpkin-950)}.pico-color-pumpkin-900{color:var(--pico-color-pumpkin-900)}.pico-color-pumpkin-850{color:var(--pico-color-pumpkin-850)}.pico-color-pumpkin-800{color:var(--pico-color-pumpkin-800)}.pico-color-pumpkin-750{color:var(--pico-color-pumpkin-750)}.pico-color-pumpkin-700{color:var(--pico-color-pumpkin-700)}.pico-color-pumpkin-650{color:var(--pico-color-pumpkin-650)}.pico-color-pumpkin-600{color:var(--pico-color-pumpkin-600)}.pico-color-pumpkin-550{color:var(--pico-color-pumpkin-550)}.pico-color-pumpkin-500{color:var(--pico-color-pumpkin-500)}.pico-color-pumpkin-450{color:var(--pico-color-pumpkin-450)}.pico-color-pumpkin-400{color:var(--pico-color-pumpkin-400)}.pico-color-pumpkin-350{color:var(--pico-color-pumpkin-350)}.pico-color-pumpkin-300{color:var(--pico-color-pumpkin-300)}.pico-color-pumpkin-250{color:var(--pico-color-pumpkin-250)}.pico-color-pumpkin-200{color:var(--pico-color-pumpkin-200)}.pico-color-pumpkin-150{color:var(--pico-color-pumpkin-150)}.pico-color-pumpkin-100{color:var(--pico-color-pumpkin-100)}.pico-color-pumpkin-50{color:var(--pico-color-pumpkin-50)}.pico-color-pumpkin{color:var(--pico-color-pumpkin)}.pico-color-orange-950{color:var(--pico-color-orange-950)}.pico-color-orange-900{color:var(--pico-color-orange-900)}.pico-color-orange-850{color:var(--pico-color-orange-850)}.pico-color-orange-800{color:var(--pico-color-orange-800)}.pico-color-orange-750{color:var(--pico-color-orange-750)}.pico-color-orange-700{color:var(--pico-color-orange-700)}.pico-color-orange-650{color:var(--pico-color-orange-650)}.pico-color-orange-600{color:var(--pico-color-orange-600)}.pico-color-orange-550{color:var(--pico-color-orange-550)}.pico-color-orange-500{color:var(--pico-color-orange-500)}.pico-color-orange-450{color:var(--pico-color-orange-450)}.pico-color-orange-400{color:var(--pico-color-orange-400)}.pico-color-orange-350{color:var(--pico-color-orange-350)}.pico-color-orange-300{color:var(--pico-color-orange-300)}.pico-color-orange-250{color:var(--pico-color-orange-250)}.pico-color-orange-200{color:var(--pico-color-orange-200)}.pico-color-orange-150{color:var(--pico-color-orange-150)}.pico-color-orange-100{color:var(--pico-color-orange-100)}.pico-color-orange-50{color:var(--pico-color-orange-50)}.pico-color-orange{color:var(--pico-color-orange)}.pico-color-sand-950{color:var(--pico-color-sand-950)}.pico-color-sand-900{color:var(--pico-color-sand-900)}.pico-color-sand-850{color:var(--pico-color-sand-850)}.pico-color-sand-800{color:var(--pico-color-sand-800)}.pico-color-sand-750{color:var(--pico-color-sand-750)}.pico-color-sand-700{color:var(--pico-color-sand-700)}.pico-color-sand-650{color:var(--pico-color-sand-650)}.pico-color-sand-600{color:var(--pico-color-sand-600)}.pico-color-sand-550{color:var(--pico-color-sand-550)}.pico-color-sand-500{color:var(--pico-color-sand-500)}.pico-color-sand-450{color:var(--pico-color-sand-450)}.pico-color-sand-400{color:var(--pico-color-sand-400)}.pico-color-sand-350{color:var(--pico-color-sand-350)}.pico-color-sand-300{color:var(--pico-color-sand-300)}.pico-color-sand-250{color:var(--pico-color-sand-250)}.pico-color-sand-200{color:var(--pico-color-sand-200)}.pico-color-sand-150{color:var(--pico-color-sand-150)}.pico-color-sand-100{color:var(--pico-color-sand-100)}.pico-color-sand-50{color:var(--pico-color-sand-50)}.pico-color-sand{color:var(--pico-color-sand)}.pico-color-grey-950{color:var(--pico-color-grey-950)}.pico-color-grey-900{color:var(--pico-color-grey-900)}.pico-color-grey-850{color:var(--pico-color-grey-850)}.pico-color-grey-800{color:var(--pico-color-grey-800)}.pico-color-grey-750{color:var(--pico-color-grey-750)}.pico-color-grey-700{color:var(--pico-color-grey-700)}.pico-color-grey-650{color:var(--pico-color-grey-650)}.pico-color-grey-600{color:var(--pico-color-grey-600)}.pico-color-grey-550{color:var(--pico-color-grey-550)}.pico-color-grey-500{color:var(--pico-color-grey-500)}.pico-color-grey-450{color:var(--pico-color-grey-450)}.pico-color-grey-400{color:var(--pico-color-grey-400)}.pico-color-grey-350{color:var(--pico-color-grey-350)}.pico-color-grey-300{color:var(--pico-color-grey-300)}.pico-color-grey-250{color:var(--pico-color-grey-250)}.pico-color-grey-200{color:var(--pico-color-grey-200)}.pico-color-grey-150{color:var(--pico-color-grey-150)}.pico-color-grey-100{color:var(--pico-color-grey-100)}.pico-color-grey-50{color:var(--pico-color-grey-50)}.pico-color-grey{color:var(--pico-color-grey)}.pico-color-zinc-950{color:var(--pico-color-zinc-950)}.pico-color-zinc-900{color:var(--pico-color-zinc-900)}.pico-color-zinc-850{color:var(--pico-color-zinc-850)}.pico-color-zinc-800{color:var(--pico-color-zinc-800)}.pico-color-zinc-750{color:var(--pico-color-zinc-750)}.pico-color-zinc-700{color:var(--pico-color-zinc-700)}.pico-color-zinc-650{color:var(--pico-color-zinc-650)}.pico-color-zinc-600{color:var(--pico-color-zinc-600)}.pico-color-zinc-550{color:var(--pico-color-zinc-550)}.pico-color-zinc-500{color:var(--pico-color-zinc-500)}.pico-color-zinc-450{color:var(--pico-color-zinc-450)}.pico-color-zinc-400{color:var(--pico-color-zinc-400)}.pico-color-zinc-350{color:var(--pico-color-zinc-350)}.pico-color-zinc-300{color:var(--pico-color-zinc-300)}.pico-color-zinc-250{color:var(--pico-color-zinc-250)}.pico-color-zinc-200{color:var(--pico-color-zinc-200)}.pico-color-zinc-150{color:var(--pico-color-zinc-150)}.pico-color-zinc-100{color:var(--pico-color-zinc-100)}.pico-color-zinc-50{color:var(--pico-color-zinc-50)}.pico-color-zinc{color:var(--pico-color-zinc)}.pico-color-slate-950{color:var(--pico-color-slate-950)}.pico-color-slate-900{color:var(--pico-color-slate-900)}.pico-color-slate-850{color:var(--pico-color-slate-850)}.pico-color-slate-800{color:var(--pico-color-slate-800)}.pico-color-slate-750{color:var(--pico-color-slate-750)}.pico-color-slate-700{color:var(--pico-color-slate-700)}.pico-color-slate-650{color:var(--pico-color-slate-650)}.pico-color-slate-600{color:var(--pico-color-slate-600)}.pico-color-slate-550{color:var(--pico-color-slate-550)}.pico-color-slate-500{color:var(--pico-color-slate-500)}.pico-color-slate-450{color:var(--pico-color-slate-450)}.pico-color-slate-400{color:var(--pico-color-slate-400)}.pico-color-slate-350{color:var(--pico-color-slate-350)}.pico-color-slate-300{color:var(--pico-color-slate-300)}.pico-color-slate-250{color:var(--pico-color-slate-250)}.pico-color-slate-200{color:var(--pico-color-slate-200)}.pico-color-slate-150{color:var(--pico-color-slate-150)}.pico-color-slate-100{color:var(--pico-color-slate-100)}.pico-color-slate-50{color:var(--pico-color-slate-50)}.pico-color-slate{color:var(--pico-color-slate)}.pico-background-red-950{background-color:var(--pico-color-red-950);color:var(--pico-color-light)}.pico-background-red-900{background-color:var(--pico-color-red-900);color:var(--pico-color-light)}.pico-background-red-850{background-color:var(--pico-color-red-850);color:var(--pico-color-light)}.pico-background-red-800{background-color:var(--pico-color-red-800);color:var(--pico-color-light)}.pico-background-red-750{background-color:var(--pico-color-red-750);color:var(--pico-color-light)}.pico-background-red-700{background-color:var(--pico-color-red-700);color:var(--pico-color-light)}.pico-background-red-650{background-color:var(--pico-color-red-650);color:var(--pico-color-light)}.pico-background-red-600{background-color:var(--pico-color-red-600);color:var(--pico-color-light)}.pico-background-red-550{background-color:var(--pico-color-red-550);color:var(--pico-color-light)}.pico-background-red-500{background-color:var(--pico-color-red-500);color:var(--pico-color-light)}.pico-background-red-450{background-color:var(--pico-color-red-450);color:var(--pico-color-light)}.pico-background-red-400{background-color:var(--pico-color-red-400);color:var(--pico-color-dark)}.pico-background-red-350{background-color:var(--pico-color-red-350);color:var(--pico-color-dark)}.pico-background-red-300{background-color:var(--pico-color-red-300);color:var(--pico-color-dark)}.pico-background-red-250{background-color:var(--pico-color-red-250);color:var(--pico-color-dark)}.pico-background-red-200{background-color:var(--pico-color-red-200);color:var(--pico-color-dark)}.pico-background-red-150{background-color:var(--pico-color-red-150);color:var(--pico-color-dark)}.pico-background-red-100{background-color:var(--pico-color-red-100);color:var(--pico-color-dark)}.pico-background-red-50{background-color:var(--pico-color-red-50);color:var(--pico-color-dark)}.pico-background-red{background-color:var(--pico-color-red);color:var(--pico-color-light)}.pico-background-pink-950{background-color:var(--pico-color-pink-950);color:var(--pico-color-light)}.pico-background-pink-900{background-color:var(--pico-color-pink-900);color:var(--pico-color-light)}.pico-background-pink-850{background-color:var(--pico-color-pink-850);color:var(--pico-color-light)}.pico-background-pink-800{background-color:var(--pico-color-pink-800);color:var(--pico-color-light)}.pico-background-pink-750{background-color:var(--pico-color-pink-750);color:var(--pico-color-light)}.pico-background-pink-700{background-color:var(--pico-color-pink-700);color:var(--pico-color-light)}.pico-background-pink-650{background-color:var(--pico-color-pink-650);color:var(--pico-color-light)}.pico-background-pink-600{background-color:var(--pico-color-pink-600);color:var(--pico-color-light)}.pico-background-pink-550{background-color:var(--pico-color-pink-550);color:var(--pico-color-light)}.pico-background-pink-500{background-color:var(--pico-color-pink-500);color:var(--pico-color-light)}.pico-background-pink-450{background-color:var(--pico-color-pink-450);color:var(--pico-color-light)}.pico-background-pink-400{background-color:var(--pico-color-pink-400);color:var(--pico-color-dark)}.pico-background-pink-350{background-color:var(--pico-color-pink-350);color:var(--pico-color-dark)}.pico-background-pink-300{background-color:var(--pico-color-pink-300);color:var(--pico-color-dark)}.pico-background-pink-250{background-color:var(--pico-color-pink-250);color:var(--pico-color-dark)}.pico-background-pink-200{background-color:var(--pico-color-pink-200);color:var(--pico-color-dark)}.pico-background-pink-150{background-color:var(--pico-color-pink-150);color:var(--pico-color-dark)}.pico-background-pink-100{background-color:var(--pico-color-pink-100);color:var(--pico-color-dark)}.pico-background-pink-50{background-color:var(--pico-color-pink-50);color:var(--pico-color-dark)}.pico-background-pink{background-color:var(--pico-color-pink);color:var(--pico-color-light)}.pico-background-fuchsia-950{background-color:var(--pico-color-fuchsia-950);color:var(--pico-color-light)}.pico-background-fuchsia-900{background-color:var(--pico-color-fuchsia-900);color:var(--pico-color-light)}.pico-background-fuchsia-850{background-color:var(--pico-color-fuchsia-850);color:var(--pico-color-light)}.pico-background-fuchsia-800{background-color:var(--pico-color-fuchsia-800);color:var(--pico-color-light)}.pico-background-fuchsia-750{background-color:var(--pico-color-fuchsia-750);color:var(--pico-color-light)}.pico-background-fuchsia-700{background-color:var(--pico-color-fuchsia-700);color:var(--pico-color-light)}.pico-background-fuchsia-650{background-color:var(--pico-color-fuchsia-650);color:var(--pico-color-light)}.pico-background-fuchsia-600{background-color:var(--pico-color-fuchsia-600);color:var(--pico-color-light)}.pico-background-fuchsia-550{background-color:var(--pico-color-fuchsia-550);color:var(--pico-color-light)}.pico-background-fuchsia-500{background-color:var(--pico-color-fuchsia-500);color:var(--pico-color-light)}.pico-background-fuchsia-450{background-color:var(--pico-color-fuchsia-450);color:var(--pico-color-light)}.pico-background-fuchsia-400{background-color:var(--pico-color-fuchsia-400);color:var(--pico-color-dark)}.pico-background-fuchsia-350{background-color:var(--pico-color-fuchsia-350);color:var(--pico-color-dark)}.pico-background-fuchsia-300{background-color:var(--pico-color-fuchsia-300);color:var(--pico-color-dark)}.pico-background-fuchsia-250{background-color:var(--pico-color-fuchsia-250);color:var(--pico-color-dark)}.pico-background-fuchsia-200{background-color:var(--pico-color-fuchsia-200);color:var(--pico-color-dark)}.pico-background-fuchsia-150{background-color:var(--pico-color-fuchsia-150);color:var(--pico-color-dark)}.pico-background-fuchsia-100{background-color:var(--pico-color-fuchsia-100);color:var(--pico-color-dark)}.pico-background-fuchsia-50{background-color:var(--pico-color-fuchsia-50);color:var(--pico-color-dark)}.pico-background-fuchsia{background-color:var(--pico-color-fuchsia);color:var(--pico-color-light)}.pico-background-purple-950{background-color:var(--pico-color-purple-950);color:var(--pico-color-light)}.pico-background-purple-900{background-color:var(--pico-color-purple-900);color:var(--pico-color-light)}.pico-background-purple-850{background-color:var(--pico-color-purple-850);color:var(--pico-color-light)}.pico-background-purple-800{background-color:var(--pico-color-purple-800);color:var(--pico-color-light)}.pico-background-purple-750{background-color:var(--pico-color-purple-750);color:var(--pico-color-light)}.pico-background-purple-700{background-color:var(--pico-color-purple-700);color:var(--pico-color-light)}.pico-background-purple-650{background-color:var(--pico-color-purple-650);color:var(--pico-color-light)}.pico-background-purple-600{background-color:var(--pico-color-purple-600);color:var(--pico-color-light)}.pico-background-purple-550{background-color:var(--pico-color-purple-550);color:var(--pico-color-light)}.pico-background-purple-500{background-color:var(--pico-color-purple-500);color:var(--pico-color-light)}.pico-background-purple-450{background-color:var(--pico-color-purple-450);color:var(--pico-color-dark)}.pico-background-purple-400{background-color:var(--pico-color-purple-400);color:var(--pico-color-dark)}.pico-background-purple-350{background-color:var(--pico-color-purple-350);color:var(--pico-color-dark)}.pico-background-purple-300{background-color:var(--pico-color-purple-300);color:var(--pico-color-dark)}.pico-background-purple-250{background-color:var(--pico-color-purple-250);color:var(--pico-color-dark)}.pico-background-purple-200{background-color:var(--pico-color-purple-200);color:var(--pico-color-dark)}.pico-background-purple-150{background-color:var(--pico-color-purple-150);color:var(--pico-color-dark)}.pico-background-purple-100{background-color:var(--pico-color-purple-100);color:var(--pico-color-dark)}.pico-background-purple-50{background-color:var(--pico-color-purple-50);color:var(--pico-color-dark)}.pico-background-purple{background-color:var(--pico-color-purple);color:var(--pico-color-light)}.pico-background-violet-950{background-color:var(--pico-color-violet-950);color:var(--pico-color-light)}.pico-background-violet-900{background-color:var(--pico-color-violet-900);color:var(--pico-color-light)}.pico-background-violet-850{background-color:var(--pico-color-violet-850);color:var(--pico-color-light)}.pico-background-violet-800{background-color:var(--pico-color-violet-800);color:var(--pico-color-light)}.pico-background-violet-750{background-color:var(--pico-color-violet-750);color:var(--pico-color-light)}.pico-background-violet-700{background-color:var(--pico-color-violet-700);color:var(--pico-color-light)}.pico-background-violet-650{background-color:var(--pico-color-violet-650);color:var(--pico-color-light)}.pico-background-violet-600{background-color:var(--pico-color-violet-600);color:var(--pico-color-light)}.pico-background-violet-550{background-color:var(--pico-color-violet-550);color:var(--pico-color-light)}.pico-background-violet-500{background-color:var(--pico-color-violet-500);color:var(--pico-color-light)}.pico-background-violet-450{background-color:var(--pico-color-violet-450);color:var(--pico-color-dark)}.pico-background-violet-400{background-color:var(--pico-color-violet-400);color:var(--pico-color-dark)}.pico-background-violet-350{background-color:var(--pico-color-violet-350);color:var(--pico-color-dark)}.pico-background-violet-300{background-color:var(--pico-color-violet-300);color:var(--pico-color-dark)}.pico-background-violet-250{background-color:var(--pico-color-violet-250);color:var(--pico-color-dark)}.pico-background-violet-200{background-color:var(--pico-color-violet-200);color:var(--pico-color-dark)}.pico-background-violet-150{background-color:var(--pico-color-violet-150);color:var(--pico-color-dark)}.pico-background-violet-100{background-color:var(--pico-color-violet-100);color:var(--pico-color-dark)}.pico-background-violet-50{background-color:var(--pico-color-violet-50);color:var(--pico-color-dark)}.pico-background-violet{background-color:var(--pico-color-violet);color:var(--pico-color-light)}.pico-background-indigo-950{background-color:var(--pico-color-indigo-950);color:var(--pico-color-light)}.pico-background-indigo-900{background-color:var(--pico-color-indigo-900);color:var(--pico-color-light)}.pico-background-indigo-850{background-color:var(--pico-color-indigo-850);color:var(--pico-color-light)}.pico-background-indigo-800{background-color:var(--pico-color-indigo-800);color:var(--pico-color-light)}.pico-background-indigo-750{background-color:var(--pico-color-indigo-750);color:var(--pico-color-light)}.pico-background-indigo-700{background-color:var(--pico-color-indigo-700);color:var(--pico-color-light)}.pico-background-indigo-650{background-color:var(--pico-color-indigo-650);color:var(--pico-color-light)}.pico-background-indigo-600{background-color:var(--pico-color-indigo-600);color:var(--pico-color-light)}.pico-background-indigo-550{background-color:var(--pico-color-indigo-550);color:var(--pico-color-light)}.pico-background-indigo-500{background-color:var(--pico-color-indigo-500);color:var(--pico-color-light)}.pico-background-indigo-450{background-color:var(--pico-color-indigo-450);color:var(--pico-color-dark)}.pico-background-indigo-400{background-color:var(--pico-color-indigo-400);color:var(--pico-color-dark)}.pico-background-indigo-350{background-color:var(--pico-color-indigo-350);color:var(--pico-color-dark)}.pico-background-indigo-300{background-color:var(--pico-color-indigo-300);color:var(--pico-color-dark)}.pico-background-indigo-250{background-color:var(--pico-color-indigo-250);color:var(--pico-color-dark)}.pico-background-indigo-200{background-color:var(--pico-color-indigo-200);color:var(--pico-color-dark)}.pico-background-indigo-150{background-color:var(--pico-color-indigo-150);color:var(--pico-color-dark)}.pico-background-indigo-100{background-color:var(--pico-color-indigo-100);color:var(--pico-color-dark)}.pico-background-indigo-50{background-color:var(--pico-color-indigo-50);color:var(--pico-color-dark)}.pico-background-indigo{background-color:var(--pico-color-indigo);color:var(--pico-color-light)}.pico-background-blue-950{background-color:var(--pico-color-blue-950);color:var(--pico-color-light)}.pico-background-blue-900{background-color:var(--pico-color-blue-900);color:var(--pico-color-light)}.pico-background-blue-850{background-color:var(--pico-color-blue-850);color:var(--pico-color-light)}.pico-background-blue-800{background-color:var(--pico-color-blue-800);color:var(--pico-color-light)}.pico-background-blue-750{background-color:var(--pico-color-blue-750);color:var(--pico-color-light)}.pico-background-blue-700{background-color:var(--pico-color-blue-700);color:var(--pico-color-light)}.pico-background-blue-650{background-color:var(--pico-color-blue-650);color:var(--pico-color-light)}.pico-background-blue-600{background-color:var(--pico-color-blue-600);color:var(--pico-color-light)}.pico-background-blue-550{background-color:var(--pico-color-blue-550);color:var(--pico-color-light)}.pico-background-blue-500{background-color:var(--pico-color-blue-500);color:var(--pico-color-light)}.pico-background-blue-450{background-color:var(--pico-color-blue-450);color:var(--pico-color-dark)}.pico-background-blue-400{background-color:var(--pico-color-blue-400);color:var(--pico-color-dark)}.pico-background-blue-350{background-color:var(--pico-color-blue-350);color:var(--pico-color-dark)}.pico-background-blue-300{background-color:var(--pico-color-blue-300);color:var(--pico-color-dark)}.pico-background-blue-250{background-color:var(--pico-color-blue-250);color:var(--pico-color-dark)}.pico-background-blue-200{background-color:var(--pico-color-blue-200);color:var(--pico-color-dark)}.pico-background-blue-150{background-color:var(--pico-color-blue-150);color:var(--pico-color-dark)}.pico-background-blue-100{background-color:var(--pico-color-blue-100);color:var(--pico-color-dark)}.pico-background-blue-50{background-color:var(--pico-color-blue-50);color:var(--pico-color-dark)}.pico-background-blue{background-color:var(--pico-color-blue);color:var(--pico-color-light)}.pico-background-azure-950{background-color:var(--pico-color-azure-950);color:var(--pico-color-light)}.pico-background-azure-900{background-color:var(--pico-color-azure-900);color:var(--pico-color-light)}.pico-background-azure-850{background-color:var(--pico-color-azure-850);color:var(--pico-color-light)}.pico-background-azure-800{background-color:var(--pico-color-azure-800);color:var(--pico-color-light)}.pico-background-azure-750{background-color:var(--pico-color-azure-750);color:var(--pico-color-light)}.pico-background-azure-700{background-color:var(--pico-color-azure-700);color:var(--pico-color-light)}.pico-background-azure-650{background-color:var(--pico-color-azure-650);color:var(--pico-color-light)}.pico-background-azure-600{background-color:var(--pico-color-azure-600);color:var(--pico-color-light)}.pico-background-azure-550{background-color:var(--pico-color-azure-550);color:var(--pico-color-light)}.pico-background-azure-500{background-color:var(--pico-color-azure-500);color:var(--pico-color-light)}.pico-background-azure-450{background-color:var(--pico-color-azure-450);color:var(--pico-color-light)}.pico-background-azure-400{background-color:var(--pico-color-azure-400);color:var(--pico-color-light)}.pico-background-azure-350{background-color:var(--pico-color-azure-350);color:var(--pico-color-dark)}.pico-background-azure-300{background-color:var(--pico-color-azure-300);color:var(--pico-color-dark)}.pico-background-azure-250{background-color:var(--pico-color-azure-250);color:var(--pico-color-dark)}.pico-background-azure-200{background-color:var(--pico-color-azure-200);color:var(--pico-color-dark)}.pico-background-azure-150{background-color:var(--pico-color-azure-150);color:var(--pico-color-dark)}.pico-background-azure-100{background-color:var(--pico-color-azure-100);color:var(--pico-color-dark)}.pico-background-azure-50{background-color:var(--pico-color-azure-50);color:var(--pico-color-dark)}.pico-background-azure{background-color:var(--pico-color-azure);color:var(--pico-color-light)}.pico-background-cyan-950{background-color:var(--pico-color-cyan-950);color:var(--pico-color-light)}.pico-background-cyan-900{background-color:var(--pico-color-cyan-900);color:var(--pico-color-light)}.pico-background-cyan-850{background-color:var(--pico-color-cyan-850);color:var(--pico-color-light)}.pico-background-cyan-800{background-color:var(--pico-color-cyan-800);color:var(--pico-color-light)}.pico-background-cyan-750{background-color:var(--pico-color-cyan-750);color:var(--pico-color-light)}.pico-background-cyan-700{background-color:var(--pico-color-cyan-700);color:var(--pico-color-light)}.pico-background-cyan-650{background-color:var(--pico-color-cyan-650);color:var(--pico-color-light)}.pico-background-cyan-600{background-color:var(--pico-color-cyan-600);color:var(--pico-color-light)}.pico-background-cyan-550{background-color:var(--pico-color-cyan-550);color:var(--pico-color-light)}.pico-background-cyan-500{background-color:var(--pico-color-cyan-500);color:var(--pico-color-light)}.pico-background-cyan-450{background-color:var(--pico-color-cyan-450);color:var(--pico-color-light)}.pico-background-cyan-400{background-color:var(--pico-color-cyan-400);color:var(--pico-color-light)}.pico-background-cyan-350{background-color:var(--pico-color-cyan-350);color:var(--pico-color-light)}.pico-background-cyan-300{background-color:var(--pico-color-cyan-300);color:var(--pico-color-dark)}.pico-background-cyan-250{background-color:var(--pico-color-cyan-250);color:var(--pico-color-dark)}.pico-background-cyan-200{background-color:var(--pico-color-cyan-200);color:var(--pico-color-dark)}.pico-background-cyan-150{background-color:var(--pico-color-cyan-150);color:var(--pico-color-dark)}.pico-background-cyan-100{background-color:var(--pico-color-cyan-100);color:var(--pico-color-dark)}.pico-background-cyan-50{background-color:var(--pico-color-cyan-50);color:var(--pico-color-dark)}.pico-background-cyan{background-color:var(--pico-color-cyan);color:var(--pico-color-light)}.pico-background-jade-950{background-color:var(--pico-color-jade-950);color:var(--pico-color-light)}.pico-background-jade-900{background-color:var(--pico-color-jade-900);color:var(--pico-color-light)}.pico-background-jade-850{background-color:var(--pico-color-jade-850);color:var(--pico-color-light)}.pico-background-jade-800{background-color:var(--pico-color-jade-800);color:var(--pico-color-light)}.pico-background-jade-750{background-color:var(--pico-color-jade-750);color:var(--pico-color-light)}.pico-background-jade-700{background-color:var(--pico-color-jade-700);color:var(--pico-color-light)}.pico-background-jade-650{background-color:var(--pico-color-jade-650);color:var(--pico-color-light)}.pico-background-jade-600{background-color:var(--pico-color-jade-600);color:var(--pico-color-light)}.pico-background-jade-550{background-color:var(--pico-color-jade-550);color:var(--pico-color-light)}.pico-background-jade-500{background-color:var(--pico-color-jade-500);color:var(--pico-color-light)}.pico-background-jade-450{background-color:var(--pico-color-jade-450);color:var(--pico-color-light)}.pico-background-jade-400{background-color:var(--pico-color-jade-400);color:var(--pico-color-light)}.pico-background-jade-350{background-color:var(--pico-color-jade-350);color:var(--pico-color-light)}.pico-background-jade-300{background-color:var(--pico-color-jade-300);color:var(--pico-color-dark)}.pico-background-jade-250{background-color:var(--pico-color-jade-250);color:var(--pico-color-dark)}.pico-background-jade-200{background-color:var(--pico-color-jade-200);color:var(--pico-color-dark)}.pico-background-jade-150{background-color:var(--pico-color-jade-150);color:var(--pico-color-dark)}.pico-background-jade-100{background-color:var(--pico-color-jade-100);color:var(--pico-color-dark)}.pico-background-jade-50{background-color:var(--pico-color-jade-50);color:var(--pico-color-dark)}.pico-background-jade{background-color:var(--pico-color-jade);color:var(--pico-color-light)}.pico-background-green-950{background-color:var(--pico-color-green-950);color:var(--pico-color-light)}.pico-background-green-900{background-color:var(--pico-color-green-900);color:var(--pico-color-light)}.pico-background-green-850{background-color:var(--pico-color-green-850);color:var(--pico-color-light)}.pico-background-green-800{background-color:var(--pico-color-green-800);color:var(--pico-color-light)}.pico-background-green-750{background-color:var(--pico-color-green-750);color:var(--pico-color-light)}.pico-background-green-700{background-color:var(--pico-color-green-700);color:var(--pico-color-light)}.pico-background-green-650{background-color:var(--pico-color-green-650);color:var(--pico-color-light)}.pico-background-green-600{background-color:var(--pico-color-green-600);color:var(--pico-color-light)}.pico-background-green-550{background-color:var(--pico-color-green-550);color:var(--pico-color-light)}.pico-background-green-500{background-color:var(--pico-color-green-500);color:var(--pico-color-light)}.pico-background-green-450{background-color:var(--pico-color-green-450);color:var(--pico-color-light)}.pico-background-green-400{background-color:var(--pico-color-green-400);color:var(--pico-color-light)}.pico-background-green-350{background-color:var(--pico-color-green-350);color:var(--pico-color-dark)}.pico-background-green-300{background-color:var(--pico-color-green-300);color:var(--pico-color-dark)}.pico-background-green-250{background-color:var(--pico-color-green-250);color:var(--pico-color-dark)}.pico-background-green-200{background-color:var(--pico-color-green-200);color:var(--pico-color-dark)}.pico-background-green-150{background-color:var(--pico-color-green-150);color:var(--pico-color-dark)}.pico-background-green-100{background-color:var(--pico-color-green-100);color:var(--pico-color-dark)}.pico-background-green-50{background-color:var(--pico-color-green-50);color:var(--pico-color-dark)}.pico-background-green{background-color:var(--pico-color-green);color:var(--pico-color-light)}.pico-background-lime-950{background-color:var(--pico-color-lime-950);color:var(--pico-color-light)}.pico-background-lime-900{background-color:var(--pico-color-lime-900);color:var(--pico-color-light)}.pico-background-lime-850{background-color:var(--pico-color-lime-850);color:var(--pico-color-light)}.pico-background-lime-800{background-color:var(--pico-color-lime-800);color:var(--pico-color-light)}.pico-background-lime-750{background-color:var(--pico-color-lime-750);color:var(--pico-color-light)}.pico-background-lime-700{background-color:var(--pico-color-lime-700);color:var(--pico-color-light)}.pico-background-lime-650{background-color:var(--pico-color-lime-650);color:var(--pico-color-light)}.pico-background-lime-600{background-color:var(--pico-color-lime-600);color:var(--pico-color-light)}.pico-background-lime-550{background-color:var(--pico-color-lime-550);color:var(--pico-color-light)}.pico-background-lime-500{background-color:var(--pico-color-lime-500);color:var(--pico-color-light)}.pico-background-lime-450{background-color:var(--pico-color-lime-450);color:var(--pico-color-light)}.pico-background-lime-400{background-color:var(--pico-color-lime-400);color:var(--pico-color-light)}.pico-background-lime-350{background-color:var(--pico-color-lime-350);color:var(--pico-color-dark)}.pico-background-lime-300{background-color:var(--pico-color-lime-300);color:var(--pico-color-dark)}.pico-background-lime-250{background-color:var(--pico-color-lime-250);color:var(--pico-color-dark)}.pico-background-lime-200{background-color:var(--pico-color-lime-200);color:var(--pico-color-dark)}.pico-background-lime-150{background-color:var(--pico-color-lime-150);color:var(--pico-color-dark)}.pico-background-lime-100{background-color:var(--pico-color-lime-100);color:var(--pico-color-dark)}.pico-background-lime-50{background-color:var(--pico-color-lime-50);color:var(--pico-color-dark)}.pico-background-lime{background-color:var(--pico-color-lime);color:var(--pico-color-dark)}.pico-background-yellow-950{background-color:var(--pico-color-yellow-950);color:var(--pico-color-light)}.pico-background-yellow-900{background-color:var(--pico-color-yellow-900);color:var(--pico-color-light)}.pico-background-yellow-850{background-color:var(--pico-color-yellow-850);color:var(--pico-color-light)}.pico-background-yellow-800{background-color:var(--pico-color-yellow-800);color:var(--pico-color-light)}.pico-background-yellow-750{background-color:var(--pico-color-yellow-750);color:var(--pico-color-light)}.pico-background-yellow-700{background-color:var(--pico-color-yellow-700);color:var(--pico-color-light)}.pico-background-yellow-650{background-color:var(--pico-color-yellow-650);color:var(--pico-color-light)}.pico-background-yellow-600{background-color:var(--pico-color-yellow-600);color:var(--pico-color-light)}.pico-background-yellow-550{background-color:var(--pico-color-yellow-550);color:var(--pico-color-light)}.pico-background-yellow-500{background-color:var(--pico-color-yellow-500);color:var(--pico-color-light)}.pico-background-yellow-450{background-color:var(--pico-color-yellow-450);color:var(--pico-color-light)}.pico-background-yellow-400{background-color:var(--pico-color-yellow-400);color:var(--pico-color-dark)}.pico-background-yellow-350{background-color:var(--pico-color-yellow-350);color:var(--pico-color-dark)}.pico-background-yellow-300{background-color:var(--pico-color-yellow-300);color:var(--pico-color-dark)}.pico-background-yellow-250{background-color:var(--pico-color-yellow-250);color:var(--pico-color-dark)}.pico-background-yellow-200{background-color:var(--pico-color-yellow-200);color:var(--pico-color-dark)}.pico-background-yellow-150{background-color:var(--pico-color-yellow-150);color:var(--pico-color-dark)}.pico-background-yellow-100{background-color:var(--pico-color-yellow-100);color:var(--pico-color-dark)}.pico-background-yellow-50{background-color:var(--pico-color-yellow-50);color:var(--pico-color-dark)}.pico-background-yellow{background-color:var(--pico-color-yellow);color:var(--pico-color-dark)}.pico-background-amber-950{background-color:var(--pico-color-amber-950);color:var(--pico-color-light)}.pico-background-amber-900{background-color:var(--pico-color-amber-900);color:var(--pico-color-light)}.pico-background-amber-850{background-color:var(--pico-color-amber-850);color:var(--pico-color-light)}.pico-background-amber-800{background-color:var(--pico-color-amber-800);color:var(--pico-color-light)}.pico-background-amber-750{background-color:var(--pico-color-amber-750);color:var(--pico-color-light)}.pico-background-amber-700{background-color:var(--pico-color-amber-700);color:var(--pico-color-light)}.pico-background-amber-650{background-color:var(--pico-color-amber-650);color:var(--pico-color-light)}.pico-background-amber-600{background-color:var(--pico-color-amber-600);color:var(--pico-color-light)}.pico-background-amber-550{background-color:var(--pico-color-amber-550);color:var(--pico-color-light)}.pico-background-amber-500{background-color:var(--pico-color-amber-500);color:var(--pico-color-light)}.pico-background-amber-450{background-color:var(--pico-color-amber-450);color:var(--pico-color-light)}.pico-background-amber-400{background-color:var(--pico-color-amber-400);color:var(--pico-color-dark)}.pico-background-amber-350{background-color:var(--pico-color-amber-350);color:var(--pico-color-dark)}.pico-background-amber-300{background-color:var(--pico-color-amber-300);color:var(--pico-color-dark)}.pico-background-amber-250{background-color:var(--pico-color-amber-250);color:var(--pico-color-dark)}.pico-background-amber-200{background-color:var(--pico-color-amber-200);color:var(--pico-color-dark)}.pico-background-amber-150{background-color:var(--pico-color-amber-150);color:var(--pico-color-dark)}.pico-background-amber-100{background-color:var(--pico-color-amber-100);color:var(--pico-color-dark)}.pico-background-amber-50{background-color:var(--pico-color-amber-50);color:var(--pico-color-dark)}.pico-background-amber{background-color:var(--pico-color-amber);color:var(--pico-color-dark)}.pico-background-pumpkin-950{background-color:var(--pico-color-pumpkin-950);color:var(--pico-color-light)}.pico-background-pumpkin-900{background-color:var(--pico-color-pumpkin-900);color:var(--pico-color-light)}.pico-background-pumpkin-850{background-color:var(--pico-color-pumpkin-850);color:var(--pico-color-light)}.pico-background-pumpkin-800{background-color:var(--pico-color-pumpkin-800);color:var(--pico-color-light)}.pico-background-pumpkin-750{background-color:var(--pico-color-pumpkin-750);color:var(--pico-color-light)}.pico-background-pumpkin-700{background-color:var(--pico-color-pumpkin-700);color:var(--pico-color-light)}.pico-background-pumpkin-650{background-color:var(--pico-color-pumpkin-650);color:var(--pico-color-light)}.pico-background-pumpkin-600{background-color:var(--pico-color-pumpkin-600);color:var(--pico-color-light)}.pico-background-pumpkin-550{background-color:var(--pico-color-pumpkin-550);color:var(--pico-color-light)}.pico-background-pumpkin-500{background-color:var(--pico-color-pumpkin-500);color:var(--pico-color-light)}.pico-background-pumpkin-450{background-color:var(--pico-color-pumpkin-450);color:var(--pico-color-light)}.pico-background-pumpkin-400{background-color:var(--pico-color-pumpkin-400);color:var(--pico-color-dark)}.pico-background-pumpkin-350{background-color:var(--pico-color-pumpkin-350);color:var(--pico-color-dark)}.pico-background-pumpkin-300{background-color:var(--pico-color-pumpkin-300);color:var(--pico-color-dark)}.pico-background-pumpkin-250{background-color:var(--pico-color-pumpkin-250);color:var(--pico-color-dark)}.pico-background-pumpkin-200{background-color:var(--pico-color-pumpkin-200);color:var(--pico-color-dark)}.pico-background-pumpkin-150{background-color:var(--pico-color-pumpkin-150);color:var(--pico-color-dark)}.pico-background-pumpkin-100{background-color:var(--pico-color-pumpkin-100);color:var(--pico-color-dark)}.pico-background-pumpkin-50{background-color:var(--pico-color-pumpkin-50);color:var(--pico-color-dark)}.pico-background-pumpkin{background-color:var(--pico-color-pumpkin);color:var(--pico-color-dark)}.pico-background-orange-950{background-color:var(--pico-color-orange-950);color:var(--pico-color-light)}.pico-background-orange-900{background-color:var(--pico-color-orange-900);color:var(--pico-color-light)}.pico-background-orange-850{background-color:var(--pico-color-orange-850);color:var(--pico-color-light)}.pico-background-orange-800{background-color:var(--pico-color-orange-800);color:var(--pico-color-light)}.pico-background-orange-750{background-color:var(--pico-color-orange-750);color:var(--pico-color-light)}.pico-background-orange-700{background-color:var(--pico-color-orange-700);color:var(--pico-color-light)}.pico-background-orange-650{background-color:var(--pico-color-orange-650);color:var(--pico-color-light)}.pico-background-orange-600{background-color:var(--pico-color-orange-600);color:var(--pico-color-light)}.pico-background-orange-550{background-color:var(--pico-color-orange-550);color:var(--pico-color-light)}.pico-background-orange-500{background-color:var(--pico-color-orange-500);color:var(--pico-color-light)}.pico-background-orange-450{background-color:var(--pico-color-orange-450);color:var(--pico-color-light)}.pico-background-orange-400{background-color:var(--pico-color-orange-400);color:var(--pico-color-dark)}.pico-background-orange-350{background-color:var(--pico-color-orange-350);color:var(--pico-color-dark)}.pico-background-orange-300{background-color:var(--pico-color-orange-300);color:var(--pico-color-dark)}.pico-background-orange-250{background-color:var(--pico-color-orange-250);color:var(--pico-color-dark)}.pico-background-orange-200{background-color:var(--pico-color-orange-200);color:var(--pico-color-dark)}.pico-background-orange-150{background-color:var(--pico-color-orange-150);color:var(--pico-color-dark)}.pico-background-orange-100{background-color:var(--pico-color-orange-100);color:var(--pico-color-dark)}.pico-background-orange-50{background-color:var(--pico-color-orange-50);color:var(--pico-color-dark)}.pico-background-orange{background-color:var(--pico-color-orange);color:var(--pico-color-light)}.pico-background-sand-950{background-color:var(--pico-color-sand-950);color:var(--pico-color-light)}.pico-background-sand-900{background-color:var(--pico-color-sand-900);color:var(--pico-color-light)}.pico-background-sand-850{background-color:var(--pico-color-sand-850);color:var(--pico-color-light)}.pico-background-sand-800{background-color:var(--pico-color-sand-800);color:var(--pico-color-light)}.pico-background-sand-750{background-color:var(--pico-color-sand-750);color:var(--pico-color-light)}.pico-background-sand-700{background-color:var(--pico-color-sand-700);color:var(--pico-color-light)}.pico-background-sand-650{background-color:var(--pico-color-sand-650);color:var(--pico-color-light)}.pico-background-sand-600{background-color:var(--pico-color-sand-600);color:var(--pico-color-light)}.pico-background-sand-550{background-color:var(--pico-color-sand-550);color:var(--pico-color-light)}.pico-background-sand-500{background-color:var(--pico-color-sand-500);color:var(--pico-color-light)}.pico-background-sand-450{background-color:var(--pico-color-sand-450);color:var(--pico-color-dark)}.pico-background-sand-400{background-color:var(--pico-color-sand-400);color:var(--pico-color-dark)}.pico-background-sand-350{background-color:var(--pico-color-sand-350);color:var(--pico-color-dark)}.pico-background-sand-300{background-color:var(--pico-color-sand-300);color:var(--pico-color-dark)}.pico-background-sand-250{background-color:var(--pico-color-sand-250);color:var(--pico-color-dark)}.pico-background-sand-200{background-color:var(--pico-color-sand-200);color:var(--pico-color-dark)}.pico-background-sand-150{background-color:var(--pico-color-sand-150);color:var(--pico-color-dark)}.pico-background-sand-100{background-color:var(--pico-color-sand-100);color:var(--pico-color-dark)}.pico-background-sand-50{background-color:var(--pico-color-sand-50);color:var(--pico-color-dark)}.pico-background-sand{background-color:var(--pico-color-sand);color:var(--pico-color-dark)}.pico-background-grey-950{background-color:var(--pico-color-grey-950);color:var(--pico-color-light)}.pico-background-grey-900{background-color:var(--pico-color-grey-900);color:var(--pico-color-light)}.pico-background-grey-850{background-color:var(--pico-color-grey-850);color:var(--pico-color-light)}.pico-background-grey-800{background-color:var(--pico-color-grey-800);color:var(--pico-color-light)}.pico-background-grey-750{background-color:var(--pico-color-grey-750);color:var(--pico-color-light)}.pico-background-grey-700{background-color:var(--pico-color-grey-700);color:var(--pico-color-light)}.pico-background-grey-650{background-color:var(--pico-color-grey-650);color:var(--pico-color-light)}.pico-background-grey-600{background-color:var(--pico-color-grey-600);color:var(--pico-color-light)}.pico-background-grey-550{background-color:var(--pico-color-grey-550);color:var(--pico-color-light)}.pico-background-grey-500{background-color:var(--pico-color-grey-500);color:var(--pico-color-light)}.pico-background-grey-450{background-color:var(--pico-color-grey-450);color:var(--pico-color-dark)}.pico-background-grey-400{background-color:var(--pico-color-grey-400);color:var(--pico-color-dark)}.pico-background-grey-350{background-color:var(--pico-color-grey-350);color:var(--pico-color-dark)}.pico-background-grey-300{background-color:var(--pico-color-grey-300);color:var(--pico-color-dark)}.pico-background-grey-250{background-color:var(--pico-color-grey-250);color:var(--pico-color-dark)}.pico-background-grey-200{background-color:var(--pico-color-grey-200);color:var(--pico-color-dark)}.pico-background-grey-150{background-color:var(--pico-color-grey-150);color:var(--pico-color-dark)}.pico-background-grey-100{background-color:var(--pico-color-grey-100);color:var(--pico-color-dark)}.pico-background-grey-50{background-color:var(--pico-color-grey-50);color:var(--pico-color-dark)}.pico-background-grey{background-color:var(--pico-color-grey);color:var(--pico-color-dark)}.pico-background-zinc-950{background-color:var(--pico-color-zinc-950);color:var(--pico-color-light)}.pico-background-zinc-900{background-color:var(--pico-color-zinc-900);color:var(--pico-color-light)}.pico-background-zinc-850{background-color:var(--pico-color-zinc-850);color:var(--pico-color-light)}.pico-background-zinc-800{background-color:var(--pico-color-zinc-800);color:var(--pico-color-light)}.pico-background-zinc-750{background-color:var(--pico-color-zinc-750);color:var(--pico-color-light)}.pico-background-zinc-700{background-color:var(--pico-color-zinc-700);color:var(--pico-color-light)}.pico-background-zinc-650{background-color:var(--pico-color-zinc-650);color:var(--pico-color-light)}.pico-background-zinc-600{background-color:var(--pico-color-zinc-600);color:var(--pico-color-light)}.pico-background-zinc-550{background-color:var(--pico-color-zinc-550);color:var(--pico-color-light)}.pico-background-zinc-500{background-color:var(--pico-color-zinc-500);color:var(--pico-color-light)}.pico-background-zinc-450{background-color:var(--pico-color-zinc-450);color:var(--pico-color-dark)}.pico-background-zinc-400{background-color:var(--pico-color-zinc-400);color:var(--pico-color-dark)}.pico-background-zinc-350{background-color:var(--pico-color-zinc-350);color:var(--pico-color-dark)}.pico-background-zinc-300{background-color:var(--pico-color-zinc-300);color:var(--pico-color-dark)}.pico-background-zinc-250{background-color:var(--pico-color-zinc-250);color:var(--pico-color-dark)}.pico-background-zinc-200{background-color:var(--pico-color-zinc-200);color:var(--pico-color-dark)}.pico-background-zinc-150{background-color:var(--pico-color-zinc-150);color:var(--pico-color-dark)}.pico-background-zinc-100{background-color:var(--pico-color-zinc-100);color:var(--pico-color-dark)}.pico-background-zinc-50{background-color:var(--pico-color-zinc-50);color:var(--pico-color-dark)}.pico-background-zinc{background-color:var(--pico-color-zinc);color:var(--pico-color-light)}.pico-background-slate-950{background-color:var(--pico-color-slate-950);color:var(--pico-color-light)}.pico-background-slate-900{background-color:var(--pico-color-slate-900);color:var(--pico-color-light)}.pico-background-slate-850{background-color:var(--pico-color-slate-850);color:var(--pico-color-light)}.pico-background-slate-800{background-color:var(--pico-color-slate-800);color:var(--pico-color-light)}.pico-background-slate-750{background-color:var(--pico-color-slate-750);color:var(--pico-color-light)}.pico-background-slate-700{background-color:var(--pico-color-slate-700);color:var(--pico-color-light)}.pico-background-slate-650{background-color:var(--pico-color-slate-650);color:var(--pico-color-light)}.pico-background-slate-600{background-color:var(--pico-color-slate-600);color:var(--pico-color-light)}.pico-background-slate-550{background-color:var(--pico-color-slate-550);color:var(--pico-color-light)}.pico-background-slate-500{background-color:var(--pico-color-slate-500);color:var(--pico-color-light)}.pico-background-slate-450{background-color:var(--pico-color-slate-450);color:var(--pico-color-dark)}.pico-background-slate-400{background-color:var(--pico-color-slate-400);color:var(--pico-color-dark)}.pico-background-slate-350{background-color:var(--pico-color-slate-350);color:var(--pico-color-dark)}.pico-background-slate-300{background-color:var(--pico-color-slate-300);color:var(--pico-color-dark)}.pico-background-slate-250{background-color:var(--pico-color-slate-250);color:var(--pico-color-dark)}.pico-background-slate-200{background-color:var(--pico-color-slate-200);color:var(--pico-color-dark)}.pico-background-slate-150{background-color:var(--pico-color-slate-150);color:var(--pico-color-dark)}.pico-background-slate-100{background-color:var(--pico-color-slate-100);color:var(--pico-color-dark)}.pico-background-slate-50{background-color:var(--pico-color-slate-50);color:var(--pico-color-dark)}.pico-background-slate{background-color:var(--pico-color-slate);color:var(--pico-color-light)}
+4
static/pico.css
··· 1 + @charset "UTF-8";/*! 2 + * Pico CSS ✨ v2.1.1 (https://picocss.com) 3 + * Copyright 2019-2025 - Licensed under MIT 4 + */:host,:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,Helvetica,Arial,"Helvetica Neue",sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:0.1rem;--pico-border-radius:0.25rem;--pico-border-width:0.0625rem;--pico-outline-width:0.125rem;--pico-transition:0.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:var(--pico-spacing);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-form-element-spacing-vertical:0.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 0.0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(0.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:0.5rem;--pico-nav-link-spacing-vertical:0.5rem;--pico-nav-link-spacing-horizontal:0.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--pico-icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--pico-icon-loading:url("data:image/svg+xml,%3Csvg fill='none' height='24' width='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' %3E%3Cstyle%3E g %7B animation: rotate 2s linear infinite; transform-origin: center center; %7D circle %7B stroke-dasharray: 75,100; stroke-dashoffset: -5; animation: dash 1.5s ease-in-out infinite; stroke-linecap: round; %7D @keyframes rotate %7B 0%25 %7B transform: rotate(0deg); %7D 100%25 %7B transform: rotate(360deg); %7D %7D @keyframes dash %7B 0%25 %7B stroke-dasharray: 1,100; stroke-dashoffset: 0; %7D 50%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -17.5; %7D 100%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -62; %7D %7D %3C/style%3E%3Cg%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='rgb(136, 145, 164)' stroke-width='4' /%3E%3C/g%3E%3C/svg%3E")}@media (min-width:576px){:host,:root{--pico-font-size:106.25%}}@media (min-width:768px){:host,:root{--pico-font-size:112.5%}}@media (min-width:1024px){:host,:root{--pico-font-size:118.75%}}@media (min-width:1280px){:host,:root{--pico-font-size:125%}}@media (min-width:1536px){:host,:root{--pico-font-size:131.25%}}a{--pico-text-decoration:underline}small{--pico-font-size:0.875em}h1,h2,h3,h4,h5,h6{--pico-font-weight:700}h1{--pico-font-size:2rem;--pico-line-height:1.125;--pico-typography-spacing-top:3rem}h2{--pico-font-size:1.75rem;--pico-line-height:1.15;--pico-typography-spacing-top:2.625rem}h3{--pico-font-size:1.5rem;--pico-line-height:1.175;--pico-typography-spacing-top:2.25rem}h4{--pico-font-size:1.25rem;--pico-line-height:1.2;--pico-typography-spacing-top:1.874rem}h5{--pico-font-size:1.125rem;--pico-line-height:1.225;--pico-typography-spacing-top:1.6875rem}h6{--pico-font-size:1rem;--pico-line-height:1.25;--pico-typography-spacing-top:1.5rem}tfoot td,tfoot th,thead td,thead th{--pico-font-weight:600;--pico-border-width:0.1875rem}code,kbd,pre,samp{--pico-font-family:var(--pico-font-family-monospace)}kbd{--pico-font-weight:bolder}:where(select,textarea),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-outline-width:0.0625rem}[type=search]{--pico-border-radius:5rem}[type=checkbox],[type=radio]{--pico-border-width:0.125rem}[type=checkbox][role=switch]{--pico-border-width:0.1875rem}[role=search]{--pico-border-radius:5rem}[role=group] [role=button],[role=group] [type=button],[role=group] [type=submit],[role=group] button,[role=search] [role=button],[role=search] [type=button],[role=search] [type=submit],[role=search] button{--pico-form-element-spacing-horizontal:2rem}details summary[role=button]::after{filter:brightness(0) invert(1)}[aria-busy=true]:not(input,select,textarea):is(button,[type=submit],[type=button],[type=reset],[role=button])::before{filter:brightness(0) invert(1)}:host(:not([data-theme=dark])),:root:not([data-theme=dark]),[data-theme=light]{color-scheme:light;--pico-background-color:#fff;--pico-color:#373c44;--pico-text-selection-color:rgba(148, 134, 225, 0.25);--pico-muted-color:#646b79;--pico-muted-border-color:rgb(231, 234, 239.5);--pico-primary:#655cd6;--pico-primary-background:#524ed2;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(101, 92, 214, 0.5);--pico-primary-hover:#4040bf;--pico-primary-hover-background:#4040bf;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(148, 134, 225, 0.5);--pico-primary-inverse:#fff;--pico-secondary:#5d6b89;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(93, 107, 137, 0.5);--pico-secondary-hover:#48536b;--pico-secondary-hover-background:#48536b;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(93, 107, 137, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#181c25;--pico-contrast-background:#181c25;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(24, 28, 37, 0.5);--pico-contrast-hover:#000;--pico-contrast-hover-background:#000;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-secondary-hover);--pico-contrast-focus:rgba(93, 107, 137, 0.25);--pico-contrast-inverse:#fff;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(129, 145, 181, 0.01698),0.0335rem 0.067rem 0.402rem rgba(129, 145, 181, 0.024),0.0625rem 0.125rem 0.75rem rgba(129, 145, 181, 0.03),0.1125rem 0.225rem 1.35rem rgba(129, 145, 181, 0.036),0.2085rem 0.417rem 2.502rem rgba(129, 145, 181, 0.04302),0.5rem 1rem 6rem rgba(129, 145, 181, 0.06),0 0 0 0.0625rem rgba(129, 145, 181, 0.015);--pico-h1-color:#2d3138;--pico-h2-color:#373c44;--pico-h3-color:#424751;--pico-h4-color:#4d535e;--pico-h5-color:#5c6370;--pico-h6-color:#646b79;--pico-mark-background-color:rgb(252.5, 230.5, 191.5);--pico-mark-color:#0f1114;--pico-ins-color:rgb(28.5, 105.5, 84);--pico-del-color:rgb(136, 56.5, 53);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(243, 244.5, 246.75);--pico-code-color:#646b79;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(251, 251.5, 252.25);--pico-form-element-selected-background-color:#dfe3eb;--pico-form-element-border-color:#cfd5e2;--pico-form-element-color:#23262c;--pico-form-element-placeholder-color:var(--pico-muted-color);--pico-form-element-active-background-color:#fff;--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(183.5, 105.5, 106.5);--pico-form-element-invalid-active-border-color:rgb(200.25, 79.25, 72.25);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:rgb(76, 154.5, 137.5);--pico-form-element-valid-active-border-color:rgb(39, 152.75, 118.75);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#bfc7d9;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#dfe3eb;--pico-range-active-border-color:#bfc7d9;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:var(--pico-background-color);--pico-card-border-color:var(--pico-muted-border-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(251, 251.5, 252.25);--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(232, 234, 237, 0.75);--pico-progress-background-color:#dfe3eb;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(76, 154.5, 137.5)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(200.25, 79.25, 72.25)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}:host(:not([data-theme=dark])) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),:root:not([data-theme=dark]) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),[data-theme=light] input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}@media only screen and (prefers-color-scheme:dark){:host(:not([data-theme])),:root:not([data-theme]){color-scheme:dark;--pico-background-color:rgb(19, 22.5, 30.5);--pico-color:#c2c7d0;--pico-text-selection-color:rgba(162, 148, 229, 0.1875);--pico-muted-color:#7b8495;--pico-muted-border-color:#202632;--pico-primary:#a294e5;--pico-primary-background:#524ed2;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(162, 148, 229, 0.5);--pico-primary-hover:#bdb2ec;--pico-primary-hover-background:#655cd6;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(162, 148, 229, 0.375);--pico-primary-inverse:#fff;--pico-secondary:#969eaf;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(150, 158, 175, 0.5);--pico-secondary-hover:#b3b9c5;--pico-secondary-hover-background:#5d6b89;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(144, 158, 190, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#dfe3eb;--pico-contrast-background:#eff1f4;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(223, 227, 235, 0.5);--pico-contrast-hover:#fff;--pico-contrast-hover-background:#fff;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-contrast-hover);--pico-contrast-focus:rgba(207, 213, 226, 0.25);--pico-contrast-inverse:#000;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698),0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024),0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03),0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036),0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302),0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06),0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);--pico-h1-color:#f0f1f3;--pico-h2-color:#e0e3e7;--pico-h3-color:#c2c7d0;--pico-h4-color:#b3b9c5;--pico-h5-color:#a4acba;--pico-h6-color:#8891a4;--pico-mark-background-color:#014063;--pico-mark-color:#fff;--pico-ins-color:#62af9a;--pico-del-color:rgb(205.5, 126, 123);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(26, 30.5, 40.25);--pico-code-color:#8891a4;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(28, 33, 43.5);--pico-form-element-selected-background-color:#2a3140;--pico-form-element-border-color:#2a3140;--pico-form-element-color:#e0e3e7;--pico-form-element-placeholder-color:#8891a4;--pico-form-element-active-background-color:rgb(26, 30.5, 40.25);--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(149.5, 74, 80);--pico-form-element-invalid-active-border-color:rgb(183.25, 63.5, 59);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#2a7b6f;--pico-form-element-valid-active-border-color:rgb(22, 137, 105.5);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#333c4e;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#202632;--pico-range-active-border-color:#2a3140;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:#181c25;--pico-card-border-color:var(--pico-card-background-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(26, 30.5, 40.25);--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(7.5, 8.5, 10, 0.75);--pico-progress-background-color:#202632;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(149.5, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}:host(:not([data-theme])) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),:root:not([data-theme]) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}}[data-theme=dark]{color-scheme:dark;--pico-background-color:rgb(19, 22.5, 30.5);--pico-color:#c2c7d0;--pico-text-selection-color:rgba(162, 148, 229, 0.1875);--pico-muted-color:#7b8495;--pico-muted-border-color:#202632;--pico-primary:#a294e5;--pico-primary-background:#524ed2;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(162, 148, 229, 0.5);--pico-primary-hover:#bdb2ec;--pico-primary-hover-background:#655cd6;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(162, 148, 229, 0.375);--pico-primary-inverse:#fff;--pico-secondary:#969eaf;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(150, 158, 175, 0.5);--pico-secondary-hover:#b3b9c5;--pico-secondary-hover-background:#5d6b89;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(144, 158, 190, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#dfe3eb;--pico-contrast-background:#eff1f4;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(223, 227, 235, 0.5);--pico-contrast-hover:#fff;--pico-contrast-hover-background:#fff;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-contrast-hover);--pico-contrast-focus:rgba(207, 213, 226, 0.25);--pico-contrast-inverse:#000;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698),0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024),0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03),0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036),0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302),0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06),0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);--pico-h1-color:#f0f1f3;--pico-h2-color:#e0e3e7;--pico-h3-color:#c2c7d0;--pico-h4-color:#b3b9c5;--pico-h5-color:#a4acba;--pico-h6-color:#8891a4;--pico-mark-background-color:#014063;--pico-mark-color:#fff;--pico-ins-color:#62af9a;--pico-del-color:rgb(205.5, 126, 123);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(26, 30.5, 40.25);--pico-code-color:#8891a4;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(28, 33, 43.5);--pico-form-element-selected-background-color:#2a3140;--pico-form-element-border-color:#2a3140;--pico-form-element-color:#e0e3e7;--pico-form-element-placeholder-color:#8891a4;--pico-form-element-active-background-color:rgb(26, 30.5, 40.25);--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(149.5, 74, 80);--pico-form-element-invalid-active-border-color:rgb(183.25, 63.5, 59);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#2a7b6f;--pico-form-element-valid-active-border-color:rgb(22, 137, 105.5);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#333c4e;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#202632;--pico-range-active-border-color:#2a3140;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:#181c25;--pico-card-border-color:var(--pico-card-background-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(26, 30.5, 40.25);--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(7.5, 8.5, 10, 0.75);--pico-progress-background-color:#202632;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(149.5, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}[data-theme=dark] input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}[type=checkbox],[type=radio],[type=range],progress{accent-color:var(--pico-primary)}*,::after,::before{box-sizing:border-box;background-repeat:no-repeat}::after,::before{text-decoration:inherit;vertical-align:inherit}:where(:host),:where(:root){-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family);text-underline-offset:var(--pico-text-underline-offset);text-rendering:optimizeLegibility;overflow-wrap:break-word;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{width:100%;margin:0}main{display:block}body>footer,body>header,body>main{width:100%;margin-right:auto;margin-left:auto;padding:var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal)}@media (min-width:576px){body>footer,body>header,body>main{max-width:510px;padding-right:0;padding-left:0}}@media (min-width:768px){body>footer,body>header,body>main{max-width:700px}}@media (min-width:1024px){body>footer,body>header,body>main{max-width:950px}}@media (min-width:1280px){body>footer,body>header,body>main{max-width:1200px}}@media (min-width:1536px){body>footer,body>header,body>main{max-width:1450px}}section{margin-bottom:var(--pico-block-spacing-vertical)}b,strong{font-weight:bolder}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}address,blockquote,dl,ol,p,pre,table,ul{margin-top:0;margin-bottom:var(--pico-typography-spacing-vertical);color:var(--pico-color);font-style:normal;font-weight:var(--pico-font-weight)}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:var(--pico-typography-spacing-vertical);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family)}h1{--pico-color:var(--pico-h1-color)}h2{--pico-color:var(--pico-h2-color)}h3{--pico-color:var(--pico-h3-color)}h4{--pico-color:var(--pico-h4-color)}h5{--pico-color:var(--pico-h5-color)}h6{--pico-color:var(--pico-h6-color)}:where(article,address,blockquote,dl,figure,form,ol,p,pre,table,ul)~:is(h1,h2,h3,h4,h5,h6){margin-top:var(--pico-typography-spacing-top)}p{margin-bottom:var(--pico-typography-spacing-vertical)}hgroup{margin-bottom:var(--pico-typography-spacing-vertical)}hgroup>*{margin-top:0;margin-bottom:0}hgroup>:not(:first-child):last-child{--pico-color:var(--pico-muted-color);--pico-font-weight:unset;font-size:1rem}:where(ol,ul) li{margin-bottom:calc(var(--pico-typography-spacing-vertical) * .25)}:where(dl,ol,ul) :where(dl,ol,ul){margin:0;margin-top:calc(var(--pico-typography-spacing-vertical) * .25)}ul li{list-style:square}mark{padding:.125rem .25rem;background-color:var(--pico-mark-background-color);color:var(--pico-mark-color);vertical-align:baseline}blockquote{display:block;margin:var(--pico-typography-spacing-vertical) 0;padding:var(--pico-spacing);border-right:none;border-left:.25rem solid var(--pico-blockquote-border-color);border-inline-start:0.25rem solid var(--pico-blockquote-border-color);border-inline-end:none}blockquote footer{margin-top:calc(var(--pico-typography-spacing-vertical) * .5);color:var(--pico-blockquote-footer-color)}abbr[title]{border-bottom:1px dotted;text-decoration:none;cursor:help}ins{color:var(--pico-ins-color);text-decoration:none}del{color:var(--pico-del-color)}::-moz-selection{background-color:var(--pico-text-selection-color)}::selection{background-color:var(--pico-text-selection-color)}:where(a:not([role=button])),[role=link]{--pico-color:var(--pico-primary);--pico-background-color:transparent;--pico-underline:var(--pico-primary-underline);outline:0;background-color:var(--pico-background-color);color:var(--pico-color);-webkit-text-decoration:var(--pico-text-decoration);text-decoration:var(--pico-text-decoration);text-decoration-color:var(--pico-underline);text-underline-offset:0.125em;transition:background-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition),-webkit-text-decoration var(--pico-transition);transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition),-webkit-text-decoration var(--pico-transition)}:where(a:not([role=button])):is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-primary-hover);--pico-underline:var(--pico-primary-hover-underline);--pico-text-decoration:underline}:where(a:not([role=button])):focus-visible,[role=link]:focus-visible{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}a[role=button]{display:inline-block}button{margin:0;overflow:visible;font-family:inherit;text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[role=button],[type=button],[type=file]::file-selector-button,[type=reset],[type=submit],button{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);--pico-color:var(--pico-primary-inverse);--pico-box-shadow:var(--pico-button-box-shadow, 0 0 0 rgba(0, 0, 0, 0));padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);border:var(--pico-border-width) solid var(--pico-border-color);border-radius:var(--pico-border-radius);outline:0;background-color:var(--pico-background-color);box-shadow:var(--pico-box-shadow);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:1rem;line-height:var(--pico-line-height);text-align:center;text-decoration:none;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}[role=button]:is(:hover,:active,:focus),[role=button]:is([aria-current]:not([aria-current=false])),[type=button]:is(:hover,:active,:focus),[type=button]:is([aria-current]:not([aria-current=false])),[type=file]::file-selector-button:is(:hover,:active,:focus),[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])),[type=reset]:is(:hover,:active,:focus),[type=reset]:is([aria-current]:not([aria-current=false])),[type=submit]:is(:hover,:active,:focus),[type=submit]:is([aria-current]:not([aria-current=false])),button:is(:hover,:active,:focus),button:is([aria-current]:not([aria-current=false])){--pico-background-color:var(--pico-primary-hover-background);--pico-border-color:var(--pico-primary-hover-border);--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0));--pico-color:var(--pico-primary-inverse)}[role=button]:focus,[role=button]:is([aria-current]:not([aria-current=false])):focus,[type=button]:focus,[type=button]:is([aria-current]:not([aria-current=false])):focus,[type=file]::file-selector-button:focus,[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus,[type=reset]:focus,[type=reset]:is([aria-current]:not([aria-current=false])):focus,[type=submit]:focus,[type=submit]:is([aria-current]:not([aria-current=false])):focus,button:focus,button:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}[type=button],[type=reset],[type=submit]{margin-bottom:var(--pico-spacing)}[type=file]::file-selector-button,[type=reset]{--pico-background-color:var(--pico-secondary-background);--pico-border-color:var(--pico-secondary-border);--pico-color:var(--pico-secondary-inverse);cursor:pointer}[type=file]::file-selector-button:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:var(--pico-secondary-hover-background);--pico-border-color:var(--pico-secondary-hover-border);--pico-color:var(--pico-secondary-inverse)}[type=file]::file-selector-button:focus,[type=reset]:focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}:where(button,[type=submit],[type=reset],[type=button],[role=button])[disabled],:where(fieldset[disabled]) :is(button,[type=submit],[type=button],[type=reset],[role=button]){opacity:.5;pointer-events:none}:where(table){width:100%;border-collapse:collapse;border-spacing:0;text-indent:0}td,th{padding:calc(var(--pico-spacing)/ 2) var(--pico-spacing);border-bottom:var(--pico-border-width) solid var(--pico-table-border-color);background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);text-align:left;text-align:start}tfoot td,tfoot th{border-top:var(--pico-border-width) solid var(--pico-table-border-color);border-bottom:0}table.striped tbody tr:nth-child(odd) td,table.striped tbody tr:nth-child(odd) th{background-color:var(--pico-table-row-stripped-background-color)}:where(audio,canvas,iframe,img,svg,video){vertical-align:middle}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}:where(iframe){border-style:none}img{max-width:100%;height:auto;border-style:none}:where(svg:not([fill])){fill:currentColor}svg:not(:host),svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-size:.875em;font-family:var(--pico-font-family)}pre code,pre samp{font-size:inherit;font-family:inherit}pre{-ms-overflow-style:scrollbar;overflow:auto}code,kbd,pre,samp{border-radius:var(--pico-border-radius);background:var(--pico-code-background-color);color:var(--pico-code-color);font-weight:var(--pico-font-weight);line-height:initial}code,kbd,samp{display:inline-block;padding:.375rem}pre{display:block;margin-bottom:var(--pico-spacing);overflow-x:auto}pre>code,pre>samp{display:block;padding:var(--pico-spacing);background:0 0;line-height:var(--pico-line-height)}kbd{background-color:var(--pico-code-kbd-background-color);color:var(--pico-code-kbd-color);vertical-align:baseline}figure{display:block;margin:0;padding:0}figure figcaption{padding:calc(var(--pico-spacing) * .5) 0;color:var(--pico-muted-color)}hr{height:0;margin:var(--pico-typography-spacing-vertical) 0;border:0;border-top:1px solid var(--pico-muted-border-color);color:inherit}[hidden],template{display:none!important}canvas{display:inline-block}input,optgroup,select,textarea{margin:0;font-size:1rem;line-height:var(--pico-line-height);font-family:inherit;letter-spacing:inherit}input{overflow:visible}select{text-transform:none}legend{max-width:100%;padding:0;color:inherit;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{padding:0}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}::-moz-focus-inner{padding:0;border-style:none}:-moz-focusring{outline:0}:-moz-ui-invalid{box-shadow:none}::-ms-expand{display:none}[type=file],[type=range]{padding:0;border-width:0}input:not([type=checkbox],[type=radio],[type=range]){height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2)}fieldset{width:100%;margin:0;margin-bottom:var(--pico-spacing);padding:0;border:0}fieldset legend,label{display:block;margin-bottom:calc(var(--pico-spacing) * .375);color:var(--pico-color);font-weight:var(--pico-form-label-font-weight,var(--pico-font-weight))}fieldset legend{margin-bottom:calc(var(--pico-spacing) * .5)}button[type=submit],input:not([type=checkbox],[type=radio]),select,textarea{width:100%}input:not([type=checkbox],[type=radio],[type=range],[type=file]),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal)}input,select,textarea{--pico-background-color:var(--pico-form-element-background-color);--pico-border-color:var(--pico-form-element-border-color);--pico-color:var(--pico-form-element-color);--pico-box-shadow:none;border:var(--pico-border-width) solid var(--pico-border-color);border-radius:var(--pico-border-radius);outline:0;background-color:var(--pico-background-color);box-shadow:var(--pico-box-shadow);color:var(--pico-color);font-weight:var(--pico-font-weight);transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}:where(select,textarea):not([readonly]):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[readonly]):is(:active,:focus){--pico-background-color:var(--pico-form-element-active-background-color)}:where(select,textarea):not([readonly]):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[role=switch],[readonly]):is(:active,:focus){--pico-border-color:var(--pico-form-element-active-border-color)}:where(select,textarea):not([readonly]):focus,input:not([type=submit],[type=button],[type=reset],[type=range],[type=file],[readonly]):focus{--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color)}:where(fieldset[disabled]) :is(input:not([type=submit],[type=button],[type=reset]),select,textarea),input:not([type=submit],[type=button],[type=reset])[disabled],label[aria-disabled=true],select[disabled],textarea[disabled]{opacity:var(--pico-form-element-disabled-opacity);pointer-events:none}label[aria-disabled=true] input[disabled]{opacity:1}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid]{padding-right:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem)!important;padding-left:var(--pico-form-element-spacing-horizontal);padding-inline-start:var(--pico-form-element-spacing-horizontal)!important;padding-inline-end:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem)!important;background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid=false]:not(select){background-image:var(--pico-icon-valid)}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid=true]:not(select){background-image:var(--pico-icon-invalid)}:where(input,select,textarea)[aria-invalid=false]{--pico-border-color:var(--pico-form-element-valid-border-color)}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus){--pico-border-color:var(--pico-form-element-valid-active-border-color)!important}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus):not([type=checkbox],[type=radio]){--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-valid-focus-color)!important}:where(input,select,textarea)[aria-invalid=true]{--pico-border-color:var(--pico-form-element-invalid-border-color)}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus){--pico-border-color:var(--pico-form-element-invalid-active-border-color)!important}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus):not([type=checkbox],[type=radio]){--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-invalid-focus-color)!important}[dir=rtl] :where(input,select,textarea):not([type=checkbox],[type=radio]):is([aria-invalid],[aria-invalid=true],[aria-invalid=false]){background-position:center left .75rem}input::-webkit-input-placeholder,input::placeholder,select:invalid,textarea::-webkit-input-placeholder,textarea::placeholder{color:var(--pico-form-element-placeholder-color);opacity:1}input:not([type=checkbox],[type=radio]),select,textarea{margin-bottom:var(--pico-spacing)}select::-ms-expand{border:0;background-color:transparent}select:not([multiple],[size]){padding-right:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem);padding-left:var(--pico-form-element-spacing-horizontal);padding-inline-start:var(--pico-form-element-spacing-horizontal);padding-inline-end:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem);background-image:var(--pico-icon-chevron);background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}select[multiple] option:checked{background:var(--pico-form-element-selected-background-color);color:var(--pico-form-element-color)}[dir=rtl] select:not([multiple],[size]){background-position:center left .75rem}textarea{display:block;resize:vertical}textarea[aria-invalid]{--pico-icon-height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2);background-position:top right .75rem!important;background-size:1rem var(--pico-icon-height)!important}:where(input,select,textarea,fieldset)+small{display:block;width:100%;margin-top:calc(var(--pico-spacing) * -.75);margin-bottom:var(--pico-spacing);color:var(--pico-muted-color)}:where(input,select,textarea,fieldset)[aria-invalid=false]+small{color:var(--pico-ins-color)}:where(input,select,textarea,fieldset)[aria-invalid=true]+small{color:var(--pico-del-color)}label>:where(input,select,textarea){margin-top:calc(var(--pico-spacing) * .25)}label:has([type=checkbox],[type=radio]){width:-moz-fit-content;width:fit-content;cursor:pointer}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:1.25em;height:1.25em;margin-top:-.125em;margin-inline-end:.5em;border-width:var(--pico-border-width);vertical-align:middle;cursor:pointer}[type=checkbox]::-ms-check,[type=radio]::-ms-check{display:none}[type=checkbox]:checked,[type=checkbox]:checked:active,[type=checkbox]:checked:focus,[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);background-image:var(--pico-icon-checkbox);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=checkbox]~label,[type=radio]~label{display:inline-block;margin-bottom:0;cursor:pointer}[type=checkbox]~label:not(:last-of-type),[type=radio]~label:not(:last-of-type){margin-inline-end:1em}[type=checkbox]:indeterminate{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);background-image:var(--pico-icon-minus);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=radio]{border-radius:50%}[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--pico-background-color:var(--pico-primary-inverse);border-width:.35em;background-image:none}[type=checkbox][role=switch]{--pico-background-color:var(--pico-switch-background-color);--pico-color:var(--pico-switch-color);width:2.25em;height:1.25em;border:var(--pico-border-width) solid var(--pico-border-color);border-radius:1.25em;background-color:var(--pico-background-color);line-height:1.25em}[type=checkbox][role=switch]:not([aria-invalid]){--pico-border-color:var(--pico-switch-background-color)}[type=checkbox][role=switch]:before{display:block;aspect-ratio:1;height:100%;border-radius:50%;background-color:var(--pico-color);box-shadow:var(--pico-switch-thumb-box-shadow);content:"";transition:margin .1s ease-in-out}[type=checkbox][role=switch]:focus{--pico-background-color:var(--pico-switch-background-color);--pico-border-color:var(--pico-switch-background-color)}[type=checkbox][role=switch]:checked{--pico-background-color:var(--pico-switch-checked-background-color);--pico-border-color:var(--pico-switch-checked-background-color);background-image:none}[type=checkbox][role=switch]:checked::before{margin-inline-start:calc(2.25em - 1.25em)}[type=checkbox][role=switch][disabled]{--pico-background-color:var(--pico-border-color)}[type=checkbox][aria-invalid=false]:checked,[type=checkbox][aria-invalid=false]:checked:active,[type=checkbox][aria-invalid=false]:checked:focus,[type=checkbox][role=switch][aria-invalid=false]:checked,[type=checkbox][role=switch][aria-invalid=false]:checked:active,[type=checkbox][role=switch][aria-invalid=false]:checked:focus{--pico-background-color:var(--pico-form-element-valid-border-color)}[type=checkbox]:checked:active[aria-invalid=true],[type=checkbox]:checked:focus[aria-invalid=true],[type=checkbox]:checked[aria-invalid=true],[type=checkbox][role=switch]:checked:active[aria-invalid=true],[type=checkbox][role=switch]:checked:focus[aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true]{--pico-background-color:var(--pico-form-element-invalid-border-color)}[type=checkbox][aria-invalid=false]:checked,[type=checkbox][aria-invalid=false]:checked:active,[type=checkbox][aria-invalid=false]:checked:focus,[type=checkbox][role=switch][aria-invalid=false]:checked,[type=checkbox][role=switch][aria-invalid=false]:checked:active,[type=checkbox][role=switch][aria-invalid=false]:checked:focus,[type=radio][aria-invalid=false]:checked,[type=radio][aria-invalid=false]:checked:active,[type=radio][aria-invalid=false]:checked:focus{--pico-border-color:var(--pico-form-element-valid-border-color)}[type=checkbox]:checked:active[aria-invalid=true],[type=checkbox]:checked:focus[aria-invalid=true],[type=checkbox]:checked[aria-invalid=true],[type=checkbox][role=switch]:checked:active[aria-invalid=true],[type=checkbox][role=switch]:checked:focus[aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true],[type=radio]:checked:active[aria-invalid=true],[type=radio]:checked:focus[aria-invalid=true],[type=radio]:checked[aria-invalid=true]{--pico-border-color:var(--pico-form-element-invalid-border-color)}[type=color]::-webkit-color-swatch-wrapper{padding:0}[type=color]::-moz-focus-inner{padding:0}[type=color]::-webkit-color-swatch{border:0;border-radius:calc(var(--pico-border-radius) * .5)}[type=color]::-moz-color-swatch{border:0;border-radius:calc(var(--pico-border-radius) * .5)}input:not([type=checkbox],[type=radio],[type=range],[type=file]):is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){--pico-icon-position:0.75rem;--pico-icon-width:1rem;padding-right:calc(var(--pico-icon-width) + var(--pico-icon-position));background-image:var(--pico-icon-date);background-position:center right var(--pico-icon-position);background-size:var(--pico-icon-width) auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=time]{background-image:var(--pico-icon-time)}[type=date]::-webkit-calendar-picker-indicator,[type=datetime-local]::-webkit-calendar-picker-indicator,[type=month]::-webkit-calendar-picker-indicator,[type=time]::-webkit-calendar-picker-indicator,[type=week]::-webkit-calendar-picker-indicator{width:var(--pico-icon-width);margin-right:calc(var(--pico-icon-width) * -1);margin-left:var(--pico-icon-position);opacity:0}@-moz-document url-prefix(){[type=date],[type=datetime-local],[type=month],[type=time],[type=week]{padding-right:var(--pico-form-element-spacing-horizontal)!important;background-image:none!important}}[dir=rtl] :is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){text-align:right}[type=file]{--pico-color:var(--pico-muted-color);margin-left:calc(var(--pico-outline-width) * -1);padding:calc(var(--pico-form-element-spacing-vertical) * .5) 0;padding-left:var(--pico-outline-width);border:0;border-radius:0;background:0 0}[type=file]::file-selector-button{margin-right:calc(var(--pico-spacing)/ 2);padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal)}[type=file]:is(:hover,:active,:focus)::file-selector-button{--pico-background-color:var(--pico-secondary-hover-background);--pico-border-color:var(--pico-secondary-hover-border)}[type=file]:focus::file-selector-button{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}[type=range]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:1.25rem;background:0 0}[type=range]::-webkit-slider-runnable-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-webkit-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-moz-range-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-moz-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-ms-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-ms-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-webkit-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]::-moz-range-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-moz-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]::-ms-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-ms-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]:active,[type=range]:focus-within{--pico-range-border-color:var(--pico-range-active-border-color);--pico-range-thumb-color:var(--pico-range-thumb-active-color)}[type=range]:active::-webkit-slider-thumb{transform:scale(1.25)}[type=range]:active::-moz-range-thumb{transform:scale(1.25)}[type=range]:active::-ms-thumb{transform:scale(1.25)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{padding-inline-start:calc(var(--pico-form-element-spacing-horizontal) + 1.75rem);background-image:var(--pico-icon-search);background-position:center left calc(var(--pico-form-element-spacing-horizontal) + .125rem);background-size:1rem auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{padding-inline-start:calc(var(--pico-form-element-spacing-horizontal) + 1.75rem)!important;background-position:center left 1.125rem,center right .75rem}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=false]{background-image:var(--pico-icon-search),var(--pico-icon-valid)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=true]{background-image:var(--pico-icon-search),var(--pico-icon-invalid)}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{background-position:center right 1.125rem}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{background-position:center right 1.125rem,center left .75rem}details{display:block;margin-bottom:var(--pico-spacing)}details summary{line-height:1rem;list-style-type:none;cursor:pointer;transition:color var(--pico-transition)}details summary:not([role]){color:var(--pico-accordion-close-summary-color)}details summary::-webkit-details-marker{display:none}details summary::marker{display:none}details summary::-moz-list-bullet{list-style-type:none}details summary::after{display:block;width:1rem;height:1rem;margin-inline-start:calc(var(--pico-spacing,1rem) * .5);float:right;transform:rotate(-90deg);background-image:var(--pico-icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:"";transition:transform var(--pico-transition)}details summary:focus{outline:0}details summary:focus:not([role]){color:var(--pico-accordion-active-summary-color)}details summary:focus-visible:not([role]){outline:var(--pico-outline-width) solid var(--pico-primary-focus);outline-offset:calc(var(--pico-spacing,1rem) * 0.5);color:var(--pico-primary)}details summary[role=button]{width:100%;text-align:left}details summary[role=button]::after{height:calc(1rem * var(--pico-line-height,1.5))}details[open]>summary{margin-bottom:var(--pico-spacing)}details[open]>summary:not([role]):not(:focus){color:var(--pico-accordion-open-summary-color)}details[open]>summary::after{transform:rotate(0)}[dir=rtl] details summary{text-align:right}[dir=rtl] details summary::after{float:left;background-position:left center}article{margin-bottom:var(--pico-block-spacing-vertical);padding:var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal);border-radius:var(--pico-border-radius);background:var(--pico-card-background-color);box-shadow:var(--pico-card-box-shadow)}article>footer,article>header{margin-right:calc(var(--pico-block-spacing-horizontal) * -1);margin-left:calc(var(--pico-block-spacing-horizontal) * -1);padding:calc(var(--pico-block-spacing-vertical) * .66) var(--pico-block-spacing-horizontal);background-color:var(--pico-card-sectioning-background-color)}article>header{margin-top:calc(var(--pico-block-spacing-vertical) * -1);margin-bottom:var(--pico-block-spacing-vertical);border-bottom:var(--pico-border-width) solid var(--pico-card-border-color);border-top-right-radius:var(--pico-border-radius);border-top-left-radius:var(--pico-border-radius)}article>footer{margin-top:var(--pico-block-spacing-vertical);margin-bottom:calc(var(--pico-block-spacing-vertical) * -1);border-top:var(--pico-border-width) solid var(--pico-card-border-color);border-bottom-right-radius:var(--pico-border-radius);border-bottom-left-radius:var(--pico-border-radius)}[role=group],[role=search]{display:inline-flex;position:relative;width:100%;margin-bottom:var(--pico-spacing);border-radius:var(--pico-border-radius);box-shadow:var(--pico-group-box-shadow,0 0 0 transparent);vertical-align:middle;transition:box-shadow var(--pico-transition)}[role=group] input:not([type=checkbox],[type=radio]),[role=group] select,[role=group]>*,[role=search] input:not([type=checkbox],[type=radio]),[role=search] select,[role=search]>*{position:relative;flex:1 1 auto;margin-bottom:0}[role=group] input:not([type=checkbox],[type=radio]):not(:first-child),[role=group] select:not(:first-child),[role=group]>:not(:first-child),[role=search] input:not([type=checkbox],[type=radio]):not(:first-child),[role=search] select:not(:first-child),[role=search]>:not(:first-child){margin-left:0;border-top-left-radius:0;border-bottom-left-radius:0}[role=group] input:not([type=checkbox],[type=radio]):not(:last-child),[role=group] select:not(:last-child),[role=group]>:not(:last-child),[role=search] input:not([type=checkbox],[type=radio]):not(:last-child),[role=search] select:not(:last-child),[role=search]>:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}[role=group] input:not([type=checkbox],[type=radio]):focus,[role=group] select:focus,[role=group]>:focus,[role=search] input:not([type=checkbox],[type=radio]):focus,[role=search] select:focus,[role=search]>:focus{z-index:2}[role=group] [role=button]:not(:first-child),[role=group] [type=button]:not(:first-child),[role=group] [type=reset]:not(:first-child),[role=group] [type=submit]:not(:first-child),[role=group] button:not(:first-child),[role=group] input:not([type=checkbox],[type=radio]):not(:first-child),[role=group] select:not(:first-child),[role=search] [role=button]:not(:first-child),[role=search] [type=button]:not(:first-child),[role=search] [type=reset]:not(:first-child),[role=search] [type=submit]:not(:first-child),[role=search] button:not(:first-child),[role=search] input:not([type=checkbox],[type=radio]):not(:first-child),[role=search] select:not(:first-child){margin-left:calc(var(--pico-border-width) * -1)}[role=group] [role=button],[role=group] [type=button],[role=group] [type=reset],[role=group] [type=submit],[role=group] button,[role=search] [role=button],[role=search] [type=button],[role=search] [type=reset],[role=search] [type=submit],[role=search] button{width:auto}@supports selector(:has(*)){[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus),[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus){--pico-group-box-shadow:var(--pico-group-box-shadow-focus-with-button)}[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) input:not([type=checkbox],[type=radio]),[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) select,[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) input:not([type=checkbox],[type=radio]),[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) select{border-color:transparent}[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus),[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus){--pico-group-box-shadow:var(--pico-group-box-shadow-focus-with-input)}[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [role=button],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=button],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=submit],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) button,[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [role=button],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=button],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=submit],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) button{--pico-button-box-shadow:0 0 0 var(--pico-border-width) var(--pico-primary-border);--pico-button-hover-box-shadow:0 0 0 var(--pico-border-width) var(--pico-primary-hover-border)}[role=group] [role=button]:focus,[role=group] [type=button]:focus,[role=group] [type=reset]:focus,[role=group] [type=submit]:focus,[role=group] button:focus,[role=search] [role=button]:focus,[role=search] [type=button]:focus,[role=search] [type=reset]:focus,[role=search] [type=submit]:focus,[role=search] button:focus{box-shadow:none}}[role=search]>:first-child{border-top-left-radius:5rem;border-bottom-left-radius:5rem}[role=search]>:last-child{border-top-right-radius:5rem;border-bottom-right-radius:5rem}[aria-busy=true]:not(input,select,textarea,html,form){white-space:nowrap}[aria-busy=true]:not(input,select,textarea,html,form)::before{display:inline-block;width:1em;height:1em;background-image:var(--pico-icon-loading);background-size:1em auto;background-repeat:no-repeat;content:"";vertical-align:-.125em}[aria-busy=true]:not(input,select,textarea,html,form):not(:empty)::before{margin-inline-end:calc(var(--pico-spacing) * .5)}[aria-busy=true]:not(input,select,textarea,html,form):empty{text-align:center}[role=button][aria-busy=true],[type=button][aria-busy=true],[type=reset][aria-busy=true],[type=submit][aria-busy=true],a[aria-busy=true],button[aria-busy=true]{pointer-events:none}:host,:root{--pico-scrollbar-width:0px}dialog{display:flex;z-index:999;position:fixed;top:0;right:0;bottom:0;left:0;align-items:center;justify-content:center;width:inherit;min-width:100%;height:inherit;min-height:100%;padding:0;border:0;-webkit-backdrop-filter:var(--pico-modal-overlay-backdrop-filter);backdrop-filter:var(--pico-modal-overlay-backdrop-filter);background-color:var(--pico-modal-overlay-background-color);color:var(--pico-color)}dialog>article{width:100%;max-height:calc(100vh - var(--pico-spacing) * 2);margin:var(--pico-spacing);overflow:auto}@media (min-width:576px){dialog>article{max-width:510px}}@media (min-width:768px){dialog>article{max-width:700px}}dialog>article>header>*{margin-bottom:0}dialog>article>header :is(a,button)[rel=prev]{margin:0;margin-left:var(--pico-spacing);padding:0;float:right}dialog>article>footer{text-align:right}dialog>article>footer [role=button],dialog>article>footer button{margin-bottom:0}dialog>article>footer [role=button]:not(:first-of-type),dialog>article>footer button:not(:first-of-type){margin-left:calc(var(--pico-spacing) * .5)}dialog>article :is(a,button)[rel=prev]{display:block;width:1rem;height:1rem;margin-top:calc(var(--pico-spacing) * -1);margin-bottom:var(--pico-spacing);margin-left:auto;border:none;background-image:var(--pico-icon-close);background-position:center;background-size:auto 1rem;background-repeat:no-repeat;background-color:transparent;opacity:.5;transition:opacity var(--pico-transition)}dialog>article :is(a,button)[rel=prev]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){opacity:1}dialog:not([open]),dialog[open=false]{display:none}:where(nav li)::before{float:left;content:"​"}nav,nav ul{display:flex}nav{justify-content:space-between;overflow:visible}nav ol,nav ul{align-items:center;margin-bottom:0;padding:0;list-style:none}nav ol:first-of-type,nav ul:first-of-type{margin-left:calc(var(--pico-nav-element-spacing-horizontal) * -1)}nav ol:last-of-type,nav ul:last-of-type{margin-right:calc(var(--pico-nav-element-spacing-horizontal) * -1)}nav li{display:inline-block;margin:0;padding:var(--pico-nav-element-spacing-vertical) var(--pico-nav-element-spacing-horizontal)}nav li :where(a,[role=link]){display:inline-block;margin:calc(var(--pico-nav-link-spacing-vertical) * -1) calc(var(--pico-nav-link-spacing-horizontal) * -1);padding:var(--pico-nav-link-spacing-vertical) var(--pico-nav-link-spacing-horizontal);border-radius:var(--pico-border-radius)}nav li :where(a,[role=link]):not(:hover){text-decoration:none}nav li [role=button],nav li [type=button],nav li button,nav li input:not([type=checkbox],[type=radio],[type=range],[type=file]),nav li select{height:auto;margin-right:inherit;margin-bottom:0;margin-left:inherit;padding:calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal)}nav[aria-label=breadcrumb]{align-items:center;justify-content:start}nav[aria-label=breadcrumb] ul li:not(:first-child){margin-inline-start:var(--pico-nav-link-spacing-horizontal)}nav[aria-label=breadcrumb] ul li a{margin:calc(var(--pico-nav-link-spacing-vertical) * -1) 0;margin-inline-start:calc(var(--pico-nav-link-spacing-horizontal) * -1)}nav[aria-label=breadcrumb] ul li:not(:last-child)::after{display:inline-block;position:absolute;width:calc(var(--pico-nav-link-spacing-horizontal) * 4);margin:0 calc(var(--pico-nav-link-spacing-horizontal) * -1);content:var(--pico-nav-breadcrumb-divider);color:var(--pico-muted-color);text-align:center;text-decoration:none;white-space:nowrap}nav[aria-label=breadcrumb] a[aria-current]:not([aria-current=false]){background-color:transparent;color:inherit;text-decoration:none;pointer-events:none}aside li,aside nav,aside ol,aside ul{display:block}aside li{padding:calc(var(--pico-nav-element-spacing-vertical) * .5) var(--pico-nav-element-spacing-horizontal)}aside li a{display:block}aside li [role=button]{margin:inherit}[dir=rtl] nav[aria-label=breadcrumb] ul li:not(:last-child) ::after{content:"\\"}progress{display:inline-block;vertical-align:baseline}progress{-webkit-appearance:none;-moz-appearance:none;display:inline-block;appearance:none;width:100%;height:.5rem;margin-bottom:calc(var(--pico-spacing) * .5);overflow:hidden;border:0;border-radius:var(--pico-border-radius);background-color:var(--pico-progress-background-color);color:var(--pico-progress-color)}progress::-webkit-progress-bar{border-radius:var(--pico-border-radius);background:0 0}progress[value]::-webkit-progress-value{background-color:var(--pico-progress-color);-webkit-transition:inline-size var(--pico-transition);transition:inline-size var(--pico-transition)}progress::-moz-progress-bar{background-color:var(--pico-progress-color)}@media (prefers-reduced-motion:no-preference){progress:indeterminate{background:var(--pico-progress-background-color) linear-gradient(to right,var(--pico-progress-color) 30%,var(--pico-progress-background-color) 30%) top left/150% 150% no-repeat;animation:progress-indeterminate 1s linear infinite}progress:indeterminate[value]::-webkit-progress-value{background-color:transparent}progress:indeterminate::-moz-progress-bar{background-color:transparent}}@media (prefers-reduced-motion:no-preference){[dir=rtl] progress:indeterminate{animation-direction:reverse}}@keyframes progress-indeterminate{0%{background-position:200% 0}100%{background-position:-200% 0}}[data-tooltip]{position:relative}[data-tooltip]:not(a,button,input,[role=button]){border-bottom:1px dotted;text-decoration:none;cursor:help}[data-tooltip]::after,[data-tooltip]::before,[data-tooltip][data-placement=top]::after,[data-tooltip][data-placement=top]::before{display:block;z-index:99;position:absolute;bottom:100%;left:50%;padding:.25rem .5rem;overflow:hidden;transform:translate(-50%,-.25rem);border-radius:var(--pico-border-radius);background:var(--pico-tooltip-background-color);content:attr(data-tooltip);color:var(--pico-tooltip-color);font-style:normal;font-weight:var(--pico-font-weight);font-size:.875rem;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;opacity:0;pointer-events:none}[data-tooltip]::after,[data-tooltip][data-placement=top]::after{padding:0;transform:translate(-50%,0);border-top:.3rem solid;border-right:.3rem solid transparent;border-left:.3rem solid transparent;border-radius:0;background-color:transparent;content:"";color:var(--pico-tooltip-background-color)}[data-tooltip][data-placement=bottom]::after,[data-tooltip][data-placement=bottom]::before{top:100%;bottom:auto;transform:translate(-50%,.25rem)}[data-tooltip][data-placement=bottom]:after{transform:translate(-50%,-.3rem);border:.3rem solid transparent;border-bottom:.3rem solid}[data-tooltip][data-placement=left]::after,[data-tooltip][data-placement=left]::before{top:50%;right:100%;bottom:auto;left:auto;transform:translate(-.25rem,-50%)}[data-tooltip][data-placement=left]:after{transform:translate(.3rem,-50%);border:.3rem solid transparent;border-left:.3rem solid}[data-tooltip][data-placement=right]::after,[data-tooltip][data-placement=right]::before{top:50%;right:auto;bottom:auto;left:100%;transform:translate(.25rem,-50%)}[data-tooltip][data-placement=right]:after{transform:translate(-.3rem,-50%);border:.3rem solid transparent;border-right:.3rem solid}[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{opacity:1}@media (hover:hover) and (pointer:fine){[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{--pico-tooltip-slide-to:translate(-50%, -0.25rem);transform:translate(-50%,.75rem);animation-duration:.2s;animation-fill-mode:forwards;animation-name:tooltip-slide;opacity:0}[data-tooltip]:focus::after,[data-tooltip]:hover::after{--pico-tooltip-caret-slide-to:translate(-50%, 0rem);transform:translate(-50%,-.25rem);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:focus::before,[data-tooltip][data-placement=bottom]:hover::after,[data-tooltip][data-placement=bottom]:hover::before{--pico-tooltip-slide-to:translate(-50%, 0.25rem);transform:translate(-50%,-.75rem);animation-name:tooltip-slide}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:hover::after{--pico-tooltip-caret-slide-to:translate(-50%, -0.3rem);transform:translate(-50%,-.5rem);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:focus::before,[data-tooltip][data-placement=left]:hover::after,[data-tooltip][data-placement=left]:hover::before{--pico-tooltip-slide-to:translate(-0.25rem, -50%);transform:translate(.75rem,-50%);animation-name:tooltip-slide}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:hover::after{--pico-tooltip-caret-slide-to:translate(0.3rem, -50%);transform:translate(.05rem,-50%);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:focus::before,[data-tooltip][data-placement=right]:hover::after,[data-tooltip][data-placement=right]:hover::before{--pico-tooltip-slide-to:translate(0.25rem, -50%);transform:translate(-.75rem,-50%);animation-name:tooltip-slide}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:hover::after{--pico-tooltip-caret-slide-to:translate(-0.3rem, -50%);transform:translate(-.05rem,-50%);animation-name:tooltip-caret-slide}}@keyframes tooltip-slide{to{transform:var(--pico-tooltip-slide-to);opacity:1}}@keyframes tooltip-caret-slide{50%{opacity:0}to{transform:var(--pico-tooltip-caret-slide-to);opacity:1}}[aria-controls]{cursor:pointer}[aria-disabled=true],[disabled]{cursor:not-allowed}[aria-hidden=false][hidden]{display:initial}[aria-hidden=false][hidden]:not(:focus){clip:rect(0,0,0,0);position:absolute}[tabindex],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation}[dir=rtl]{direction:rtl}@media (prefers-reduced-motion:reduce){:not([aria-busy=true]),:not([aria-busy=true])::after,:not([aria-busy=true])::before{background-attachment:initial!important;animation-duration:1ms!important;animation-delay:-1ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-delay:0s!important;transition-duration:0s!important}}
+1
static/placeholder.png
··· 1 + placeholder for badge images
+35
templates/base.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en" data-theme="auto"> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1"> 6 + <title>{% block title %}{{ title | default("Showcase") }}{% endblock %}</title> 7 + <meta name="description" 8 + content="{% block description %}Badge awards showcase for the AT Protocol community{% endblock %}"> 9 + <link rel="stylesheet" href="/static/pico.css"> 10 + <link rel="stylesheet" href="/static/pico.colors.css"> 11 + </head> 12 + <body> 13 + <header> 14 + <nav> 15 + <ul> 16 + <li><strong>Showcase</strong></li> 17 + </ul> 18 + <ul> 19 + <li><a href="/">Home</a></li> 20 + <li><a href="https://preview.smokesignal.events/">Smoke Signal</a></li> 21 + </ul> 22 + </nav> 23 + </header> 24 + <main> 25 + {% block content %}{% endblock %} 26 + </main> 27 + <footer> 28 + <p> 29 + Powered by <a href="https://tangled.sh/@smokesignal.events/showcase">Showcase</a> - 30 + Part of <a href="https://bsky.app/profile/smokesignal.events">@smokesignal.events</a> - 31 + A badge showcases for the <a href="https://atproto.com">AT Protocol</a> community 32 + </p> 33 + </footer> 34 + </body> 35 + </html>
+37
templates/identity.html
··· 1 + {% extends "base.html" %} 2 + {% block title %}{{ title }} - Showcase{% endblock %} 3 + {% block content %} 4 + 5 + 6 + <section> 7 + <hgroup> 8 + <h1>@{{ subject }}</h1> 9 + <p>Badge awards for this identity ({{ awards | length }})</p> 10 + </hgroup> 11 + {% if awards %} 12 + {% for award in awards %} 13 + <article> 14 + {% if award.badge_image %} 15 + <figure> 16 + <img src="/badge/{{ award.badge_image }}" height="64" width="64" alt="{{ award.badge_name }}" /> 17 + </figure> 18 + {% endif %} 19 + <p> 20 + "<strong>{{ award.badge_name }}</strong>" 21 + awarded at {{ award.created_at }} 22 + <br /> 23 + Signed by: 24 + {% for signer in award.signers %} 25 + {% if loop.index > 1 %}, {% endif %}<a href="https://bsky.app/profile/{{ signer }}">@{{ signer }}</a> 26 + {% endfor %} 27 + </p> 28 + </article> 29 + {% endfor %} 30 + {% else %} 31 + <p><strong><em>This identity hasn't received any badge awards yet.</em></strong></p> 32 + {% endif %} 33 + </section> 34 + <section> 35 + <a href="/" role="button" >Back to all awards</a> 36 + </section> 37 + {% endblock %}
+31
templates/index.html
··· 1 + {% extends "base.html" %} 2 + {% block title %}{{ title }} - Showcase{% endblock %} 3 + {% block content %} 4 + <section> 5 + <h1>{{ title }}</h1> 6 + {% if awards %} 7 + {% for award in awards %} 8 + <article> 9 + {% if award.badge_image %} 10 + <figure> 11 + <img src="/badge/{{ award.badge_image }}" height="64" width="64" alt="{{ award.badge_name }}" /> 12 + </figure> 13 + {% endif %} 14 + <p> 15 + "<strong>{{ award.badge_name }}</strong>" 16 + awarded to 17 + <a href="/badges/{{ award.did }}">@{{ award.handle }}</a> 18 + at {{ award.created_at }} 19 + <br /> 20 + Signed by: 21 + {% for signer in award.signers %} 22 + {% if loop.index > 1 %}, {% endif %}<a href="https://bsky.app/profile/{{ signer }}">@{{ signer }}</a> 23 + {% endfor %} 24 + </p> 25 + </article> 26 + {% endfor %} 27 + {% else %} 28 + <p><strong><em>There are no awards to display.</em></strong></p> 29 + {% endif %} 30 + </section> 31 + {% endblock %}