Skip to content

feat: authenticate yoast-ai requests with MyYoast OAuth tokens#23265

Open
thijsoo wants to merge 9 commits into
trunkfrom
1207-authenticate-yoast-ai-requests-with-myyoast-oauth-tokens
Open

feat: authenticate yoast-ai requests with MyYoast OAuth tokens#23265
thijsoo wants to merge 9 commits into
trunkfrom
1207-authenticate-yoast-ai-requests-with-myyoast-oauth-tokens

Conversation

@thijsoo
Copy link
Copy Markdown
Contributor

@thijsoo thijsoo commented May 13, 2026

Context

  • The plugin's outbound yoast-ai calls currently authenticate via the legacy Token_Manager flow (callback-delivered access_jwt stored in user meta + Authorization: Bearer …). That handshake fails on unreachable / firewalled / local / Cloudflare-protected sites, which is the main reason AI features break in those environments.
  • PR Add MyYoast OAuth client #23130 introduced MyYoast_Client (src/myyoast-client/), a full OAuth/OIDC client with DPoP support. yoast-ai is being updated in Yoast/yoast-ai#337 to accept DPoP-bound MyYoast access tokens additively, alongside the legacy path.
  • This PR wires the plugin side: every outbound yoast-ai request picks one of two auth strategies via a factory. The legacy path stays fully functional and is the runtime fallback whenever the OAuth path is unavailable or fails.

Summary

This PR can be summarized in the following changelog entry:

  • Adds an authentication strategy factory for yoast-ai requests so the plugin can use MyYoast-issued OAuth tokens on sites where the legacy callback flow fails.

Relevant technical choices:

  • Site-wide OAuth, per-user identity. The OAuth connection is site-level: once any admin completes the MyYoast authorization-code flow, every WP user on the site uses the OAuth path. The shared client_credentials token is reused across users; the current user's WP id is self-reported in the request body for endpoints that need per-user identity. Required because MyYoast only embeds the site_url claim in client_credentials tokens after the client has at least one verified redirect URI.

Test instructions

Test instructions for the acceptance test before the PR gets merged

This PR can be acceptance tested by following these steps:

Do these tests on a site that is reachable from the outside.
Make sure you have the newest test helper installed to be able to choose which my yoast staging to use.
Also make sure you enable DB or file logging in the test helper
** Feature flag off (default behaviour, should be unchanged):**

  1. Make sure YOAST_SEO_MYYOAST_CONNECTION is NOT defined in wp-config.php (or is false).
  2. Open a post in the block editor.
  3. In the Yoast sidebar, trigger an AI suggestion (e.g. "Generate titles" for SEO title).
  4. Expected: Suggestions come back exactly as they do today on trunk. No regressions in AI features.

Feature flag on but site not yet connected to MyYoast:

  1. Add define( 'YOAST_SEO_MYYOAST_CONNECTION', true ); to wp-config.php.
  2. Trigger an AI suggestion.
  3. Expected: — features work identically because the factory falls back to the legacy Token strategy when the site isn't connected.

Path C — Feature flag on AND site connected (requires yoast-ai#337 deployed to staging):

  1. With the flag on, complete the MyYoast OAuth connect flow.
  2. Go to /wp-admin/tools.php?page=yoast-test-helper.
  3. In Domain Dropdown select staging-5 as your MyYoast testing domain.
  4. Log in on this staging.
  5. Create a Personal access token and copy this token.
  6. Paste the token in MyYoast PAT for this environment and save.
  7. Click Fetch credentials and make sure it now says Stored credentials: yes
  8. In the WP cli run wp yoast auth register to register the site you are using with the staging my yoast (at a later stage this will be replaced by some UI).
  9. Trigger an AI suggestion as in Path A.
  10. Expected: Suggestions come back successfully. On the server side, the outbound call carries Authorization: DPoP … plus a DPoP proof header.
  11. Without disconnecting, deregister the MyYoast client with wp yoast auth deregister.
  12. Trigger another AI suggestion.
  13. Expected: Features still work — falls back to the legacy path because is_site_connected() returns false after deregister.
  14. Validate that the fallback is happening in the logs in the test helper.

Path D — Consent revocation still works:

  1. With the feature flag off (Path A baseline), simulate consent revocation by having the AI service return a 403 (or test through the actual revocation flow if available).
  2. Expected: User's consent is cleared; AI features prompt for consent again.

Relevant test scenarios

  • Changes should be tested with the browser console open
  • Changes should be tested on different posts/pages/taxonomies/custom post types/custom taxonomies
  • Changes should be tested on different editors (Default Block/Gutenberg/Classic/Elementor/other)
  • Changes should be tested on different browsers
  • Changes should be tested on multisite

Test instructions for QA when the code is in the RC

  • QA should use the same steps as above.

QA can test this PR by following these steps:

  • See "Test instructions for the acceptance test" above. The feature-flag-off Path A is the most important — it must be byte-for-byte equivalent to current trunk behaviour. Paths B–D depend on yoast-ai#337 + MyYoast staging being available.

Impact check

This PR affects the following parts of the plugin, which may require extra testing:

  • AI Generator (title / meta description / SEO title suggestions).
  • AI Content Planner (next-post suggestions, content outline).
  • AI usage counter (Get_Usage_Route).
  • AI consent revocation flow (still handled in the providers, but the catch order changed to let Insufficient_Scope_Exception propagate unrevoked).

Other environments

  • This PR also affects Shopify. I have added a changelog entry starting with [shopify-seo], added test instructions for Shopify and attached the Shopify label to this PR.
  • This PR also affects Yoast SEO for Google Docs. I have added a changelog entry starting with [yoast-doc-extension], added test instructions for Yoast SEO for Google Docs and attached the Google Docs Add-on label to this PR.

Documentation

  • I have written documentation for this change. PHPDoc on the new strategies / factory / Insufficient_Scope_Exception explains the why; inline comments call out the site-wide auth model and the rationale for not routing AI traffic through MyYoast_Client::authenticated_request().

Quality assurance

  • I have tested this code to the best of my abilities.
  • During testing, I had activated all plugins that Yoast SEO provides integrations for.
  • I have added unit tests to verify the code works as intended.
  • If any part of the code is behind a feature flag, my test instructions also cover cases where the feature flag is switched off.
  • I have written this PR in accordance with my team's definition of done.
  • I have checked that the base branch is correctly set.
  • I have run grunt build:images and committed the results, if my PR introduces or edits images or SVGs. (Not applicable — no image changes.)

Innovation

  • No innovation project is applicable for this PR.
  • This PR falls under an innovation project. I have attached the innovation label.
  • I have added my hours to the WBSO document.

Fixes Yoast/reserved-tasks#1207

thijsoo and others added 4 commits May 13, 2026 09:05
Extends the AI HTTP layer so callers (specifically the upcoming OAuth
auth strategy) can inspect response headers on thrown exceptions and
build a DPoP proof's `htu` claim against the same URL the request will
actually hit.

- Request: add `with_added_headers()` and `with_added_body()` immutable
  helpers so strategies can decorate a request without callers
  pre-building auth headers.
- Response: add headers field, lower-cased at the boundary by
  Response_Parser::normalize_headers() so callers can do plain array
  lookups.
- Remote_Request_Exception and Payment_Required_Exception: optional
  response_headers constructor argument + getter so 401s can carry the
  DPoP-Nonce / WWW-Authenticate metadata needed to detect nonce
  challenges and insufficient_scope errors.
- New Insufficient_Scope_Exception subclass of Forbidden_Exception so
  callers can distinguish scope errors from consent revocation.
- API_Client: expose get_url() so the OAuth path can resolve the same
  URL the actual request will use (via the existing
  Yoast\\WP\\SEO\\ai_api_url filter).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The AI module's new OAuth auth strategy needs to build DPoP-bound
requests without routing AI traffic through MyYoast_Client's own
HTTP_Client. Three small additions to the facade enable that:

- create_dpop_proof( method, url, Token_Set ): delegates to
  DPoP_Handler::create_proof().
- store_dpop_nonce( headers ): wrapper for
  DPoP_Handler::handle_nonce_response() so the AI strategy can stash a
  server-issued nonce before retry.
- is_site_connected(): O(1) option-backed flag indicating whether any
  admin has completed the auth-code flow on this site. Set by
  exchange_authorization_code() on first successful exchange, cleared
  by deregister(). Gates the OAuth path so we don't issue
  client_credentials tokens before MyYoast has a verified redirect URI
  for the client (which would yield tokens missing the site_url claim
  yoast-ai requires).

New DPoP_Proof_Provider_Interface port in application/ports/ keeps the
Application layer free of direct Infrastructure dependencies;
DPoP_Handler implements it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces src/ai/authentication/ — a small onion module that selects
between two auth strategies for outbound yoast-ai calls:

- OAuth_Auth_Strategy: fetches a site-wide MyYoast OAuth token via
  client_credentials, generates a DPoP proof, decorates the AI Request
  with `Authorization: DPoP <token>` + `DPoP: <proof>` headers and a
  `user_id` body field, then dispatches via the existing AI
  Request_Handler/API_Client (so response parsing and exception
  mapping stay uniform). Bounded retries: nonce-challenge retry per
  RFC 9449 §8, invalid_token retry, then fall back to the Token
  strategy on persistent failure.
- Token_Auth_Strategy: the legacy `access_jwt` flow. Pulls JWT via
  Token_Manager, attaches `Authorization: Bearer …`, retries once on
  401 with fresh tokens.
- Auth_Strategy_Factory: selects via wpseo_ai_auth_method filter
  override → YOAST_SEO_MYYOAST_CONNECTION feature flag →
  MyYoast_Client::is_registered() → MyYoast_Client::is_site_connected().

The OAuth model is site-wide: once any admin completes the auth-code
flow, every WP user on the site uses the OAuth path. The shared
site-level token is reused across users; the current user's WP id is
self-reported in the request body for endpoints that need per-user
identity.

Insufficient_Scope_Exception is thrown distinctly from generic
Forbidden_Exception so callers can distinguish scope errors from
consent revocation.

Includes 16 unit tests covering factory selection (5), OAuth strategy
(8 — including shared-token-across-users, nonce challenge retry,
invalid_token retry, repeated 401 fallback, insufficient_scope, token
request failed, DPoP proof failure, non-user paths), and Token
strategy (3 — happy path, 401 retry, persistent 401).

Refs: Yoast/reserved-tasks#1207

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tory

Switches the four direct yoast-ai consumers from constructing auth
headers inline to asking the new Auth_Strategy_Factory for a strategy
and delegating the dispatch:

- Suggestions_Provider
- Get_Usage_Route
- Content_Outline_Command_Handler
- Content_Suggestion_Command_Handler

Each caller now passes only its domain headers (X-Yst-Cohort, etc.)
into the Request and lets the chosen strategy add the auth header.
The inline 401-retry block previously living in Suggestions_Provider
moves into Token_Auth_Strategy so all callers benefit uniformly.

The Forbidden_Exception → consent_revoke mapping stays in the caller
layer (consent semantics belong there, not in the auth layer), but
each caller now catches Insufficient_Scope_Exception first and
re-throws it untouched — scope errors are a deployment/token-issuance
problem and must not silently revoke the user's consent.

Tests updated to mock Auth_Strategy_Factory + Auth_Strategy_Interface
instead of Token_Manager + Request_Handler. Behavioural assertions on
the route/provider stay the same.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thijsoo thijsoo added innovation Innovative issue. Relating to performance, memory or data-flow. changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog labels May 13, 2026
thijsoo and others added 2 commits May 13, 2026 09:27
- Inline the wpseo_ai_auth_method filter name in the factory so phpcs
  can see the wpseo_ prefix (it can't follow the constant through
  apply_filters()'s dynamic-hookname check).
- Drop unused $request parameters from three test closures that pass
  through Mockery::andReturnUsing(). The closures only care about call
  count, not the arguments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…atch

Splits the conflated `send()` responsibility on Auth_Strategy_Interface
into a pure decorator + a recovery hook, and introduces AI_Request_Sender
that owns dispatch, retry orchestration, and the optional fallback path.

Auth_Strategy_Interface (new shape):
  - decorate(Request, WP_User): Request — attach auth headers/body.
  - on_failure(Request, WP_User, Remote_Request_Exception, int): bool —
    strategy-specific recovery; returns true to retry, false to give up.
    May throw to override propagation with a typed exception.

AI_Request_Sender (new): the only public dispatch surface for AI calls.
Runs decorate → handle → on_failure loop with MAX_ATTEMPTS = 3. On
on_failure returning false, tries the configured fallback strategy.
Hard cap is enforced by throwing Auth_Strategy_Loop_Exception (a
LogicException sentinel — distinct from Token_Manager's RuntimeException
so callers can tell a buggy strategy from a real token-retrieval failure)
and is deliberately fail-closed: loop-exhausted does NOT trigger fallback.

OAuth_Auth_Strategy and Token_Auth_Strategy:
  - Lost their Request_Handler dependency (dispatch moved to the sender).
  - OAuth lost its Token_Auth_Strategy dependency (fallback wiring moved
    to the factory).
  - decorate() translates internal exceptions (Token_Request_Failed,
    DPoP_Proof_Exception) into Bad_Request_Exception so callers see a
    uniform exception surface.
  - on_failure handles strategy-specific recovery + exception translation.

AI_Request_Sender_Factory (renamed from Auth_Strategy_Factory): now
returns a configured AI_Request_Sender (primary + optional fallback)
instead of a strategy. Same selection logic as before; callers do
`$sender = $factory->create($user); $sender->send($request, $user);`.

Code-review fixes applied on top of the refactor:

  - Auth_Strategy_Loop_Exception (typed LogicException) replaces the
    bare RuntimeException sentinel that previously collided with
    Token_Manager::get_or_request_access_token's documented RuntimeException.

  - OAuth_Forbidden_Exception (extends Forbidden_Exception): OAuth's
    on_failure now translates non-scope 403s into this typed exception,
    matching the pattern we already use for Insufficient_Scope_Exception.
    The sender catches both before the generic Remote_Request_Exception
    so neither triggers fallback (consent semantics don't apply on the
    OAuth wire; falling back to Token would mask the real failure mode).
    Callers (Suggestions_Provider, both content-planner handlers) catch
    OAuth_Forbidden_Exception ahead of Forbidden_Exception so consent
    isn't revoked on the user's behalf for an OAuth-wire 403.

  - MyYoast_Client::is_site_connected() reverted to has_any_user_token()
    — the original name accurately reflects what the underlying flag
    tracks (whether any user has ever obtained a token via the auth-code
    flow on this site). Docblock notes that the internal option key
    keeps its historical SITE_CONNECTED_OPTION name to avoid storage
    migration.

Refs: Yoast/reserved-tasks#1207

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coveralls
Copy link
Copy Markdown

coveralls commented May 13, 2026

Coverage Report for CI Build 0

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Warning

No base build found for commit b17f347 on trunk.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 50.229%

Details

  • Patch coverage: 128 uncovered changes across 11 files (149 of 277 lines covered, 53.79%).

Uncovered Changes

Top 10 Files by Coverage Impact Changed Covered %
src/ai/authentication/application/oauth-auth-strategy.php 75 48 64.0%
src/ai/authentication/application/ai-request-sender-factory.php 28 4 14.29%
src/ai/content-planner/application/content-suggestion-command-handler.php 16 0 0.0%
src/myyoast-client/application/myyoast-client.php 24 8 33.33%
src/ai/authentication/application/ai-request-sender.php 32 17 53.13%
src/ai/http-request/domain/request.php 14 0 0.0%
src/ai/http-request/domain/exceptions/remote-request-exception.php 4 0 0.0%
src/ai/http-request/domain/response.php 4 0 0.0%
src/ai/http-request/application/response-parser.php 11 8 72.73%
src/ai/http-request/infrastructure/api-client.php 8 5 62.5%

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 41438
Covered Lines: 20814
Line Coverage: 50.23%
Coverage Strength: 4.0 hits per line

💛 - Coveralls

thijsoo and others added 2 commits May 13, 2026 13:48
Wires up LoggerAwareInterface + NullLogger across the four authentication
classes, matching the pattern used by MyYoast_Client and its handlers.
The compiled DI container auto-wires the real Yoast\WP\SEO\Loggers\Logger
at runtime; the NullLogger default keeps tests silent.

Log points are concentrated where they aid debugging:

- AI_Request_Sender_Factory: debug per selection branch (why oauth vs
  token was chosen — useful for "why didn't OAuth fire on this site?"
  reports).
- OAuth_Auth_Strategy::decorate: warning on the two exception
  translations (Token_Request_Failed → Bad_Request,
  DPoP_Proof_Exception → Bad_Request), since the original cause is
  otherwise hidden behind the generic Bad_Request.
- OAuth_Auth_Strategy::on_failure: debug on recovery actions (nonce
  stash, cached-token clear); warning on insufficient_scope and
  OAuth_Forbidden propagation.
- Token_Auth_Strategy::on_failure: debug on the 401 → clear-JWTs retry.
- AI_Request_Sender::send: warning when the primary exhausts recovery
  and fallback engages; error when the loop-budget sentinel trips
  (paired with the typed Auth_Strategy_Loop_Exception throw).

Also moves Auth_Strategy_Loop_Exception from the application layer to
domain/exceptions/ so all three new exception types
(Insufficient_Scope, OAuth_Forbidden, Auth_Strategy_Loop) consistently
live in a Domain layer. New namespace:
Yoast\\WP\\SEO\\AI\\Authentication\\Domain\\Exceptions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thijsoo thijsoo marked this pull request as ready for review May 18, 2026 12:42
Copy link
Copy Markdown
Member

@diedexx diedexx left a comment

Choose a reason for hiding this comment

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

I put us in quite the awkward spot where the MyYoast client wants to do and handle request nonce errors itself. As does the Yoast AI client. Alternatively we expose some of its internals, like you did with the DPoP utilities. We should be careful that we don't end up repeating the same dpop/retry pattern over and over, everywhere we use one of our resource servers.
I think there could be solution where we don't have to leak internals of either auth approach. I'd love to brainstorm some ideas with you if you're open to it

*
* @return bool True if the flag is currently set.
*/
public function has_any_user_token(): bool {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please use consistent naming with the mark_site_connected and clear_site_connected methods. The comment above this makes it sound like there's been a refactor/change of heart while working on this branch, but I don't think this reached any release yet.

Comment on lines +501 to +503
if ( $this->has_any_user_token() ) {
return;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

update_option will do a get_option and short-circuit when there's no change to the value. We can simplify this method.

Suggested change
if ( $this->has_any_user_token() ) {
return;
}

*/
public function decorate( Request $request, WP_User $user ): Request {
try {
$token_set = $this->myyoast_client->get_site_token( [ self::AI_SCOPE ] );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should also require a resource parameter (RFC 8707), which the client doesn't support yet. I'll open up a PR to add support for it later.

} catch ( Token_Request_Failed_Exception | Token_Storage_Exception $exception ) {
$this->logger->warning( 'OAuth decorate: site token unavailable ({error}); surfacing as OAUTH_TOKEN_UNAVAILABLE.', [ 'error' => $exception->getMessage() ] );
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception data, not output.
throw new Bad_Request_Exception( 'OAUTH_TOKEN_UNAVAILABLE', 0, 'OAUTH_TOKEN_UNAVAILABLE', $exception );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure Bad_Request_Exception is the right fit here. It's linked to the HTTP error respose type, but no request has been made. Having a layer-specific exception type would help understand and resolve the true error. e.g. Unmet_Auth_Strategy_Condition_Exception or something like that

} catch ( DPoP_Proof_Exception $exception ) {
$this->logger->warning( 'OAuth decorate: DPoP proof generation failed ({error}); surfacing as DPOP_PROOF_FAILED.', [ 'error' => $exception->getMessage() ] );
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception data, not output.
throw new Bad_Request_Exception( 'DPOP_PROOF_FAILED', 0, 'DPOP_PROOF_FAILED', $exception );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same here

Comment on lines +44 to +61
/**
* Path prefixes whose handler reads user identity from the request body. We forward the WP user id
* for these because the site-level OAuth token is shared across users (client_credentials) and
* yoast-ai needs the body field to run per-user license/usage checks.
*
* The set is deliberately narrow: only POST endpoints that today rely on JWT-encoded user identity
* AND build their request body via Suggestions_Provider / the content-planner handlers. The usage
* endpoint (/usage/...) is a GET with an empty body — API_Client drops the body for GET requests,
* so adding /usage/ here would silently lose the user_id. The yoast-ai team will need to expose
* usage identity through a different surface (query parameter or token claim) when the OAuth path
* becomes the default; tracked separately from this issue.
*
* @var string[]
*/
private const USER_BOUND_PATH_PREFIXES = [
'/openai/suggestions/',
'/content-planner/',
];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The id is extracted on the global level: when oauth is used, we want the externalUserId in either the query params (GET) or request body (POST/PUT/ETC). Easiest to just always include it when going through the oauth strategy

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For simplicity sake we could consider always sending it. Even for the legacy path, as it's not strictly part of auth.

* Thrown when the yoast-ai service returns a 403 on the MyYoast OAuth wire that isn't a scope error.
*
* Distinct from a generic Forbidden_Exception because the recovery path differs: a Token-flow 403
* means consent was revoked (the caller clears the user's consent), but on the OAuth wire there is
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is not right. even with oauth, we expliclty need to note user consent. With the legacy flow the authorization implicitly marks consent, but this doesn't happen with the new flow.

*
* @return void
*/
public function handle_nonce_response( array $response_headers ): void;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this is a bit of a smell. A Proof Provider doesn't know the first thing about nonces.

public function exchange_authorization_code( int $user_id, string $code, string $state ): Token_Set {
$token_set = $this->auth_code_handler->exchange_code( $user_id, $code, $state );
$this->user_token_storage->store( $user_id, $token_set );
$this->mark_site_connected();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ideally the myyoast-client facade is only a window to the rest of this module; a single entrypoint. It shouldn't be responsible for state/data integrity. You could end up with a faulty state when using the auth_code_handler directly. Maybe this connected state should be part of the Registered_Client model and its storage. The Auth_Code_Handler and Client_Registration could then be responsible for setting and clearing it (the latter would automatically be done by deregistering if the client and its connected state share the same storage).

…istration

Push ownership of the "site has completed the auth-code flow" flag out of
the MyYoast_Client facade. The flag now lives behind the
Client_Registration_Interface port (is_site_connected / mark_site_connected);
Authorization_Code_Handler sets it on a successful exchange, and the
registration adapter clears it as part of forget_registration /
delete_local_data so deregistration and stale-redirect-URI re-registration
both reset it for free. The facade keeps a one-line is_site_connected()
pass-through; the prior has_any_user_token / mark_site_connected /
clear_site_connected members are removed outright, with no deprecation
shim, since none of the new names have shipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog innovation Innovative issue. Relating to performance, memory or data-flow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants