Compare commits

...

10 Commits

48 changed files with 353 additions and 312 deletions

View File

@@ -11,3 +11,13 @@ spa/node_modules/
spa/dist/
spa/.env
spa/.env.local
.sqlx/
.superpowers/
.worktrees/
.claude/
.github/
.gitea/
imgs/
screenshots/
test_data/
*.md

View File

@@ -22,15 +22,10 @@ jobs:
with:
components: rustfmt, clippy
# Caches ~/.cargo registry/git and target/, keyed on toolchain + Cargo.lock;
# prunes workspace-crate artifacts so the cache holds only compiled deps.
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-
uses: Swatinem/rust-cache@v2
- name: fmt
run: cargo fmt --all -- --check
@@ -38,8 +33,9 @@ jobs:
- name: clippy
run: cargo clippy --workspace --all-targets -- -D warnings
# --workspace covers tui (it is outside default-members).
- name: test
run: cargo test
run: cargo test --workspace
- name: test (federation off)
run: cargo test --workspace --no-default-features
@@ -47,4 +43,4 @@ jobs:
# Layering guards. These live in the Makefile and were previously local-only,
# so a PR could go green in CI while violating them.
- name: guards
run: make check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-dockerfile-manifests
run: make check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-federation-off-deps check-nats-isolated check-no-second-web-stack

View File

@@ -22,15 +22,10 @@ jobs:
with:
components: rustfmt, clippy
# Caches ~/.cargo registry/git and target/, keyed on toolchain + Cargo.lock;
# prunes workspace-crate artifacts so the cache holds only compiled deps.
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-
uses: Swatinem/rust-cache@v2
- name: fmt
run: cargo fmt --all -- --check
@@ -38,8 +33,9 @@ jobs:
- name: clippy
run: cargo clippy --workspace --all-targets -- -D warnings
# --workspace covers tui (it is outside default-members).
- name: test
run: cargo test
run: cargo test --workspace
- name: test (federation off)
run: cargo test --workspace --no-default-features
@@ -47,7 +43,7 @@ jobs:
# Layering guards. These live in the Makefile and were previously local-only,
# so a PR could go green in CI while violating them.
- name: guards
run: make check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-dockerfile-manifests
run: make check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-federation-off-deps check-nats-isolated check-no-second-web-stack
docker:
name: Build & Push Docker Image
@@ -61,6 +57,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
@@ -87,3 +86,5 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: FEATURES=federation
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -26,7 +26,7 @@ cd spa && bun install && bun run dev
make check # the full gate; plain `make` runs it too
```
`make check` runs, in order: `fmt-check`, `clippy` (workspace, all targets, `-D warnings`), `test`, `test-federation-off` (workspace tests with default features off), then the layering guards (`check-appcontext`, `check-handler-deps`, `check-handler-repos`, `check-http-adapter-deps`, `check-federation-off-social`, `check-sqlite-is-ap-free`, `check-binaries-are-thin`, `check-dockerfile-manifests`). CI runs the same steps.
`make check` runs, in order: `fmt-check`, `clippy` (workspace, all targets, `-D warnings`), `test`, `test-federation-off` (workspace tests with default features off), then the layering guards (`check-appcontext`, `check-handler-deps`, `check-handler-repos`, `check-http-adapter-deps`, `check-federation-off-social`, `check-sqlite-is-ap-free`, `check-binaries-are-thin`, `check-federation-off-deps`, `check-nats-isolated`, `check-no-second-web-stack`). CI runs the same steps.
For the SPA also run:
@@ -57,6 +57,8 @@ spa/ → React SPA (TanStack Router + shadcn/ui)
Cargo features: `federation` (default; ActivityPub) and `nats`. `sqlite` and `sqlite-federation` remain as deprecated aliases so old build commands keep working.
**Dependencies.** Every external crate is declared once in the root `[workspace.dependencies]`, usually with `default-features = false`; member crates write `foo = { workspace = true, features = [...] }` and enable only the features they use. A new crate goes into both `members` and `default-members` in the root `Cargo.toml` (only `tui` is left out of `default-members`). `cargo machete` should stay clean, and each crate must compile on its own (`cargo check -p <crate> --all-targets`), not just through feature unification in the binaries.
### Adding a new feature
1. **Domain first** — models in `domain/src/models/`, ports in `domain/src/ports/`, events in `domain/src/events.rs`

154
Cargo.lock generated
View File

@@ -757,7 +757,7 @@ dependencies = [
"object-storage",
"poster-fetcher",
"reqwest 0.13.3",
"rss 0.1.0",
"rss-adapter",
"rustls",
"serde_json",
"sqlite",
@@ -2022,11 +2022,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 5.3.0",
"wasip2",
"wasm-bindgen",
]
[[package]]
@@ -2036,11 +2034,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.1",
"wasip2",
"wasip3",
"wasm-bindgen",
]
[[package]]
@@ -2220,7 +2220,6 @@ dependencies = [
"tower-http",
"tracing",
"utoipa",
"utoipa-scalar",
"utoipa-swagger-ui",
"uuid",
]
@@ -2932,9 +2931,9 @@ dependencies = [
[[package]]
name = "lru-slab"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f"
[[package]]
name = "mac"
@@ -3053,6 +3052,7 @@ dependencies = [
"config",
"domain",
"reqwest 0.13.3",
"rustls",
"serde",
]
@@ -3749,6 +3749,7 @@ dependencies = [
"config",
"domain",
"reqwest 0.13.3",
"rustls",
]
[[package]]
@@ -3862,9 +3863,9 @@ dependencies = [
[[package]]
name = "quinn"
version = "0.11.9"
version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11"
dependencies = [
"bytes",
"cfg_aliases",
@@ -3882,14 +3883,15 @@ dependencies = [
[[package]]
name = "quinn-proto"
version = "0.11.14"
version = "0.11.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe"
dependencies = [
"bytes",
"getrandom 0.3.4",
"getrandom 0.4.2",
"lru-slab",
"rand 0.9.4",
"rand 0.10.1",
"rand_pcg",
"ring",
"rustc-hash",
"rustls",
@@ -3903,16 +3905,16 @@ dependencies = [
[[package]]
name = "quinn-udp"
version = "0.5.14"
version = "0.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -4012,6 +4014,15 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_pcg"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
dependencies = [
"rand_core 0.10.1",
]
[[package]]
name = "ratatui"
version = "0.30.0"
@@ -4343,15 +4354,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rss"
version = "0.1.0"
dependencies = [
"chrono",
"domain",
"rss 2.0.13",
]
[[package]]
name = "rss"
version = "2.0.13"
@@ -4363,6 +4365,15 @@ dependencies = [
"quick-xml 0.39.4",
]
[[package]]
name = "rss-adapter"
version = "0.1.0"
dependencies = [
"chrono",
"domain",
"rss",
]
[[package]]
name = "rust-embed"
version = "8.11.0"
@@ -5456,9 +5467,9 @@ dependencies = [
[[package]]
name = "thread_local"
version = "1.1.9"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
dependencies = [
"cfg-if",
]
@@ -5530,6 +5541,7 @@ dependencies = [
"config",
"domain",
"reqwest 0.13.3",
"rustls",
"serde",
]
@@ -6005,18 +6017,6 @@ dependencies = [
"uuid",
]
[[package]]
name = "utoipa-scalar"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59559e1509172f6b26c1cdbc7247c4ddd1ac6560fe94b584f81ee489b141f719"
dependencies = [
"axum",
"serde",
"serde_json",
"utoipa",
]
[[package]]
name = "utoipa-swagger-ui"
version = "9.0.2"
@@ -6437,7 +6437,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -6547,15 +6547,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -6589,30 +6580,13 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
@@ -6625,12 +6599,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
@@ -6643,12 +6611,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
@@ -6661,24 +6623,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
@@ -6691,12 +6641,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
@@ -6709,12 +6653,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
@@ -6727,12 +6665,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
@@ -6745,12 +6677,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.7.15"

View File

@@ -1,11 +1,37 @@
[workspace]
default-members = [
"crates/adapters/auth",
"crates/adapters/event-publisher",
"crates/adapters/metadata",
"crates/adapters/poster-fetcher",
"crates/adapters/object-storage",
"crates/adapters/rss-adapter",
"crates/adapters/sqlite",
"crates/adapters/sqlite-federation",
"crates/adapters/activitypub",
"crates/adapters/export",
"crates/adapters/nats",
"crates/adapters/tmdb-enrichment",
"crates/adapters/image-converter",
"crates/adapters/http-axum",
"crates/adapters/importer",
"crates/adapters/media-server",
"crates/event-payload",
"crates/api-types",
"crates/application",
"crates/domain",
"crates/bootstrap",
"crates/server",
"crates/worker",
"crates/config",
]
members = [
"crates/adapters/auth",
"crates/adapters/event-publisher",
"crates/adapters/metadata",
"crates/adapters/poster-fetcher",
"crates/adapters/object-storage",
"crates/adapters/rss",
"crates/adapters/rss-adapter",
"crates/adapters/sqlite",
"crates/adapters/sqlite-federation",
"crates/adapters/activitypub",
@@ -32,20 +58,20 @@ resolver = "2"
edition = "2024"
[workspace.dependencies]
tokio = { version = "1.0", features = ["macros", "net", "rt", "rt-multi-thread", "sync", "time"] }
tokio = { version = "1.0", default-features = false }
bytes = "1"
futures = "0.3"
async-stream = "0.3"
dotenvy = "0.15"
serde = { version = "1.0", features = ["derive"] }
serde = { version = "1.0", default-features = false, features = ["std"] }
serde_json = "1.0"
anyhow = "1.0"
thiserror = "2.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
tracing-subscriber = { version = "0.3.23", default-features = false }
async-trait = "0.1.92"
uuid = { version = "1.23.0", features = ["v4", "v5", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.23.0", default-features = false, features = ["std"] }
chrono = { version = "0.4", default-features = false, features = ["std"] }
chrono-tz = "0.10"
sqlx = { version = "0.8.6", default-features = false, features = [
"runtime-tokio",
@@ -59,10 +85,38 @@ tempfile = "3"
reqwest = { version = "0.13", default-features = false, features = ["json", "query", "charset", "http2", "system-proxy", "rustls-no-provider"] }
sha2 = "0.10"
hex = "0.4"
object_store = { version = "0.11", features = ["aws"] }
axum = { version = "0.8.8", features = ["macros", "multipart"] }
object_store = { version = "0.11", default-features = false, features = ["aws"] }
axum = { version = "0.8.8", default-features = false }
csv = "1"
figment = { version = "0.10", features = ["toml"] }
rustls = { version = "0.23", default-features = false }
tower = { version = "0.5", default-features = false }
tower-http = { version = "0.6.8", default-features = false }
utoipa = "5.5.0"
utoipa-swagger-ui = "9.0.2"
askama = "0.16.0"
infer = "0.19.0"
percent-encoding = "2"
axum-governor = "2"
email_address = "0.2.9"
jsonwebtoken = "9"
argon2 = { version = "0.5", default-features = false }
rand_core = { version = "0.6", default-features = false }
async-nats = "0.48.0"
k-ap = { version = "0.5.1", registry = "gitea" }
url = "2"
ammonia = "4"
calamine = "0.35"
image = { version = "0.25", default-features = false }
ravif = { version = "0.11", default-features = false }
webp = "0.3"
rss = { version = "2" }
ratatui = "0.30.0"
keyring-core = "1.0.0"
apple-native-keyring-store = { version = "1.0.0", features = ["keychain"] }
zbus-secret-service-keyring-store = { version = "1.0.0" }
windows-native-keyring-store = { version = "1.0.0" }
directories = "6"
api-types = { path = "crates/api-types" }
domain = { path = "crates/domain" }
@@ -75,7 +129,7 @@ metadata = { path = "crates/adapters/metadata" }
poster-fetcher = { path = "crates/adapters/poster-fetcher" }
object-storage = { path = "crates/adapters/object-storage" }
event-publisher = { path = "crates/adapters/event-publisher" }
rss = { path = "crates/adapters/rss" }
rss-adapter = { path = "crates/adapters/rss-adapter" }
export = { path = "crates/adapters/export" }
sqlite = { path = "crates/adapters/sqlite" }
sqlite-federation = { path = "crates/adapters/sqlite-federation" }
@@ -92,3 +146,7 @@ debug = 1 # line tables only — still debuggable, much faste
[profile.dev.package."*"]
opt-level = 2 # compile deps faster at runtime; paid once, cached after
[profile.release]
codegen-units = 1
strip = true

View File

@@ -6,67 +6,46 @@ RUN npm ci
COPY spa/ .
RUN npm run build
# ----- build -----
FROM rust:slim-bookworm AS builder
WORKDIR /build
# Cache dependency compilation separately from source
#
# Every workspace member's Cargo.toml must be listed here by hand — `cargo fetch`
# below reads the whole workspace graph, so a missing manifest fails the build with
# "failed to read .../Cargo.toml". Adding a crate to crates/ means adding a line here.
COPY Cargo.toml Cargo.lock ./
COPY .cargo ./.cargo
COPY crates/adapters/activitypub/Cargo.toml crates/adapters/activitypub/Cargo.toml
COPY crates/adapters/auth/Cargo.toml crates/adapters/auth/Cargo.toml
COPY crates/event-payload/Cargo.toml crates/event-payload/Cargo.toml
COPY crates/adapters/event-publisher/Cargo.toml crates/adapters/event-publisher/Cargo.toml
COPY crates/adapters/nats/Cargo.toml crates/adapters/nats/Cargo.toml
COPY crates/adapters/metadata/Cargo.toml crates/adapters/metadata/Cargo.toml
COPY crates/adapters/poster-fetcher/Cargo.toml crates/adapters/poster-fetcher/Cargo.toml
COPY crates/adapters/object-storage/Cargo.toml crates/adapters/object-storage/Cargo.toml
COPY crates/adapters/export/Cargo.toml crates/adapters/export/Cargo.toml
COPY crates/adapters/importer/Cargo.toml crates/adapters/importer/Cargo.toml
COPY crates/adapters/media-server/Cargo.toml crates/adapters/media-server/Cargo.toml
COPY crates/adapters/rss/Cargo.toml crates/adapters/rss/Cargo.toml
COPY crates/adapters/sqlite/Cargo.toml crates/adapters/sqlite/Cargo.toml
COPY crates/adapters/sqlite-federation/Cargo.toml crates/adapters/sqlite-federation/Cargo.toml
COPY crates/api-types/Cargo.toml crates/api-types/Cargo.toml
COPY crates/application/Cargo.toml crates/application/Cargo.toml
COPY crates/adapters/tmdb-enrichment/Cargo.toml crates/adapters/tmdb-enrichment/Cargo.toml
COPY crates/domain/Cargo.toml crates/domain/Cargo.toml
COPY crates/bootstrap/Cargo.toml crates/bootstrap/Cargo.toml
COPY crates/adapters/http-axum/Cargo.toml crates/adapters/http-axum/Cargo.toml
COPY crates/server/Cargo.toml crates/server/Cargo.toml
COPY crates/tui/Cargo.toml crates/tui/Cargo.toml
COPY crates/adapters/image-converter/Cargo.toml crates/adapters/image-converter/Cargo.toml
COPY crates/worker/Cargo.toml crates/worker/Cargo.toml
COPY crates/config/Cargo.toml crates/config/Cargo.toml
# Stub every crate so cargo can resolve and fetch deps
RUN find crates -name "Cargo.toml" | sed 's|/Cargo.toml||' | \
xargs -I{} sh -c 'mkdir -p {}/src && echo "fn main(){}" > {}/src/main.rs && echo "" > {}/src/lib.rs'
# ----- chef base -----
FROM rust:slim-bookworm AS chef
# libwebp-dev: required at build time by the `webp` crate (C bindings)
RUN apt-get update && apt-get install -y --no-install-recommends \
libwebp-dev \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
RUN cargo install cargo-chef --locked --version 0.1.78
WORKDIR /build
RUN cargo fetch
# ----- planner: dependency recipe (changes only when manifests/lockfile change) -----
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
# Now copy real sources (invalidates cache only on source changes)
COPY crates ./crates
# ----- build -----
FROM chef AS builder
# All sqlx queries use the runtime API (no query! macros), so no database
# or .sqlx cache is needed at compile time.
#
# Features: federation (ActivityPub), nats (EVENT_BUS_BACKEND=nats).
# Build without federation: --build-arg FEATURES=
# To add NATS support: --build-arg FEATURES=federation,nats
# Declared before cook so cook and build compile identical feature sets.
ARG FEATURES=federation
RUN cargo build --release -p server -p worker --no-default-features --features "${FEATURES}"
# .cargo carries the registry config needed to fetch private crates
COPY .cargo ./.cargo
COPY --from=planner /build/recipe.json recipe.json
# target/ is a normal layer (no cache mount) so the cooked deps live in the image
# layer cache; only the registry/git download caches are mounted.
RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,target=/usr/local/cargo/git,sharing=locked \
cargo chef cook --release -p server -p worker --no-default-features --features "${FEATURES}" --recipe-path recipe.json
# All sqlx queries use the runtime API (no query! macros), so no database
# or .sqlx cache is needed at compile time.
COPY Cargo.toml Cargo.lock ./
COPY crates ./crates
RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,target=/usr/local/cargo/git,sharing=locked \
cargo build --release -p server -p worker --no-default-features --features "${FEATURES}"
# ----- runtime -----
FROM debian:bookworm-slim

View File

@@ -1,6 +1,6 @@
.DEFAULT_GOAL := check
check: fmt-check clippy test test-federation-off check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-dockerfile-manifests
check: fmt-check clippy test test-federation-off check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-federation-off-deps check-nats-isolated check-no-second-web-stack
@echo "✅ All checks passed"
check-appcontext:
@@ -87,21 +87,43 @@ check-sqlite-is-ap-free:
fi
@echo "✅ sqlite crate depends on no k-ap, activitypub, or *-federation crate"
check-dockerfile-manifests:
@members=$$(awk '/^members = \[/{f=1;next} f&&/^\]/{f=0} f' Cargo.toml | sed -n 's/^[[:space:]]*"\(.*\)",\{0,1\}[[:space:]]*$$/\1/p'); \
copied=$$(sed -n 's/^COPY \([^[:space:]]*\)\/Cargo\.toml[[:space:]].*/\1/p' Dockerfile | grep -v '^\(Cargo\.toml\|\.\)$$' || true); \
bad=""; \
for m in $$members; do \
echo "$$copied" | grep -qxF "$$m" || bad="$$bad missing-COPY:$$m"; \
done; \
for c in $$copied; do \
echo "$$members" | grep -qxF "$$c" || bad="$$bad not-a-member:$$c"; \
done; \
if [ -n "$$bad" ]; then \
echo "❌ Dockerfile manifest COPY lines out of sync with workspace members:$$bad"; \
CARGO_TREE_NAMES := cargo tree -p server -p worker -e normal --prefix none
check-federation-off-deps:
@tree=$$($(CARGO_TREE_NAMES) --no-default-features) || exit 1; \
hits=$$(echo "$$tree" | grep -E '^(k-ap|activitypub_federation|activitypub|sqlite-federation) ' | sort -u); \
if [ -n "$$hits" ]; then \
echo "❌ federation-only crates in the --no-default-features server/worker tree:"; \
echo "$$hits"; \
exit 1; \
fi; \
echo "✅ Dockerfile copies a manifest for every workspace member"
echo "✅ federation-off tree has no k-ap, activitypub_federation, activitypub, or sqlite-federation"
check-nats-isolated:
@tree=$$($(CARGO_TREE_NAMES)) || exit 1; \
nats_tree=$$($(CARGO_TREE_NAMES) --features nats) || exit 1; \
if echo "$$tree" | grep -qE '^(async-nats|nats) '; then \
echo "❌ nats crates in the default server/worker tree:"; \
echo "$$tree" | grep -E '^(async-nats|nats) ' | sort -u; \
exit 1; \
fi; \
if ! echo "$$nats_tree" | grep -qE '^async-nats '; then \
echo "❌ async-nats missing from the --features nats server/worker tree — feature is not wired"; \
exit 1; \
fi; \
echo "✅ async-nats only appears with --features nats"
check-no-second-web-stack:
@for f in "" "--features nats"; do \
tree=$$($(CARGO_TREE_NAMES) $$f) || exit 1; \
hits=$$(echo "$$tree" | grep -E '^(actix|aws-lc)' | sort -u); \
if [ -n "$$hits" ]; then \
echo "❌ actix/aws-lc crates in the server/worker tree ($${f:-default}):"; \
echo "$$hits"; \
exit 1; \
fi; \
done; \
echo "✅ no actix-* or aws-lc-* in the default or nats server/worker tree"
fmt:
cargo fmt
@@ -145,4 +167,4 @@ dev-spa:
spa-dev:
@npm --prefix spa run dev
.PHONY: check fmt fmt-check clippy test test-federation-off fix dev dev-stop dev-worker dev-spa spa-dev check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-dockerfile-manifests
.PHONY: check fmt fmt-check clippy test test-federation-off fix dev dev-stop dev-worker dev-spa spa-dev check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-federation-off-deps check-nats-isolated check-no-second-web-stack

View File

@@ -68,7 +68,7 @@ Open `http://localhost:3000`. The HTTP server and background worker start togeth
- CSV and JSON diary export
- File importer: upload CSV, TSV, JSON, or XLSX from any source (Letterboxd, IMDb, etc.), map columns to domain fields via a step-by-step wizard or REST API, save mapping profiles for repeat imports
- REST API v1 (`/api/v1/`) with full feature parity with the HTML interface
- OpenAPI documentation at `/docs` (Swagger UI) and `/scalar` (Scalar)
- OpenAPI documentation at `/docs` (Swagger UI)
- CSRF protection on all HTML form routes (double-submit cookie, defense-in-depth on top of `SameSite=Strict`)
- Per-IP rate limiting via token bucket (production-grade, backed by `axum-governor`)
- Single-page app at `/app/` — React + TanStack Router + shadcn/ui, built with Vite, served from the backend with client-side routing fallback
@@ -100,7 +100,7 @@ application — use cases (commands + queries), business logic orchestra
server — thin HTTP binary (dotenv, config, bootstrap::build_server, axum::serve); features (federation, nats) forward to bootstrap
worker — thin worker binary (bootstrap::build_worker, run until ctrl-c)
adapters/
http-axum — Axum HTTP adapter: JSON API, classic HTML UI (askama templates), OpenAPI spec, Swagger UI + Scalar, federation-gated routes; library crate, no binary of its own
http-axum — Axum HTTP adapter: JSON API, classic HTML UI (askama templates), OpenAPI spec, Swagger UI, federation-gated routes; library crate, no binary of its own
auth — JWT issuance and validation (Argon2 passwords)
sqlite — SQLite repositories (one module per aggregate under repositories/), search (FTS5), social graph, durable event queue, connection factory + migrations; the only storage adapter
metadata — TMDB / OMDb HTTP client
@@ -174,6 +174,13 @@ Copy `.env.example` to `.env` and set the values you need. Malformed values (a n
Build without federation with `--no-default-features`. `sqlite` and `sqlite-federation` are deprecated aliases kept so old build commands still work.
### Build notes
- `cargo build` / `cargo test` build every crate except the deprecated `tui`, which sits outside `default-members`. Use `-p tui` for it; `make check` and CI use `--workspace`, so it is still linted and tested.
- Release builds use `codegen-units = 1` and `strip = true` (smaller binaries; thin LTO was measured and made builds slower and binaries larger, so it is off).
- SQLite is compiled from source (bundled `libsqlite3-sys`), so the first clean build spends about a minute on it; incremental builds and the Docker/CI caches don't repeat that.
- TLS uses rustls with the `ring` provider only; `aws-lc` and `actix` are kept out of the graph by `make check-no-second-web-stack`.
The `worker` binary must run alongside `server` to process events:
```bash
@@ -198,7 +205,6 @@ All REST endpoints are under `/api/v1/`. Authentication uses `Authorization: Bea
Interactive API documentation is available at runtime:
- **Swagger UI** — `http://localhost:3000/docs`
- **Scalar** — `http://localhost:3000/scalar`
An [Insomnia](https://insomnia.rest/) collection covering all endpoints is included at [`movies-diary.insomnia.json`](movies-diary.insomnia.json). Import it via **File → Import**, set `base_url` and `token` in the environment, and you're ready to go.
@@ -273,7 +279,7 @@ docker run -p 3000:3000 --env-file .env -v movies-diary-data:/data movies-diary
docker run --env-file .env -v movies-diary-data:/data --entrypoint ./worker movies-diary
```
Default build enables `federation`; add NATS with `--build-arg FEATURES=federation,nats`. SQLite is the only database backend (see [ADR-0012](docs/adr/0012-sqlite-is-the-only-backend.md)).
Default build enables `federation`; add NATS with `--build-arg FEATURES=federation,nats`. The Dockerfile uses [cargo-chef](https://github.com/LukeMathWalker/cargo-chef): dependencies are compiled in their own layer keyed on the manifests, so a source-only change rebuilds just the workspace crates. BuildKit cache mounts keep the cargo registry between builds. No Dockerfile edit is needed when a crate is added. SQLite is the only database backend (see [ADR-0012](docs/adr/0012-sqlite-is-the-only-backend.md)).
## Media Server Integration

View File

@@ -5,20 +5,22 @@ edition.workspace = true
[dependencies]
config = { workspace = true }
k-ap = { version = "0.5.1", registry = "gitea" }
k-ap = { workspace = true }
domain = { workspace = true }
axum = { workspace = true }
serde = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
uuid = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true, features = ["v4", "serde"] }
chrono = { workspace = true, features = ["clock", "serde"] }
anyhow = { workspace = true }
tracing = { workspace = true }
async-trait = { workspace = true }
url = { version = "2", features = ["serde"] }
ammonia = "4"
url = { workspace = true, features = ["serde"] }
ammonia = { workspace = true }
tokio = { workspace = true, features = ["time", "sync"] }
[dev-dependencies]
tokio = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros"] }
domain = { workspace = true, features = ["test-helpers"] }

View File

@@ -8,9 +8,9 @@ config = { workspace = true }
async-trait = { workspace = true }
domain = { workspace = true }
anyhow = { workspace = true }
chrono = { workspace = true }
chrono = { workspace = true, features = ["clock"] }
uuid = { workspace = true }
serde = { workspace = true }
jsonwebtoken = "9"
argon2 = { version = "0.5", features = ["std"] }
rand_core = { version = "0.6", features = ["getrandom"] }
serde = { workspace = true, features = ["derive"] }
jsonwebtoken = { workspace = true }
argon2 = { workspace = true, features = ["std"] }
rand_core = { workspace = true, features = ["getrandom"] }

View File

@@ -7,5 +7,8 @@ edition.workspace = true
config = { workspace = true }
domain = { workspace = true }
async-trait = { workspace = true }
tokio = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
futures = { workspace = true }
[dev-dependencies]
tokio = { workspace = true, features = ["rt", "macros"] }

View File

@@ -13,4 +13,4 @@ async-stream = { workspace = true }
[dev-dependencies]
domain = { workspace = true, features = ["test-helpers"] }
uuid = { workspace = true }
tokio = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros"] }

View File

@@ -11,31 +11,30 @@ default = ["federation"]
federation = ["application/federation"]
[dependencies]
tower-http = { version = "0.6.8", features = ["cors", "fs", "trace", "tracing"] }
infer = "0.19.0"
percent-encoding = "2"
axum-governor = "2"
tower-http = { workspace = true, features = ["cors", "fs", "trace", "tracing"] }
infer = { workspace = true }
percent-encoding = { workspace = true }
axum-governor = { workspace = true }
axum = { workspace = true }
serde = { workspace = true }
axum = { workspace = true, features = ["macros", "multipart", "http1", "json", "query", "form", "tokio", "matched-path", "original-uri", "tower-log", "tracing"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tracing = { workspace = true }
tokio = { workspace = true }
uuid = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true, features = ["v4", "serde"] }
chrono = { workspace = true, features = ["clock", "serde"] }
chrono-tz = { workspace = true }
futures = { workspace = true }
api-types = { workspace = true }
domain = { workspace = true }
application = { workspace = true }
askama = { version = "0.16.0" }
utoipa = { version = "5.5.0", features = ["axum_extras", "uuid"] }
utoipa-scalar = { version = "0.3.0", features = ["axum"], default-features = false }
utoipa-swagger-ui = { version = "9.0.2", features = ["axum", "vendored"] }
askama = { workspace = true }
utoipa = { workspace = true, features = ["axum_extras", "uuid"] }
utoipa-swagger-ui = { workspace = true, features = ["axum", "vendored"] }
[dev-dependencies]
bytes = { workspace = true }
futures = { workspace = true }
tower = { version = "0.5", features = ["util"] }
tower = { workspace = true, features = ["util"] }
tokio = { workspace = true, features = ["rt", "macros"] }
domain = { workspace = true, features = ["test-helpers"] }

View File

@@ -16,7 +16,6 @@ use utoipa::{
Modify, OpenApi,
openapi::security::{Http, HttpAuthScheme, SecurityScheme},
};
use utoipa_scalar::{Scalar, Servable};
use utoipa_swagger_ui::SwaggerUi;
struct SecurityAddon;
@@ -62,9 +61,7 @@ pub fn spec() -> utoipa::openapi::OpenApi {
}
pub fn serve(router: Router) -> Router {
tracing::info!("API docs at /docs (Swagger) and /scalar");
tracing::info!("API docs at /docs (Swagger)");
let spec = spec();
router
.merge(SwaggerUi::new("/docs").url("/openapi.json", spec.clone()))
.merge(Scalar::with_url("/scalar", spec))
router.merge(SwaggerUi::new("/docs").url("/openapi.json", spec))
}

View File

@@ -9,12 +9,12 @@ domain = { workspace = true }
async-trait = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
tokio = { workspace = true }
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"] }
ravif = { version = "0.11", default-features = false }
webp = "0.3"
tokio = { workspace = true, features = ["rt"] }
image = { workspace = true, features = ["jpeg", "png", "webp"] }
ravif = { workspace = true }
webp = { workspace = true }
[dev-dependencies]
object-storage = { workspace = true }
object_store = { workspace = true }
uuid = { workspace = true }
uuid = { workspace = true, features = ["v4"] }

View File

@@ -10,4 +10,4 @@ xlsx = ["dep:calamine"]
domain = { workspace = true }
serde_json = { workspace = true }
csv = { workspace = true }
calamine = { version = "0.35", optional = true }
calamine = { workspace = true, optional = true }

View File

@@ -5,5 +5,5 @@ edition.workspace = true
[dependencies]
domain = { workspace = true }
serde = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }

View File

@@ -8,5 +8,6 @@ config = { workspace = true }
anyhow = { workspace = true }
async-trait = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true }
rustls = { workspace = true, features = ["ring", "std"] }
serde = { workspace = true, features = ["derive"] }
domain = { workspace = true }

View File

@@ -16,6 +16,8 @@ pub(crate) struct OmdbProvider {
impl OmdbProvider {
pub(crate) fn new(api_key: String) -> Self {
// reqwest is built with rustls-no-provider; install ring so this works without bootstrap.
let _ = rustls::crypto::ring::default_provider().install_default();
Self {
client: reqwest::Client::new(),
api_key,

View File

@@ -15,6 +15,8 @@ pub(crate) struct TmdbProvider {
impl TmdbProvider {
pub(crate) fn new(api_key: String) -> Self {
// reqwest is built with rustls-no-provider; install ring so this works without bootstrap.
let _ = rustls::crypto::ring::default_provider().install_default();
Self {
client: reqwest::Client::new(),
api_key,

View File

@@ -0,0 +1,6 @@
//! Fresh test binary, no bootstrap: constructing the client must not panic
//! for lack of a rustls CryptoProvider.
#[test]
fn omdb_client_builds_without_bootstrap() {
let _ = metadata::MetadataClientImpl::new_omdb("key".into());
}

View File

@@ -0,0 +1,6 @@
//! Fresh test binary, no bootstrap: constructing the client must not panic
//! for lack of a rustls CryptoProvider.
#[test]
fn tmdb_client_builds_without_bootstrap() {
let _ = metadata::MetadataClientImpl::new_tmdb("key".into());
}

View File

@@ -5,7 +5,7 @@ edition.workspace = true
[dependencies]
config = { workspace = true }
async-nats = "0.48.0"
async-nats = { workspace = true }
domain = { workspace = true }
event-payload = { workspace = true }
@@ -13,7 +13,7 @@ async-trait = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
serde_json = { workspace = true }
uuid = { workspace = true }
chrono = { workspace = true }
tokio = { workspace = true }
uuid = { workspace = true, features = ["serde"] }
chrono = { workspace = true, features = ["serde"] }
tokio = { workspace = true, features = ["rt", "sync"] }
futures = { workspace = true }

View File

@@ -14,5 +14,5 @@ futures = { workspace = true }
object_store = { workspace = true }
[dev-dependencies]
tokio = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros"] }
uuid = { workspace = true }

View File

@@ -8,4 +8,5 @@ config = { workspace = true }
domain = { workspace = true }
async-trait = { workspace = true }
reqwest = { workspace = true }
rustls = { workspace = true, features = ["ring", "std"] }
anyhow = { workspace = true }

View File

@@ -16,6 +16,8 @@ pub struct ReqwestPosterFetcher {
impl ReqwestPosterFetcher {
pub fn new(config: PosterFetcherConfig) -> anyhow::Result<Self> {
// reqwest is built with rustls-no-provider; install ring so this works without bootstrap.
let _ = rustls::crypto::ring::default_provider().install_default();
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(config.timeout_seconds))
.build()?;

View File

@@ -0,0 +1,8 @@
//! Fresh test binary, no bootstrap: constructing the client must not panic
//! for lack of a rustls CryptoProvider.
use poster_fetcher::{PosterFetcherConfig, ReqwestPosterFetcher};
#[test]
fn poster_fetcher_builds_without_bootstrap() {
ReqwestPosterFetcher::new(PosterFetcherConfig { timeout_seconds: 5 }).expect("client builds");
}

View File

@@ -1,10 +1,10 @@
[package]
name = "rss"
name = "rss-adapter"
version = "0.1.0"
edition.workspace = true
[dependencies]
rss-feed = { package = "rss", version = "2", features = ["atom"] }
rss = { workspace = true, features = ["atom"] }
domain = { workspace = true }
[dev-dependencies]

View File

@@ -1,8 +1,8 @@
use domain::errors::DomainError;
use domain::models::DiaryEntry;
use domain::ports::{FeedChannel, RssFeedRenderer};
use rss_feed::extension::atom::{self, AtomExtensionBuilder, Link};
use rss_feed::{ChannelBuilder, GuidBuilder, ItemBuilder};
use rss::extension::atom::{self, AtomExtensionBuilder, Link};
use rss::{ChannelBuilder, GuidBuilder, ItemBuilder};
pub struct RssAdapter {
base_url: String,

View File

@@ -3,7 +3,7 @@ use domain::models::ReviewEdit;
use domain::ports::{FeedChannel, RssFeedRenderer};
use domain::testing::{test_movie_with_year, test_review};
use domain::value_objects::{Comment, UserId};
use rss::RssAdapter;
use rss_adapter::RssAdapter;
fn channel<'a>(title: &'a str) -> FeedChannel<'a> {
FeedChannel {

View File

@@ -6,17 +6,17 @@ edition.workspace = true
[dependencies]
sqlx = { workspace = true }
activitypub = { workspace = true }
k-ap = { version = "0.5.1", registry = "gitea" }
k-ap = { workspace = true }
sqlite = { workspace = true }
domain = { workspace = true }
anyhow = { workspace = true }
serde_json = { workspace = true }
uuid = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true, features = ["v4", "serde"] }
chrono = { workspace = true, features = ["clock", "serde"] }
tracing = { workspace = true }
async-trait = { workspace = true }
[dev-dependencies]
tokio = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros"] }
domain = { workspace = true, features = ["test-helpers"] }
uuid = { workspace = true }

View File

@@ -12,11 +12,11 @@ event-payload = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
anyhow = { workspace = true }
uuid = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true, features = ["v4", "serde"] }
chrono = { workspace = true, features = ["clock", "serde"] }
tracing = { workspace = true }
async-trait = { workspace = true }
tokio = { workspace = true }
tokio = { workspace = true, features = ["rt", "sync", "time", "macros", "rt-multi-thread"] }
futures = { workspace = true }
async-stream = { workspace = true }

View File

@@ -7,6 +7,7 @@ edition.workspace = true
config = { workspace = true }
domain = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true }
rustls = { workspace = true, features = ["ring", "std"] }
serde = { workspace = true, features = ["derive"] }
async-trait = { workspace = true }
chrono = { workspace = true }

View File

@@ -12,6 +12,8 @@ impl TmdbEnrichmentClient {
.tmdb_api_key
.clone()
.ok_or_else(|| DomainError::InfrastructureError("TMDB_API_KEY is not set".into()))?;
// reqwest is built with rustls-no-provider; install ring so this works without bootstrap.
let _ = rustls::crypto::ring::default_provider().install_default();
Ok(Self {
api_key,
http: reqwest::Client::new(),

View File

@@ -0,0 +1,12 @@
//! Fresh test binary, no bootstrap: constructing the client must not panic
//! for lack of a rustls CryptoProvider.
use tmdb_enrichment::TmdbEnrichmentClient;
#[test]
fn tmdb_enrichment_builds_without_bootstrap() {
let cfg = config::MetadataConfig {
tmdb_api_key: Some("key".into()),
..Default::default()
};
TmdbEnrichmentClient::from_config(&cfg).expect("client builds");
}

View File

@@ -4,9 +4,9 @@ version = "0.1.0"
edition.workspace = true
[dependencies]
serde = { workspace = true }
serde = { workspace = true, features = ["derive"] }
uuid = { workspace = true }
utoipa = { version = "5.5.0", features = ["axum_extras", "uuid"] }
utoipa = { workspace = true, features = ["axum_extras", "uuid"] }
domain = { workspace = true }
[dev-dependencies]

View File

@@ -7,11 +7,11 @@ edition.workspace = true
async-trait = { workspace = true }
domain = { workspace = true }
config = { workspace = true }
uuid = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
chrono = { workspace = true, features = ["clock"] }
tracing = { workspace = true }
futures = { workspace = true }
tokio = { workspace = true }
tokio = { workspace = true, features = ["rt", "sync", "time", "macros"] }
sha2 = { workspace = true }
rand = { workspace = true }
hex = { workspace = true }
@@ -22,5 +22,5 @@ xlsx = []
federation = []
[dev-dependencies]
tokio = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread"] }
domain = { workspace = true, features = ["test-helpers"] }

View File

@@ -26,18 +26,18 @@ tmdb-enrichment = { workspace = true }
image-converter = { workspace = true }
export = { workspace = true }
importer = { workspace = true }
rss = { workspace = true }
rss-adapter = { workspace = true }
media-server = { workspace = true }
sqlite = { workspace = true }
sqlx = { workspace = true }
anyhow = { workspace = true }
async-trait = { workspace = true }
axum = { workspace = true }
tokio = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros", "sync", "time", "net"] }
futures = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "ansi", "tracing-log", "smallvec"] }
rustls = { workspace = true, features = ["ring", "std", "tls12", "logging"] }
nats = { workspace = true, optional = true }
activitypub = { workspace = true, optional = true }
@@ -45,8 +45,8 @@ sqlite-federation = { workspace = true, optional = true }
[dev-dependencies]
reqwest = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true }
chrono = { workspace = true, features = ["clock"] }
uuid = { workspace = true, features = ["v4"] }
serde_json = { workspace = true }
tower = { version = "0.5", features = ["util"] }
tower = { workspace = true, features = ["util"] }
domain = { workspace = true, features = ["test-helpers"] }

View File

@@ -9,7 +9,7 @@ use export::ExportAdapter;
use http_axum::context::AppContext;
use http_axum::{openapi, router, state::AppState};
use importer::ImporterDocumentParser;
use rss::RssAdapter;
use rss_adapter::RssAdapter;
use crate::database::{self, DatabaseOutput};
use crate::{Repositories, event_bus, federation};

View File

@@ -20,8 +20,8 @@ pub fn install_crypto_provider() {
mod tests {
use super::*;
/// Runs alone in the lib test binary, so no other code (k-ap included)
/// has installed a provider first.
/// Only test in this crate's lib test binary, so nothing else in this
/// process installs a provider before `install_crypto_provider` runs.
#[test]
fn https_client_builds_after_install() {
install_crypto_provider();

View File

@@ -252,7 +252,6 @@ async fn feeds_and_openapi_are_served() {
("/openapi.json", 200),
("/docs", 303),
("/docs/", 200),
("/scalar", 200),
] {
let uri = expand(&seed, template);
let (status, _, _) = send(&seed.router, get(&uri, None)).await;

View File

@@ -5,7 +5,7 @@ version = "0.1.0"
[dependencies]
thiserror.workspace = true
serde.workspace = true
serde = { workspace = true, features = ["derive"] }
figment.workspace = true
tracing.workspace = true

View File

@@ -4,20 +4,20 @@ version = "0.1.0"
edition.workspace = true
[dependencies]
uuid = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true, features = ["v4", "v5", "serde"] }
chrono = { workspace = true, features = ["clock", "serde"] }
async-trait = { workspace = true }
thiserror = { workspace = true }
bytes = { workspace = true }
futures = { workspace = true }
serde = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
email_address = "0.2.9"
email_address = { workspace = true }
[dev-dependencies]
domain = { path = ".", features = ["test-helpers"] }
tokio = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros"] }
[features]
test-helpers = []

View File

@@ -5,7 +5,7 @@ edition.workspace = true
[dependencies]
domain = { workspace = true }
serde = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true }
serde = { workspace = true, features = ["derive"] }
chrono = { workspace = true, features = ["serde"] }
uuid = { workspace = true, features = ["serde"] }
serde_json = { workspace = true }

View File

@@ -16,8 +16,8 @@ sqlite-federation = ["federation"]
[dependencies]
bootstrap = { workspace = true }
config = { workspace = true }
axum = { workspace = true }
tokio = { workspace = true }
axum = { workspace = true, features = ["http1", "tokio"] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "net"] }
anyhow = { workspace = true }
dotenvy = { workspace = true }
tracing = { workspace = true }

View File

@@ -13,24 +13,22 @@ linux-zbus = [
windows = ["dep:windows-native-keyring-store"]
[dependencies]
ratatui = "0.30.0"
keyring-core = "1.0.0"
directories = "6"
ratatui = { workspace = true }
keyring-core = { workspace = true }
directories = { workspace = true }
csv = { workspace = true }
apple-native-keyring-store = { version = "1.0.0", optional = true, features = [
"keychain",
] }
zbus-secret-service-keyring-store = { version = "1.0.0", optional = true }
windows-native-keyring-store = { version = "1.0.0", optional = true }
apple-native-keyring-store = { workspace = true, optional = true }
zbus-secret-service-keyring-store = { workspace = true, optional = true }
windows-native-keyring-store = { workspace = true, optional = true }
api-types = { workspace = true }
reqwest = { workspace = true }
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }
serde = { workspace = true }
rustls = { workspace = true, features = ["ring", "std", "tls12", "logging"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tokio = { workspace = true }
tokio = { workspace = true, features = ["rt", "rt-multi-thread", "sync", "macros", "time"] }
anyhow = { workspace = true }
uuid = { workspace = true }
uuid = { workspace = true, features = ["serde"] }
thiserror = { workspace = true }

View File

@@ -14,7 +14,7 @@ sqlite-federation = ["federation"]
[dependencies]
bootstrap = { workspace = true }
config = { workspace = true }
tokio = { workspace = true, features = ["signal"] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal", "sync"] }
anyhow = { workspace = true }
dotenvy = { workspace = true }
tracing = { workspace = true }