Skip to content

Add LogoutAll: sign a user out of the backend on every device - #76

Merged
boomzero merged 4 commits into
masterfrom
feat/logout-all
Oct 7, 2026
Merged

boomzero merged 4 commits into
masterfrom
feat/logout-all

Conversation

@boomzero

@boomzero boomzero commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

What does this PR aim to accomplish?:

Adds "log out everywhere". Logout from #75 only revokes the current device's token. If a token leaks, or the user just wants to sign out every browser, there was no way to kill all of them.

How does this PR accomplish the above?:

New endpoint LogoutAll. It accepts any auth, token or legacy, and:

  • deletes every session_token row for the user (RevokeAllSessionTokens in Source/SessionToken.ts)
  • deletes the user's phpsessid cache rows, so old script versions also have to re-verify with xmoj
  • closes the user's open notification sockets through a new internal /disconnect channel on the Durable Object. It is guarded by the same X-Notification-Token as /notify, and closes with code 4001, which tells the client its token is gone. A failure here doesn't fail the request.

What it does and doesn't do:

  • Devices with a live xmoj session: they exchange their PHPSESSID for a new token on their next request, as on first install. The user isn't kicked out of anything they're actually using.
  • A token on its own: for example one copied off a device, or one whose device's xmoj session is dead. It is locked out for good. That is the security value.
  • xmoj itself: this doesn't log anyone out of xmoj. Only xmoj can do that (or a password change). The userscript's confirmation therefore tells users to change their XMOJ password first if they suspect someone else has their account.

Close-handshake fix. This is an existing bug, which this feature exposed. Our compatibility_date (2023-09-17) predates the runtime answering close frames automatically, and webSocketClose never called close() back. Cloudflare's docs say you must do that on old dates, or clients see 1006 closes. The handler now replies with the client's code. I left compatibility_date alone: in Chrome the handler fix alone delivers a clean 4001 "Logged out", and bumping three years of runtime defaults wasn't needed for this.

No migration is needed.

Testing

  • npm test: 140 pass, 5 of them new:
    • LogoutAll deletes exactly the user's tokens and phpsessid rows, and calls /disconnect with the internal token
    • LogoutAll still succeeds if the DO is unreachable
    • /disconnect closes all of that user's sockets with 4001 and leaves other users connected
    • /disconnect without the internal token returns 401
    • webSocketClose replies with the client's code, and swaps the reserved 1005/1006 for 1000
  • wrangler deploy --dry-run bundles.
  • End to end, local wrangler dev: device B ran the userscript's real auth and socket code in Chrome, while device A and a "thief" holding only a copy of B's token ran in Node.
    • A's LogoutAll closed B's socket with a clean 4001, and B dropped its token.
    • The thief's copy was refused with TokenInvalid.
    • B, still logged in to xmoj, quietly got a new token and reconnected its socket.

The userscript side (a 注销所有设备 menu entry) is in XMOJ-Script-dev/XMOJ-Script#1064.

Does this PR need a documentation update?:

No. CLAUDE.md is updated in this PR.


By submitting this pull request, I confirm the following:

  1. I have read and understood the contributor's guide, as well as this entire template. I understand which branch to base my commits and Pull Requests against. (dev for XMOJ-Script, master for everything else).
  2. I have commented on my proposed changes within the code.
  3. I have tested my changes.
  4. I am willing to help maintain this change if there are issues with it later.
  5. It is compatible with the GNU General Public License v3.0
  6. I have squashed any insignificant commits. (git rebase)
  7. I have checked that another pull request for this purpose does not exist.
  8. I have considered and confirmed that this submission will be valuable to others.
  9. I accept that this submission may not be used, and the pull request can be closed at the will of the maintainer.
  10. I give this submission freely and claim no ownership to its content.
  11. I have verified that my changes work correctly in both the new UI and the old/classic UI. (Backend only, so there is no UI here. Item 11 applies to the userscript PR, Authenticate to the backend with our own session token XMOJ-Script#1064.)

  • I have read the above and my PR is ready for review. Check this box to confirm

🤖 Generated with Claude Code

https://claude.ai/code/session_017dqWu3YgeVRGwBt5pXDq4A

Revokes every session token the user holds, drops their cached PHPSESSID
checks (so old clients re-verify with xmoj too), and closes their open
notification sockets with code 4001. Devices with a live xmoj session just
exchange it for a new token; a token on its own is locked out.

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

sourcery-ai Bot commented Oct 7, 2026

Copy link
Copy Markdown

Sorry @boomzero, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 3 days and 2 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T11:07:24.710138Z 8883f74 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sourcery-ai

sourcery-ai Bot commented Oct 7, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements LogoutAll to sign a user out of the backend on every device by deleting all session credentials and cached legacy sessions, disconnecting that user’s notification sockets, and relying on live XMOJ sessions to re-authenticate on subsequent requests.

Sequence diagram for LogoutAll across devices

sequenceDiagram
    participant Client as Authenticated client
    participant Process
    participant Database
    participant Notifications as NotificationManager
    participant Devices as Other connected devices

    Client->>Process: LogoutAll({})
    Process->>Database: RevokeAllSessionTokens(userId)
    Process->>Database: Delete phpsessid rows for userId
    Process->>Notifications: POST /disconnect with X-Notification-Token
    Notifications->>Devices: close sockets with code 4001
    Notifications-->>Process: OK
    Process-->>Client: LogoutAll success
Loading

Sequence diagram for device re-authentication after LogoutAll

sequenceDiagram
    participant Device
    participant Process
    participant Database
    participant XMOJ

    Device->>Process: Next request with old token
    Process->>Database: ResolveSessionToken(oldToken)
    Database-->>Process: Token not found
    Process-->>Device: Re-authentication required
    Device->>XMOJ: Prove live XMOJ session
    XMOJ-->>Device: Verified PHPSESSID
    Device->>Process: Login with PHPSESSID
    Process->>Database: IssueSessionToken(userId)
    Database-->>Process: New token
    Process-->>Device: New backend token
Loading

File-Level Changes

Change Details Files
Added a backend-wide logout operation that invalidates all of a user’s authentication state.
  • Introduced LogoutAll with support for both current-token and legacy authentication flows.
  • Revoked all session tokens and deleted cached PHPSESSID records for the authenticated user.
  • Documented the new logout semantics in the authentication guidance.
Source/Process.ts
Source/SessionToken.ts
CLAUDE.md
Added authenticated notification-socket disconnection for global logout.
  • Added an internal /disconnect Durable Object endpoint protected by X-Notification-Token.
  • Closed only the target user’s sockets with code 4001, allowing clients to re-authenticate while preserving other users’ connections.
  • Made notification disconnection best-effort so Durable Object failures do not fail LogoutAll.
Source/Process.ts
Source/NotificationManager.ts
Added regression coverage for global logout and socket isolation.
  • Verified deletion of the user’s tokens and PHPSESSID rows and dispatch of the internal disconnect request.
  • Verified logout succeeds when the notification Durable Object is unreachable.
  • Verified disconnect authorization, close code, and isolation from other users’ sockets.
test/process.test.js
test/notification_manager.test.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8883f7495c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Source/Process.ts

@cubic-dev-ai cubic-dev-ai Bot 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.

All reported issues were addressed across 6 files

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

Comment thread Source/Process.ts
Comment thread CLAUDE.md Outdated
Comment thread Source/NotificationManager.ts Outdated
Our compatibility date predates automatic close replies, and
webSocketClose never called close() back, so the handshake was left
half-done. Cloudflare's docs require the reply on old dates to avoid 1006
closes. Checked in Chrome: a LogoutAll now reaches clients as a clean
4001 "Logged out".

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

@cubic-dev-ai cubic-dev-ai Bot 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.

All reported issues were addressed across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

Comment thread Source/NotificationManager.ts Outdated
…e code

- The worker forwards the token's hash and the Durable Object re-checks it
  when admitting a socket. The socket is registered before the D1 check,
  so /disconnect either closes it or ran first and the token is gone; a
  handshake racing LogoutAll can no longer stay subscribed.
- webSocketClose answers codes close() rejects (1001 when a page closes,
  and others) with 1000 instead of leaving the handshake unanswered.
- /disconnect returns 400 for a body it can't read instead of failing
  silently.
- CLAUDE.md: the socket close is best-effort.

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

@cubic-dev-ai cubic-dev-ai Bot 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.

0 issues found across 5 files (changes from recent commits).

Requires human review: Adds LogoutAll revoking all tokens, cached PHPSESSIDs, and notification sockets, plus WebSocket close/race fixes. The decisive tradeoffs are the new auth/data-handling policy and API surface.

Turn on auto-fix | Re-trigger cubic

fetch had grown to handle /notify, /disconnect and the socket handshake with
its token re-check in one body. No behaviour change.

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

@cubic-dev-ai cubic-dev-ai Bot 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.

0 issues found across 1 file (changes from recent commits).

Requires human review: Adds LogoutAll revoking all tokens, cached PHPSESSIDs and open notification sockets, plus WebSocket close/race fixes. Decisive tradeoffs are the new public API surface and authentication/data-handling policy.

Turn on auto-fix | Re-trigger cubic

@boomzero
boomzero merged commit c684ecd into master Oct 7, 2026
6 of 7 checks passed
@boomzero
boomzero deleted the feat/logout-all branch October 7, 2026 11:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant