Skip to content

feat: AJAX uses token authentication - #181

Merged
dcalhoun merged 31 commits into
trunkfrom
feat/authorize-ajax-with-application-passwords
Mar 31, 2026
Merged

dcalhoun merged 31 commits into
trunkfrom
feat/authorize-ajax-with-application-passwords

Conversation

@dcalhoun

@dcalhoun dcalhoun commented Sep 19, 2025 •

Copy link
Copy Markdown
Member

Related:

What?

Authenticate AJAX requests with application passwords sent via an authorization header. Vendor wp-util.js and configure the WordPress AJAX and media globals.

Why?

The GutenbergKit editor does not have authorization cookies, so we must rely upon a different authorization mechanism. Additionally, GutenbergKit excludes core WordPress assets from the editor assets endpoint, so wp-util.js (which provides wp.ajax and wp.template) must be vendored and loaded directly.

Ref CMM-713. Close CMM-768.

How?

  • Set the Authorization header via jQuery.ajaxPrefilter and by overloading the window.wp.ajax utilities. This general-purpose AJAX auth is always initialized.
  • Vendor and load wp-util.js after jQuery and lodash are on window, since its IIFE captures jQuery via closure at execution time.
  • Filter the lodash-js-after inline script from editor assets. WordPress's _.noConflict() call wipes window._ because GutenbergKit doesn't load Underscore.js.
  • Alias wp.media.ajax and wp.media.post to the authenticated wp.ajax methods, since WordPress core's media-models.js is not loaded.
  • Conditionally initialize the VideoPress AJAX bridge when videopress/video is not in allowed_block_types, so core/video blocks extended to rely upon VideoPress upload services continue to work.
  • Derive the Android WebViewAssetLoader domain from siteURL so that the editor document shares the site's origin, making REST API and admin-ajax.php requests same-origin and eliminating CORS restrictions without server-side headers.
  • Fix the iOS demo app to use the home URL from the REST API response instead of url, which returns http:// for WordPress.com sites.

Testing Instructions

1. Verify AJAX requests use token authentication

  1. make build
  2. Run the demo app in Xcode/Android Studio (use production build, not dev server).
  3. Open the editor for a site with an active Jetpack plugin connection (n.b., the demo app now supports WP.com sites).
  4. Open the Safari/Chrome developer tools on your development machine and inspect the network requests for the iOS (Developer Tools menu)/Android (chrome://inspect) device.
  5. Manually perform an AJAX request in the dev tools console via:
    wp.ajax.send('videopress-get-upload-jwt')
      .done( ( response ) => console.log( 'Success:', response ) )
      .fail( ( error ) => console.error( 'Error:', error ) );
  6. Verify the requests include the Authorization header with the token.
  7. Verify a 200 response with an upload token in the body.

2. Verify VideoPress bridge continues functioning

  1. make build
  2. Run the demo app in Xcode/Android Studio (use production build, not dev server).
  3. Open the editor for a site with an active Jetpack plugin connection (n.b., the demo app now supports WP.com sites).
  4. Open the Safari/Chrome developer tools on your development machine and inspect the network requests for the iOS (Developer Tools menu)/Android (chrome://inspect) device.
  5. Insert a Video block.
  6. Attach media to the block via upload.
  7. Verify the upload succeeds.
  8. Verify the request relies upon the REST API, not admin-ajax.
  9. Verify a 200 response with an upload token in the body.

Accessibility Testing Instructions

N/A, no navigation changes.

Screenshots or screencast

N/A, no visual changes.

@dcalhoun dcalhoun added the [Type] Enhancement A suggestion for improvement. label Sep 19, 2025
@dcalhoun
dcalhoun force-pushed the feat/authorize-ajax-with-application-passwords branch from a051ea4 to 682e0df Compare September 19, 2025 20:00
@dcalhoun dcalhoun changed the title feat: Authorize AJAX with application passwords feat: Authorize AJAX with token authentication Sep 24, 2025
@dcalhoun dcalhoun changed the title feat: Authorize AJAX with token authentication feat: AJAX uses token authentication Sep 25, 2025
@dcalhoun
dcalhoun force-pushed the feat/authorize-ajax-with-application-passwords branch from 686ebf7 to bbaf9c4 Compare January 13, 2026 19:35
@dcalhoun

Copy link
Copy Markdown
Member Author

@claude

@claude

This comment was marked as outdated.

Comment thread src/utils/ajax.js Outdated
Comment thread src/utils/ajax.js Outdated
Comment thread src/utils/ajax.test.js
@dcalhoun
dcalhoun force-pushed the feat/authorize-ajax-with-application-passwords branch from 394bce2 to ea73b3c Compare February 12, 2026 17:50
@dcalhoun
dcalhoun force-pushed the feat/authorize-ajax-with-application-passwords branch 2 times, most recently from e2ead31 to 80aca45 Compare March 25, 2026 19:58
dcalhoun and others added 11 commits March 26, 2026 13:06
Include authorization header in AJAX requets, as we do not have cookies
to send in the mobile app environment.
If we configure AJAX before loading the library, the configuration is
overridden.
This global is often used by WordPress Admin page scripts.
Useful when needing to allow CORS for specific domains.
Address PR feedback about potential race condition. The code now checks
if `window.wp.ajax.send` and `window.wp.ajax.post` are functions before
wrapping them. This prevents TypeError when calling the wrapped function
if the original method was undefined during configuration.

Update tests to verify that missing methods remain undefined rather than
being wrapped with an undefined reference.

Co-authored-by: Claude <noreply@anthropic.com>
When `videopress/video` is not in `allowed_block_types`, initialize the
VideoPress AJAX bridge to handle `core/video` blocks extended to rely
upon VideoPress upload services. AJAX auth is always initialized.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
dcalhoun and others added 6 commits March 26, 2026 13:07
Move the iOS and Android code examples out of the Android-specific
requirement so they are not visually nested under that bullet point.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
configureAjax() now initializes wp.ajax, wp.ajax.settings, and the AJAX
URL before the VideoPress bridge runs, making the duplicate setup in
initializeVideoPressAjaxBridge() unnecessary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Wrap `new URL(siteURL)` in try/catch so a malformed siteURL logs a
  warning instead of throwing.
- Guard `configureMediaAjax` against missing `wp.ajax.send`/`post`
  (e.g., if wp-util.js failed to load).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Throw IllegalArgumentException if the value contains a scheme, path, or
is blank, so callers get a clear error instead of a malformed asset URL
at runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Record the upstream commit hash and rationale for vendoring so future
maintainers know where the file came from and when to update it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Account.WpCom.username stores just the hostname (e.g.,
"dcpaid.wordpress.com") since it is extracted via URI.host during OAuth.
ConfigurationItem was using this bare hostname as siteUrl, producing
invalid AJAX endpoints. Prepend "https://" to match the self-hosted
flow, which receives a full URL from the callback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dcalhoun
dcalhoun force-pushed the feat/authorize-ajax-with-application-passwords branch from c09e90a to 282c499 Compare March 26, 2026 17:08
dcalhoun and others added 2 commits March 26, 2026 13:11
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dcalhoun
dcalhoun marked this pull request as ready for review March 26, 2026 17:24
Comment thread android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt Outdated
return EditorConfigurationBuilder(
postType: selectedPostTypeDetails,
siteURL: URL(string: apiRoot.siteUrlString())!,
siteURL: URL(string: apiRoot.homeUrlString())!,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ensures WP.com sites use TLS for the siteURL configuration in the demo app.

Comment on lines +147 to +151
if ( ! allowedBlockTypes?.includes( 'videopress/video' ) ) {
// The VideoPress block isn't available, so initialize the bridge to handle
// any `core/video` blocks extended to rely upon VideoPress upload services.
initializeVideoPressAjaxBridge();
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Retained to continue support for WP.com core/video blocks that are extended to rely upon VideoPress upload services. This can be removed in the future once videopress/video support is enabled and deemed stable.

accountId = account.id,
name = account.username,
siteUrl = account.username,
siteUrl = "https://${account.username}",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The account.username is protocol-less, which causes errors as a GBK.siteURL value. It is safe to assume TLS for WP.com sites.

Comment on lines -30 to -38
// Initialize wp.ajax if not already present
window.wp.ajax = window.wp.ajax || {};
window.wp.ajax.settings = window.wp.ajax.settings || {};

// Set up AJAX settings with site URL
const { siteURL } = getGBKit();
if ( siteURL ) {
window.wp.ajax.settings.url = `${ siteURL }/wp-admin/admin-ajax.php`;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Now redundant of the AJAX-specific configuration that runs before this bridge.

@dcalhoun
dcalhoun requested a review from kean March 26, 2026 17:24
Derive the WebViewAssetLoader domain from the configured siteURL
instead of defaulting to the synthetic appassets.androidplatform.net
domain. This makes REST API and admin-ajax.php requests same-origin,
eliminating CORS restrictions without requiring server-side headers.

- Restrict shouldOverrideUrlLoading to /assets/ paths on the asset
  domain so arbitrary site pages don't load inside the WebView.
- Reorder shouldInterceptRequest to check the cache interceptor
  before the asset loader, preventing cached JS/CSS from being
  short-circuited when both share the site domain.
- Remove the now-unnecessary assetLoaderDomain configuration option
  from EditorConfiguration.
- Update AJAX documentation to reflect the simplified setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dcalhoun
dcalhoun requested a review from nbradbury March 28, 2026 01:06
@nbradbury

Copy link
Copy Markdown
Contributor

@dcalhoun This is looking good, but I'm not sure about "Step 8: Verify the request relies upon the REST API, not admin-ajax." What exactly should I be looking for?

@dcalhoun

Copy link
Copy Markdown
Member Author

@dcalhoun This is looking good, but I'm not sure about "Step 8: Verify the request relies upon the REST API, not admin-ajax." What exactly should I be looking for?

@nbradbury it references using Chrome to inspect the Network activity of the mobile device/emulator. When inspecting, the Network tab should show requests. Uploading a Video should result in fetch() requests to the REST API, not jQuery.ajax() requests to the admin-ajax.php path. This occurs because the VideoPress bridge remains in place, which converts certain AJAX actions to REST API requests.

Below is a screenshot of example admin-ajax.php (autosave) and REST API (Jetpack AI Assistant) requests from a Chrome browser session.

Screenshot 2026-03-30 at 17 13 25

@nbradbury

Copy link
Copy Markdown
Contributor

@dcalhoun Thanks for the clarification, looks all good to me! @kean Feel free to approve this after you test the iOS side.

@kean kean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I tested the changes and verified that the "Authorization" header was present and that the existing upload media functionality is working. The code looks good.

@dcalhoun
dcalhoun merged commit d72cc75 into trunk Mar 31, 2026
20 checks passed
@dcalhoun
dcalhoun deleted the feat/authorize-ajax-with-application-passwords branch March 31, 2026 14:36
dcalhoun added a commit that referenced this pull request Sep 24, 2026
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
dcalhoun added a commit that referenced this pull request Sep 25, 2026
`onPageStarted` received the loaded URL and discarded it, so any page
reaching the main frame was handed `window.GBKit` — the site credential
and the upload server's port and token. `shouldOverrideUrlLoading` admits
several site URLs into that frame, and since #181 the editor shares an
origin with the site, so those pages are served by the site's own theme
and plugins.

Check the destination before advertising the globals or starting the
upload server, matching the dev server by authority so a local site on
another port of the same host is not mistaken for the editor. Readiness
still resets for any page, since navigating away from the editor leaves
it unusable either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zAv5Tqtqr1bcYWVDJHGAh
dcalhoun added a commit that referenced this pull request Sep 25, 2026
`onPageStarted` received the loaded URL and discarded it, so any page
reaching the main frame was handed `window.GBKit` — the site credential
and the upload server's port and token. `shouldOverrideUrlLoading` admits
several site URLs into that frame, and since #181 the editor shares an
origin with the site, so those pages are served by the site's own theme
and plugins.

Check the destination before advertising the globals or starting the
upload server, matching the dev server by authority so a local site on
another port of the same host is not mistaken for the editor. Readiness
still resets for any page, since navigating away from the editor leaves
it unusable either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zAv5Tqtqr1bcYWVDJHGAh
dcalhoun added a commit that referenced this pull request Sep 28, 2026
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
dcalhoun added a commit that referenced this pull request Sep 29, 2026
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
dcalhoun added a commit that referenced this pull request Sep 30, 2026
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
dcalhoun added a commit that referenced this pull request Sep 30, 2026
… editor frame (#730)

* fix(android): inject the editor globals into the editor document only

`onPageStarted` received the loaded URL and discarded it, so any page
reaching the main frame was handed `window.GBKit` — the site credential
and the upload server's port and token. `shouldOverrideUrlLoading` admits
several site URLs into that frame, and since #181 the editor shares an
origin with the site, so those pages are served by the site's own theme
and plugins.

Check the destination before advertising the globals or starting the
upload server, matching the dev server by authority so a local site on
another port of the same host is not mistaken for the editor. Readiness
still resets for any page, since navigating away from the editor leaves
it unusable either way.

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

* fix(android): match the REST API by its configured root, not by substring

The navigation policy admitted any site URL whose path contained
`/wp-json/` or whose query contained `rest_route=`. Both are satisfied by
ordinary pages — `/blog/wp-json/a-post`, or any URL carrying
`?utm_campaign=rest_route=x` — which WordPress serves with the site's
theme and plugins, inside the editor's own frame.

Compare against the configured `siteApiRoot` instead: a path under its
path root, or `rest_route` as an actual query parameter. Reading the root
also settles the cases the characters cannot, so the same path is the API
on a subdirectory install and a page on a root install.

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

* fix(android): match a rest_route API root by its query alone

Without pretty permalinks the API root is `/index.php?rest_route=/`, and `/index.php` also serves ordinary pages, so matching its path admitted them into the editor frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs

* fix(android): match the API root path by whole path segments

A root without a trailing slash, such as `/wp-json`, otherwise prefixes page slugs like `/wp-json-tutorial/`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs

* fix(android): ignore an empty rest_route when matching the REST API

WordPress skips an empty route, including `0`, and renders the requested page with the site's theme instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs

* test(android): assert nothing reaches a non-editor page on start

Checking only the last evaluated script would pass if another script ran after an injection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs

* fix(android): match editor assets by the scheme the asset loader serves

The asset loader serves one scheme, so the same path over the other reaches the site over the network, yet it was admitted and handed the editor globals. One helper now backs both checks so they can't drift apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs

* docs(android): note that only the editor document receives the globals

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs

* fix(android): match the editor document by its exact asset path

The asset loader also serves the host app's other bundled pages, some of which load third-party scripts, and those received the editor globals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs

* fix(android): let a rest_route parameter decide over the API root path

WordPress lets the parameter override the route a `/wp-json/` path sets, so `/wp-json/?rest_route=` serves the themed front page, yet it passed the path check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs

* fix(android): open REST API navigations in the browser

The editor reaches the REST API by fetch, which never passes through `shouldOverrideUrlLoading`, so the allowlist only admitted navigations. Those let site pages whose URLs WordPress reads differently from Android, and http API URLs on https sites, replace the editor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs

* test(android): explain how a site page still reaches the editor frame

Since the REST allowlist was removed, the navigation policy admits no site pages, but loads it never sees still can.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs

* test(android): assert a non-editor page leaves the upload server down

Nothing pinned the editor-only check above the server start, so reordering them would have passed every test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs

* refactor(android): set the asset scheme before installing the WebView client

The client reads it, so assigning it alongside the asset authority avoids relying on no navigation running in between.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs

* docs(android): correct why isAssetUrl matches one scheme

A local http site's asset loader serves https too, so the other scheme doesn't always reach the site over the network.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs

* fix(android): compare origin hosts case-insensitively

Chromium lowercases the host it reports, so a dev server URL written with capitals, like a Mac's mDNS name, never matched and the editor received no globals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs

* refactor(android): recognize the editor document by the URL it loaded

Re-deriving the editor URL duplicated loadEditor's choice and let any page on the dev server's host receive the globals. Comparing against the recorded URL keeps the two in step and requires the exact document.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs

* test(android): cover injection after a reload and on a local http site

Neither path had a positive case, so dropping the globals from either would have passed every test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs

* test(android): build one view per navigation test

Each URL built its own editor view, loading the editor eight times where two views suffice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbnGRWvgz7WNn6cGFRKxs

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
dcalhoun added a commit that referenced this pull request Sep 30, 2026
* refactor: read the editor configuration from the injected global only

`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

* fix(ios): stop persisting the editor configuration

`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

* fix(android): stop persisting the editor configuration

`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

* docs: describe where the local server's token lives

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

* docs(android): correct the CORS rationale's origin premise

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

* refactor(ios): drop the dead completion value from the configuration 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

* test: cover getGBKit's injected-global-only contract

`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

* docs: say when the configuration scrub runs

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

* docs: rest the CORS rationale on the token alone

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants