Skip to content

fix(api,web): MFA REQUIRED_SHARE auth flow + E2E test coverage#213

Merged
FSM1 merged 19 commits into
mainfrom
feat/e2e-mfa-flows
Feb 27, 2026
Merged

fix(api,web): MFA REQUIRED_SHARE auth flow + E2E test coverage#213
FSM1 merged 19 commits into
mainfrom
feat/e2e-mfa-flows

Conversation

@FSM1

@FSM1 FSM1 commented Feb 27, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fix DTO validation: LoginDto regex now accepts pending-core-kit-* placeholder publicKey for REQUIRED_SHARE temp auth (was rejected with 400)
  • Fix user lookup: Auth service now resolves REQUIRED_SHARE users by identity (authMethod identifierHash) when the user already has a real Core Kit publicKey — previously created a duplicate user, sending device approval requests to the wrong userId
  • Fix ephemeral key format: useDeviceApproval generates uncompressed secp256k1 ephemeral public key (130 hex chars) instead of compressed (66 hex chars)
  • Fix premature redirects: Login.tsx guards redirect with !isRequiredShare; SettingsPage skips auth redirect during MFA enrollment
  • Fix navigation centralization: Removed navigate('/files') from login functions — Login.tsx redirect effect handles navigation centrally
  • Add data-testid attributes to all MFA components for E2E targeting
  • Add E2E test suite (TC-MFA-01 through TC-MFA-05) covering full MFA lifecycle with real Core Kit on Sapphire Devnet

Test plan

  • TC-MFA-01: Wallet login + MFA enrollment (captures recovery phrase)
  • TC-MFA-02: Security tab reflects MFA enabled state
  • TC-MFA-03: Device approval — approve (two browser contexts)
  • TC-MFA-04: Recovery phrase restore on new device
  • TC-MFA-05: Device approval — deny
  • Existing E2E tests still pass (--grep-invert @mfa)
  • After staging deploy, run pnpm exec playwright test tests/mfa-flows.spec.ts against staging API

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Multi-device MFA: device approval/denial flows and recovery-phrase restore
    • Login accepts placeholder wallet keys and can issue scoped temporary tokens for device-approval flows
    • Route-level scope checks to allow scoped-token access to specific endpoints
  • Bug Fixes

    • Fixed MFA status false-positive and stabilized redirect behavior during enrollment/login
    • Improved device-approval stability across simultaneous device sessions
  • Tests

    • Comprehensive E2E MFA suite plus wallet-login and MFA test helpers/mocks
  • Chores

    • Added test hooks across MFA UI and updated E2E workflow env config

FSM1 and others added 5 commits February 27, 2026 01:26
Moved to done/, beginning implementation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 0310757ed1e8
Three fixes for the MFA device approval flow:

1. LoginDto regex now accepts `pending-core-kit-*` placeholder publicKey
   for REQUIRED_SHARE temp auth (previously rejected with 400).
2. useDeviceApproval generates uncompressed secp256k1 ephemeral public
   key (65 bytes = 130 hex chars) instead of compressed (33 bytes).
3. Login.tsx redirect guards against isRequiredShare to prevent unmounting
   DeviceWaitingScreen when temp access token sets isAuthenticated.
4. SettingsPage skips auth redirect during MFA enrollment to prevent
   logout from token expiry during long Devnet enableMFA() calls.
5. useAuth removes navigate('/files') from login functions — Login.tsx
   redirect effect handles navigation centrally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 26ff72f8d979
Add E2E-targeting attributes to all MFA UI components:
- MfaEnrollmentWizard: wizard steps, buttons, phrase grid, checkbox
- DeviceWaitingScreen: root, countdown, recovery link, status
- DeviceApprovalModal: modal, approve/deny buttons
- RecoveryInput: root, textarea, submit/back buttons
- RecoveryPhraseGrid: root container
- SecurityTab: root, status badge, enable button

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: e20d33bb4994
Serial test suite (TC-MFA-01 through TC-MFA-05) covering:
- Wallet login and MFA enrollment with recovery phrase capture
- MFA status reflected in Security tab
- Device approval (approve) across two browser contexts
- Recovery phrase restore on new device
- Device approval (deny) with status verification

Uses @johanneskares/wallet-mock for real Core Kit initialization
on Sapphire Devnet. Tagged @mfa for optional CI filtering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 21cba03804d9
When MFA is enabled and a new device sends a placeholder publicKey
(pending-core-kit-{userId}), the auth service was only searching for
users whose publicKey matched the placeholder pattern. After the first
login, users have a real Core Kit publicKey, so the placeholder search
failed and created a NEW user — causing device approval requests to
go to the wrong userId.

Fix: when publicKey is a placeholder, also look up by authMethod
identifierHash (the hashed verifierId from the JWT). This finds the
existing user regardless of their current publicKey.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 6a373586dd93
@coderabbitai

coderabbitai Bot commented Feb 27, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@FSM1 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 17 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between b7c9906 and f41bb95.

📒 Files selected for processing (1)
  • apps/web/src/hooks/useDeviceApproval.ts

Walkthrough

Handle placeholder publicKey ("pending-core-kit-{userId}") and REQUIRED_SHARE temp-auth flows: resolve/reuse placeholder identities, emit scoped temp access tokens (no refresh), enforce route-level scoped JWTs, add AllowScope/JwtAuthGuard, add Playwright E2E MFA tests/helpers, add MFA UI test hooks, and centralize pending-auth state and redirect gating.

Changes

Cohort / File(s) Summary
Backend Auth Core
apps/api/src/auth/auth.service.ts, apps/api/src/auth/auth.controller.ts, apps/api/src/auth/services/token.service.ts, apps/api/src/auth/services/token.service.spec.ts, apps/api/src/auth/strategies/jwt.strategy.ts, apps/api/src/common/types.ts
Add placeholder publicKey resolution and REQUIRED_SHARE flows; support CreateTokenOptions (scope, skipRefreshToken) to emit scoped/no-refresh tokens; attach scope to validated user; return/omit refresh token appropriately.
Auth Guards & Decorator
apps/api/src/auth/decorators/allow-scope.decorator.ts, apps/api/src/auth/guards/jwt-auth.guard.ts, apps/api/src/device-approval/device-approval.controller.ts
New AllowScope decorator and scope-enforcing JwtAuthGuard; apply @AllowScope('device-approval') to device-approval controller so scoped tokens can access specific routes.
Auth DTOs / Client / Web Model
apps/api/src/auth/dto/login.dto.ts, packages/api-client/openapi.json, apps/web/src/api/models/loginDto.ts
Widen LoginDto.publicKey validation/docs to accept uncompressed secp256k1 keys or pending-core-kit-{userId} placeholder; regenerate OpenAPI client schema and update web model docs.
Backend Auth Tests
apps/api/src/auth/auth.service.spec.ts, apps/api/src/auth/strategies/jwt.strategy.spec.ts, apps/api/src/auth/services/token.service.spec.ts
Add tests for placeholder resolution, REQUIRED_SHARE lookup/creation, scoped token payload/signing, skip-refresh-token behavior, and scope attachment in JwtStrategy.validate.
E2E Tests & Helpers
tests/e2e/tests/mfa-flows.spec.ts, tests/e2e/utils/mfa-helpers.ts, tests/e2e/utils/wallet-login-helpers.ts, tests/e2e/tests/wallet-login.spec.ts
New Playwright MFA E2E suite and helpers: mock wallet local transport/context, wallet login flow that detects REQUIRED_SHARE, enrollment/recovery/device-approval helpers, and CI-stable local transport changes.
Web MFA UI Test Hooks
apps/web/src/components/mfa/...
apps/web/src/components/mfa/DeviceApprovalModal.tsx, apps/web/src/components/mfa/DeviceWaitingScreen.tsx, apps/web/src/components/mfa/MfaEnrollmentWizard.tsx, apps/web/src/components/mfa/RecoveryInput.tsx, apps/web/src/components/mfa/RecoveryPhraseGrid.tsx, apps/web/src/components/mfa/SecurityTab.tsx
Add data-testid attributes across MFA components; SecurityTab stops re-reading MFA status after enable and relies on store updates.
Frontend Hooks & Device Approval
apps/web/src/hooks/useAuth.ts, apps/web/src/hooks/useDeviceApproval.ts
Move pending auth state to shared store and read via store getters; remove inline navigations in favor of centralized redirect; add generation/handled-ID guards and uncompressed ephemeral-key handling to avoid stale device-approval races.
Routing / Settings
apps/web/src/routes/Login.tsx, apps/web/src/routes/SettingsPage.tsx
Refine redirect logic to avoid navigating during REQUIRED_SHARE or MFA enrollment by gating redirects on isRequiredShare/isEnrolling and current pathname.
Stores
apps/web/src/stores/auth.store.ts
Add pendingCipherboxJwt, pendingAuthMethod, and setPendingAuth to store; reset on logout.
Planning & CI
.planning/STATE.md, .github/workflows/e2e.yml
Update planning state/todo and inject IDENTIY_JWT_PRIVATE_KEY into E2E workflow env.
API Client Schema (regen)
packages/api-client/openapi.json
Document publicKey placeholder option in generated OpenAPI client schema.
Tests Typings & Mocks
apps/api/src/shares/..., apps/api/src/shares/*.spec.ts
Tighten mock typings to User/RequestWithUser and adjust test mocks accordingly.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant API as API Service
    participant DB as Database

    Client->>API: POST /login (publicKey: 04... or pending-core-kit-...)
    alt publicKey is placeholder
        API->>DB: Query user by publicKey = placeholder
        DB-->>API: user?
        alt user found
            API->>API: validate JWT, extract payload.sub
            API->>DB: Query user by identifierHash (payload.sub)
            DB-->>API: user?
            alt incoming publicKey is real
                API->>DB: Update user's publicKey to real key
            else
                API->>API: keep placeholder (temp auth)
            end
        else
            API->>DB: Lookup by identifierHash or create temp-auth user
        end
    else publicKey is real
        API->>DB: Find or create user by real publicKey
    end
    API-->>Client: return auth response (tokens | scoped temp auth)
Loading
sequenceDiagram
    actor SecondaryDevice
    actor PrimaryDevice
    participant API as API Service
    participant DB as Database

    SecondaryDevice->>API: Login with placeholder -> receive temp token + approval id
    API->>DB: create device-approval record
    DB-->>API: approval id
    API-->>SecondaryDevice: temp auth response
    SecondaryDevice->>SecondaryDevice: show DeviceWaitingScreen (polling)

    PrimaryDevice->>API: Poll pending device-approvals
    API-->>PrimaryDevice: list (includes approval id)
    PrimaryDevice->>API: POST approve/deny approval id
    API->>DB: update approval status
    DB-->>API: updated status
    API-->>SecondaryDevice: poll returns approved/denied

    alt approved
        SecondaryDevice->>API: complete auth (send real publicKey)
        API->>DB: upgrade temp auth -> full session
        SecondaryDevice-->>SecondaryDevice: navigate to /files
    else denied
        SecondaryDevice-->>SecondaryDevice: show denial state
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(api,web): MFA REQUIRED_SHARE auth flow + E2E test coverage' accurately summarizes the main changes: fixing the MFA authentication flow for REQUIRED_SHARE scenarios and adding comprehensive E2E test coverage.
Docstring Coverage ✅ Passed Docstring coverage is 82.61% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/e2e-mfa-flows

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 and usage tips.

Three new test cases for the placeholder publicKey resolution:
- Placeholder login finding placeholder user does NOT overwrite publicKey
- REQUIRED_SHARE temp auth finds existing user by identity (authMethod
  identifierHash) when user has real Core Kit publicKey
- REQUIRED_SHARE creates new user when no existing identity found

Raises auth.service.ts branch coverage from 82.35% to 85.29%
(threshold: 84%).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: d5c212a611af

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
tests/e2e/utils/mfa-helpers.ts (1)

129-135: Prefer data-testid over CSS class for locator stability.

Using .recovery-phrase-word CSS class is less stable than data-testid—styling refactors could silently break this helper. All other locators in this file use data-testid for consistency.

♻️ Suggested change
-  const wordSpans = grid.locator('.recovery-phrase-word');
+  const wordSpans = grid.locator('[data-testid="recovery-phrase-word"]');

This requires adding data-testid="recovery-phrase-word" to the word spans in RecoveryPhraseGrid component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/utils/mfa-helpers.ts` around lines 129 - 135, Replace the brittle
CSS class selector in the helper with a stable data-testid: update the locator
invocation (wordSpans = grid.locator(...)) to use
'[data-testid="recovery-phrase-word"]' instead of '.recovery-phrase-word', and
also add data-testid="recovery-phrase-word" to the RecoveryPhraseGrid
component's word span elements so the test selector exists; keep the rest of the
loop that reads textContent from wordSpans and pushes trimmed text unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/auth/auth.service.ts`:
- Around line 73-123: Add unit tests for the new placeholder resolution branches
in the auth service: cover (1) placeholder→real key upgrade where
userRepository.findOne returns a placeholderUser (publicKey starts with
'pending-core-kit-') and loginDto.publicKey is a real key so
placeholderUser.publicKey is updated and saved via userRepository.save; (2)
REQUIRED_SHARE identity fallback hit where loginDto.publicKey starts with
'pending-core-kit-' and siweService.hashIdentifier(identifier) leads
authMethodRepository.findOne to return an authMethod with a user (assert
returned user used); (3) REQUIRED_SHARE miss where no authMethod is found
(assert behavior falls back to new-user path/no user); and (4) new-user fallback
when no verifierId/identifier matches. Use mocks for userRepository,
authMethodRepository, siweService.hashIdentifier, and assert the correct
repository calls (findOne/save) and logger messages for placeholderUser and
REQUIRED_SHARE branches.
- Around line 94-96: The query that finds placeholderUser uses
Like(`pending-core-kit-${verifierId}%`) which can match multiple publicKey
values sharing a prefix; update the userRepository.findOne call in
auth.service.ts to match the exact placeholder string instead of a pattern
(construct the exact publicKey value "pending-core-kit-${verifierId}" and use
that for equality in the where clause or the ORM's Equal comparator) so findOne
only resolves the intended placeholder user by exact publicKey match.
- Around line 90-121: The current REQUIRED_SHARE fallback builds identifier as
payload.email || payload.sub which causes provider-sub-based auth methods to be
missed when email is present; update the logic around identifier,
siweService.hashIdentifier, and the authMethodRepository.findOne call in the
login flow so you search for auth methods using both the email-derived hash and
the provider-sub-derived hash (when payload.sub exists) instead of only the
email hash: compute hashes for payload.email (normalized) and payload.sub
separately via siweService.hashIdentifier and query authMethodRepository.findOne
for either identifierHash (email) or subHash (provider sub), preferring a match
by provider-sub when present, then set user = authMethod.user if found.

In `@tests/e2e/utils/mfa-helpers.ts`:
- Around line 80-91: Replace the Promise.race pattern (the parallel
gridLocator.waitFor(...) and errorLocator.waitFor(...)) because the losing
waitFor can later reject; instead create a combined locator using Playwright's
locator.or() (e.g., combined = gridLocator.or(errorLocator)), call
combined.waitFor({ state: 'visible', timeout: enableTimeout }) to wait for
either to appear, then determine which one matched by checking
gridLocator.isVisible() (or errorLocator.isVisible()) and handle the error path
by reading errorLocator.textContent() and throwing as before; remove the two
separate waitFor calls and the Promise.race usage.

---

Nitpick comments:
In `@tests/e2e/utils/mfa-helpers.ts`:
- Around line 129-135: Replace the brittle CSS class selector in the helper with
a stable data-testid: update the locator invocation (wordSpans =
grid.locator(...)) to use '[data-testid="recovery-phrase-word"]' instead of
'.recovery-phrase-word', and also add data-testid="recovery-phrase-word" to the
RecoveryPhraseGrid component's word span elements so the test selector exists;
keep the rest of the loop that reads textContent from wordSpans and pushes
trimmed text unchanged.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa84ed1 and f5daa73.

📒 Files selected for processing (19)
  • .planning/STATE.md
  • .planning/todos/done/2026-02-27-e2e-mfa-flows-wallet-login.md
  • apps/api/src/auth/auth.service.ts
  • apps/api/src/auth/dto/login.dto.ts
  • apps/web/src/api/models/loginDto.ts
  • apps/web/src/components/mfa/DeviceApprovalModal.tsx
  • apps/web/src/components/mfa/DeviceWaitingScreen.tsx
  • apps/web/src/components/mfa/MfaEnrollmentWizard.tsx
  • apps/web/src/components/mfa/RecoveryInput.tsx
  • apps/web/src/components/mfa/RecoveryPhraseGrid.tsx
  • apps/web/src/components/mfa/SecurityTab.tsx
  • apps/web/src/hooks/useAuth.ts
  • apps/web/src/hooks/useDeviceApproval.ts
  • apps/web/src/routes/Login.tsx
  • apps/web/src/routes/SettingsPage.tsx
  • packages/api-client/openapi.json
  • tests/e2e/tests/mfa-flows.spec.ts
  • tests/e2e/utils/mfa-helpers.ts
  • tests/e2e/utils/wallet-login-helpers.ts

Comment thread apps/api/src/auth/auth.service.ts
Comment thread apps/api/src/auth/auth.service.ts Outdated
Comment thread apps/api/src/auth/auth.service.ts
Comment thread tests/e2e/utils/mfa-helpers.ts

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

🧹 Nitpick comments (1)
apps/api/src/auth/auth.service.spec.ts (1)

580-583: Consider removing the unused third mock.

The comment on line 583 indicates the userId fallback lookup is "not reached" in this test scenario. The third .mockResolvedValueOnce(mockAuthMethod) appears unnecessary and could be removed to clarify the expected call sequence.

♻️ Suggested simplification
       authMethodRepository.findOne
         .mockResolvedValueOnce(mockAuthMethod) // identity lookup with relations
-        .mockResolvedValueOnce(mockAuthMethod) // userId + identifierHash lookup
-        .mockResolvedValueOnce(mockAuthMethod); // userId fallback (not reached)
+        .mockResolvedValueOnce(mockAuthMethod); // userId + identifierHash lookup
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/auth/auth.service.spec.ts` around lines 580 - 583, The third
.mockResolvedValueOnce(mockAuthMethod) on authMethodRepository.findOne is unused
(comment says "not reached") and should be removed to simplify the test and
clarify expected call sequence; update the test to only set
mockResolvedValueOnce twice for authMethodRepository.findOne (for the identity
lookup and the userId+identifierHash lookup) and remove the redundant mock that
references mockAuthMethod.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/api/src/auth/auth.service.spec.ts`:
- Around line 580-583: The third .mockResolvedValueOnce(mockAuthMethod) on
authMethodRepository.findOne is unused (comment says "not reached") and should
be removed to simplify the test and clarify expected call sequence; update the
test to only set mockResolvedValueOnce twice for authMethodRepository.findOne
(for the identity lookup and the userId+identifierHash lookup) and remove the
redundant mock that references mockAuthMethod.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5daa73 and 4c88ac0.

📒 Files selected for processing (1)
  • apps/api/src/auth/auth.service.spec.ts

FSM1 and others added 2 commits February 27, 2026 04:10
…SHARE

Address CodeRabbit review feedback:
- Replace Like wildcard with exact equality for placeholder publicKey matching
- Replace identifier-hash lookup with direct userId lookup (payload.sub)
  for REQUIRED_SHARE temp auth — works for all auth types (email, Google, wallet)
- Replace Promise.race with Playwright locator.or() in E2E helpers
- Remove unused Like import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 5ce214f0d0b1
Use the staging signing key so Core Kit loginWithJWT validates
against the matching JWKS endpoint during MFA E2E tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: b770b3385c78

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/e2e/utils/mfa-helpers.ts (1)

122-139: Consider using data-testid for recovery phrase word elements.

Line 126 uses a class selector (.recovery-phrase-word) while all other locators in this file consistently use data-testid attributes. Class selectors can break if styling is refactored, whereas data-testid is explicitly for testing and more resilient.

♻️ Suggested refactor
-  const wordSpans = grid.locator('.recovery-phrase-word');
+  const wordSpans = grid.locator('[data-testid="recovery-phrase-word"]');

This would require adding data-testid="recovery-phrase-word" to each word span in the RecoveryPhraseGrid component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/utils/mfa-helpers.ts` around lines 122 - 139, In
extractRecoveryPhrase, replace the fragile class selector
'.recovery-phrase-word' with a data-testid selector (e.g.
'[data-testid="recovery-phrase-word"]') and update the RecoveryPhraseGrid
component to add data-testid="recovery-phrase-word" to each word span; then use
grid.locator('[data-testid="recovery-phrase-word"]') in the
extractRecoveryPhrase function to collect and trim the 24 words as before.
apps/api/src/auth/auth.service.ts (1)

86-87: Comment drift in REQUIRED_SHARE explanation.

Line 86-87 still describes auth-method identifier-hash lookup, but the implemented branch uses direct user.id = payload.sub lookup. Please align the comment with actual behavior to avoid future regressions.

Based on learnings, in this flow payload.sub from the CipherBox-issued JWT is the userId.

Also applies to: 108-110

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/auth/auth.service.ts` around lines 86 - 87, Update the
misleading comment around REQUIRED_SHARE to reflect that this branch looks up
the user by the CipherBox JWT subject (payload.sub) which is the userId, not by
an auth-method identifier hash; edit the comments near the REQUIRED_SHARE
handling in auth.service.ts (and the similar comment at the later block around
lines 108-110) so they reference payload.sub as the source of user.id and
describe the direct user.id = payload.sub lookup performed by the code (refer to
the function/method handling the CipherBox JWT user resolution in this file).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/auth/auth.service.ts`:
- Around line 111-120: The REQUIRED_SHARE "temp auth" path mints full sessions
because when loginDto.publicKey.startsWith('pending-core-kit-') you fall through
to the normal token issuance (createTokens(user.id, user.publicKey)); change
token issuance for that branch so tokens are scoped/short‑lived: either call a
new createTempTokens (or pass an options object to createTokens) that embeds a
temp marker (e.g., authLevel: "temp" or scope: "temp") and uses a much shorter
refresh/access TTL, and then update JwtAuthGuard/token validation to reject
tokens without the expected authLevel/scope for protected routes (or add
route-level scope guards on device-approval endpoints). Locate the logic around
the REQUIRED_SHARE check (loginDto.publicKey.startsWith('pending-core-kit-')),
the createTokens call, and JwtAuthGuard to implement these changes.

---

Nitpick comments:
In `@apps/api/src/auth/auth.service.ts`:
- Around line 86-87: Update the misleading comment around REQUIRED_SHARE to
reflect that this branch looks up the user by the CipherBox JWT subject
(payload.sub) which is the userId, not by an auth-method identifier hash; edit
the comments near the REQUIRED_SHARE handling in auth.service.ts (and the
similar comment at the later block around lines 108-110) so they reference
payload.sub as the source of user.id and describe the direct user.id =
payload.sub lookup performed by the code (refer to the function/method handling
the CipherBox JWT user resolution in this file).

In `@tests/e2e/utils/mfa-helpers.ts`:
- Around line 122-139: In extractRecoveryPhrase, replace the fragile class
selector '.recovery-phrase-word' with a data-testid selector (e.g.
'[data-testid="recovery-phrase-word"]') and update the RecoveryPhraseGrid
component to add data-testid="recovery-phrase-word" to each word span; then use
grid.locator('[data-testid="recovery-phrase-word"]') in the
extractRecoveryPhrase function to collect and trim the 24 words as before.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4c88ac0 and 521a112.

📒 Files selected for processing (4)
  • .github/workflows/e2e.yml
  • apps/api/src/auth/auth.service.spec.ts
  • apps/api/src/auth/auth.service.ts
  • tests/e2e/utils/mfa-helpers.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/src/auth/auth.service.spec.ts

Comment thread apps/api/src/auth/auth.service.ts
The secret is scoped to the 'staging' GitHub environment. Without
declaring the environment, the E2E job gets an empty value and the
API falls back to an ephemeral keypair — causing Core Kit JWT
verification to fail with "crypto/rsa: verification error".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: f8295f801413
FSM1 and others added 4 commits February 27, 2026 04:44
The staging environment has protection rules requiring approval,
which blocks the E2E job from starting. Moved the
IDENTITY_JWT_PRIVATE_KEY to a repo-level secret instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 85529942788d
The mock wallet from @johanneskares/wallet-mock doesn't handle
eth_chainId — unhandled methods fall through to the configured viem
transport. Using http() (default mainnet RPC) caused eth_chainId to
make a real HTTP request that hung in CI. Replace with a custom
transport that returns mock data for all fallthrough RPC methods.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 8e28ad0a0afc
After enableMfa() completes, the store is already set to
isMfaEnabled=true. Calling checkMfaStatus() in handleEnrollmentComplete
re-reads from Core Kit's getKeyDetails() which can return stale
totalFactors on Sapphire Devnet before metadata propagation completes,
overwriting the store back to false and showing [DISABLED].

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 3a2e842b2ada
The REQUIRED_SHARE login path was issuing full unrestricted session
tokens (15m access + 7d refresh) for temporary auth. Scoped tokens
now restrict access to device-approval endpoints only and skip
refresh token generation.

Changes:
- Add scope claim to JWT payload for scoped tokens
- TokenService.createTokens accepts scope and skipRefreshToken opts
- JwtAuthGuard enforces scope via @AllowScope decorator metadata
- Device-approval controller decorated with @AllowScope('device-approval')
- Auth controller skips refresh cookie for scoped tokens

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: f62a6e2f0cc7

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
apps/api/src/auth/services/token.service.spec.ts (1)

117-133: Test could verify refresh token is still created when only scope is provided.

This test passes { scope: ['device-approval'] } without skipRefreshToken, which should still create a refresh token. Consider adding an assertion that refreshTokenRepo.save was called:

       expect(jwtService.sign).toHaveBeenCalledWith(
         { sub: 'user-123', publicKey: 'public-key', scope: ['device-approval'] },
         { expiresIn: '15m' }
       );
+      expect(refreshTokenRepo.save).toHaveBeenCalled();
     });

This would make the test more complete by verifying the expected behavior difference between scope-only vs scope + skipRefreshToken.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/auth/services/token.service.spec.ts` around lines 117 - 133,
Update the test for createTokens to also assert a refresh token is created when
only options.scope is provided: after calling service.createTokens('user-123',
'public-key', { scope: ['device-approval'] }), add an expectation that
refreshTokenRepo.save was called (e.g., toHaveBeenCalled() or
toHaveBeenCalledTimes(1)) to ensure refreshTokenRepo.save is invoked; keep the
existing jwtService.sign assertion and do not set skipRefreshToken in this test
so it verifies the branch that persists a refresh token.
apps/api/src/auth/dto/login.dto.ts (1)

23-27: Consider tightening the placeholder suffix pattern.

The current regex pending-core-kit-.+ accepts any non-empty suffix. If the placeholder format is always pending-core-kit-{UUID}, you could constrain it further to prevent malformed values from reaching downstream validation:

-@Matches(/^(04[0-9a-fA-F]{128}|pending-core-kit-.+)$/, {
+@Matches(/^(04[0-9a-fA-F]{128}|pending-core-kit-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i, {

That said, since the auth service validates payload.sub as a valid userId anyway, the current pattern is functional—this is just a defense-in-depth suggestion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/auth/dto/login.dto.ts` around lines 23 - 27, Tighten the Matches
regex on the publicKey property in login.dto.ts: replace the permissive
"pending-core-kit-.+" branch in the `@Matches`(...) decorator with a
UUID-constraining pattern so placeholders must be "pending-core-kit-{UUID}" (use
a standard UUID regex: 8-4-4-4-12 hex groups, case-insensitive). Update the
decorator on the publicKey field (the `@Matches`(...) usage) to allow either the
uncompressed secp256k1 key or "pending-core-kit-" followed by a valid UUID.
apps/api/src/auth/services/token.service.ts (1)

37-42: Consider a typed interface for JWT payload.

Using Record<string, unknown> loses type safety. A typed interface would improve maintainability and catch payload issues at compile time.

♻️ Optional: Define a typed payload interface
+interface AccessTokenPayload {
+  sub: string;
+  publicKey: string;
+  scope?: string[];
+}
+
 async createTokens(
   userId: string,
   publicKey: string,
   options?: CreateTokenOptions
 ): Promise<TokenPair> {
-  const payload: Record<string, unknown> = { sub: userId, publicKey };
-  if (options?.scope) {
-    payload.scope = options.scope;
-  }
+  const payload: AccessTokenPayload = { sub: userId, publicKey };
+  if (options?.scope) {
+    payload.scope = options.scope;
+  }
   const accessToken = this.jwtService.sign(payload, { expiresIn: '15m' });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/auth/services/token.service.ts` around lines 37 - 42, Define a
proper typed interface for the JWT payload (e.g. JwtPayload with sub: string,
publicKey: string, and optional scope?: string | string[]) and replace the loose
Record<string, unknown> usage in token.service.ts: use the JwtPayload type for
the payload variable, ensure the optional scope is typed and assigned correctly,
and update the call to this.jwtService.sign to use the JwtPayload type (or
generics) so the payload structure is checked at compile time (refer to payload,
userId, publicKey, options?.scope, accessToken, and this.jwtService.sign).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/auth/guards/jwt-auth.guard.ts`:
- Around line 11-14: The current async canActivate in JwtAuthGuard awaits
super.canActivate(context) but doesn't handle the Observable<boolean>
possibility; change the canActivate signature to return Promise<boolean> |
boolean | Observable<boolean> and update the implementation to detect if
super.canActivate(context) returns an Observable (from super.canActivate),
convert it to a Promise using rxjs firstValueFrom, then await/return the
boolean; reference canActivate and super.canActivate(context) in JwtAuthGuard
when applying the change.

---

Nitpick comments:
In `@apps/api/src/auth/dto/login.dto.ts`:
- Around line 23-27: Tighten the Matches regex on the publicKey property in
login.dto.ts: replace the permissive "pending-core-kit-.+" branch in the
`@Matches`(...) decorator with a UUID-constraining pattern so placeholders must be
"pending-core-kit-{UUID}" (use a standard UUID regex: 8-4-4-4-12 hex groups,
case-insensitive). Update the decorator on the publicKey field (the
`@Matches`(...) usage) to allow either the uncompressed secp256k1 key or
"pending-core-kit-" followed by a valid UUID.

In `@apps/api/src/auth/services/token.service.spec.ts`:
- Around line 117-133: Update the test for createTokens to also assert a refresh
token is created when only options.scope is provided: after calling
service.createTokens('user-123', 'public-key', { scope: ['device-approval'] }),
add an expectation that refreshTokenRepo.save was called (e.g.,
toHaveBeenCalled() or toHaveBeenCalledTimes(1)) to ensure refreshTokenRepo.save
is invoked; keep the existing jwtService.sign assertion and do not set
skipRefreshToken in this test so it verifies the branch that persists a refresh
token.

In `@apps/api/src/auth/services/token.service.ts`:
- Around line 37-42: Define a proper typed interface for the JWT payload (e.g.
JwtPayload with sub: string, publicKey: string, and optional scope?: string |
string[]) and replace the loose Record<string, unknown> usage in
token.service.ts: use the JwtPayload type for the payload variable, ensure the
optional scope is typed and assigned correctly, and update the call to
this.jwtService.sign to use the JwtPayload type (or generics) so the payload
structure is checked at compile time (refer to payload, userId, publicKey,
options?.scope, accessToken, and this.jwtService.sign).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe7790a and 5526570.

📒 Files selected for processing (12)
  • apps/api/src/auth/auth.controller.ts
  • apps/api/src/auth/auth.service.spec.ts
  • apps/api/src/auth/auth.service.ts
  • apps/api/src/auth/decorators/allow-scope.decorator.ts
  • apps/api/src/auth/dto/login.dto.ts
  • apps/api/src/auth/guards/jwt-auth.guard.ts
  • apps/api/src/auth/services/token.service.spec.ts
  • apps/api/src/auth/services/token.service.ts
  • apps/api/src/auth/strategies/jwt.strategy.ts
  • apps/api/src/common/types.ts
  • apps/api/src/device-approval/device-approval.controller.ts
  • apps/web/src/components/mfa/SecurityTab.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/components/mfa/SecurityTab.tsx

Comment thread apps/api/src/auth/guards/jwt-auth.guard.ts
FSM1 and others added 2 commits February 27, 2026 12:49
Move pending auth state (pendingCipherboxJwt, pendingAuthMethod) from
useState in useAuth to Zustand store so it's shared across hook instances.
Add generation counter to useDeviceApproval to prevent stale requestApproval
completions from overwriting active ephemeral keys. Use stable refs for
requestApproval/cancelRequest in DeviceWaitingScreen to prevent effect
re-fires on upstream dependency cascades. Track handled request IDs to
filter stale polling responses after approve/deny.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 298b5fb3187c
Add test for payload.scope attachment to fix branch coverage (75% → 83%)
which was below the 80% threshold.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: fbf6eb4f48fe

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/hooks/useDeviceApproval.ts`:
- Around line 282-300: cancelRequest currently clears local state but doesn't
invalidate in-flight requestApproval calls; update cancelRequest to advance the
generation token stored in requestGenRef (e.g., increment requestGenRef.current
or set a new sentinel) after capturing/clearing requestIdRef so any concurrent
requestApproval that checks a captured generation will detect the mismatch and
abort re-registering pollers/requests; ensure requestApproval uses the
generation token from requestGenRef to ignore stale completions. Reference
cancelRequest, requestGenRef, requestApproval, requestIdRef,
stopRequesterPolling, clearEphemeralKey, and setApprovalStatus when making this
change.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5526570 and db48e11.

📒 Files selected for processing (6)
  • apps/api/src/auth/strategies/jwt.strategy.spec.ts
  • apps/web/src/components/mfa/DeviceWaitingScreen.tsx
  • apps/web/src/hooks/useAuth.ts
  • apps/web/src/hooks/useDeviceApproval.ts
  • apps/web/src/stores/auth.store.ts
  • tests/e2e/utils/mfa-helpers.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/e2e/utils/mfa-helpers.ts

Comment thread apps/web/src/hooks/useDeviceApproval.ts
…any from specs

Increment requestGenRef in cancelRequest so stale requestApproval()
completions detect the mismatch and abort (CodeRabbit review feedback).

Replace all `as any` casts in shares spec files with proper types
(RequestWithUser, User, ShareInvite) to resolve @typescript-eslint/
no-explicit-any lint warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 830be6c4b914

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

🧹 Nitpick comments (3)
apps/web/src/hooks/useDeviceApproval.ts (3)

403-413: Duplicated polling logic could be extracted.

The poll function here mirrors lines 316-328. Consider extracting a shared fetchFilteredPending helper to reduce duplication and ensure consistent handling.

💡 Example extraction
const fetchFilteredPending = useCallback(async () => {
  const pending = await deviceApprovalApi.getPending();
  const handled = handledRequestIdsRef.current;
  return handled.size > 0
    ? pending.filter((r) => r.requestId && !handled.has(r.requestId))
    : pending;
}, []);

Then use in both pollPendingRequests and the visibility effect.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/hooks/useDeviceApproval.ts` around lines 403 - 413, Extract the
duplicated polling/filtering logic into a shared helper (e.g.,
fetchFilteredPending) that calls deviceApprovalApi.getPending(), reads
handledRequestIdsRef.current, and returns the filtered pending array; make it a
stable function (useCallback) and replace the logic currently in poll (inside
useDeviceApproval) and the similar logic in pollPendingRequests / the visibility
effect to call fetchFilteredPending and setPendingRequests with its result.

319-324: Non-null assertion on r.requestId may mask type issues.

If the PendingApproval type allows requestId to be optional/undefined, this assertion could cause unexpected behavior. Consider a safer check.

💡 Suggested safer filter
-        const filtered =
-          handled.size > 0 ? pending.filter((r) => !handled.has(r.requestId!)) : pending;
+        const filtered =
+          handled.size > 0
+            ? pending.filter((r) => r.requestId && !handled.has(r.requestId))
+            : pending;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/hooks/useDeviceApproval.ts` around lines 319 - 324, The filter
uses a non-null assertion on r.requestId which can hide missing IDs; update the
filter in useDeviceApproval so it explicitly handles undefined requestId values
instead of using r.requestId!. For example, change the predicate used with
pending.filter to check if r.requestId is null/undefined and keep those items,
otherwise check handledRequestIdsRef.current.has(r.requestId) (references:
handledRequestIdsRef, pending, setPendingRequests, PendingApproval, requestId).
Ensure the logic still only removes items whose defined requestId is present in
handled.

71-74: handledRequestIdsRef is never cleared — potential memory accumulation.

The Set accumulates request IDs indefinitely. While individual IDs are small, clearing this when polling stops would prevent unbounded growth during long sessions.

💡 Proposed fix to clear on polling stop
  const stopApproverPolling = useCallback(() => {
    if (approverPollRef.current) {
      clearInterval(approverPollRef.current);
      approverPollRef.current = null;
    }
    isPollingPendingRef.current = false;
+   handledRequestIdsRef.current.clear();
  }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/hooks/useDeviceApproval.ts` around lines 71 - 74,
handledRequestIdsRef in useDeviceApproval is never cleared which can lead to
unbounded Set growth; add logic to clear handledRequestIdsRef.current (call
.clear()) when polling stops and on component unmount. Locate the polling effect
or functions in useDeviceApproval (the effect that starts/stops polling or the
startPolling/stopPolling handlers) and invoke
handledRequestIdsRef.current.clear() in the stop path and in the effect cleanup
so the Set is reset whenever polling is terminated or the hook unmounts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/src/hooks/useDeviceApproval.ts`:
- Around line 403-413: Extract the duplicated polling/filtering logic into a
shared helper (e.g., fetchFilteredPending) that calls
deviceApprovalApi.getPending(), reads handledRequestIdsRef.current, and returns
the filtered pending array; make it a stable function (useCallback) and replace
the logic currently in poll (inside useDeviceApproval) and the similar logic in
pollPendingRequests / the visibility effect to call fetchFilteredPending and
setPendingRequests with its result.
- Around line 319-324: The filter uses a non-null assertion on r.requestId which
can hide missing IDs; update the filter in useDeviceApproval so it explicitly
handles undefined requestId values instead of using r.requestId!. For example,
change the predicate used with pending.filter to check if r.requestId is
null/undefined and keep those items, otherwise check
handledRequestIdsRef.current.has(r.requestId) (references: handledRequestIdsRef,
pending, setPendingRequests, PendingApproval, requestId). Ensure the logic still
only removes items whose defined requestId is present in handled.
- Around line 71-74: handledRequestIdsRef in useDeviceApproval is never cleared
which can lead to unbounded Set growth; add logic to clear
handledRequestIdsRef.current (call .clear()) when polling stops and on component
unmount. Locate the polling effect or functions in useDeviceApproval (the effect
that starts/stops polling or the startPolling/stopPolling handlers) and invoke
handledRequestIdsRef.current.clear() in the stop path and in the effect cleanup
so the Set is reset whenever polling is terminated or the hook unmounts.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db48e11 and bc2c470.

📒 Files selected for processing (4)
  • apps/api/src/shares/invites.controller.spec.ts
  • apps/api/src/shares/share-invites.controller.spec.ts
  • apps/api/src/shares/shares.service.spec.ts
  • apps/web/src/hooks/useDeviceApproval.ts

FSM1 and others added 2 commits February 27, 2026 13:21
super.canActivate() can return boolean | Promise<boolean> | Observable<boolean>
per the CanActivate interface contract. Use firstValueFrom from rxjs to
convert Observable case before awaiting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: b4b5b389586a
…ceApproval

- Extract duplicated poll+filter logic into fetchFilteredPending callback
  used by both pollPendingRequests and the visibility effect
- Replace non-null assertion (r.requestId!) with explicit truthy check
- Clear handledRequestIdsRef when approver polling stops to prevent
  unbounded Set growth during long sessions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: b111b0fa91ff

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/web/src/hooks/useDeviceApproval.ts (1)

245-256: Zero-fill superseded ephemeral secret before early return.

When a requestApproval() generation is stale, the function returns without explicitly wiping the local ephemeral.secretKey. That key material remains in memory until GC.

♻️ Suggested hardening
 const requestApproval = useCallback(async () => {
   const gen = ++requestGenRef.current;
+  let localEphemeralSecret: Uint8Array | null = null;

   setApprovalStatus('requesting');
   setApprovalError(null);

   try {
     // 1. Generate ephemeral secp256k1 keypair
     // keygen() returns compressed pubkey (33 bytes) by default, but the
     // backend DTO requires uncompressed (65 bytes = 130 hex chars).
     const ephemeral = secp256k1.keygen();
-    ephemeralPrivKeyRef.current = ephemeral.secretKey;
-    const uncompressedPubKey = secp256k1.getPublicKey(ephemeral.secretKey, false);
+    localEphemeralSecret = ephemeral.secretKey;
+    ephemeralPrivKeyRef.current = localEphemeralSecret;
+    const uncompressedPubKey = secp256k1.getPublicKey(localEphemeralSecret, false);
     const ephemeralPubKeyHex = bytesToHex(uncompressedPubKey);

@@
     if (gen !== requestGenRef.current) {
+      localEphemeralSecret?.fill(0);
       // Stale: a newer requestApproval already started. Cancel this request
       // on the backend and bail — the newer call will set its own refs.
       try {
         await deviceApprovalApi.cancel(requestId);
       } catch {
         /* best-effort */
       }
       return;
     }

@@
   } catch (err) {
     // Only handle errors for the active generation
-    if (gen !== requestGenRef.current) return;
+    if (gen !== requestGenRef.current) {
+      localEphemeralSecret?.fill(0);
+      return;
+    }
     clearEphemeralKey();
     setApprovalError(err instanceof Error ? err.message : 'Failed to create approval request');
     setApprovalStatus('error');
   }
 }, [startRequesterPolling, clearEphemeralKey]);

Also applies to: 264-265

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/hooks/useDeviceApproval.ts` around lines 245 - 256, When
detecting a stale generation in the requestApproval flow (the if-check comparing
gen to requestGenRef.current), zero out the ephemeral secret before returning:
set ephemeral.secretKey (and any other ephemeral secret fields on the ephemeral
object) to a zeroed/empty value prior to calling deviceApprovalApi.cancel and
the early return; do the same zero-fill in the other early-return path
referenced in the comment (the latter ephemeral wipe around the requestId cancel
branch). Locate symbols requestApproval, gen, requestGenRef,
ephemeral.secretKey, deviceApprovalApi.cancel, and requestId to apply this
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/hooks/useDeviceApproval.ts`:
- Around line 105-106: The fetchFilteredPending() call can resolve after polling
has been stopped and then call setPendingRequests with stale data; add a
post-await guard that verifies polling is still active before updating state.
Introduce or reuse a ref flag (e.g., isPollingRef.current) that
stopApproverPolling() sets false and the poll starter sets true, and in the
places that call await fetchFilteredPending() (referenced by
fetchFilteredPending and the code paths that call setPendingRequests) check
isPollingRef.current (or that handledRequestIdsRef hasn’t been cleared)
immediately after the await and return early if polling stopped so
setPendingRequests is not invoked with stale results.

---

Nitpick comments:
In `@apps/web/src/hooks/useDeviceApproval.ts`:
- Around line 245-256: When detecting a stale generation in the requestApproval
flow (the if-check comparing gen to requestGenRef.current), zero out the
ephemeral secret before returning: set ephemeral.secretKey (and any other
ephemeral secret fields on the ephemeral object) to a zeroed/empty value prior
to calling deviceApprovalApi.cancel and the early return; do the same zero-fill
in the other early-return path referenced in the comment (the latter ephemeral
wipe around the requestId cancel branch). Locate symbols requestApproval, gen,
requestGenRef, ephemeral.secretKey, deviceApprovalApi.cancel, and requestId to
apply this change.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc2c470 and b7c9906.

📒 Files selected for processing (2)
  • apps/api/src/auth/guards/jwt-auth.guard.ts
  • apps/web/src/hooks/useDeviceApproval.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/src/auth/guards/jwt-auth.guard.ts

Comment thread apps/web/src/hooks/useDeviceApproval.ts
Check isPollingPendingRef after await to prevent setPendingRequests from
being called with stale data when the response resolves after polling
has been stopped.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: ffe4c5f371e3
@FSM1 FSM1 merged commit 133a541 into main Feb 27, 2026
14 checks passed
FSM1 added a commit that referenced this pull request Feb 27, 2026
Verify and close two active debug sessions after confirming all
issues are fixed on main with commit/PR cross-references.

corekit-auth-uat: 4 issues resolved (ISSUE-001 tab crash, ISSUE-003
JWKS persistence, ISSUE-004 SecurityTab wiring, ISSUE-002 documented).
Added post-session follow-up fixes (#205, #210, #213) and E2E coverage
mapping for 19 previously-skipped UAT test cases.

mfa-banner-footer-overlap: grid-area fix (#143) and z-index follow-up
verified on main with file/line references.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: b1ae6e4f2098
FSM1 added a commit that referenced this pull request Feb 27, 2026
Verify and close two active debug sessions after confirming all
issues are fixed on main with commit/PR cross-references.

corekit-auth-uat: 4 issues resolved (ISSUE-001 tab crash, ISSUE-003
JWKS persistence, ISSUE-004 SecurityTab wiring, ISSUE-002 documented).
Added post-session follow-up fixes (#205, #210, #213) and E2E coverage
mapping for 19 previously-skipped UAT test cases.

mfa-banner-footer-overlap: grid-area fix (#143) and z-index follow-up
verified on main with file/line references.


Entire-Checkpoint: b1ae6e4f2098

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
@FSM1 FSM1 deleted the feat/e2e-mfa-flows branch March 4, 2026 01:16
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