Skip to content

fix(ui): persist Control UI gateway token in sessionStorage#39615

Open
KathenZK wants to merge 3 commits intoopenclaw:mainfrom
KathenZK:fix/control-ui-session-token
Open

fix(ui): persist Control UI gateway token in sessionStorage#39615
KathenZK wants to merge 3 commits intoopenclaw:mainfrom
KathenZK:fix/control-ui-session-token

Conversation

@KathenZK
Copy link
Copy Markdown

@KathenZK KathenZK commented Mar 8, 2026

Summary

  • persist the Control UI gateway token in sessionStorage
  • keep token imports from ?token= / #token= working as before
  • continue avoiding localStorage persistence for gateway auth

Why

Today the Control UI imports token from the URL, strips it from the address bar, and keeps it in memory only. That means a normal page refresh drops the token and disconnects the UI.

This is especially painful for loopback/local dashboard use, where the expected UX is:

  • refresh should keep working
  • closing the tab should clear the token

sessionStorage is a reasonable middle ground:

  • survives refreshes within the same tab
  • is cleared when the tab/window session ends
  • still avoids long-lived localStorage persistence

Implementation

  • load token from sessionStorage during settings bootstrap
  • save token to sessionStorage in saveSettings
  • keep persisted UI settings in localStorage token-free
  • update tests to verify URL token hydration + session-only persistence

Tests

  • ui/src/ui/storage.node.test.ts
  • ui/src/ui/navigation.browser.test.ts

Notes

This preserves the existing security intent of not storing the gateway token in localStorage, while fixing the refresh-disconnect UX.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 8, 2026

Greptile Summary

This PR fixes the refresh-disconnect UX for the Control UI by persisting the gateway auth token in sessionStorage instead of keeping it purely in memory. The token is saved on every saveSettings call and loaded during settings bootstrap, while localStorage remains intentionally token-free.

Key changes:

  • storage.ts: adds helpers to load and persist the session token; loadSettings now prefers a stored session token over the empty default; saveSettings now writes the token to sessionStorage.
  • storage.node.test.ts: stubs sessionStorage in test setup and updates two test cases to verify session-only token persistence and correct legacy-token scrubbing.
  • navigation.browser.test.ts: adds assertions that URL-imported tokens are written to sessionStorage.

One logic gap is present: the early-return path in loadSettings (lines 52–54) that fires when localStorage has no entry returns defaults without consulting sessionStorage. If localStorage is ever cleared while a valid session token still exists in sessionStorage, the token will be silently discarded on the next load. The same session-token load call used in the main branch should also be applied in this branch.

Confidence Score: 3/5

  • Safe to merge with one logic gap, but the gap is limited to an uncommon edge case (localStorage manually cleared while sessionStorage still holds a token).
  • The core approach is sound and the tests cover the happy-path URL-token and legacy-scrub scenarios well. The one identified issue — loadSessionToken() not being called in the !raw early-return branch of loadSettings — means a session token is lost whenever localStorage is absent, creating an observable inconsistency. It is not reachable in normal usage (because saveSettings always writes both stores together), but it is a real divergence from the stated contract and worth fixing before merge.
  • ui/src/ui/storage.ts — the !raw early-return branch (lines 52–54) should also call loadSessionToken() to be consistent with the main branch.

Comments Outside Diff (1)

  1. ui/src/ui/storage.ts, line 52-54 (link)

    Session token ignored when localStorage is empty

    When localStorage has no entry for the settings key (e.g. after a user manually clears localStorage), the function returns defaults — which hardcodes token: "" — without ever consulting sessionStorage. A valid session token already present in sessionStorage is silently discarded.

    Concrete scenario: the user has an active tab (sessionStorage has the token, localStorage has settings), manually clears localStorage via DevTools, then refreshes. loadSettings() hits this early return, ignores the session token, and the UI appears logged out even though sessionStorage is still intact.

    The same loadSessionToken() call used in the main branch should also be applied here so both code paths are consistent.

Last reviewed commit: 53ef2d9

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 53ef2d97ea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

: defaults.gatewayUrl,
// Gateway auth is intentionally in-memory only; scrub any legacy persisted token on load.
token: defaults.token,
token: loadSessionToken() ?? defaults.token,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Read session token even when local settings are absent

The new token hydration only runs inside the branch that successfully reads/parses openclaw.control.settings.v1, so when that localStorage key is missing (for example after key reset, manual localStorage clear, or any path where only sessionStorage remains), loadSettings() returns defaults and drops a valid session token on refresh. This breaks the commit’s stated session-persistence behavior because token recovery is now unintentionally gated on unrelated persisted UI settings.

Useful? React with 👍 / 👎.

Comment on lines +105 to +106
persistSettings(next);
persistSessionToken(next.token);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist session token before writing local settings

saveSettings() writes localStorage first and only then writes sessionStorage, so if localStorage.setItem throws (e.g., quota/security errors in private or restricted contexts), the function exits before persisting the gateway token to sessionStorage. In those environments the new refresh-survives-within-tab behavior silently fails even though sessionStorage itself could still be usable.

Useful? React with 👍 / 👎.

@KathenZK
Copy link
Copy Markdown
Author

KathenZK commented Mar 8, 2026

Repro that motivated this patch:

  1. Open Control UI with a tokenized URL, e.g. http://127.0.0.1:18789/#token=...
  2. The UI imports the token and strips it from the address bar
  3. Refresh the page
  4. Control UI drops back to an unauthenticated state and disconnects

Observed result today:

  • refresh loses the gateway token because it only lives in memory
  • the URL token is already stripped, so the user has no stable refresh path

Expected result:

  • refresh should stay connected within the same tab/session
  • closing the tab/window should still clear the token

That’s why sessionStorage felt like the right middle ground here.

@KathenZK
Copy link
Copy Markdown
Author

KathenZK commented Mar 8, 2026

Good catch — I’ve addressed that edge case.

What changed in the follow-up commit:

  • defaults.token now initializes from loadSessionToken() ?? ""
  • so the token is still restored even when localStorage is empty or parsing fails
  • added a test covering the localStorage missing + sessionStorage token present case

This keeps the two code paths consistent and matches the intended contract for session-scoped token persistence.

@KathenZK
Copy link
Copy Markdown
Author

Refreshing this PR and clarifying scope.

This PR fixes the Control UI refresh/token-persistence problem only by keeping the gateway token in sessionStorage for the lifetime of the tab/session.

What it fixes:

  • token imported from ?token= / #token= survives normal page refresh
  • closing the tab/window still clears the token
  • localStorage remains token-free

What it does not try to fix:

I also addressed the earlier edge case noted in review:

  • session token is now restored even when localStorage is empty
  • added coverage for that path in tests

CI is green after the update. If maintainers still want this split, targeted UX fix, this should be ready for review/merge.

@KathenZK KathenZK force-pushed the fix/control-ui-session-token branch from ae263b5 to 159f125 Compare March 10, 2026 08:14
@KathenZK
Copy link
Copy Markdown
Author

It looks like main has since moved to a more complete gateway-scoped session-token approach, which overlaps with the original intent of this PR.

The original goal here was to fix the refresh/token-loss UX and establish session-scoped persistence without falling back to localStorage.

If maintainers consider the current mainline implementation to have fully superseded this PR, I’m happy for this to be closed as covered by the newer approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant