Skip to content

RTC: Allow local users to collaborate via PingHub using blog-token auth#47835

Open
mmtr wants to merge 15 commits intotrunkfrom
dotcom-16476-rtc-allows-local-editing-without-enabling-collaboration
Open

RTC: Allow local users to collaborate via PingHub using blog-token auth#47835
mmtr wants to merge 15 commits intotrunkfrom
dotcom-16476-rtc-allows-local-editing-without-enabling-collaboration

Conversation

@mmtr
Copy link
Copy Markdown
Member

@mmtr mmtr commented Mar 30, 2026

Fixes DOTCOM-16476

Proposed changes

Local users (accounts not linked to WordPress.com) were unable to use real-time collaboration because PingHub JWT tokens could only be issued to connected users. Previously, the token endpoint returned a 403 user_not_connected error, causing a "Connection lost" modal in the editor.

Instead of falling back to HTTP-polling (which has separate server-side Yjs state from WebSocket and cannot bridge changes between transports), this PR enables local users to authenticate with PingHub via the site's blog token.

The Jetpack site requests a JWT on behalf of the local user using wpcom_json_api_request_as_blog(), passing the local user's identity in the request body. The WP.com endpoint issues a JWT with a site-user:<blogId>:<localUserId> subject claim, which PingHub's auth layer recognizes and trusts (since the blog token already proves the request comes from the authenticated site, which verified edit_posts capability before requesting the token).

This means all users get the same WebSocket transport — no transport split, no modal, no conditional logic.

Jetpack-side changes (this PR)

  • class-rest-pinghub-token.php: For connected users, unchanged (wpcom_json_api_request_as_user). For unconnected users, uses wpcom_json_api_request_as_blog() with local_user_id in the request body.
  • class-rtc.php: Removed the http-polling fallback for unconnected users. All users now get pinghub as the default provider.
  • rtc.ts: Replaces default providers (instead of appending) since all users use PingHub.
  • Tests: Updated to reflect the new behavior (blog-token path returns null in test environment without a real WPCOM connection).

WP.com-side changes (separate commit)

  • jetpack-pinghub.php: Accepts blog-token auth (is_jetpack_authorized_for_site()). For blog-token requests, reads local_user_id from the body and issues a JWT with sub: "site-user:<blogId>:<localUserId>".
  • pinghub-auth.php: Recognizes site-user: sub format in JWT validation. Returns a negative sentinel ID that never collides with real WP.com user IDs.
  • pinghub-auth-rtc-helper.php: For site-user sentinels, skips is_user_member_of_blog() and trusts that the Jetpack site verified capabilities. Verifies the blog_id in the sentinel matches the room's blog_id.

Related product discussion/links

  • gCVuZ-p2

Does this pull request change what data or activity we track or use?

No.

Testing instructions

  1. Apply 211297-ghe-Automattic/wpcom to your sandbox
  2. Apply these Jetpack changes to a WoW dev site.
  3. Add the wpcom-features-edge sticker to your site.
  4. Make sure RTC is enabled in Options > Writing.
  5. Log in as a local user (an account not linked to WordPress.com).
  6. Open a post for editing.
  7. Verify the editor loads normally with no error modals and the WebSocket connection succeeds.
  8. Have a connected user open the same post simultaneously.
  9. Verify both users can see each other's changes in real time via WebSocket (check the Network tab to confirm no HTTP-polling requests to wp-sync/v1/updates).
  10. Verify cursor presence and awareness works for both users.

Local users (no Jetpack user token) hitting the pinghub-token endpoint
were getting a generic 500. Now:

- PHP: detect missing user token early and return 403 user_not_connected
  instead of making a doomed WPCOM API call.
- Bridge: on user_not_connected, fire wpcom-rtc-user-not-connected on
  window and block all further connect() attempts to avoid an infinite
  reconnection loop.
- gutenberg-rtc-notices: new RtcUserNotConnectedModal listens for that
  event and shows a modal with a "Link account with WordPress.com" CTA
  (using the authorization URL passed from PHP via wpcomRtcNotices config).
- Tests: new REST_Pinghub_Token_Test covers the 403 path and the
  WP_Error passthrough in create_item().

Fixes https://linear.app/a8c/issue/DOTCOM-16476

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mmtr mmtr self-assigned this Mar 30, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 30, 2026

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (WordPress.com Site Helper), and enable the dotcom-16476-rtc-allows-local-editing-without-enabling-collaboration branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack-mu-wpcom-plugin dotcom-16476-rtc-allows-local-editing-without-enabling-collaboration

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions
Copy link
Copy Markdown
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!

Allowing users to dismiss the modal and edit locally risks data loss
when WS-based RTC is active. Remove the X button and click-outside
behaviour, and replace the dismiss link with "Back to posts".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jp-launch-control
Copy link
Copy Markdown

jp-launch-control bot commented Mar 30, 2026

Code Coverage Summary

Coverage changed in 2 files.

File Coverage Δ% Δ Uncovered
projects/packages/rtc/src/class-rtc.php 132/159 (83.02%) 0.11% 0 💚
projects/packages/rtc/src/rest-api/class-rest-pinghub-token.php 32/67 (47.76%) 47.76% -19 💚

Full summary · PHP report

mmtr and others added 5 commits March 30, 2026 13:17
…cated context

get_authorization_url() crashes when no user is logged in because it tries to
read user properties from the integer 0 returned by get_current_user_id(). Add
a user_id guard so the URL is only generated for actually-logged-in users who
have not yet linked their account.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phan doesn't evaluate function_exists() guards, so it sees the stub
definition in tests/php/bootstrap.php as a redefinition of the real
function (which is included via parse_file_list from jetpack-mu-wpcom).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…enerating connect URL

get_authorization_url() internally queries for site creation date in a way
that crashes in WorDBless test environments. Semantically, a user-connect URL
only makes sense when the site itself is Jetpack-connected (has blog token),
so adding is_connected() as a prerequisite is both correct and test-safe.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nnected errors

When the PingHub JWT endpoint returns user_not_connected, both our new
RtcUserNotConnectedModal and Gutenberg's built-in SyncConnectionErrorModal
were appearing simultaneously. Add a module-level event listener that sets
userNotConnectedReceived, and register an editor.SyncConnectionErrorModal
filter (always-on, unlike the limit-notices filter) that returns null when
that flag is set, letting our modal handle the prompt exclusively.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ng a modal

Instead of loading the PingHub provider and showing a modal when the JWT
fetch fails, exclude pinghub from the default providers list when the
current user lacks a WP.com connection. This lets Gutenberg's built-in
HTTP-polling handle RTC transparently — local users can edit alongside
connected (WS) users with slightly higher latency but no interruption.

Reverts all client-side user_not_connected handling (bridge flag, window
event, modal, SyncConnectionErrorModal filter, connectUserUrl config)
since the PingHub provider is never instantiated for these users.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions github-actions bot added the Docs label Mar 30, 2026
@mmtr mmtr changed the title RTC: prompt unconnected users to link their WP.com account RTC: fall back to HTTP-polling for unconnected users Mar 30, 2026
mmtr and others added 3 commits March 30, 2026 15:52
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mmtr mmtr changed the title RTC: fall back to HTTP-polling for unconnected users RTC: Fall back to HTTP-polling for unconnected users Mar 30, 2026
@mmtr mmtr changed the title RTC: Fall back to HTTP-polling for unconnected users RTC: Fall back to HTTP-polling for local users Mar 30, 2026
@mmtr mmtr removed the Docs label Mar 30, 2026
mmtr and others added 4 commits March 30, 2026 16:22
…are state

The sync.providers filter was replacing existing providers with PingHub,
which killed Gutenberg's built-in HTTP-polling. Change to appending so
connected users run both transports. The shared client-side Yjs doc
bridges them: polling updates flow to PingHub and vice versa.

This lets unconnected users (http-polling only) collaborate with
connected users (http-polling + WS) through the common polling channel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When getProviders() returns empty (no jetpackRTC config), return [] to
disable RTC entirely instead of passing through existing providers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of falling back to HTTP-polling for unconnected users (which has
separate server-side Yjs state from WebSocket), use the site's blog token
to request PingHub JWTs on behalf of local users. The WPCOM endpoint
embeds a site-user identity in the JWT so PingHub can authenticate them.

This means all users get the same WebSocket transport — no transport
split, no modal, no conditional logic.

Changes:
- class-rest-pinghub-token.php: use wpcom_json_api_request_as_blog()
  for unconnected users, passing local_user_id in the request body
- class-rtc.php: remove http-polling fallback for unconnected users;
  all users now get pinghub as the default provider
- rtc.ts: replace default providers instead of appending, since all
  users use PingHub
- Tests updated to reflect new behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mmtr mmtr changed the title RTC: Fall back to HTTP-polling for local users RTC: Allow local users to collaborate via PingHub using blog-token auth Apr 10, 2026
@mmtr mmtr added the [Status] Needs Review This PR is ready for review. label Apr 10, 2026
…ows-local-editing-without-enabling-collaboration
@mmtr mmtr marked this pull request as ready for review April 10, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant