Found while fixing #111 (PR #171). PR #171 clears hackon:* localStorage at all three boundaries the
app controls — sign out, sign in, and accept-invite's own logout. This is the one path it cannot close
from application code.
What happens
PortalProvider (in @open-mercato/ui, node_modules — read-only framework code) swallows a 401
from /portal/profile without signing the user out or redirecting to login. When a portal session
cookie expires or is cleared out-of-band, no logout code ever runs, so:
- The portal keeps rendering with whatever state it already had.
hackon:* localStorage keys survive, because clearing them is attached to the logout call sites.
- The user sits in a half-authenticated shell where reads 401 silently.
This is the residual half of #111: the sweep-on-login that PR #171 added is defence in depth for
exactly this case, but it only helps once the user logs in again.
Why it is filed rather than fixed
PortalProvider lives in the framework. Per AGENTS.md the options are to eject (heavy, for one
behaviour) or to fix it upstream in @open-mercato/ui. Upstream is the right home: "a 401 from the
profile endpoint means the session is gone, so sign out and redirect" is correct for every consumer,
not just this app.
Related, same root
The portal only learns the competition stage when CompetitionProvider remounts, so a parked tab
never observes a stage advance. PR #168 makes the chrome react once the value changes, but nothing
pushes the new value. The app already has an SSE bridge (.ai/guides/events.md → DOM Event Bridge);
publishing stage changes over it would close this properly. Feature-sized, and worth considering
alongside the 401 handling since both are "the portal does not notice that the world changed".
Found while fixing #111 (PR #171). PR #171 clears
hackon:*localStorage at all three boundaries theapp controls — sign out, sign in, and accept-invite's own logout. This is the one path it cannot close
from application code.
What happens
PortalProvider(in@open-mercato/ui,node_modules— read-only framework code) swallows a401from
/portal/profilewithout signing the user out or redirecting to login. When a portal sessioncookie expires or is cleared out-of-band, no logout code ever runs, so:
hackon:*localStorage keys survive, because clearing them is attached to the logout call sites.This is the residual half of #111: the sweep-on-login that PR #171 added is defence in depth for
exactly this case, but it only helps once the user logs in again.
Why it is filed rather than fixed
PortalProviderlives in the framework. Per AGENTS.md the options are to eject (heavy, for onebehaviour) or to fix it upstream in
@open-mercato/ui. Upstream is the right home: "a 401 from theprofile endpoint means the session is gone, so sign out and redirect" is correct for every consumer,
not just this app.
Related, same root
The portal only learns the competition stage when
CompetitionProviderremounts, so a parked tabnever observes a stage advance. PR #168 makes the chrome react once the value changes, but nothing
pushes the new value. The app already has an SSE bridge (
.ai/guides/events.md→ DOM Event Bridge);publishing stage changes over it would close this properly. Feature-sized, and worth considering
alongside the 401 handling since both are "the portal does not notice that the world changed".