+26
.env.dev
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
static/.DS_Store
This is a binary file and will not be displayed.
static/badges/bafkreidjzewhcu2rk2537dkhzuhu54gcoae4tvmukcu7gmczx3qofwg2xa.png
static/badges/bafkreidjzewhcu2rk2537dkhzuhu54gcoae4tvmukcu7gmczx3qofwg2xa.png
This is a binary file and will not be displayed.
static/badges/bafkreigb3vxlgckp66o2bffnwapagshpt2xdcgdltuzjymapobvgunxmfi.png
static/badges/bafkreigb3vxlgckp66o2bffnwapagshpt2xdcgdltuzjymapobvgunxmfi.png
This is a binary file and will not be displayed.
+4
static/pico.colors.css
+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
+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
static/placeholder.png
···
1
+
placeholder for badge images
+35
templates/base.html
+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
+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
+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 %}