All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Highlights: global command-line output contract (ADR-T-010), container infrastructure refactor (ADR-T-009), native role-based authorization replacing Casbin (ADR-T-008), RSA-signed JWTs with revocation support (ADR-T-007), domain-scoped error system (ADR-T-006), MSRV raised to 1.88.
- MSRV raised from 1.85 to 1.88.
- First-party command-line entrypoints are now governed by ADR-T-010's
JSON-only output contract. Stdout is reserved for machine-readable result
data, stderr is reserved for machine-readable diagnostics/control records, and
stdout-producing commands refuse direct terminal stdout.
parse_torrentnow emits JSON result data on stdout.create_test_torrent,import_tracker_statistics,seeder, andupgradenow keep stdout empty while reporting status and diagnostics as JSON on stderr. The container entry script also reports validation failures, status records, utility failures, and debug phase records as JSON/NDJSON on stderr instead of plain text or shell trace output. Scripts that scraped previous plain-text command or startup output must switch to exit codes and JSON/NDJSON stderr parsing. - The
torrust-indexserver's application logs now use JSON records on stderr instead of the previous human-formatted tracing output. Log consumers should parse stderr as NDJSON or pipe it through a JSON viewer. torrust-index-auth-keypairandtorrust-index-health-checkstdout JSON now includes a top-levelschemafield. Scripts that expected an exact object shape must tolerate or consume the schema field.database.connect_urlandtracker.tokenare now mandatory schema fields with no defaults. Supply them via env-var override (TORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URL,..._TRACKER__TOKEN) or a side-loadedindex.toml. Missing values fail at parse time with a precisemissing fielderror (ADR-T-009 §D2).- TLS configuration renamed from
[net.tsl]to[net.tls]in operator TOMLs and from"tsl"to"tls"in the settings JSON API. Clean break, no compatibility alias (ADR-T-009 §D3). TORRUST_INDEX_DATABASE_DRIVERno longer dispatches the application's runtime driver — that is derived from the URL scheme ofdatabase.connect_url(ADR-T-009 §D2). It is retained as an input-validation gate at container start (sqlite3/mysql); both values seed the same driver-agnosticindex.container.tomltemplate into/etc/torrust/index/on first boot.TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__*_PEMand..._PATHare mutually exclusive within a single key, both keys must use the same delivery mechanism, and the pair must either both be set or both be absent (ADR-T-009 §D3).- Container
USER_IDvalidation changed from>= 1000to "non-negative integer, not0". The property the entry script actually enforces is "do not run as root" (ADR-T-009 §D7). - JWT signing changed from HMAC-HS256 to RS256. Existing tokens are invalidated; users must re-login (ADR-T-007).
- JWT claims redesigned from
UserClaims { user, exp }toSessionClaims { sub, iss, aud, iat, exp, role, username, gen }(ADR-T-007). - Auth config keys
auth.user_claim_token_pepper,auth.session_signing_key, andauth.email_verification_signing_keyreplaced byauth.private_key_path/auth.public_key_path(or inline PEM). Deployers must generate an RSA key pair (ADR-T-007). administrator: boolreplaced byrole: Stringacross the API (TokenResponse,UserCompact, etc.); the legacyadmin: boolfield is removed entirely (ADR-T-008).administratorcolumn dropped fromtorrust_users;role: TEXTis now sole authority (ADR-T-008).ACTIONenum renamed toAction(variants unchanged) (ADR-T-008).ServiceError(41 variants) andServiceResultreplaced by domain-scoped enums:AuthError,UserError,TorrentError,CategoryTagError, with a thinApiErrorwrapper (ADR-T-006).
- ADR-T-010 establishes a repository-wide JSON-only output contract for first-party command-line entrypoints. Stdout is reserved for result data; stderr carries diagnostics and control records; commands that emit stdout result data refuse direct terminal stdout.
torrust-index-cli-commonprovides the shared implementation for that contract: JSONclaphelp/version/usage handling, JSON panic diagnostics, JSON stderr tracing, TTY refusal, stdout JSON emission, command runners, baseline exit-code classes, control-plane record types, and redaction helpers.- Regression coverage now protects the contract with CLI behavior tests, binary-boundary checks, and workspace lint rules denying accidental raw stream output or direct process exits outside the shared CLI boundary.
- Helper binaries (
torrust-index-auth-keypair,torrust-index-config-probe, andtorrust-index-health-check) share the JSON CLI boundary. Their successful stdout payloads remain single JSON objects, now explicitly versioned with a top-levelschemafield, while help, version, argv errors, TTY refusal, panic diagnostics, and tracing are emitted as JSON records on stderr. - The
torrust-indexserver and root Rust binaries return explicitExitCodevalues at theirmainboundaries and install the shared JSON panic hook. Central application logging uses JSON tracing on stderr, with a non-emptyRUST_LOGtaking precedence over the configured default filter. parse_torrentis a stdout-result command. It emits one JSON object withschema,torrent,original_v1_info_hash, andinput_byte_length, leaves stdout empty on failure, and refuses direct terminal stdout with a JSON diagnostic record.create_test_torrent,import_tracker_statistics,seeder, andupgradeare no-stdout side-effect commands. They keep stdout empty, report status and diagnostics as JSON/NDJSON on stderr, and propagate command failures instead of printing plain text or relying on panic output.- Command-reachable shared libraries use the command diagnostic path instead of raw stream output. Shutdown notices are structured tracing records, mail template failures are returned to callers, terminal color formatting is removed from command paths, and parsing helpers leave reporting decisions to their command callers.
- The container entry script follows the JSON stderr contract during startup:
it captures helper stdout internally, keeps its own stdout empty before
su-exec, checks forjqbefore JSON-dependent helpers run, emits explicitDEBUG=1phase records instead ofset -x, and wraps controlled utility failures with captured stderr fields. - Operator documentation and command examples describe the completed contract across the README, container guide, upgrade notes, and command module docs.
- ADR-T-009 itself.
torrust-index-configworkspace crate (packages/index-config/) containing the parsing surface of the configuration system. Leaf crate — notokio,reqwest,sqlx,hyper,rustls,native-tls, oropensslin its dep closure.- Helper-binary crates split into leaves with no HTTP/TLS in their
dep closure:
torrust-index-cli-common(shared ADR-T-010 scaffolding —refuse_if_stdout_is_tty,init_json_tracing,emit,BaseArgs),torrust-index-health-check(stdlib-only, Happy Eyeballs IPv6/IPv4 fallback),torrust-index-auth-keypair(RSA-2048 key generator),torrust-index-config-probe(resolved-config JSON emitter), andtorrust-index-entry-script(test-only host-side driver). - Sourced POSIX
shlibrary atshare/container/entry_script_lib_shcontaining the entry script's pure helpers (inst,key_configured,validate_auth_keys,seed_sqlite). Shipped as0444 root:rootand sourced — not exec'd — by both the entry script and the host-side test crate (§D3). - Top-level
Makefilewithmake up-dev(plaindocker compose up) andmake up-prod(validates required credential env vars, runs with the override excluded) (§D1). compose.override.yamlauto-loaded by Compose v2, re-introducing themailcatchersidecar,tty: true, and permissive${VAR:-default}substitutions on top of the production-shaped baseline (§D1).contrib/dev-tools/su-exec/AUDIT.mdrecording provenance and a SHA-256-anchored append-only audit log for the vendoredsu-exec.c. CI fails the build when the file changes without a matching audit entry (§D8).jq_donorContainerfile stage providingjq(and required shared libs) from a pristinerust:slim-trixiebase; both runtime images copy/usr/bin/jqas0500 root:root. Used during the pre-su-execphase to parse the config probe's and keypair helper's JSON output. Anldd-based allow-list catches future transitive-dep changes at build time (§D5).- Container runtime split into parallel
runtime_releaseandruntime_debugstages over a shared base-agnosticruntime_assetsbundle, withbusybox_donor,busybox_preflight,etc_seed,adduser_preflight, and apreflight_gateaggregator (§D4). - Curated busybox applet subset in the release runtime: a single
root-only
/bin/busybox(0700 root:root) plus symlinks forsh,adduser,addgroup,install,mkdir,dirname,chown,chmod,tr,mktemp,cat,printf,rm,echo,grep. The unprivilegedtorrustuser getsEACCESon busybox after privilege drop (§D4). - Container auto-generation of persistent auth keys on first boot:
the entry script runs
torrust-index-auth-keypair, splits the JSON output withjq, and writes PEMs to/etc/torrust/index/auth/on the volume. HEALTHCHECKdirective on thedebugbuild target (was previously omitted); debugCMDis now["/usr/bin/torrust-index"]so debug is a drop-in replacement for release (Phase 4).Info::from_envconstructor ontorrust-index-config— the JSON-safe sibling ofInfo::newthat skips diagnosticprintln!s and filters empty-string env vars.pub const DEFAULT_CONFIG_TOML_PATHshared between the application, helper binaries, and integration tests.ApiToken::is_empty()accessor so the config probe can rejecttracker.token = ""at the container boundary.# ENTRY_ENV_VARS:/# END_ENTRY_ENV_VARScanonical manifest block in the entry script; CI verifies every name is documented indocs/containers.md.EXPOSE ${IMPORTER_API_PORT}/tcpin Containerfile; port 3002 mapped in compose.restart: unless-stoppedon index and tracker compose services.DEBUG=1env-var gate for entry-script JSON phase diagnostics.#[doc(hidden)] pub mod test_helpersintorrust-index-configexposingPLACEHOLDER_TOMLandplaceholder_settings()— single source of truth for the ~40 tests across both crates that previously relied on the removedSettings::default()fixture (Phase 5).Configuration::for_tests(test-only,pub(crate)) replacing the deletedimpl Default for Configuration(Phase 5).clear_inherited_config_env()test helper that stripsTORRUST_INDEX_CONFIG_OVERRIDE_*andTORRUST_INDEX_CONFIG_TOML[_PATH]inside afigment::Jailso default-configuration assertions stay deterministic (Phase 5).- Inverted shipped-sample test suite asserting no shipped TOML
carries
connect_url,token,[mail.smtp], or auth key paths, and that the schema rejects each sample with a missing-field error (Phase 5, §D2). - New loader tests
missing_database_connect_url_is_rejectedandmissing_database_section_is_rejected(Phase 5). - Default
TORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URLincompose.yaml, alongside the existingTRACKER__TOKENdefault; matching exports in the mysql and sqlite e2e runner scripts.
- Configuration parsing surface moved from
src/config/into the newtorrust-index-configworkspace crate.src/config/mod.rsis now a thin re-export shim plus the runtimeConfigurationwrapper; existinguse crate::config::*;call sites compile unchanged. - Permission value types (
Role,Action,Effect,PermissionOverride,RoleParseError) moved totorrust_index_config::permissionsand re-exported fromcrate::services::authorization. ThePermissionstrait andPermissionMatrixruntime policy stay in the root crate. load_settingsno longer ends withfigment.join(Serialized::defaults(Settings::default())). Optional sub-sections still default through their per-field#[serde(default)]attributes; mandatory fields no longer have a silent fallback.check_mandatory_optionsno longer coverstracker.token; its absence now surfaces through serde for a single consistent error shape across all missing mandatory fields.Info::newroutes its "loading extra configuration from …" diagnostics throughtracing(stderr) instead ofprintln!(stdout).- Container
HEALTHCHECKinvokestorrust-index-health-check(washealth_check); the binary is rewritten in stdlib-only Rust. - Entry script invokes
torrust-index-auth-keypair(wastorrust-generate-auth-keypair) and consumes its JSON output viajq -r .private_key_peminstead ofsedPEM-block extraction. - Entry script uses busybox short-option form for
adduserso the same invocation works on both runtime bases (distrolesscc-debian13ships/etc/passwdand/etc/groupbut not/etc/shadow). - Helper binaries (
torrust-index-health-check,torrust-index-auth-keypair) tightened from world-executable to0500 root:root. The application binary keeps0755. PATHpinned in both runtime bases (/usr/local/bin:/bin:/usr/bin:/sbinfor release;/usr/local/bin:/busybox:/bin:/usr/bin:/sbinfor debug) so the entry script's bare-name lookups resolve deterministically.- Helper-binary TTY-refusal exit code unified on
2(was1for the keypair helper) via the sharedrefuse_if_stdout_is_tty. .containerignorenow excludes/adr/and/docs/from the build context.- Container base images upgraded from Debian bookworm to trixie
(
rust:bookworm→rust:trixie,cc-debian12→cc-debian13). cargo-binstallbootstrap pinned to tagv1.18.1(wasmain).- MySQL compose image pinned to
8.0.45; auth flag changed from--default-authentication-pluginto--authentication-policy. - Dev-only compose ports (tracker, MySQL, mailcatcher) bound to
127.0.0.1. .dockerignorerenamed to.containerignorefor Podman compatibility.- Removed redundant
--tests --benches --examplesfrom Containerfile (covered by--all-targets). - All shipped sample TOMLs under
share/default/config/no longer carryconnect_url,token,[mail.smtp]values, or[auth]key paths. Bare-metal developers who copyindex.development.sqlite3.tomlverbatim must now supplyconnect_urlandtokenthemselves (Phase 5, §D2). - DEV-ONLY credential comments in
compose.yaml. docs/containers.md: documented healthcheck behaviour, busybox applet subset, Podman--format dockerrequirement forHEALTHCHECK, entry-script debugging; fixedUSER_UID→USER_IDtypo.
- Build-time
ARG API_PORT/ARG IMPORTER_API_PORT. The runtimeENVdefaults are retained so the listener andHEALTHCHECKresolve correctly; runtime--envoverrides continue to work (§D6). - Monolithic
runtimeContainerfile stage and its ad-hoccp -spbusybox-applet copy, superseded by the curated symlink loop and the release/debug split. RUN envandCMD ["sh"]from the previous debug target — debug now ships the sameENTRYPOINT/CMD/HEALTHCHECKas release; operators reach a shell withdocker run … sh.impl Default for Settings,impl Default for Tracker,impl Default for Database, the matching#[serde(default = …)]attributes, and the now-deadTracker::default_token()/Settings::default_tracker()(Phase 5).impl Default for Configuration— superseded byConfiguration::for_tests(Phase 5).contrib/dev-tools/container/build.shandrun.sh— both stale, passed wrong build-args and assumed aDockerfilethat no longer exists.- Stale
TORRUST_TRACKER_USER_UIDexport from E2E container scripts.
- ADR-T-008 itself.
- Native
PermissionMatrixreplacing Casbin: compile-time checkedRoleandActionenums with an exhaustive default-deny policy table. Permissionstrait abstraction for the authorization backend, consumed by theRequirePermission<A>extractor viaAppData.permissions.role: TEXTcolumn ontorrust_users(migration for SQLite and MySQL); existingadministrator = truerows migrated torole = 'admin', others torole = 'registered'.role: Stringfield onTokenResponse,UserCompact,UserProfile, andUserFullAPI response models.RequirePermission<A>Axum extractor enforcing role-based authorization at the HTTP boundary before the handler runs.ActionMarkertrait andaction_markers!macro mapping zero-sized types toActionvariants for compile-time handler–permission binding. A compile-time sync assertion catches divergence betweenaction_markers!andAction::ALL.Actorstruct yielded byRequirePermissioncarrying the resolveduser_idandRole, withtry_user_id()andis_authenticated()helpers.- E2E tests for non-owner update and delete denial
(
and_non_ownersmodule under torrent contract tests).
- All HTTP handlers requiring authorization use
RequirePermission<A>extractors instead of callingauthorization::Service::authorize(). - Service methods no longer receive
maybe_user_idfor authorization — they receive an already-authorizedActoror are called unconditionally. Unauthorized requests are rejected at the extractor boundary (fail-fast). - First-user auto-admin grant in
RegistrationService::registernow logs awarn!on failure instead of silently discarding theResultviadrop(). - v1→v2 upgrade path:
insert_imported_userwrites therolecolumn instead of the removedadministratorcolumn.
admin: boolfromTokenResponse,LoggedInUserData, andTokenRenewalData— superseded byrole: String.UserCompact::is_admin()— no longer needed.casbincrate dependency and all Casbin-related code (CasbinConfiguration,CasbinEnforcer, the SCREAMING_CASEACTIONenum,unstable.auth.casbinconfig section).authorization::Servicestruct — replaced byRequirePermission<A>consultingPermissionMatrixdirectly.ExtractLoggedInUserandExtractOptionalLoggedInUserextractors — replaced byRequirePermission<A>.- Dead
ActionvariantsGetSettingsandGetCanonicalInfoHash.
- ADR-T-007 itself.
- Centralised JWT module (
src/jwt.rs) consolidating alljsonwebtokenusage: key loading, signing, verification, algorithm configuration. SessionClaimswith RFC 7519 registered claims (sub,iss,aud,iat,exp) plus advisoryrole,username, and revocationgenfields.VerifyClaimswithaud: "email-verification"for purpose separation.- RSA key pair configuration:
auth.private_key_path/auth.public_key_path(or inline PEM via..._pem). - Ephemeral auto-generated RSA-2048 key pair when no keys are configured. Sessions do not survive server restarts with ephemeral keys.
kid(Key ID) header in every JWT for future key rotation.- Configurable token lifetimes:
auth.session_token_lifetime_secs(default: 2 weeks) andauth.email_verification_token_lifetime_secs(default: ~10 years). token_generationcolumn ontorrust_users(SQLite + MySQL migrations).- Token revocation: password changes, role changes (admin grant),
and bans increment
token_generation; tokens with an oldergenclaim are rejected. JsonWebToken::validate_sessionas the sole entry point for verifying a session JWT, the token-generation counter, and the banned-user check. All callers delegate here.BearerTokenextractor rejects missing/malformedAuthorizationheaders at the extraction boundary (AuthError::TokenNotFound/AuthError::TokenInvalid).ExtractOptionalLoggedInUserreturnsNonefor anonymous requests instead of erroring.AuthError::TokenRevokedvariant for revoked-token responses.- Crate tests for the JWT module (session +
email-verification round-trips, audience cross-contamination,
tampered/garbage tokens) and for
parse_token.
Authentication::get_user_id_from_bearer_tokentakesBearerTokendirectly instead ofOption<BearerToken>.parse_tokenreturnsResultinstead of panicking on malformed headers.- JWT
expvalidation relies solely on thejsonwebtokenlibrary; the redundant manual expiration check is removed. - Token signing uses
Resultpropagation instead of.unwrap()/.expect(). UserClaimsis now a type alias forSessionClaims.VerifyClaimsmoved frommailerinto thejwtmodule (re-exported for backward compatibility).- JWT session token
roleclaim carries the databaseroledirectly ("registered","admin") instead of the previous mapping ("user","admin").
bearer_token::Extractwrapper struct (replaced byBearerTokendirectly).get_optional_logged_in_userfree function (logic moved into extractors).get_claims_from_bearer_tokenprivate method onAuthentication(inlined).ClaimTokenPepper/JwtSigningSecret/user_claim_token_pepperconfig keys.
- ADR-T-006 itself.
- ~190 crate-level tests for the domain error system
(
src/tests/errors/): status-code mapping, display messages,Fromimpl coverage, andApiErrordelegation.
- Service functions return domain-specific
Result<T, DomainError>instead ofResult<T, ServiceError>. - Each domain error co-locates its HTTP status-code mapping via a
status_code()method. - Error
Fromimpls usetracing::error!instead ofeprintln!. - Standardise all error derives on
thiserror.
ServiceErrorenum andServiceResulttype alias fromsrc/errors.rs.http_status_code_for_service_errorandmap_database_error_to_service_errorhelpers.IntoResponseimpl fordatabase::Error(now handled by domain errors).
- Dev-only ports (MySQL 3306, tracker 6969/7070/1212, mailcatcher
1025/1080) no longer bind to
0.0.0.0; bound to127.0.0.1. - Entry script debug mode now emits structured JSON phase records instead of
enabling
set -x, avoiding shell-trace leakage of env vars into logs. - Compose credentials annotated as DEV-ONLY with TODO for Docker secrets migration (ADR-T-009 §S1).
- MySQL compose healthcheck was referencing a non-existent Docker
secret (
/run/secrets/db-password); now uses$$MYSQL_ROOT_PASSWORD. - Entry script
USER_IDvalidation:-z "$USER_ID" && "$USER_ID" -lt 1000always short-circuited to an error whenUSER_IDwas unset; corrected to||. - Containerfile release
HEALTHCHECKtrailing whitespace removed.
- ADR-T-004: Document rationale for removing
located-errorpackage. - ADR-T-005: Document rationale for Rust edition 2024 migration.
- BREAKING: Raise MSRV from 1.83 to 1.85.
- BREAKING: Migrate workspace to Rust edition 2024.
- BREAKING: Bump workspace version from
3.1.0-developto4.0.0-develop. - Upgrade
jsonwebtokenfrom 9.3 to 10 (withrust_cryptofeature). - Upgrade
randfrom 0.9 to 0.10; renamerand::Rngtorand::RngExt. - Promote
rust-2024-compatibilitylint group fromwarntodeny. - Reformat imports across ~55 files to edition 2024 style.
- Simplify error types in
configandweb::api::server— replaceLocatedError<'static, dyn Error + Send + Sync>withArc<dyn Error + Send + Sync>. - Emit
tracing::error!events whereLocatedErrorpreviously logged context.
- BREAKING: Remove first-party
torrust-index-located-errorpackage (packages/located-error/). Usetracingfor error-origin context instead.