Skip to content

fix(auth): end sessions the server has rejected (auth-public-client ^0.6.0) - #467

Draft
interacsean wants to merge 2 commits into
mainfrom
claude/auth-public-client-v0-6-0-1850f8
Draft

fix(auth): end sessions the server has rejected (auth-public-client ^0.6.0)#467
interacsean wants to merge 2 commits into
mainfrom
claude/auth-public-client-v0-6-0-1850f8

Conversation

@interacsean

Copy link
Copy Markdown
Contributor

What

Raises @tailor-platform/auth-public-client from ^0.5.1 to ^0.6.0, so a session the server has refused is torn down instead of replayed forever.

Why

On 0.5.x an expired or revoked grant left isAuthenticated true and reattached the dead token to every request. Apps sat in a permanent {"errors":[{"message":"unauthorized","type":"Gateway"}]} loop that only a manual IndexedDB clear recovered from. Two paths kept the dead session alive: rejection was recognised only by three literal error codes (so a WWW-Authenticate challenge, which carries no .error, was missed entirely), and the proactive refresh flattened the rejection to null before any code could act on it.

^0.5.1 does not span 0.6.0, so consumers do not pick this up automatically — the pin is the delivery mechanism.

No source change was needed — verified, not assumed

Reading 0.6.0's shipped code rather than trusting the release notes:

  • logoutImpl emits logout then auth_state_changed.
  • resetState leaves isReady: true with isAuthenticated: false.

That is exactly the shape attemptAutoLogin gates on (packages/core/src/contexts/auth-context.tsx), so the fix reaches consumers through the subscription already in place. A .d.ts diff of 0.5.1 → 0.6.0 confirms the type surface is purely additive: one new export (getValidAccessTokenOrThrow), AuthClient unchanged.

Transient failures are unaffected — a 5xx, a timeout, and a network failure all still leave the session intact.

Consumer-visible behaviour change

fetch and getAuthHeaders no longer throw Error("No valid access token") on a rejected refresh; they throw the underlying error. Apps that detected dead sessions by matching that message have silently stopped detecting them and should listen for logout / auth_state_changed instead. Called out in the changeset.

Grepping the installed package for that string does not tell you whether you are affected — the throw survives for the genuinely-no-token case, so the grep passes either way.

Changes

  • packages/core/package.json^0.5.1^0.6.0 (+ lockfile; includes a no-op pnpm reorder of @microsoft/api-extractor, same specifier and version)
  • packages/core/src/contexts/auth-context.test.tsx — regression test that the 0.6.0 teardown sequence (logout immediately followed by auth_state_changed) yields exactly one login redirect, not two
  • docs/concepts/authentication.md — new "Session expiry" section; corrects three places that described fetch as handling refresh but not rejection
  • .changeset/ — patch

@tailor-platform/auth-public-client is permanently exempt from the repo's minimumReleaseAge gate (pnpm-workspace.yaml), so the 9-day-old release installs cleanly in CI.

Testing

  • build, type-check, lint, fmt:check pass; 1580 tests pass
  • Both example apps boot clean — all requests 200, no console or server errors

Not verified here: the original deadlock cannot be reproduced in this repo — it needs a real expired or revoked refresh token against a live workspace, and app-shell has none. The browser check proves the dependency loads and integrates, not that the deadlock is gone. End-to-end confirmation has to come from a consuming app; Denim Tears is the natural candidate, and its app-side mitigation (tailor-professional-service/denim-tears#1479) should become removable once this ships.

Not done

getValidAccessTokenOrThrow is not re-exported. app-shell exposes no auth utils today, only the AuthClient type — adding one is an API design decision rather than part of a dependency bump. Happy to add it if wanted.

References

🤖 Generated with Claude Code

Raises @tailor-platform/auth-public-client from ^0.5.1 to ^0.6.0.

When a grant expired or was revoked, 0.5.x left isAuthenticated true and
reattached the dead token to every request, stranding apps in a permanent
gateway-unauthorized loop that only a manual IndexedDB clear recovered
from (planning#1495, auth-public-client#113). 0.6.0 acts on any
token-endpoint rejection and emits logout + auth_state_changed, which is
already what AuthProvider reacts to — so the fix lands without a source
change here. Transient failures (5xx, timeout, network) still leave the
session intact.

Consumers who detected dead sessions by catching
Error("No valid access token") lose that signal: fetch and
getAuthHeaders now throw the underlying error. Called out in the
changeset.

- add a regression test covering the 0.6.0 teardown event sequence
  (logout followed by auth_state_changed) yielding exactly one login
- document session-expiry behaviour in the authentication guide

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@interacsean

Copy link
Copy Markdown
Contributor Author

/review

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Code Review completed successfully!

Code review complete — no issues found. PR #467 is a clean dependency bump of @tailor-platform/auth-public-client from ^0.5.1 to ^0.6.0. The existing auth_state_changed subscription integrates correctly with the 0.6.0 teardown sequence, the regression test is valid, and the behavior change is properly documented. Verdict: Approve.

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Code Metrics Report

main (41ae0e3) #467 (6c3cac1) +/-
Coverage 90.1% 90.1% 0.0%
Test Execution Time 2m5s 1m59s -6s
Details
  |                     | main (41ae0e3) | #467 (6c3cac1) | +/-  |
  |---------------------|----------------|----------------|------|
  | Coverage            |          90.1% |          90.1% | 0.0% |
  |   Files             |            152 |            152 |    0 |
  |   Lines             |           5201 |           5201 |    0 |
  |   Covered           |           4690 |           4690 |    0 |
+ | Test Execution Time |           2m5s |          1m59s |  -6s |

Code coverage of files in pull request scope (0.0% → 0.0%)

Files Coverage +/- Status
packages/core/package.json 0.0% 0.0% modified

Reported by octocov

Two issues from adversarial review of this PR.

The regression test was a tautology. Asserting only the final login count
passes even with the auth_state_changed filter removed, because
loginInFlightRef absorbs the duplicate call — verified by injecting that
exact regression and watching the test stay green. It now asserts that a
`logout` event alone is inert before firing auth_state_changed, which is
what makes it discriminating; the same mutation now fails it.

The transient-failure guarantee was wrong. isAuthoritativeRefreshRejection
keys off the response shape, not the underlying cause: oauth4webapi builds
a ResponseBodyError for any 4xx carrying an `error` code, and 0.6.0 treats
everything except use_dpop_nonce as authoritative. So
400 {"error":"server_error"} ends the session, as does a 5xx carrying a
WWW-Authenticate header (checkOAuthBodyError runs the challenge check
before the 4xx-only body parse). Only a timeout, a network failure, and a
bare 5xx are actually safe. Docs and changeset now say that instead of
promising blip tolerance the code does not deliver.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant