Skip to content

Feature/sidebar mode audit#2674

Merged
piyalbasu merged 25 commits intofeature/sidebar-mode-featurefrom
feature/sidebar-mode-audit
Apr 14, 2026
Merged

Feature/sidebar mode audit#2674
piyalbasu merged 25 commits intofeature/sidebar-mode-featurefrom
feature/sidebar-mode-audit

Conversation

@piyalbasu
Copy link
Copy Markdown
Contributor

@piyalbasu piyalbasu commented Apr 1, 2026

This pull request introduces several improvements and bug fixes related to the extension's sidebar functionality, including more robust sidebar connection management, improved message routing, and better handling of signing request rejection. It also adds and updates tests to ensure correct behavior and compatibility across Chrome and Firefox.

Sidebar connection and lifecycle management:

  • Added a new module (sidebarPort.ts) to manage the sidebar's long-lived port, allowing direct communication and safer handling of sidebar state transitions. [1] [2]
  • Improved sidebar disconnect logic to only reject signing requests handled by the sidebar, preventing accidental cancellation of requests managed by popups.
  • Enhanced logic to differentiate between sidebar reloads and true disconnects, deferring cleanup to avoid prematurely rejecting requests.
  • Introduced sidebarQueueUuids to track which signing requests are handled by the sidebar. [1] [2]

Message types and API changes:

  • Added new REJECT_SIGNING_REQUEST service type and message, and removed unused sidebar registration messages. [1] [2] [3]
  • Added rejectSigningRequest API method for programmatically rejecting signing requests.

Sidebar user experience and manifest:

  • Updated the manifest to add a sidebar_action entry for better Firefox support and improved sidebar icon handling.
  • Clarified and improved logic for the "open sidebar by default" feature, including Chrome/Firefox compatibility notes.

Message routing and navigation:

  • Changed sidebar navigation to use direct port messaging instead of broadcasting, improving reliability and security. [1] [2]

Testing improvements:

  • Added and updated tests for sidebar behavior, settings, and port management to ensure robust handling of edge cases and cross-browser support. [1] [2]

These changes collectively make the sidebar more robust, reliable, and compatible across browsers, while ensuring that signing requests and user actions are handled safely and predictably.

Screen.Recording.2026-04-02.at.1.08.39.PM.mov

piyalbasu and others added 3 commits April 1, 2026 13:12
…eliability improvements

Port all sidebar changes from sidebar-mode-audit: add Firefox sidebar_action
support, harden trust boundaries (sender validation, port origin checks,
isFromExtensionPage guard on OPEN_SIDEBAR), replace broadcast messaging with
direct port communication, add safeResolve to prevent double-resolving promises,
fix boolean coercion for isOpenSidebarByDefault, reject pending signing requests
on sidebar close, remove dead SIDEBAR_REGISTER/UNREGISTER code, and add
extension protocol guard to isSidebarMode.

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

Prevent rapid-fire signing phishing in sidebar mode by showing an
interstitial screen when a new signing request arrives while the user
is already reviewing one. Also hardens the SidebarSigningListener with
a route allowlist (only known signing routes are navigable) and runtime
type guards on port messages.

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

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves the browser extension’s “sidebar mode” integration by moving sidebar communication to a long-lived port, strengthening cross-browser (Chrome/Firefox) sidebar behavior, and adding UI + tests to handle concurrent signing flows more safely.

Changes:

  • Introduces a long-lived sidebar port connection and uses it for sidebar navigation + cleanup on disconnect.
  • Adds Firefox sidebar support (sidebar_action) and updates popup/sidebar opening logic for both Chrome and Firefox.
  • Adds a “new signing request” interstitial route/view plus new unit tests for sidebar and settings behavior.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
extension/src/popup/views/ConfirmSidebarRequest/index.tsx Adds interstitial screen to gate concurrent signing requests in sidebar mode
extension/src/popup/views/ConfirmSidebarRequest/styles.scss Styles for the new interstitial view
extension/src/popup/Router.tsx Registers the new interstitial route
extension/src/popup/metrics/views.ts Maps new route to a metrics event name
extension/src/popup/constants/routes.ts Adds confirmSidebarRequest route constant
extension/src/popup/constants/metricsNames.ts Adds a metric name for the new interstitial view
extension/src/popup/components/SidebarSigningListener/index.tsx Switches from broadcast messages to port-based sidebar navigation; adds route gating
extension/src/popup/components/account/AccountHeader/index.tsx Shows sidebar option for Chrome sidePanel and Firefox sidebarAction
extension/src/popup/helpers/navigate.ts Ensures sidebar open supports Firefox and closes popup after opening
extension/src/popup/helpers/isSidebarMode.ts Hardens sidebar-mode detection to extension origins only
extension/src/popup/locales/en/translation.json Adds new strings for sidebar mode + interstitial
extension/src/popup/locales/pt/translation.json Adds new strings for sidebar mode + interstitial (but see comment)
extension/src/background/index.ts Adds sidebar port connection listener; rejects/cleans queues on sidebar disconnect; adds Firefox click handling
extension/src/background/messageListener/freighterApiMessageListener.ts Uses port for sidebar navigation; uses browser.runtime.getURL; adds safeResolve guards
extension/src/background/messageListener/popupMessageListener.ts Removes sidebar register/unregister messages; restricts OPEN_SIDEBAR to extension-page senders
extension/src/background/messageListener/handlers/loadSettings.ts Changes parsing of isOpenSidebarByDefault (but see comment)
extension/src/background/messageListener/handlers/saveSettings.ts Changes parsing/return of isOpenSidebarByDefault (but see comment)
extension/src/background/messageListener/tests/sidebar.test.ts Adds tests for sidebar authorization + port/windowId management
extension/src/background/messageListener/tests/loadSaveSettings.test.ts Adds tests for sidebar-by-default setting parsing/behavior (but see comment)
extension/public/static/manifest/v3.json Adds Firefox sidebar_action manifest configuration
@shared/constants/services.ts Removes sidebar register/unregister service types; adds SIDEBAR_NAVIGATE constant
@shared/api/types/message-request.ts Removes SidebarRegister/Unregister message types
Comments suppressed due to low confidence (3)

extension/src/background/messageListener/freighterApiMessageListener.ts:446

  • browser.windows.onRemoved.addListener here does not filter by the popup window id, meaning unrelated window closes can trigger a declined response. Additionally, the anonymous listener can’t be removed and will accumulate across requests. Filter on popup.id and remove the listener after resolve.
        } else {
          browser.windows.onRemoved.addListener(() =>
            safeResolve({
              // return 2 error formats: one for clients running older versions of freighter-api, and one to adhere to the standard wallet interface
              apiError: FreighterApiDeclinedError,
              error: FreighterApiDeclinedError.message,
            }),
          );

extension/src/background/messageListener/freighterApiMessageListener.ts:535

  • This browser.windows.onRemoved listener is unscoped (no windowId check) and anonymous (can’t be removed), so it can decline requests when unrelated windows close and will leak listeners over time. Scope it to popup.id and unregister it after resolving.
        } else {
          browser.windows.onRemoved.addListener(() =>
            safeResolve({
              // return 2 error formats: one for clients running older versions of freighter-api, and one to adhere to the standard wallet interface
              apiError: FreighterApiDeclinedError,
              error: FreighterApiDeclinedError.message,
            }),
          );

extension/src/background/messageListener/freighterApiMessageListener.ts:638

  • browser.windows.onRemoved.addListener is added without checking which window was removed, so any window close can cause a decline. Because the callback is anonymous it can’t be removed, causing listener buildup. Use a named handler with removedWindowId === popup.id and remove it after resolve.
        } else {
          browser.windows.onRemoved.addListener(() =>
            safeResolve({
              // return 2 error formats: one for clients running older versions of freighter-api, and one to adhere to the standard wallet interface
              apiError: FreighterApiDeclinedError,
              error: FreighterApiDeclinedError.message,
            }),
          );

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread extension/src/background/index.ts Outdated
Comment thread extension/src/background/index.ts
Comment thread extension/src/background/messageListener/handlers/loadSettings.ts
Comment thread extension/src/background/messageListener/handlers/saveSettings.ts Outdated
Comment thread extension/src/background/messageListener/__tests__/loadSaveSettings.test.ts Outdated
Comment thread extension/src/popup/locales/pt/translation.json Outdated
Comment thread extension/src/background/messageListener/freighterApiMessageListener.ts Outdated
@piyalbasu piyalbasu added the don't review yet wip / tests in progress / missing videos or screenshots / pending self code-review label Apr 1, 2026
piyalbasu and others added 3 commits April 1, 2026 17:01
…form

Firefox requires sidebarAction.open() in a synchronous user gesture
handler and has no equivalent to Chrome's setPanelBehavior. Remove the
broken setPopup/onClicked approach, hide the preference toggle on
Firefox, add open_at_install: false to prevent auto-opening, and fix
boolean storage reads for isOpenSidebarByDefault.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
browser.storage.local preserves types, so getItem returns boolean
true/false, not string "true"/"false". Update test mocks to match.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…after safeResolve

Agent-Logs-Url: https://github.com/stellar/freighter/sessions/33844927-58ec-481b-9846-80c0e28a34b4

Co-authored-by: piyalbasu <6789586+piyalbasu@users.noreply.github.com>
@piyalbasu piyalbasu removed the don't review yet wip / tests in progress / missing videos or screenshots / pending self code-review label Apr 1, 2026
Left-align layout, use smaller square icon, pill-shaped buttons,
updated button variants and text per new design.

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

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread extension/src/popup/views/ConfirmSidebarRequest/index.tsx Outdated
Comment thread extension/src/background/index.ts Outdated
Comment thread extension/src/background/index.ts
Comment thread extension/src/popup/locales/pt/translation.json Outdated
@piyalbasu
Copy link
Copy Markdown
Contributor Author

@copilot apply changes based on the comments in this thread

piyalbasu and others added 2 commits April 2, 2026 13:38
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 2, 2026

@copilot apply changes based on the comments in this thread

Both issues from that review are addressed in 26200d0 — see replies on the individual comment threads above.

@piyalbasu
Copy link
Copy Markdown
Contributor Author

Code review

Found 3 issues:

  1. setAllowedStatus is missing sidebarQueueUuids tracking, unlike every other signing handler

Every other handler (requestAccess, submitToken, submitTransaction, submitBlob, submitAuthEntry) adds its UUID to sidebarQueueUuids when routed to the sidebar and deletes it in safeResolve. setAllowedStatus was updated with the safeResolve pattern in this PR but omits both sidebarQueueUuids.add(uuid) and sidebarQueueUuids.delete(uuid). If the sidebar disconnects while a setAllowedStatus request is pending, the disconnect handler in index.ts won't find the UUID in sidebarQueueUuids, so the dapp's promise hangs until the TTL timeout instead of being rejected immediately.

await openSigningWindow(`/grant-access?${encodeOrigin}`, 400);
return new Promise((resolve) => {
let resolved = false;
const safeResolve = (value: any) => {
if (resolved) return;
resolved = true;
resolve(value);
};
setTimeout(
() =>
safeResolve({
apiError: FreighterApiDeclinedError,
error: FreighterApiDeclinedError.message,
}),
QUEUE_ITEM_TTL_MS,

  1. ConfirmSidebarRequest "Reject" button navigates home but does not reject the pending signing request

handleReject only calls navigate(ROUTES.account). The new signing request that triggered this interstitial is still sitting in responseQueue with no resolution. The dapp is left with a hanging promise until the 5-minute TTL fires or the sidebar disconnects. All other rejection paths call responseQueueItem.response(undefined) to signal rejection back to the dapp immediately.

};
const handleReject = () => {
navigate(ROUTES.account);
};

  1. openSidebar() in navigate.ts moves window.close() inside the try block, so the popup is never closed on error

Previously window.close() ran unconditionally after the try/catch. Now if chrome.sidePanel.setOptions or .open throws (e.g., API unavailable), execution jumps to catch and window.close() is skipped, leaving the popup orphaned.

await chrome.sidePanel.open({ windowId: win.id! });
}
window.close();
} catch (e) {
console.error("Failed to open sidebar:", e);
}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

- Add sidebarQueueUuids tracking to setAllowedStatus handler so sidebar
  disconnect properly rejects the pending promise instead of hanging
- ConfirmSidebarRequest "Reject" now extracts UUID from the next route
  and calls rejectAccess so the dapp promise resolves immediately
- Ensure window.close() runs in the catch block of openSidebar() so the
  popup is closed even when the side panel API throws

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

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread extension/src/popup/constants/metricsNames.ts Outdated
Comment thread extension/src/popup/views/ConfirmSidebarRequest/index.tsx
…reload

chrome.sidePanel.open() can reload the sidebar page, causing a brief
port disconnect/reconnect. The disconnect handler was immediately
rejecting all in-flight signing requests before the new port could
connect.

Use a cancellable deferred cleanup: schedule rejection on disconnect,
but cancel it if a new sidebar port connects before it fires.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
piyalbasu and others added 2 commits April 2, 2026 16:47
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…mSidebarRequest reject

Agent-Logs-Url: https://github.com/stellar/freighter/sessions/da099665-1448-4e91-b2eb-4daa952a09bb

Co-authored-by: piyalbasu <6789586+piyalbasu@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 2, 2026

Code review

Found 3 issues:...

Addressed in c6e0f88:

  1. setAllowedStatus sidebarQueueUuids tracking — already present in the current code (both sidebarQueueUuids.add and .delete were in place from the previous commit), no change needed.
  2. ConfirmSidebarRequest Reject not rejecting — fixed by introducing REJECT_SIGNING_REQUEST and rejectSigningRequest handler that cleans all five queues; handleReject now calls it.
  3. openSidebar() window.close() — already fixed in the current code (window.close() is in both the try and catch blocks), no change needed.

piyalbasu and others added 2 commits April 2, 2026 16:58
Move sidebarQueueUuids population from freighterApiMessageListener
(eager, on openSigningWindow return) into the MARK_QUEUE_ACTIVE handler
in popupMessageListener (on signing view mount/unmount), matching how
activeQueueUuids is managed. Extract duplicated onWindowRemoved logic
into a shared rejectOnWindowClose helper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
16 tests covering deferred disconnect cleanup (cancellation on
reconnect, queue rejection, stale port handling) and sidebarQueueUuids
management via MARK_QUEUE_ACTIVE.

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

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread extension/public/static/manifest/v3.json
Comment thread extension/src/background/messageListener/handlers/rejectSigningRequest.ts Outdated
Comment thread extension/src/background/messageListener/popupMessageListener.ts
Extract setSidebarPort, clearSidebarPort, getSidebarPort from
freighterApiMessageListener into their own module to reduce coupling.
Update all imports across source and test files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add CLAUDE.md pointing to specs/ directory and a comprehensive
sidebar mode spec covering architecture, signing flow, Firefox
limitations, and E2E testing constraints.

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

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread extension/src/background/index.ts Outdated
Comment thread extension/src/background/messageListener/popupMessageListener.ts
piyalbasu and others added 2 commits April 3, 2026 12:06
…cleanup

Two fixes to sidebar queue management:

1. Move sidebarQueueUuids tracking from MARK_QUEUE_ACTIVE (view mount
   time) to openSigningWindow (routing time). This ensures requests
   behind the ConfirmSidebarRequest interstitial are properly cleaned
   up if the sidebar disconnects before the signing view mounts.

2. Only schedule deferred disconnect cleanup when the disconnecting
   port is the currently active sidebar port. Previously, a stale port
   disconnecting after a newer port connected would still schedule
   cleanup, potentially rejecting requests on the live sidebar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@@ -0,0 +1,153 @@
# Sidebar Mode Implementation Spec
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added this spec because there ended up being some "gotchas" with this implementation.

First, there's the difference in browser implementation

Second, the inability to add e2e tests with Playwright

I asked Claude how we can help future agents have an easier time making changes and it guided me towards this solution: this type of spec that is linked in CLAUDE.MD should help make any architectural decisions and prevent agents from going down dead-ends we've already investigated in the future

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's also nice to have a human readable description of what we're doing here for future human devs! 😄

Includes isSidebarMode() in the view metrics for signTransaction,
signAuthEntry, signMessage, grantAccess, and addToken so we can
distinguish sidebar vs popup signing requests in analytics.

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

CassioMG commented Apr 3, 2026

@piyalbasu overall I think the PR is looking good! Posting here some findings and suggestions from superpowers for you to take a look - thanks!

Important

I1. REJECT_SIGNING_REQUEST handler lacks isFromExtensionPage guard

File: extension/src/background/messageListener/popupMessageListener.tsREJECT_SIGNING_REQUEST case (~line 355)

Unlike OPEN_SIDEBAR which explicitly checks if (!isFromExtensionPage) return { error: "Unauthorized" }, REJECT_SIGNING_REQUEST has no sender validation. This handler resolves a dApp's pending signing promise with undefined — defense-in-depth warrants an explicit guard.

Fix: Add if (!isFromExtensionPage) return { error: "Unauthorized" } at the top of the case.

I2. Duplicated queue cleanup logic

Files: extension/src/background/index.ts (disconnect handler, ~lines 121-145) and extension/src/background/messageListener/handlers/rejectSigningRequest.ts

The disconnect handler duplicates the exact same iterate-find-splice-resolve pattern across all 5 queues. If either is updated (e.g., a new queue type), the other must be updated in lockstep.

Fix: Have the disconnect handler call rejectSigningRequest for each UUID, or extract a shared removeFromAllQueues(uuid, queues) helper.

I3. ConfirmSidebarRequest uses raw HTML instead of design system components

File: extension/src/popup/views/ConfirmSidebarRequest/index.tsx — lines 68-76

Uses <h1> with custom CSS font-size/font-weight and <div> for body text. Per project conventions: "Always use Text or Heading from @stellar/design-system — never raw HTML text elements with custom font styles."

Fix:

// Replace:
<h1 className="ConfirmSidebarRequest__title">{t("New Signing Request")}</h1>
<div className="ConfirmSidebarRequest__body">{t("A new signing request...")}</div>

// With:
<Heading as="h1" size="md" addlClassName="ConfirmSidebarRequest__title">{t("New Signing Request")}</Heading>
<Text as="p" size="sm" addlClassName="ConfirmSidebarRequest__body">{t("A new signing request...")}</Text>

Also in styles.scss: replace font-weight: 500 with var(--font-weight-medium), font-weight: 400 with var(--font-weight-regular), and remove the hardcoded font-family: "Inter", sans-serif.

I4. requestAccess and setAllowedStatus missing rejectOnWindowClose for popup windows (pre-existing)

File: extension/src/background/messageListener/freighterApiMessageListener.ts

requestAccess (~line 174) has if (popup === null) (sidebar TTL fallback) but no else if (popup !== null) branch calling rejectOnWindowClose. If the user closes the popup window, the dApp promise hangs until TTL (~5 min). Same for setAllowedStatus (~line 770).

Compare with submitTransaction, submitBlob, submitAuthEntry which all have the three-way branch: undefined (error), null (sidebar TTL), !== null (popup window close via rejectOnWindowClose).

Note: Pre-existing — not introduced by this PR. But since this audit added rejectOnWindowClose as a consolidated helper, it's a good opportunity to fix these two.

Fix: Add else if (popup !== null) { rejectOnWindowClose(popup.id!, safeResolve); } to both flows.

I5. submitToken missing sidebar TTL fallback (pre-existing)

File: extension/src/background/messageListener/freighterApiMessageListener.ts — submitToken (~line 283)

Has popup === undefined (error) and popup !== null (window close via rejectOnWindowClose), but no popup === null (sidebar) branch with TTL fallback. Inconsistent with the other signing flows. The sidebar disconnect cleanup via sidebarQueueUuids covers this, but the explicit TTL fallback is a safety net.

Note: Pre-existing.


Code Quality

C1. import statement placed after function definition

File: extension/src/background/messageListener/freighterApiMessageListener.ts — ~line 126

const rejectOnWindowClose = (...) => { ... };

import { DataStorageAccess } from "background/helpers/dataStorageAccess";

Import placed between the rejectOnWindowClose function and the main export. Works due to hoisting, but imports should be at the top of the file.

C2. safeResolve typed as (value: any) => void

File: extension/src/background/messageListener/freighterApiMessageListener.tsrejectOnWindowClose and all safeResolve closures

Using any loses type safety on the resolve value. Could be typed generically or at minimum use unknown.

C3. SIDEBAR_NAVIGATE exported as plain string constant, not in an enum

File: @shared/constants/services.ts:77

All other service types use the SERVICE_TYPES or EXTERNAL_SERVICE_TYPES enums. A comment explaining why it's separate (only used on ports, not through standard message routing) would help.

C4. safeResolve duplicated 6 times inline

File: extension/src/background/messageListener/freighterApiMessageListener.ts

The identical closure appears in all 6 signing handlers. Consider extracting a makeSafeResolve helper:

const makeSafeResolve = <T>(resolve: (value: T) => void) => {
  let resolved = false;
  return (value: T) => {
    if (resolved) return;
    resolved = true;
    resolve(value);
  };
};

Suggestions

S1. Derive SIGNING_ROUTE_PREFIXES from ALLOWED_NAV_PREFIXES

File: extension/src/popup/components/SidebarSigningListener/index.tsx

Two arrays that must stay in sync. Derive one from the other:

const SIGNING_ROUTE_PREFIXES = [...ALLOWED_NAV_PREFIXES, ROUTES.confirmSidebarRequest];

S2. Extract 500ms debounce to a named constant

File: extension/src/background/index.ts — ~line 147

export const SIDEBAR_DISCONNECT_DEBOUNCE_MS = 500;

S3. Guard ConfirmSidebarRequest route against non-sidebar access

File: extension/src/popup/Router.tsx

The route is registered globally. Consider redirecting to ROUTES.account if !isSidebarMode().

S4. window.close() in navigate.ts — use finally

File: extension/src/popup/helpers/navigate.ts

window.close() now appears in both try and catch. A finally block would be clearer.


Missing Test Coverage

  1. rejectSigningRequest handler — No dedicated tests. Should verify: UUID found in responseQueue -> response called with undefined; UUID not found -> Sentry exception captured; missing UUID -> early return; all queue types cleaned up.

  2. ConfirmSidebarRequest componentisValidNextRoute contains security-sensitive open-redirect validation. Should test edge cases: //evil.com, javascript:alert(1), empty strings, encoded characters, valid routes.

  3. SidebarSigningListener route allowlist — No tests verifying disallowed routes are blocked.


Nits

Location Issue
ConfirmSidebarRequest/styles.scss:27 --sds-clr-amber-* tokens — verify these exist in the SDS token set (CLAUDE.md only documents grayscale and red)
ConfirmSidebarRequest/styles.scss:37 font-weight: 500 → use var(--font-weight-medium)
ConfirmSidebarRequest/styles.scss:45 font-weight: 400 → use var(--font-weight-regular)
queueCleanup.ts:18-22 Comment could link to the actual cleanup code location

Summary Table

Category Count Items
Important 5 I1-I5 (I4-I5 pre-existing)
Code Quality 4 C1-C4
Suggestions 4 S1-S4
Missing Tests 3 rejectSigningRequest, ConfirmSidebarRequest, route allowlist
Nits 4 Style conventions

piyalbasu and others added 2 commits April 3, 2026 14:40
- M1: Make sender required in popupMessageListener so omitting it
  fails-closed instead of bypassing the extension-page guard
- M2: Add sender.id check to sidebar port validation for
  defense-in-depth alongside the existing tab and URL checks
- M3: Add isFromExtensionPage guard to REJECT_SIGNING_REQUEST handler
- M4: Add runtime windowId type validation in OPEN_SIDEBAR handler

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Important fixes:
- I2: Extract shared removeUuidFromAllQueues helper to eliminate duplicated
  queue cleanup logic between disconnect handler and rejectSigningRequest
- I3: Replace raw HTML <h1>/<div> with <Heading>/<Text> from @stellar/design-system
- I4: Add rejectOnWindowClose to requestAccess and setAllowedStatus so popup
  close rejects the dapp promise immediately instead of hanging until TTL
- I5: Add sidebar TTL fallback to submitToken for consistency with other handlers

Code quality:
- C1: Move misplaced DataStorageAccess import to top of file
- C2: Type safeResolve as unknown instead of any
- C3: Add comment explaining why SIDEBAR_NAVIGATE is a plain constant
- C4: Extract makeSafeResolve helper to replace 6 duplicated inline closures

Suggestions:
- S1: Derive SIGNING_ROUTE_PREFIXES from ALLOWED_NAV_PREFIXES
- S2: Extract SIDEBAR_DISCONNECT_DEBOUNCE_MS named constant
- S3: Guard ConfirmSidebarRequest route with SidebarOnlyRoute redirect
- S4: Use finally for window.close() in openSidebar

Nits:
- Replace hardcoded font-weight values with CSS custom properties
- Remove hardcoded font-family (inherited from design system)

Tests:
- Add rejectSigningRequest handler tests (6 cases)
- Add isValidNextRoute open-redirect validation tests (10 cases)
- Add SidebarSigningListener route allowlist tests (23 cases)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@piyalbasu piyalbasu merged commit a7211e0 into feature/sidebar-mode-feature Apr 14, 2026
3 checks passed
@piyalbasu piyalbasu deleted the feature/sidebar-mode-audit branch April 14, 2026 01:38
piyalbasu added a commit that referenced this pull request Apr 14, 2026
* Add sidebar mode support (#2632)

* Add sidebar mode support

- Route signing flows (grant access, sign tx, sign blob, sign auth entry) through the sidebar when it is open
- Add SIDEBAR_REGISTER/UNREGISTER handlers to track the active sidebar window
- Add SidebarSigningListener component so the sidebar can receive and navigate to signing requests
- Add "Sidebar mode" button in AccountHeader menu to open the sidebar manually
- Add openSidebar() helper for Chrome (sidePanel API) and Firefox (sidebarAction)
- Fix isFullscreenMode to exclude sidebar mode
- Add "Open sidebar mode by default" toggle in Settings > Preferences, persisted to local storage and applied on background startup via chrome.sidePanel.setPanelBehavior

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix sidebar mode bugs: signing window, cross-browser compat, promise timeouts, feature detection, and popup close behavior

- Fix setAllowedStatus bypassing openSigningWindow, causing signing windows to not appear when sidebar mode is disabled
- Replace chrome.storage.session with in-memory variable for cross-browser (Firefox) compatibility
- Add error handling for setPanelBehavior() calls
- Add 5-minute timeout to reject hanging Promises when sidebar closes mid-signing
- Replace Arc browser UA sniffing with feature detection (typeof globalThis.chrome?.sidePanel?.open)
- Fix popup not closing when opening sidebar mode
- Add missing isOpenSidebarByDefault to SignTransaction test mocks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix signing popup not appearing after sidebar is closed

Replace unreliable beforeunload-based SIDEBAR_UNREGISTER with a long-lived
port connection. Background now clears sidebarWindowId via onDisconnect when
the sidebar closes, preventing stale IDs from routing signing requests through
a closed sidebar instead of opening a popup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix flaky e2e test: restore test.slow() for View failed transaction

The test.slow() call was removed during the 5.38.0 refactoring, reducing
the timeout from 45s to 15s. This causes intermittent CI failures when
network stubs take longer to respond under sequential execution
(IS_INTEGRATION_MODE=true with workers=1).

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

* Fix e2e account history stubs to use context.route() instead of page.route()

Chrome extension service workers make fetch requests outside the page context,
so page.route() does not intercept them. Switching to context.route() ensures
the account history stubs are applied at the browser context level, which covers
both page and service worker requests.

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

* Fix View failed transaction test: use addInitScript to mock fetch

Playwright's context.route()/page.route() do not reliably intercept fetch
requests from Chrome extension popup pages in CI headless mode. Instead,
use page.addInitScript() to patch window.fetch directly in the extension
page's JavaScript environment. This approach injects code before any page
scripts run and is guaranteed to intercept the account-history fetch calls
regardless of the network interception mechanism.

Keep context.route() as a network-level fallback for environments where
route interception does work.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Piyal Basu <pbasu235@gmail.com>

* Feature/sidebar mode audit (#2674)

* feat: harden sidebar mode with Firefox support, security fixes, and reliability improvements

Port all sidebar changes from sidebar-mode-audit: add Firefox sidebar_action
support, harden trust boundaries (sender validation, port origin checks,
isFromExtensionPage guard on OPEN_SIDEBAR), replace broadcast messaging with
direct port communication, add safeResolve to prevent double-resolving promises,
fix boolean coercion for isOpenSidebarByDefault, reject pending signing requests
on sidebar close, remove dead SIDEBAR_REGISTER/UNREGISTER code, and add
extension protocol guard to isSidebarMode.

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

* security: add interstitial gate and route allowlist for sidebar signing flow

Prevent rapid-fire signing phishing in sidebar mode by showing an
interstitial screen when a new signing request arrives while the user
is already reviewing one. Also hardens the SidebarSigningListener with
a route allowlist (only known signing routes are navigable) and runtime
type guards on port messages.

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

* fix: add missing Portuguese translations for sidebar mode strings

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

* fix: remove Firefox "open sidebar by default" — not supported by platform

Firefox requires sidebarAction.open() in a synchronous user gesture
handler and has no equivalent to Chrome's setPanelBehavior. Remove the
broken setPopup/onClicked approach, hide the preference toggle on
Firefox, add open_at_install: false to prevent auto-opening, and fix
boolean storage reads for isOpenSidebarByDefault.

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

* fix: update loadSaveSettings tests to mock booleans instead of strings

browser.storage.local preserves types, so getItem returns boolean
true/false, not string "true"/"false". Update test mocks to match.

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

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: use named onRemoved handler checking popup.id and self-removing after safeResolve

Agent-Logs-Url: https://github.com/stellar/freighter/sessions/33844927-58ec-481b-9846-80c0e28a34b4

Co-authored-by: piyalbasu <6789586+piyalbasu@users.noreply.github.com>

* redesign ConfirmSidebarRequest to match updated Figma spec

Left-align layout, use smaller square icon, pill-shaped buttons,
updated button variants and text per new design.

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

* Update extension/src/popup/locales/pt/translation.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update extension/src/popup/views/ConfirmSidebarRequest/index.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: track sidebar UUIDs separately and guard port/windowId clear on disconnect

Agent-Logs-Url: https://github.com/stellar/freighter/sessions/7e15d7bf-0539-4ceb-80a3-eb9ae1ce7a9b

Co-authored-by: piyalbasu <6789586+piyalbasu@users.noreply.github.com>

* fix: address code review issues for sidebar signing flow

- Add sidebarQueueUuids tracking to setAllowedStatus handler so sidebar
  disconnect properly rejects the pending promise instead of hanging
- ConfirmSidebarRequest "Reject" now extracts UUID from the next route
  and calls rejectAccess so the dapp promise resolves immediately
- Ensure window.close() runs in the catch block of openSidebar() so the
  popup is closed even when the side panel API throws

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

* fix: defer sidebar disconnect cleanup to avoid rejecting during page reload

chrome.sidePanel.open() can reload the sidebar page, causing a brief
port disconnect/reconnect. The disconnect handler was immediately
rejecting all in-flight signing requests before the new port could
connect.

Use a cancellable deferred cleanup: schedule rejection on disconnect,
but cancel it if a new sidebar port connects before it fires.

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

* Update extension/src/popup/constants/metricsNames.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: add REJECT_SIGNING_REQUEST handler to clean all queues on ConfirmSidebarRequest reject

Agent-Logs-Url: https://github.com/stellar/freighter/sessions/da099665-1448-4e91-b2eb-4daa952a09bb

Co-authored-by: piyalbasu <6789586+piyalbasu@users.noreply.github.com>

* refactor: unify sidebarQueueUuids lifecycle with activeQueueUuids

Move sidebarQueueUuids population from freighterApiMessageListener
(eager, on openSigningWindow return) into the MARK_QUEUE_ACTIVE handler
in popupMessageListener (on signing view mount/unmount), matching how
activeQueueUuids is managed. Extract duplicated onWindowRemoved logic
into a shared rejectOnWindowClose helper.

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

* test: add coverage for sidebar disconnect cleanup and MARK_QUEUE_ACTIVE

16 tests covering deferred disconnect cleanup (cancellation on
reconnect, queue rejection, stale port handling) and sidebarQueueUuids
management via MARK_QUEUE_ACTIVE.

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

* refactor: move sidebar port helpers to background/helpers/sidebarPort

Extract setSidebarPort, clearSidebarPort, getSidebarPort from
freighterApiMessageListener into their own module to reduce coupling.
Update all imports across source and test files.

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

* fix: rejectSigningRequest returns consistent empty object instead of void

Agent-Logs-Url: https://github.com/stellar/freighter/sessions/2d47d7af-dc71-4412-8251-1486f2c3b5df

Co-authored-by: piyalbasu <6789586+piyalbasu@users.noreply.github.com>

* docs: add sidebar mode implementation spec

Add CLAUDE.md pointing to specs/ directory and a comprehensive
sidebar mode spec covering architecture, signing flow, Firefox
limitations, and E2E testing constraints.

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

* fix: track sidebar-routed UUIDs at routing time and guard disconnect cleanup

Two fixes to sidebar queue management:

1. Move sidebarQueueUuids tracking from MARK_QUEUE_ACTIVE (view mount
   time) to openSigningWindow (routing time). This ensures requests
   behind the ConfirmSidebarRequest interstitial are properly cleaned
   up if the sidebar disconnects before the signing view mounts.

2. Only schedule deferred disconnect cleanup when the disconnecting
   port is the currently active sidebar port. Previously, a stale port
   disconnecting after a newer port connected would still schedule
   cleanup, potentially rejecting requests on the live sidebar.

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

* docs: update sidebar spec to reflect queue tracking changes

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

* feat: add sidebarMode property to signing view metrics

Includes isSidebarMode() in the view metrics for signTransaction,
signAuthEntry, signMessage, grantAccess, and addToken so we can
distinguish sidebar vs popup signing requests in analytics.

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

* fix: harden sidebar message handling per security review

- M1: Make sender required in popupMessageListener so omitting it
  fails-closed instead of bypassing the extension-page guard
- M2: Add sender.id check to sidebar port validation for
  defense-in-depth alongside the existing tab and URL checks
- M3: Add isFromExtensionPage guard to REJECT_SIGNING_REQUEST handler
- M4: Add runtime windowId type validation in OPEN_SIDEBAR handler

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

* fix: address all findings from Cassio's code review

Important fixes:
- I2: Extract shared removeUuidFromAllQueues helper to eliminate duplicated
  queue cleanup logic between disconnect handler and rejectSigningRequest
- I3: Replace raw HTML <h1>/<div> with <Heading>/<Text> from @stellar/design-system
- I4: Add rejectOnWindowClose to requestAccess and setAllowedStatus so popup
  close rejects the dapp promise immediately instead of hanging until TTL
- I5: Add sidebar TTL fallback to submitToken for consistency with other handlers

Code quality:
- C1: Move misplaced DataStorageAccess import to top of file
- C2: Type safeResolve as unknown instead of any
- C3: Add comment explaining why SIDEBAR_NAVIGATE is a plain constant
- C4: Extract makeSafeResolve helper to replace 6 duplicated inline closures

Suggestions:
- S1: Derive SIGNING_ROUTE_PREFIXES from ALLOWED_NAV_PREFIXES
- S2: Extract SIDEBAR_DISCONNECT_DEBOUNCE_MS named constant
- S3: Guard ConfirmSidebarRequest route with SidebarOnlyRoute redirect
- S4: Use finally for window.close() in openSidebar

Nits:
- Replace hardcoded font-weight values with CSS custom properties
- Remove hardcoded font-family (inherited from design system)

Tests:
- Add rejectSigningRequest handler tests (6 cases)
- Add isValidNextRoute open-redirect validation tests (10 cases)
- Add SidebarSigningListener route allowlist tests (23 cases)

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: piyalbasu <6789586+piyalbasu@users.noreply.github.com>

---------

Co-authored-by: charles <62265764+sdfcharles@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: piyalbasu <6789586+piyalbasu@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants