Skip to content

Check the Bluesky connection when the editor opens - #264

Draft
pfefferle wants to merge 3 commits into
trunkfrom
add/verify-connection-before-publish
Draft

pfefferle wants to merge 3 commits into
trunkfrom
add/verify-connection-before-publish

Conversation

@pfefferle

@pfefferle pfefferle commented Sep 3, 2026

Copy link
Copy Markdown
Member

Proposed changes:

The editor decided whether a post would reach Bluesky by reading three stored fields and never leaving the site. That is the right answer almost everywhere, but it cannot answer the one question the panel is actually asked.

A refresh token the user revoked from their Bluesky account is byte-for-byte identical on disk to a working one. Revocation happens at the auth server and writes nothing locally, so is_connected() has no way to see it. The panel would promise a share that was already impossible, and the author only found out afterwards.

So the editor asks, when it opens.

  • API::get_session() wraps com.atproto.server.getSession, the cheapest authenticated read the PDS offers.
  • Atmosphere\verify_connection() runs it through the ordinary request path, which matters: a dead session travels the existing 401 → refresh → mark_needs_reauth() ladder on its own. The probe records nothing itself. Its whole job is to make local state honest before the decision reads it, so every surface already built on is_connected() and needs_reauth() reports the truth with the copy it already has. No new strings, nothing new to translate.
  • Block_Editor::script_data() calls it before resolving the share status the document panel renders, so an author with a revoked session gets the reconnect prompt on the first paint — nothing written, nothing invested. The pre-publish panel calls it too; behind the shared cache that costs nothing and still covers a session that dies after the editor loaded.

Design notes worth reviewing

It fails open. A timeout, a 5xx, or a rate limit says the check did not complete, not that the session is dead. Those leave stored state untouched and the panel still says the post will publish. Blocking an author because our own probe could not get through would be a worse bug than the staleness this fixes.

It reads the outcome instead of classifying the error. On WP_Error, verify_connection() re-reads is_connected() rather than inspecting the error code. When the request path gives up on the credentials it flags the row itself, and that flag is what every other caller sees. This keeps the helper correct no matter which layer decided, and picks up any future path reaching the same conclusion by another route.

Timeout. Bounded at 5 seconds rather than the shared 30. This runs while the editor renders, which is the difference between a slow load and a frozen one, and since the probe fails open, giving up early costs a stale verdict for one cache window while waiting costs the author their editor.

Where it is and is not gated. In the pre-publish decision it runs last, after every local gate — those all return without consulting the connection, so a private or opted-out post would otherwise spend a round-trip on an answer nothing reads. On editor load it is deliberately not gated on whether sharing is switched on: the connection is shared infrastructure and the reconnect prompt is connection level, not cross-posting level. The enqueue already limits it to editors for supported post types.

Caching. Successes only, 15 minutes, filterable via atmosphere_session_verify_ttl. A permanent rejection needs no cache entry: it lives on the connection row, where is_connected() reads it without touching the network. Cleared on both disconnect() and handle_callback() — neither connect surface goes through disconnect(), so without the second one a reconnect (especially an account switch) would inherit the previous session's verdict.

Known gap, deliberately not closed

This verifies that the credentials still authenticate, not that the account may post. A deactivated, suspended, or taken-down account is rejected as a 400/403 rather than a 401, so it never enters the refresh ladder and nothing flags the connection — the probe reports healthy. getSession carries active and status fields that would settle it, but acting on them means new copy on several surfaces. test_taken_down_account_is_a_known_gap pins the current behaviour so closing it stays a deliberate change with a failing test behind it.

Other information:

  • Have you written new tests for your changes, if applicable?

tests/phpunit/tests/class-test-block-editor.php covers the editor-load path: a revoked session surfacing as needs_reconnect on first paint, an unreachable PDS not being read as a disconnection, and the short timeout actually reaching the transport.

tests/phpunit/tests/class-test-verify-connection.php (10 tests) covers the disconnected short-circuit, cache hit / $force bypass / zero-TTL, the revoked-session path, inconclusive 5xx and 429 handling, cache clearing on both disconnect and reconnect, and the documented takedown gap. Two more in class-test-pre-publish-controller.php cover the endpoint end-to-end.

One fixture change worth a look: that controller's set_up() held 'access_token' => 'test-token', a placeholder nothing can decrypt. Now that the endpoint exercises the credentials, that is a broken connection rather than a connected site, and two tests failed. It holds real encrypted credentials instead. Every test covering the disconnected / needs_reauth states still exits verify_connection() at the is_connected() early return before any network call, so none of their assertions were weakened.

Testing instructions:

Automated

npm run env-test -- --filter='Test_Verify_Connection|Test_Pre_Publish_Controller|Test_Block_Editor'

Full suite and lint clean: 1381 tests, PHPCS 43/43.

Two of these were verified red before their fix — test_revoked_session_fails_verification_and_flags_the_connection and test_reconnect_clears_the_cached_verdict.

Manual

  1. Connect a Bluesky account under Settings → ATmosphere.
  2. Revoke the plugin's access from the Bluesky side, leaving the stored credentials in place.
  3. Open any post in the editor.

Before: the ATmosphere panel says the post will be shared. You write it, click Publish, confirm, and the share fails afterwards.

After: the panel carries the reconnect prompt as soon as the editor loads, before anything is written.

To check the fail-open path, block outbound requests to the PDS instead of revoking. The panel should still say the post will publish.

To check the reconnect path, reconnect without disconnecting first and confirm the panel re-probes rather than reusing the previous verdict.

Changelog entry

Committed on the branch as .github/changelog/add-verify-connection-before-publish.

  • Automatically create a changelog entry from the details below.

`is_connected()` reads three stored fields and never leaves the site,
which is the right answer almost everywhere. It cannot answer the one
question the pre-publish panel is actually asked. A refresh token the
user revoked from their Bluesky account is byte-for-byte identical on
disk to a working one — revocation happens at the auth server and writes
nothing locally — so the panel would promise a share that was already
impossible, and the author only found out afterwards.

`verify_connection()` asks. `com.atproto.server.getSession` is the
cheapest authenticated read the PDS offers, and running it through the
ordinary request path means a dead session travels the existing 401 →
refresh → `mark_needs_reauth()` route on its own. The probe records
nothing itself; its whole job is to make local state honest before the
decision reads it, so every surface already built on `is_connected()`
reports the truth with the copy it already has.

Failure is not evidence. A timeout, a 5xx, or a rate limit says the
check did not complete, not that the session is dead, so those leave the
stored state untouched — blocking an author over our own inability to
ask would be worse than the staleness this fixes. The verdict is cached
for fifteen minutes, and only ever on success: a permanent rejection
lives on the connection row, where it needs no cache.

The controller's fixture held a placeholder access token that nothing
could decrypt. Now that the endpoint exercises the credentials, that is
a broken connection rather than a connected site, so it holds real
encrypted ones.
Three follow-ups from review.

The cached verdict survived a reconnect. Only `disconnect()` cleared it,
but neither connect surface goes through `disconnect()` — the settings
field and the Connectors card both authorize straight over a live
connection. Switching accounts therefore inherited the previous
session's clean bill of health for the rest of the TTL, which is exactly
the staleness the probe exists to remove.

The probe also ran on answers that could never depend on it. Every gate
above the connection check returns without consulting it, so a site with
sharing switched off, or a private, password-protected, or opted-out
post, was spending a live PDS round-trip on a result nothing reads. It
now runs last, and the site status is re-resolved afterwards so a
session it just flagged reports the flagged reason rather than the one
read before the probe.

Documents the gap this does not close: deactivated, suspended, and
taken-down accounts come back as 400/403 rather than 401, so they never
enter the refresh ladder and nothing flags the connection. Acting on
`getSession`'s `active`/`status` fields needs new copy on every surface,
so a test pins the current behaviour instead, and closing the gap stays
a deliberate change with a failing test behind it.
Copilot AI lite review requested due to automatic review settings September 3, 2026 15:29
@pfefferle pfefferle self-assigned this Sep 3, 2026
@pfefferle
pfefferle requested a review from a team September 3, 2026 15:29
@github-actions github-actions Bot added [Feature] API PDS API client [Feature] OAuth OAuth flow and authentication [Tests] Includes Tests PR includes test changes labels Sep 3, 2026

Copilot AI 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.

🟢 Approval recommended

The changes are cohesive, use existing error/refresh plumbing correctly, and are backed by targeted unit and integration tests covering the key failure modes and cache invalidation paths.

Pull request overview

This PR makes the pre-publish panel’s “will this share to Bluesky?” answer reflect the actual session state by performing a lightweight authenticated probe (com.atproto.server.getSession) before the panel reads local connection state. It uses the existing auth/refresh/error-handling pipeline so that revoked sessions are surfaced via the already-established needs_reauth() / is_connected() semantics, with success-only caching to avoid excessive round-trips.

Changes:

  • Add API::get_session() to call com.atproto.server.getSession through the standard authenticated request path.
  • Add Atmosphere\verify_connection() (success-only cached) and use it in the pre-publish decision flow as the final gate.
  • Clear the verification cache on both disconnect and reconnect, and add PHPUnit coverage for the new probe behavior and REST endpoint integration.
File summaries
File Description
includes/class-api.php Adds get_session() helper to hit com.atproto.server.getSession via existing request/refresh machinery.
includes/functions.php Introduces verify_connection() plus constants for transient key and TTL, implementing success-only caching and fail-open behavior on inconclusive errors.
includes/rest/admin/class-pre-publish-controller.php Moves the “connected?” decision to be based on a live verification probe (as the last gate) and re-resolves share status after probing.
includes/oauth/class-client.php Clears the session-verification transient on OAuth callback and on disconnect to prevent inheriting a prior account’s cached verdict.
tests/phpunit/tests/class-test-verify-connection.php Adds a focused test suite covering cache behavior, revoked-session path, inconclusive failures, and cache clearing on reconnect/disconnect.
tests/phpunit/tests/rest/admin/class-test-pre-publish-controller.php Updates fixtures to use decryptable credentials and adds end-to-end tests for revoked-session detection and fail-open unreachable-PDS behavior.
.github/changelog/add-verify-connection-before-publish Adds an end-user changelog entry describing the improved pre-publish connection check.
Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Verifying at the pre-publish panel was too late to be much use: the
author has already written the post and clicked Publish by then. The
point of catching a dead connection is to catch it before any of that
work happens, so the probe now runs in `Block_Editor::script_data()`,
before the share status the banner renders is resolved. An author with a
revoked session sees the reconnect prompt on the first paint of the
editor, with nothing invested yet.

The pre-publish call stays. It costs nothing behind the shared cache and
still covers a session that dies mid-session, after the editor loaded.

Not gated on whether sharing is switched on: the connection is shared
infrastructure and the reconnect prompt is connection level, not
cross-posting level. The enqueue already limits this to editors for
supported post types.

Bounds the probe with a 5-second timeout rather than the shared
30-second one. This now runs while the editor renders, which is the
difference between a slow load and a frozen one, and the probe fails
open — so giving up early costs a stale verdict for one cache window
while waiting costs the author their editor.
@pfefferle pfefferle changed the title Verify the Bluesky connection before the pre-publish panel answers Check the Bluesky connection when the editor opens Sep 3, 2026
@pfefferle
pfefferle marked this pull request as draft September 3, 2026 19:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] API PDS API client [Feature] OAuth OAuth flow and authentication [Tests] Includes Tests PR includes test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants