Skip to content

fix: stop persisting the editor configuration in localStorage - #613

Merged
dcalhoun merged 9 commits into
trunkfrom
task/remove-gbkit-global-from-local-storage
Sep 30, 2026
Merged

dcalhoun merged 9 commits into
trunkfrom
task/remove-gbkit-global-from-local-storage

Conversation

@dcalhoun

@dcalhoun dcalhoun commented Sep 1, 2026 •

Copy link
Copy Markdown
Member

What?

Stop persisting editor configuration in localStorage.

Why?

This fallback storage was originally introduced to survive events like editor reloading. However, it became unnecessary once we ensured the configuration globals were injected for these types of events.

The persisted values could also become stale mid-session—e.g., if the media upload server stopped, the port and token would no longer be valid.

How?

Remove the localStorage set calls. Clear out existing localStorage values on editor launch.

Testing Instructions

  1. iOS demo: open a post, then force a WebContent process termination (Activity Monitor → quit com.apple.WebKit.WebContent for the Simulator). The editor reloads with the current post. The demo's "Trigger Editor Crash" action is not a substitute here — it throws in JS so the error boundary catches it, leaving the page and its process intact, where this step exercises the reload that replays the document-start script.
  2. Android demo: open an existing post via Browse, then rotate the device. EditorActivity declares no configChanges, so it is recreated and the editor reloads — it should render the post's saved title and content rather than the load-error view. Unsaved edits are not preserved across recreation in the demo, whose latestContent uses remember; that is unrelated to this change.
  3. iOS: upgrade from a build before this change with a post open, then reopen a post. Safari Web Inspector's Storage tab for the editor page shows no GBKit key.

Accessibility Testing Instructions

N/A, no user-facing changes.

Screenshots or screencast

N/A, no user-facing changes.


AI-generated details

Problem

window.GBKit carries the site credential and the local server's port and token, all valid only for the load that injected them. Both platforms mirrored it into localStorage, which persists on disk across launches, and getGBKit read that copy back whenever the global was missing.

Impact

A copy of the configuration could outlive the session that issued it, and on Android it persisted under the site's own origin rather than an origin only the editor loads. #611 reads the relay's port and token through getGBKit too, so landing this first keeps a stale copy from pointing the relay at a stopped server.

Nothing needed the copy. The persisted copy arrived with the global in #14, when iOS injected the configuration with evaluateJavaScript after the page had loaded; #15 moved iOS to a document-start WKUserScript, which WebKit replays on every navigation, including the reload after a WebContent process termination. Android re-injects on every page start and wipes web storage before each load. Boot also waits for window.GBKit and, outside ?dev_mode, aborts when it never arrives, so the fallback was unreachable in production.

Mechanism

  • getGBKit returns window.GBKit || {}; the storage fallback is gone.
  • iOS swaps the setItem in its document-start WKUserScript for a removeItem, scrubbing an upgraded device the next time the editor loads. A test pins the script's shape.
  • Android drops the setItem and the matching removeItem in clearConfig, which is now documented as teardown-only since window.GBKit is the editor's only source of configuration.
  • Both CORS rationales now rest on the token alone — loopback-only, per-session, never persisted — rather than on claims about which origins or documents can read it.

Testing

make test-web-unit
make test-ios-library-simulator
make test-android-library-unit

The getGBKit test is a tripwire rather than a tautology: verified by reinstating the storage fallback and confirming the case fails.

Manual checks, each confirming the editor recovers without a persisted copy:

  1. iOS demo: open a post, then force a WebContent process termination (Activity Monitor → quit com.apple.WebKit.WebContent for the Simulator). The editor reloads with the current post. The demo's "Trigger Editor Crash" action is not a substitute here — it throws in JS so the error boundary catches it, leaving the page and its process intact, where this step exercises the reload that replays the document-start script.
  2. Android demo: open an existing post via Browse, then rotate the device (emulator: Ctrl+F11). EditorActivity declares no configChanges, so it is recreated and the editor reloads — it should render the post's saved title and content rather than the load-error view. Unsaved edits are not preserved across recreation in the demo, whose latestContent uses remember; that is unrelated to this change.
  3. iOS: upgrade from a build before this change with a post open, then reopen a post. Safari Web Inspector's Storage tab for the editor page shows no GBKit key.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JsMCoa3jNEuvnhxicDVRBV

@github-actions github-actions Bot added the [Type] Bug An existing feature does not function as intended label Sep 1, 2026
@wpmobilebot

wpmobilebot commented Sep 1, 2026 •

Copy link
Copy Markdown

XCFramework Build

This PR's XCFramework is available for testing. Add the following to your Package.swift:

.package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/613")

Built from 8b27d20

@dcalhoun
dcalhoun force-pushed the task/remove-gbkit-global-from-local-storage branch from b7d738a to f32075c Compare September 2, 2026 16:20
@dcalhoun
dcalhoun changed the base branch from task/stabilize-rest-request-relay to trunk September 22, 2026 15:45
@dcalhoun
dcalhoun force-pushed the task/remove-gbkit-global-from-local-storage branch 3 times, most recently from 1b3b90a to 407fcbc Compare September 24, 2026 13:35
@dcalhoun
dcalhoun force-pushed the task/remove-gbkit-global-from-local-storage branch 2 times, most recently from 7cc97cf to c139012 Compare September 29, 2026 18:37
@dcalhoun
dcalhoun changed the base branch from trunk to fix/autosave-monitor-interval September 29, 2026 18:37
@dcalhoun
dcalhoun added this pull request to stack #745 September 29, 2026 18:38
Base automatically changed from fix/autosave-monitor-interval to trunk September 30, 2026 02:07
dcalhoun and others added 9 commits September 29, 2026 22:07
`getGBKit` fell back to a copy of the configuration in `localStorage`.
Boot waits for `window.GBKit` before anything reads the configuration,
and outside `?dev_mode` aborts when it never arrives, so the fallback
could only ever serve a previous session's values to a dev-mode page
with no host.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsMCoa3jNEuvnhxicDVRBV
`GBKit` carries the site credential and the local server's port and
tokens, all valid only for the load that injected them, and iOS mirrored
it into `localStorage`, which the default website data store keeps on
disk across launches. The document-start user script replays the global
on every navigation, including the reload after a WebContent process
termination, so the copy had no reader.

Remove the key as the configuration is injected so devices upgraded from
an earlier version are scrubbed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsMCoa3jNEuvnhxicDVRBV
`GBKit` carries the site credential and the local server's port and
token, all valid only for the load that injected them. The view
re-injects the global on every page start and wipes web storage before
each load, so the `localStorage` copy had no reader and nothing left to
clear on detach.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsMCoa3jNEuvnhxicDVRBV
The CORS rationale on both platforms named `localStorage` alongside
`window.GBKit` as where the editor holds the per-session bearer token.
The token now lives in the injected global only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsMCoa3jNEuvnhxicDVRBV
The comment justified `*` partly by claiming the editor loads from
file:// (Origin null) and so can't be allowlisted. That holds on iOS,
but the Android editor has loaded from the site's own origin since #181,
where echoing the origin would be perfectly possible.

Name the document, not the origin, as what scopes the token: same-origin
site pages exist on Android, and it is the per-document JS global that
keeps them from reading it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zAv5Tqtqr1bcYWVDJHGAh
…script

The trailing `"done";` dates from #14, when the configuration was injected
with `evaluateJavaScript`, which needed a serializable trailing expression.
#15 moved the script to a document-start `WKUserScript`, whose completion
value WebKit discards, and the line has been inert since.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zAv5Tqtqr1bcYWVDJHGAh
`getGBKit` now ignores any persisted copy, but nothing held that contract
in place: the suite only ever exercised it through `window.GBKit`, so
restoring the storage fallback would have left every test green.

Verified as a tripwire — reinstating the fallback fails the second case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zAv5Tqtqr1bcYWVDJHGAh
The iOS comment promised that an upgraded device "is scrubbed" without
saying this only happens on the next editor load, and left no signal for
when the migration can be dropped.

Document `clearConfig` as teardown-only: with the storage fallback gone,
`window.GBKit` is the editor's only source of configuration, so clearing
it under a live editor now leaves that editor unusable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zAv5Tqtqr1bcYWVDJHGAh
Both comments claimed no other document can read the editor's global, which overstated the guarantee and disagreed across platforms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEok5vK3cCWGfcQp8zEGWW
@dcalhoun
dcalhoun force-pushed the task/remove-gbkit-global-from-local-storage branch from c139012 to 8b27d20 Compare September 30, 2026 02:07
@dcalhoun
dcalhoun marked this pull request as ready for review September 30, 2026 17:12
@dcalhoun
dcalhoun merged commit 261ac3b into trunk Sep 30, 2026
28 checks passed
@dcalhoun
dcalhoun deleted the task/remove-gbkit-global-from-local-storage branch September 30, 2026 20:42
jkmassel added a commit that referenced this pull request Oct 2, 2026
…g stops

`revokeNativeUploadEndpoint()` cleared the endpoint from three places: the
live page, the injected user script, and the `localStorage` copy of `GBKit`
that `getGBKit()` fell back to. #613 removed that copy — `getGBKit()` reads
`window.GBKit` alone, and the document-start script now removes the key so
nothing session-scoped persists across launches.

Merged together, the revoke no longer cleared anything there. It created
the key instead: `JSON.parse(localStorage.getItem('GBKit') || '{}')` found
nothing, so it stored `{"nativeUploadPort":null,"nativeUploadToken":null}`.
No credential, and the next document start removed it again, but it is a
write to storage #613 exists to keep empty, for a reader that is gone.

Drop the block. Two copies hold the endpoint now, and the doc says so.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants