Skip to content

feat(app): device sessions screen and owner session revoke - #147

Merged
JOY (JOY) merged 1 commit into
mainfrom
web-revoke-device
Oct 15, 2026
Merged

JOY (JOY) merged 1 commit into
mainfrom
web-revoke-device

Conversation

@JOY

@JOY JOY (JOY) commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Closes #89

What

  • Settings, "Thiết bị đang đăng nhập" (signed-in user): the sessions Crove has seen, newest activity first, with the current session marked from the access token's session_id (this browser also gets a "Chrome trên Windows" style label; the API records no user agent, so other sessions show a short id). Revoke one, "Đăng xuất các thiết bị khác", and "Đăng xuất khỏi mọi nơi" (all: true), each with an inline confirmation. Revoking the current session, or everything, signs this browser out locally; auth-js broadcasts SIGNED_OUT so other tabs go to /login too. Revoked sessions sit in a collapsed "Đã thu hồi (n)" list.
  • Members list (owner only, not for themself): "Thu hồi phiên" opens the member's sessions seen in this org, revoke one or all, with a confirmation and an optional reason. The copy says it only blocks this org; the member keeps their other orgs and stays signed in.
  • STEP_UP_REQUIRED and 403 answers get clear Vietnamese messages (admins are told only the owner may do this). Revoke is R1 today; the step-up message is there in case a deployment tightens it.

Files

New: apps/app/src/lib/sessions.ts (pure helpers), apps/app/src/components/sessions.tsx, apps/app/test/sessions.test.ts. settings.tsx only gets the wiring (kept small because #101 also edits it). No protected paths.

Tests

  • Unit: token session_id decoding (base64url, malformed tokens), sorting, other-session ids, labels, error messages.
  • No Playwright setup on main yet (test: Playwright end-to-end test of the Inbox flow on docker compose #91 is open), so no e2e spec. Checked in a browser against the built SPA with mocked API answers: owner revoke with reason (request carries reason and sessionId), STEP_UP_REQUIRED message, 403 message, revoke other devices, revoke current session and sign out everywhere (both tabs land on /login, local session cleared).
  • pnpm lint, pnpm typecheck, pnpm test, pnpm --filter @crove/app build pass.

🤖 Generated with Claude Code


📌 TL;DR

Implements the "Device Sessions" feature (ADR-0028) allowing users to view and revoke their own active sessions, and enabling organization owners to revoke member sessions within a specific org. Includes pure utility functions for session parsing/sorting and comprehensive unit tests.

🎯 Type of Change

  • 🚀 New feature
  • 🐛 Bugfix
  • 🧹 Refactor
  • ⚡ Performance
  • 📚 Documentation
  • ⚙️ CI / Configuration

🔍 Changes Walkthrough

File Summary of Changes
apps/app/src/components/sessions.tsx New UI components: MySessions (user's own device list with revoke/sign-out actions) and MemberSessions (owner-only panel to revoke a member's org-scoped sessions). Handles confirmation dialogs, error states, and local sign-out logic.
apps/app/src/lib/sessions.ts New pure utility module: SessionRow type, JWT session_id extraction, session sorting logic, user-agent parsing for display labels, and error message mapping for specific API error codes (e.g., STEP_UP_REQUIRED).
apps/app/src/routes/settings.tsx Integrates the new session components into the Settings page. Adds MySessions card at the bottom and embeds MemberSessions toggle/panel within the organization member list for owners.
apps/app/test/sessions.test.ts New unit tests for sessions.ts utilities, covering JWT parsing edge cases, sorting order, user-agent detection, and error message formatting.

📊 Architectural Flow

sequenceDiagram
    participant UI as Settings Page
    participant Comp as Session Components
    participant Lib as Session Utils
    participant API as oRPC Client
    participant Server as Backend

    Note over UI,Comp: User views Settings
    UI->>Comp: Render MySessions / MemberSessions
    Comp->>API: Query sessions.list
    API->>Server: GET /sessions
    Server-->>API: Session List
    API-->>Comp: Data
    Comp->>Lib: sortSessions, describeUserAgent
    Lib-->>Comp: Formatted Data
    Comp-->>UI: Render Session List

    Note over UI,Server: User initiates Revoke
    UI->>Comp: Click "Revoke"
    Comp->>Comp: Show ConfirmBox
    UI->>Comp: Confirm Action
    Comp->>API: Mutation sessions.revoke
    API->>Server: POST /sessions/revoke
    Server-->>API: Success / Error
    API-->>Comp: Result
    alt Current Session Revoked
        Comp->>Comp: signOutHere()
    else Other Session
        Comp->>API: Invalidate Queries
    end
    Comp-->>UI: Update UI State
Loading

Settings gets "Thiết bị đang đăng nhập" for the signed-in user: the
sessions Crove has seen, the current one marked from the access token's
session_id, revoke one, sign out the other devices, and sign out
everywhere. Revoking the current session signs this browser out and
auth-js tells the other tabs.

In the member list the owner can open "Thu hồi phiên" for a member and
revoke one or all of their sessions in this org, with a confirmation
and an optional reason; the copy explains it only blocks this org.

STEP_UP_REQUIRED and 403 answers get clear Vietnamese messages.

Closes #89

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

@claude claude 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.

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

@vercel

vercel Bot commented Oct 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
crove Skipped Skipped Oct 15, 2026 4:26pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Oct 15, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 48 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ee3aa1f5-b34e-4f86-aae7-89380a745c17
📥 Commits

Reviewing files that changed from the base of the PR and between b21d931 and e8c3e94.

📒 Files selected for processing: 4
  • apps/app/src/components/sessions.tsx
  • apps/app/src/lib/sessions.ts
  • apps/app/src/routes/settings.tsx
  • apps/app/test/sessions.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@JOY
JOY (JOY) merged commit ce6de98 into main Oct 15, 2026
12 of 13 checks passed

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces device session management, allowing users to view and revoke their own active sessions and organization owners to manage member sessions scoped to their organization. The feedback suggests improving the multi-session revocation logic by using Promise.allSettled instead of Promise.all to handle partial failures and avoid duplicate query invalidations. Additionally, it recommends expanding the user agent parser to correctly identify mobile Edge and Firefox on iOS, along with adding corresponding unit tests.

Comment on lines +106 to +113
const revokeOthers = useMutation({
mutationFn: async (ids: string[]) => {
const results = await Promise.all(ids.map((sessionId) => client.sessions.revoke({ userId, sessionId })));
return results.flatMap((r) => r.revoked);
},
onSuccess: done,
onSettled: () => void queryClient.invalidateQueries({ queryKey: orpc.sessions.key() }),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using Promise.all to revoke multiple sessions means that if any single session revocation fails (e.g., due to a network timeout or if it was already revoked), the entire operation will reject, potentially leaving other sessions unrevoked. Additionally, calling done on success invalidates orpc.sessions.key(), but onSettled also invalidates the same key, causing duplicate query invalidations and redundant network requests. Using Promise.allSettled and removing onSettled resolves both issues.

Suggested change
const revokeOthers = useMutation({
mutationFn: async (ids: string[]) => {
const results = await Promise.all(ids.map((sessionId) => client.sessions.revoke({ userId, sessionId })));
return results.flatMap((r) => r.revoked);
},
onSuccess: done,
onSettled: () => void queryClient.invalidateQueries({ queryKey: orpc.sessions.key() }),
});
const revokeOthers = useMutation({
mutationFn: async (ids: string[]) => {
const results = await Promise.allSettled(ids.map((sessionId) => client.sessions.revoke({ userId, sessionId })));
return results
.filter((r): r is PromiseFulfilledResult<{ revoked: string[] }> => r.status === 'fulfilled')
.flatMap((r) => r.value.revoked);
},
onSuccess: done,
});

Comment on lines +45 to +55
const browser = /Edg\//.test(userAgent)
? 'Edge'
: /OPR\/|Opera/.test(userAgent)
? 'Opera'
: /Firefox\//.test(userAgent)
? 'Firefox'
: /Chrome\/|CriOS\//.test(userAgent)
? 'Chrome'
: /Safari\//.test(userAgent)
? 'Safari'
: null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The user agent parser does not detect Edge on mobile (EdgA/, EdgiOS/) or Firefox on iOS (FxiOS/). Because these user agents contain Safari/, they fall through and are incorrectly identified as Safari. Updating the regexes ensures mobile Edge and Firefox are correctly classified.

Suggested change
const browser = /Edg\//.test(userAgent)
? 'Edge'
: /OPR\/|Opera/.test(userAgent)
? 'Opera'
: /Firefox\//.test(userAgent)
? 'Firefox'
: /Chrome\/|CriOS\//.test(userAgent)
? 'Chrome'
: /Safari\//.test(userAgent)
? 'Safari'
: null;
const browser = /Edg\/|EdgA\/|EdgiOS\//.test(userAgent)
? 'Edge'
: /OPR\/|Opera/.test(userAgent)
? 'Opera'
: /Firefox\/|FxiOS\//.test(userAgent)
? 'Firefox'
: /Chrome\/|CriOS\//.test(userAgent)
? 'Chrome'
: /Safari\//.test(userAgent)
? 'Safari'
: null;

Comment on lines +74 to +89
it('describes common browsers', () => {
expect(
describeUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36'),
).toBe('Chrome trên Windows');
expect(
describeUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0',
),
).toBe('Edge trên Windows');
expect(
describeUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1'),
).toBe('Safari trên iOS');
expect(describeUserAgent('Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0')).toBe('Firefox trên Linux');
expect(describeUserAgent('curl/8.0')).toBeNull();
expect(describeUserAgent(null)).toBeNull();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Add unit tests for mobile Edge and Firefox on iOS to verify the updated user agent parser behaves correctly.

  it('describes common browsers', () => {
    expect(
      describeUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36'),
    ).toBe('Chrome trên Windows');
    expect(
      describeUserAgent(
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0',
      ),
    ).toBe('Edge trên Windows');
    expect(
      describeUserAgent(
        'Mozilla/5.0 (iPhone; CPU iPhone OS 12_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 EdgiOS/42.5.2.0 Mobile/16C50 Safari/605.1.15',
      ),
    ).toBe('Edge trên iOS');
    expect(
      describeUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1'),
    ).toBe('Safari trên iOS');
    expect(
      describeUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/13.2b9418 Mobile/16A366 Safari/605.1.15'),
    ).toBe('Firefox trên iOS');
    expect(describeUserAgent('Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0')).toBe('Firefox trên Linux');
    expect(describeUserAgent('curl/8.0')).toBeNull();
    expect(describeUserAgent(null)).toBeNull();
  });

JOY (JOY) pushed a commit that referenced this pull request Oct 15, 2026
… step-up (#32)

Rebased onto main with member invites, owner confirmations (#119), the
sessions screen (#147, #178) and MFA_REQUIRED (#126), so the earlier
commits of this PR are squashed into one.

- auth-js uses PKCE. settleCallback() runs before the router: it explains
  failed links by known error codes only (a link's error_description is
  attacker-written), notes a link opened in another browser, sends
  recovery links to /reset-password, and removes the one-time code and
  error parameters from the URL and history.
- /reset-password asks for the TOTP code first when the user has a
  factor; /mfa is the sign-in challenge, and every data route waits for
  aal2. MFA_REQUIRED from the kernel (#126) now sends the user to /mfa
  instead of showing an error; /mfa signs out through the shared sign-out
  that revokes the Crove session first (#178).
- Settings: two-factor card (TOTP enroll, verify, disable).
- Step-up (P11): approving, inviting, changing a role or removing a member
  (inside #119's confirmation) and rotating the inbound address show a
  prompt on STEP_UP_REQUIRED that names the pending action, asks for a
  fresh code or points to enabling two-factor, and repeats the refused
  request as sent (an approval prompted on one revision gets
  STALE_REVISION if someone saved another meanwhile).
- Compose: GOTRUE_MAILER_URLPATHS_* include the /auth/v1 prefix (email
  links were broken on self-host), and AUTH_STEP_UP defaults to required.
- e2e with email confirmation on (compose.e2e.yml, Mailpit, RFC 6238
  TOTP, an inbound address for the rotation): confirmation and reused
  links, a forged link error, recovery, TOTP enrollment and the sign-in
  challenge, refresh and sign-out, the invite, owner confirmation, role
  change and removal step-up flows across two browsers, the rotation
  step-up, and the Inbox approval with its denied and aal2 audit rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LrvuThKGaPX1uDwUiwmGQ
JOY (JOY) pushed a commit that referenced this pull request Oct 16, 2026
… step-up (#32)

Rebased onto main with member invites, owner confirmations (#119), the
sessions screen (#147, #178) and MFA_REQUIRED (#126), so the earlier
commits of this PR are squashed into one.

- auth-js uses PKCE. settleCallback() runs before the router: it explains
  failed links by known error codes only (a link's error_description is
  attacker-written), notes a link opened in another browser, sends
  recovery links to /reset-password, and removes the one-time code and
  error parameters from the URL and history.
- /reset-password asks for the TOTP code first when the user has a
  factor; /mfa is the sign-in challenge, and every data route waits for
  aal2. MFA_REQUIRED from the kernel (#126) now sends the user to /mfa
  instead of showing an error; /mfa signs out through the shared sign-out
  that revokes the Crove session first (#178).
- Settings: two-factor card (TOTP enroll, verify, disable).
- Step-up (P11): approving, inviting, changing a role or removing a member
  (inside #119's confirmation) and rotating the inbound address show a
  prompt on STEP_UP_REQUIRED that names the pending action, asks for a
  fresh code or points to enabling two-factor, and repeats the refused
  request as sent (an approval prompted on one revision gets
  STALE_REVISION if someone saved another meanwhile).
- Compose: GOTRUE_MAILER_URLPATHS_* include the /auth/v1 prefix (email
  links were broken on self-host), and AUTH_STEP_UP defaults to required.
- e2e with email confirmation on (compose.e2e.yml, Mailpit, RFC 6238
  TOTP, an inbound address for the rotation): confirmation and reused
  links, a forged link error, recovery, TOTP enrollment and the sign-in
  challenge, refresh and sign-out, the invite, owner confirmation, role
  change and removal step-up flows across two browsers, the rotation
  step-up, and the Inbox approval with its denied and aal2 audit rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LrvuThKGaPX1uDwUiwmGQ
JOY (JOY) pushed a commit that referenced this pull request Oct 16, 2026
… step-up (#32)

Rebased onto main with member invites, owner confirmations (#119), the
sessions screen (#147, #178) and MFA_REQUIRED (#126), so the earlier
commits of this PR are squashed into one.

- auth-js uses PKCE. settleCallback() runs before the router: it explains
  failed links by known error codes only (a link's error_description is
  attacker-written), notes a link opened in another browser, sends
  recovery links to /reset-password, and removes the one-time code and
  error parameters from the URL and history.
- /reset-password asks for the TOTP code first when the user has a
  factor; /mfa is the sign-in challenge, and every data route waits for
  aal2. MFA_REQUIRED from the kernel (#126) now sends the user to /mfa
  instead of showing an error; /mfa signs out through the shared sign-out
  that revokes the Crove session first (#178).
- Settings: two-factor card (TOTP enroll, verify, disable).
- Step-up (P11): approving, inviting, changing a role or removing a member
  (inside #119's confirmation) and rotating the inbound address show a
  prompt on STEP_UP_REQUIRED that names the pending action, asks for a
  fresh code or points to enabling two-factor, and repeats the refused
  request as sent (an approval prompted on one revision gets
  STALE_REVISION if someone saved another meanwhile).
- Compose: GOTRUE_MAILER_URLPATHS_* include the /auth/v1 prefix (email
  links were broken on self-host), and AUTH_STEP_UP defaults to required.
- e2e with email confirmation on (compose.e2e.yml, Mailpit, RFC 6238
  TOTP, an inbound address for the rotation): confirmation and reused
  links, a forged link error, recovery, TOTP enrollment and the sign-in
  challenge, refresh and sign-out, the invite, owner confirmation, role
  change and removal step-up flows across two browsers, the rotation
  step-up, and the Inbox approval with its denied and aal2 audit rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LrvuThKGaPX1uDwUiwmGQ
JOY (JOY) pushed a commit that referenced this pull request Oct 16, 2026
… step-up (#32)

Rebased onto main with member invites, owner confirmations (#119), the
sessions screen (#147, #178) and MFA_REQUIRED (#126), so the earlier
commits of this PR are squashed into one.

- auth-js uses PKCE. settleCallback() runs before the router: it explains
  failed links by known error codes only (a link's error_description is
  attacker-written), notes a link opened in another browser, sends
  recovery links to /reset-password, and removes the one-time code and
  error parameters from the URL and history.
- /reset-password asks for the TOTP code first when the user has a
  factor; /mfa is the sign-in challenge, and every data route waits for
  aal2. MFA_REQUIRED from the kernel (#126) now sends the user to /mfa
  instead of showing an error; /mfa signs out through the shared sign-out
  that revokes the Crove session first (#178).
- Settings: two-factor card (TOTP enroll, verify, disable).
- Step-up (P11): approving, inviting, changing a role or removing a member
  (inside #119's confirmation) and rotating the inbound address show a
  prompt on STEP_UP_REQUIRED that names the pending action, asks for a
  fresh code or points to enabling two-factor, and repeats the refused
  request as sent (an approval prompted on one revision gets
  STALE_REVISION if someone saved another meanwhile).
- Compose: GOTRUE_MAILER_URLPATHS_* include the /auth/v1 prefix (email
  links were broken on self-host), and AUTH_STEP_UP defaults to required.
- e2e with email confirmation on (compose.e2e.yml, Mailpit, RFC 6238
  TOTP, an inbound address for the rotation): confirmation and reused
  links, a forged link error, recovery, TOTP enrollment and the sign-in
  challenge, refresh and sign-out, the invite, owner confirmation, role
  change and removal step-up flows across two browsers, the rotation
  step-up, and the Inbox approval with its denied and aal2 audit rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LrvuThKGaPX1uDwUiwmGQ
JOY (JOY) pushed a commit that referenced this pull request Oct 16, 2026
… step-up (#32)

Rebased onto main with member invites, owner confirmations (#119), the
sessions screen (#147, #178) and MFA_REQUIRED (#126), so the earlier
commits of this PR are squashed into one.

- auth-js uses PKCE. settleCallback() runs before the router: it explains
  failed links by known error codes only (a link's error_description is
  attacker-written), notes a link opened in another browser, sends
  recovery links to /reset-password, and removes the one-time code and
  error parameters from the URL and history.
- /reset-password asks for the TOTP code first when the user has a
  factor; /mfa is the sign-in challenge, and every data route waits for
  aal2. MFA_REQUIRED from the kernel (#126) now sends the user to /mfa
  instead of showing an error; /mfa signs out through the shared sign-out
  that revokes the Crove session first (#178).
- Settings: two-factor card (TOTP enroll, verify, disable).
- Step-up (P11): approving, inviting, changing a role or removing a member
  (inside #119's confirmation) and rotating the inbound address show a
  prompt on STEP_UP_REQUIRED that names the pending action, asks for a
  fresh code or points to enabling two-factor, and repeats the refused
  request as sent (an approval prompted on one revision gets
  STALE_REVISION if someone saved another meanwhile).
- Compose: GOTRUE_MAILER_URLPATHS_* include the /auth/v1 prefix (email
  links were broken on self-host), and AUTH_STEP_UP defaults to required.
- e2e with email confirmation on (compose.e2e.yml, Mailpit, RFC 6238
  TOTP, an inbound address for the rotation): confirmation and reused
  links, a forged link error, recovery, TOTP enrollment and the sign-in
  challenge, refresh and sign-out, the invite, owner confirmation, role
  change and removal step-up flows across two browsers, the rotation
  step-up, and the Inbox approval with its denied and aal2 audit rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LrvuThKGaPX1uDwUiwmGQ
JOY (JOY) pushed a commit that referenced this pull request Oct 16, 2026
… step-up (#32)

Rebased onto main with member invites, owner confirmations (#119), the
sessions screen (#147, #178) and MFA_REQUIRED (#126), so the earlier
commits of this PR are squashed into one.

- auth-js uses PKCE. settleCallback() runs before the router: it explains
  failed links by known error codes only (a link's error_description is
  attacker-written), notes a link opened in another browser, sends
  recovery links to /reset-password, and removes the one-time code and
  error parameters from the URL and history.
- /reset-password asks for the TOTP code first when the user has a
  factor; /mfa is the sign-in challenge, and every data route waits for
  aal2. MFA_REQUIRED from the kernel (#126) now sends the user to /mfa
  instead of showing an error; /mfa signs out through the shared sign-out
  that revokes the Crove session first (#178).
- Settings: two-factor card (TOTP enroll, verify, disable).
- Step-up (P11): approving, inviting, changing a role or removing a member
  (inside #119's confirmation) and rotating the inbound address show a
  prompt on STEP_UP_REQUIRED that names the pending action, asks for a
  fresh code or points to enabling two-factor, and repeats the refused
  request as sent (an approval prompted on one revision gets
  STALE_REVISION if someone saved another meanwhile).
- Compose: GOTRUE_MAILER_URLPATHS_* include the /auth/v1 prefix (email
  links were broken on self-host), and AUTH_STEP_UP defaults to required.
- e2e with email confirmation on (compose.e2e.yml, Mailpit, RFC 6238
  TOTP, an inbound address for the rotation): confirmation and reused
  links, a forged link error, recovery, TOTP enrollment and the sign-in
  challenge, refresh and sign-out, the invite, owner confirmation, role
  change and removal step-up flows across two browsers, the rotation
  step-up, and the Inbox approval with its denied and aal2 audit rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LrvuThKGaPX1uDwUiwmGQ
JOY (JOY) pushed a commit that referenced this pull request Oct 16, 2026
… step-up (#32)

Rebased onto main with member invites, owner confirmations (#119), the
sessions screen (#147, #178) and MFA_REQUIRED (#126), so the earlier
commits of this PR are squashed into one.

- auth-js uses PKCE. settleCallback() runs before the router: it explains
  failed links by known error codes only (a link's error_description is
  attacker-written), notes a link opened in another browser, sends
  recovery links to /reset-password, and removes the one-time code and
  error parameters from the URL and history.
- /reset-password asks for the TOTP code first when the user has a
  factor; /mfa is the sign-in challenge, and every data route waits for
  aal2. MFA_REQUIRED from the kernel (#126) now sends the user to /mfa
  instead of showing an error; /mfa signs out through the shared sign-out
  that revokes the Crove session first (#178).
- Settings: two-factor card (TOTP enroll, verify, disable).
- Step-up (P11): approving, inviting, changing a role or removing a member
  (inside #119's confirmation) and rotating the inbound address show a
  prompt on STEP_UP_REQUIRED that names the pending action, asks for a
  fresh code or points to enabling two-factor, and repeats the refused
  request as sent (an approval prompted on one revision gets
  STALE_REVISION if someone saved another meanwhile).
- Compose: GOTRUE_MAILER_URLPATHS_* include the /auth/v1 prefix (email
  links were broken on self-host), and AUTH_STEP_UP defaults to required.
- e2e with email confirmation on (compose.e2e.yml, Mailpit, RFC 6238
  TOTP, an inbound address for the rotation): confirmation and reused
  links, a forged link error, recovery, TOTP enrollment and the sign-in
  challenge, refresh and sign-out, the invite, owner confirmation, role
  change and removal step-up flows across two browsers, the rotation
  step-up, and the Inbox approval with its denied and aal2 audit rows.

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

This branch was previously deployed

1 inactive deployment
Preview — e8c3e94e Deployed Oct 15, 2026 by vercel[bot]
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.

Revoke-device screen: list and revoke sessions (ADR-0028)

1 participant