Preserve Afonso’s same-directory temp-file replacement on top of the configurable cache root from #708. Cover the complete explicit-root round trip and document the exact process-crash and concurrent-writer guarantees.
cx
`save_cached_bundle` wrote in place with `std::fs::write`, which truncates
the file before the first byte of the replacement is written. Any kill in
that window leaves an empty or half-written `bundle_tokens.json`, which
parses as nothing, so the tokens are gone and the next launch pays a full
live extraction: a ~7 MB bundle.js fetch behind a 45s timeout, up to three
attempts.
The window is not rare. `refresh_bundle_if_changed` rewrites the whole file
on every launch that finds the version unchanged, purely to touch
`fetched_at`, so a warm cache is destroyed and rebuilt each time the app
starts.
The bytes now land in a sibling temp file that is flushed and renamed over
the target, so a reader sees either the previous cache or the new one. The
replacement is owner-only where writing in place left it at whatever the
umask allowed; these are app-level values scraped from a public bundle
rather than user credentials, so that is tidiness rather than a fix for a
vulnerability.
Both `client::tests::offline_gate_*` failed on every run, not intermittently:
reqwest is built with `rustls-tls-webpki-roots-no-provider`, so BUILDING a
client panics with "No provider set" until a process-level CryptoProvider is
installed. The application installs one at startup via
`qbz_app::ensure_crypto_provider`, but a test binary has no startup, and this
crate sits below `qbz-app` and cannot call it.
Confirmed pre-existing by stashing the unrelated work and re-running: same two
failures, same panic.
Worth knowing beyond these two tests: the panic fires at client CONSTRUCTION,
before any request and regardless of scheme, so it hits a plain `http://` URL
too. Any future tool or test that reaches for the download path without an
application around it will land on it.
cc
Add /track/lyricsUrl (signed RPC, method name "tracklyricsUrl") plus
the CDN document fetch through the offline choke point. Wire shapes
were validated against live captures (2026-06-10) and diverge from the
inferred yaml: numeric track_id in the step-1 envelope, CDN document
wrapped in {translation_langs, publishers, writers, original}, "wsync"
word-synced discriminator instead of "lsync" (per-word timestamps),
plain lines as objects, and miss = HTTP 404 error body mapped to a
typed Ok(None) instead of an error. Unit tests deserialize fixture
excerpts of the captures; examples/lyrics_probe.rs is the read-only
live probe used for the capture.
Also expose qbz_credentials::load_oauth_token_from_file so a stale
keyring entry cannot shadow the authoritative encrypted token file.
cc
Startup blocked on Qobuz's ~7 MB web bundle download with no cache or
timeout, so a slow CDN left the app on a blank screen for 25-30s before
the login screen appeared. Cache the extracted tokens on disk (keyed by
bundle version), apply them instantly on warm starts and refresh in the
background, and bound the cold fetch with a per-request timeout + retry.
The legacy api client shares the same cache. Emits BundleFetchStarted so
the UI can show a connecting state on the first run.
cc
Create the foundational workspace structure for the multi-crate
architecture refactor. This phase establishes placeholder crates
that will be populated in subsequent phases.
Crates created:
- qbz-models: Shared types, events, and traits (Tier 0)
- qbz-audio: Audio backend system (Tier 1)
- qbz-player: Playback engine and queue (Tier 1)
- qbz-qobuz: Qobuz API client (Tier 1)
- qbz-core: Orchestrator (Tier 3)
All crates compile successfully with placeholder implementations.
The workspace uses shared dependencies via workspace.dependencies.
cc