fix: review round 2 — a signed-out tab acts as nobody, honest share-page failures

Deep-review round 2 over main..improvement/watch-background-audio.

Account resolution (server)
- A request naming an account that is no longer signed in to this browser
  (X-Siftlode-Account or ?account=) now acts as nobody instead of the
  session's default account. A tab still pinned to an account signed out in
  another tab went on acting as the default: its player checkpoints and
  preference saves landed in that account, the messages socket and Plex
  playback ran as it, and its Sign out signed THAT account out. A hidden tab
  never polls /api/me, so a tab playing in the background kept doing it.
  The /api/me bootstrap probe alone keeps the fallback, which is how such a
  tab learns who it now is: the app's 401 handler reloads, and BootGate
  re-pins the tab.

BootGate
- On an identity change the tab reloads without moving its pin first. The
  tree being torn down still runs its unmount saves before the page goes,
  and with the pin already moved they wrote into the new account's local
  keys (a player volume still pending). The reloaded page corrects the pin.

Share page (/watch)
- Only a 404 says the link is gone. A 5xx, a dropped connection, and the
  unlock's own rate limit all rendered "no longer available ... expired or
  revoked" to a viewer with a valid link, with no way to try again. A load
  failure now offers Retry; too many password tries says so and keeps the
  form; an unlock that gets no answer says so instead of doing nothing.
  Strings in en/hu/de.

Downloads
- A cookie-authed asset is recognised under the current signature version
  and the one before it, so a job queued before a deploy that bumps the
  version still downloads with its cookie instead of failing on the login
  wall with an error that says no cookie was used.

The app chunk is preloaded through lazyWithPreload, so a failed fetch no
longer leaves an unhandled rejection beside the ErrorBoundary. The panel
layout seed is one helper (seedLayout).

Tests: the stale-tab refusal, its harmless Sign out and the probe fallback;
the pre-bump authed asset; BootGate keeping the pin on reload and staying
mounted through a same-account change; the reveal clock's later-commit
hand-off; the prefs, panel layout and demo-scope seeds from the me cache;
the share page's failure states. Each mutation-checked RED for its own
reason.
This commit is contained in:
2026-09-15 01:35:25 +02:00
parent e795d191c5
commit f569b19c49
18 changed files with 433 additions and 40 deletions
+19
View File
@@ -57,6 +57,25 @@ def test_a_non_wallet_account_header_falls_back_to_the_default(client, make_user
assert me.json()["email"] == "alice2@example.com" # still Alice, not Mallory
def test_a_tab_pinned_to_an_account_signed_out_elsewhere_acts_as_nobody(client, make_user):
# Two accounts in one browser: a tab pinned to Alice, Bob the default. Alice signs out in another
# tab. The pinned tab used to go on acting as the default, Bob — its player checkpoints and pref
# saves landed in his account, and its own Sign out signed HIM out.
alice = make_user(email="alice3@example.com", password=_PW)
make_user(email="bob3@example.com", password=_PW)
assert _login(client, "alice3@example.com", _PW).status_code == 200
assert _login(client, "bob3@example.com", _PW).status_code == 200 # Bob is now the default
tab = {ACTIVE_ACCOUNT_HEADER: str(alice.id)}
assert client.get("/api/feed", headers=tab).status_code == 200
assert client.post("/auth/logout", headers=tab).status_code == 200 # Alice signs out
assert client.get("/api/feed", headers=tab).status_code == 401
assert client.post("/auth/logout", headers=tab).status_code == 200
assert client.get("/api/feed").status_code == 200 # Bob is still signed in, the session intact
# The bootstrap probe still answers as the default: how the stale tab learns who it is now.
assert client.get("/api/me", headers=tab).json()["email"] == "bob3@example.com"
def test_logout_others_revokes_the_other_session_but_keeps_the_caller(client, make_user):
# Two independent cookie jars (browsers) signed into the same account. `logout-others` from one
# bumps the account epoch so the OTHER's cookie is rejected on its next request, while the caller
@@ -72,6 +72,21 @@ def test_authed_sig_differs_per_user_and_from_the_shared_sig():
assert a != shared and b != shared and a != b
def test_an_authed_asset_queued_before_a_signature_version_bump_still_gets_its_cookie(monkeypatch):
# A job keeps the asset it was queued with. A deploy that bumps the signature version between the
# queueing and the download used to make the user's private asset stop counting as theirs, so the
# download went without its cookie and failed on the login wall.
spec = formats.normalize({"mode": "a"})
queued = MediaAsset(format_sig=credentials.authed_format_sig(spec, 1, "facebook"))
rebuild = credentials.rebuild_salt(7, credentials.cookie_salt(1, "facebook"))
rebuilt = MediaAsset(id=7, format_sig=formats.format_sig(spec, salt=rebuild))
monkeypatch.setattr(formats, "_AUDIO_SIG_VERSION", formats._AUDIO_SIG_VERSION + 1) # the deploy
for asset in (queued, rebuilt):
assert credentials.is_authed_asset(asset, spec, 1, FB_URL)
assert not credentials.is_authed_asset(asset, spec, 2, FB_URL) # still private to its user
def test_write_cookiefile_is_private_and_survives_the_retry_wipe(tmp_path):
staging = tmp_path / "staging"
staging.mkdir()