A stateless RESTful API gateway that posts to Bilibili dynamics, receives Aliyun OSS EventBridge webhooks, and refreshes Aliyun CDN. Built with Axum framework, with container support via Docker and CI/CD support via GitHub Actions.
- Stateless architecture - No database, pure HTTP API service
- Bilibili integration - Post dynamics with multipart file upload
- Aliyun OSS EventBridge - Receive and process OSS events via webhooks
- Aliyun CDN refresh - Cache invalidation support
- JWT authentication - ES256 (ECDSA P-256) for protected routes
- OpenAPI documentation - Scalar UI at
/api/scalar - mimalloc allocator - High-performance memory allocation
- Axum 0.8 + Tower middleware
- Reqwest for external APIs
- Utoipa for OpenAPI spec generation
- ES256 JWT (ECDSA P-256) for authentication
- Aliyun V3 signature for OSS EventBridge
The configuration is divided into several main sections:
- Logger
- Server
- Bilibili (API credentials)
- Aliyun (OSS/CDN credentials)
- JWT (ES256 keys)
- Sentry (Optional)
Controls the application's logging behavior.
| Field | Description | Options |
|---|---|---|
enable |
Enable log writing to stdout | true/false |
level |
Set logging level | trace, debug, info, warn, error |
format |
Set logger format | compact, pretty, json |
override_filter |
Override default tracing filter | Any valid tracing filter string |
Configures the web server settings.
[server]
binding = "0.0.0.0"
port = 25150
host = "http://localhost"| Field | Description |
|---|---|
binding |
Server binding address (defaults to "0.0.0.0") |
port |
Port number for the server |
host |
Web server host URL |
Bilibili API credentials for posting dynamics.
[bilibili]
sessdata = "your_bilibili_sessdata_cookie"
bili_jct = "your_bilibili_bili_jct"| Field | Description |
|---|---|
sessdata |
Bilibili SESSDATA cookie value |
bili_jct |
Bilibili bili_jct cookie value (for CSRF) |
Aliyun OSS and CDN credentials.
[aliyun]
access_key_id = "your_aliyun_access_key_id"
access_key_secret = "your_aliyun_access_key_secret"
[aliyun.bucket_url_map]
prts-static = "https://static.prts.wiki/{object_key}"
ak-media = "https://media.prts.wiki/{object_key}"| Field | Description |
|---|---|
access_key_id |
Aliyun Access Key ID |
access_key_secret |
Aliyun Access Key Secret |
bucket_url_map |
Bucket to URL template mapping (optional) |
The {object_key} placeholder in bucket_url_map will be URL-encoded and replaced with the actual object key.
ES256 (ECDSA P-256) keys for API authentication.
[jwt]
private_key = """-----BEGIN EC PRIVATE KEY-----
YOUR_PRIVATE_KEY_HERE
-----END EC PRIVATE KEY-----"""
public_key = """-----BEGIN PUBLIC KEY-----
YOUR_PUBLIC_KEY_HERE
-----END PUBLIC KEY-----"""| Field | Description |
|---|---|
private_key |
ES256 private key (PEM format) |
public_key |
ES256 public key (PEM format) |
# Generate private key (prime256v1 = P-256)
openssl ecparam -genkey -name prime256v1 -noout -out private.pem
# Extract public key
openssl ec -in private.pem -pubout -out public.pemOptional configuration for Sentry error tracking and monitoring.
[sentry]
dsn = "https://your-sentry-dsn@sentry.io/project-id"
traces_sample_rate = 1.0| Field | Description | Required |
|---|---|---|
dsn |
Sentry DSN for error reporting | No |
traces_sample_rate |
Sampling rate for performance traces (0.0-1.0) | No |
| Method | Path | Description |
|---|---|---|
| GET | /api/_ping |
Health check (ping) |
| GET | /api/_health |
Health check (detailed) |
| POST | /api/aliyun/events |
OSS EventBridge webhook |
All protected routes require Authorization: Bearer <token> header.
| Method | Path | Description |
|---|---|---|
| POST | /api/bilibili/createDynamic |
Create Bilibili dynamic with file upload |
| Path | Description |
|---|---|
/api/scalar |
Scalar UI (OpenAPI) |
/api/openapi.json |
OpenAPI specification |
Protected routes use ES256 JWT. Generate a token:
cargo run -- generate-jwt --config config.toml --subject user_idUse the token in requests:
curl -X POST http://localhost:25150/api/bilibili/createDynamic \
-H "Authorization: Bearer <token>" \
-F "file=@/path/to/image.jpg" \
-F "text=Hello Bilibili"EventBridge webhooks use a custom header x-eventbridge-signature-token for authentication, verified using the same JWT verification as Bilibili routes.
# Build the project
cargo build
# Run the server
cargo run -- server --config config.toml
# Generate a JWT token
cargo run -- generate-jwt --config config.toml --subject user_id
# Format code
cargo fmt
# Run linter (must pass in CI)
cargo clippy --all-features -- -D warnings
# Install development tools
just init
# Generate changelog for release
just pre-release <version>docker build -t janus:local .
docker run --rm -p 25150:25150 \
--mount type=bind,src="$(pwd)/config.toml",dst=/app/config.toml,readonly \
janus:local
# Override the default server command to use other CLI commands
docker run --rm janus:local /app/janus versionCreate config.toml from example.toml and fill in your credentials before starting the
container. The container runs as UID/GID 10001:10001; the mounted configuration must be
readable by that user. Bind the server to 0.0.0.0 in the configuration to expose it outside
the container. Local configuration, keys, and build artifacts are excluded from the build
context.
Dependency compilation is cached separately from application code with cargo-chef.
CI and release builds share a scoped BuildKit cache, and release images include the commit
SHA in /app/janus version.
- 4-space indentation for Rust files
- 100 character line limit
- Run
cargo fmtbefore committing - Run
cargo clippy --all-features -- -D warnings(CI enforces this)
# Run tests
cargo testCI checks formatting, runs Clippy for all targets and features, runs the tests with the
lockfile enforced, and builds the Docker image. A container smoke test verifies the CLI and
the /api/_ping endpoint using example.toml.
just pre-release <version>- Generate CHANGELOG.md via git-cliffcargo release <version>- Create git tag- GitHub Action builds and pushes Docker image to GHCR
See the example.toml file for a complete example configuration.
main.rs(15 lines): Sets mimalloc, callsapp::run()lib.rs(11 lines): Public exports:aliyun,app,auth,errorapp.rs(84 lines): CLI parser -server,generate-jwt,version
bilibili_config: BilibiliConfig- API credentialsaliyun_config: AliyunConfig- OSS/CDN credentialsjwt_config: JwtConfig- ES256 private/public keyshttp_client: reqwest::Client- Shared HTTP client- NO database or repository
src/
├── main.rs # CLI entry
├── lib.rs # Public exports
├── app.rs # CLI + server startup
├── config.rs # TOML config
├── state.rs # AppState
├── error.rs # AppError
├── auth.rs # JWT ES256
├── middleware.rs # Tower layers
├── tracing.rs # Logging setup
├── shutdown.rs # Graceful shutdown
├── aliyun/ # OSS signature + CDN
│ ├── cdn.rs
│ └── signature.rs
└── routes/ # HTTP handlers
├── bilibili_handlers.rs
├── aliyun_handlers.rs
└── misc_handlers.rs
MIT