Skip to content

feat(accounts): Add external reading API integration and account storage change notification functionality #761

Open
chdown wants to merge 2 commits into
qixing-jk:mainfrom
chdown:main
Open

feat(accounts): Add external reading API integration and account storage change notification functionality #761
chdown wants to merge 2 commits into
qixing-jk:mainfrom
chdown:main

Conversation

@chdown
Copy link
Copy Markdown

@chdown chdown commented Apr 26, 2026

feat(accounts): Add external reading API integration and account storage change notification functionality

  • Add the notification function for storage changes in the account storage service
  • Implement the constant definition and message handler for the external read API
  • Add the interface component for the external read API settings
  • Integrate the external read API into the background script
  • Add relevant unit tests and integration tests
  • Implement the function of bridging notifications to the page context in the content script

Summary by CodeRabbit

Release Notes

  • New Features
    • Introduced External Read API integration for secure external access to stored data via access tokens
    • Added token management capabilities: generate, copy to clipboard, regenerate, and delete tokens
    • Added optional storage change notifications for external applications
    • Included privacy warnings detailing accessible data fields and access control
    • Localized settings in English, Japanese, Simplified Chinese, and Traditional Chinese

chdown added 2 commits April 26, 2026 23:02
…age change notification functionality

- Add the notification function for storage changes in the account storage service
- Implement the constant definition and message handler for the external read API
- Add the interface component for the external read API settings
- Integrate the external read API into the background script
- Add relevant unit tests and integration tests
- Implement the function of bridging notifications to the page context in the content script
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

📝 Walkthrough

Walkthrough

This pull request introduces an External Read API integration that enables external applications to access a user's persisted data (accounts and API credential profiles) via secure tokens. It adds runtime action routing, context methods for preference management, content/background message handlers, a settings UI component, localization strings, storage change notifications, and comprehensive tests.

Changes

Cohort / File(s) Summary
Runtime Actions & Constants
src/constants/runtimeActions.ts, src/services/integrations/externalReadApi/constants.ts
Added new ExternalReadApi prefix and two runtime action IDs (NotifyContent, StorageChanged) for routing external read API events. Defined namespace and event name constants for event handling.
User Preferences & Context
src/contexts/UserPreferencesContext.tsx, src/services/preferences/userPreferences.ts
Extended user preferences to support externalReadApi configuration with token management. Added context methods updateExternalReadApi and resetExternalReadApiConfig for preference persistence and updates. Introduced ExternalReadApiPreferences and ExternalReadApiAccessToken interfaces.
Background Message & Event Handling
src/entrypoints/background/index.ts, src/entrypoints/background/runtimeMessages.ts, src/services/integrations/externalReadApi/index.ts
Initialized external read API setup in background entrypoint. Added runtime message routing for storage-change events. Implemented comprehensive external read API module with request validation, token-based access control, snapshot/capability exports, storage change tracking, deduplication, and diff computation for multiple data types.
Content Script Message Handlers
src/entrypoints/content/messageHandlers/handlers/externalReadApi.ts, src/entrypoints/content/messageHandlers/handlers/index.ts, src/entrypoints/content/messageHandlers/index.ts
Added handleExternalReadApiNotify handler that receives external API notifications, validates/sanitizes payload data, and bridges notifications to the page context via CustomEvent and window.postMessage. Re-exported handler through module index and integrated into content script message routing.
UI Settings Components
src/features/BasicSettings/components/tabs/General/ExternalReadApiSettings.tsx, src/features/BasicSettings/components/tabs/General/GeneralTab.tsx
Implemented new settings page for managing external read API configuration, including token generation, copying, regeneration, and deletion with clipboard and toast notifications. Integrated component into General tab layout.
Localization
src/locales/en/settings.json, src/locales/ja/settings.json, src/locales/zh-CN/settings.json, src/locales/zh-TW/settings.json
Added externalReadApi configuration blocks in four languages with UI labels, descriptions, token management actions, status messages, and privacy warnings regarding data access and token invalidation.
Storage Change Notifications
src/services/accounts/accountStorage.ts, src/services/apiCredentialProfiles/apiCredentialProfilesStorage.ts
Implemented storage change notification helpers that send runtime messages to external read API with old/new config snapshots. Updated mutation/import flows to track pre-write state and notify after storage changes.
Manifest Configuration
wxt.config.ts
Added externally_connectable configuration allowing external connections from all http and https URLs for external read API accessibility.
Tests
tests/entrypoints/background/backgroundSuspendCleanup.test.ts, tests/entrypoints/background/runtimeMessages.more.test.ts, tests/entrypoints/content/messageHandlers/handlers/externalReadApi.test.ts, tests/entrypoints/options/ExternalReadApiSettings.test.tsx, tests/services/integrations/externalReadApi.test.ts, tests/services/userPreferences.test.ts, tests/features/BasicSettings/PermissionOnboardingDialog.languageSelection.test.tsx, tests/entrypoints/options/pages/UsageAnalytics/UsageAnalyticsCharts.test.tsx
Added comprehensive tests for external read API notification handler, settings UI component, full integration flow with token validation and snapshot exports, and user preferences defaults. Updated existing tests to mock required browser APIs and stub new components.

Sequence Diagram(s)

sequenceDiagram
    actor External Client
    participant Background
    participant Storage
    participant User Prefs

    External Client->>Background: External message (getCapabilities/getSnapshot + token)
    activate Background
    Background->>User Prefs: Validate token exists & enabled
    alt Token invalid/disabled
        Background->>External Client: Error response
    else Token valid
        Background->>Storage: Export accounts & profiles
        activate Storage
        Storage-->>Background: Data snapshot
        deactivate Storage
        Background->>External Client: Snapshot/capabilities response
    end
    deactivate Background
Loading
sequenceDiagram
    participant Storage
    participant Background
    participant ExternalReadApi
    participant Content Script
    participant All Tabs

    Storage->>Background: Storage change event
    activate Background
    Background->>ExternalReadApi: notifyExternalReadApiAccountStorageChanged()
    activate ExternalReadApi
    ExternalReadApi->>ExternalReadApi: Compute diff (created/updated/deleted)
    ExternalReadApi->>ExternalReadApi: Dedup via signature check
    alt Duplicate
        ExternalReadApi-->>Background: Suppress notification
    else New change
        ExternalReadApi->>Content Script: Runtime message (ExternalReadApiNotifyContent)
        activate Content Script
        Content Script->>All Tabs: Broadcast via window.postMessage()
        deactivate Content Script
    end
    deactivate ExternalReadApi
    deactivate Background
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

size:XXL


🐰 Hops with glee through data flows!
Tokens bloom, snapshots dance,
External readers peek and prance,
Storage whispers change so quick—
A rabbit's API magic trick!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding an external reading API integration with account storage change notifications across multiple systems.
Docstring Coverage ✅ Passed Docstring coverage is 93.48% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Apr 26, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/services/apiCredentialProfiles/apiCredentialProfilesStorage.ts (1)

632-649: ⚠️ Potential issue | 🟡 Minor

mergeConfig notifies with a coerced local rather than the on-disk previous.

Unlike importConfig (line 616) which clones the raw read result, mergeConfig passes the coerced local (line 635-637) as oldValue. This means external subscribers see the post-coercion shape on oldValue and the merged shape on newValue. If coercion was a no-op for the current data, the diff equals the actual change; otherwise the first event after a coercion-affecting upgrade may include synthetic differences.

For consistency with importConfig, consider snapshotting the raw readConfig() result before coercion:

♻️ Proposed adjustment
   async mergeConfig(raw: unknown): Promise<ApiCredentialProfilesConfig> {
     return this.withStorageWriteLock(async () => {
       const now = Date.now()
-      const local = coerceApiCredentialProfilesConfig(await this.readConfig(), {
-        now,
-      })
+      const previous = cloneConfig(await this.readConfig())
+      const local = coerceApiCredentialProfilesConfig(previous, { now })
       const incoming = coerceApiCredentialProfilesConfig(raw, { now })

       const merged = mergeApiCredentialProfilesConfigs({
         local,
         incoming,
         now,
       })

       await this.saveConfig(merged)
-      await notifyExternalReadApiProfilesStorageChanged(local, merged)
+      await notifyExternalReadApiProfilesStorageChanged(previous, merged)
       return merged
     })
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/apiCredentialProfiles/apiCredentialProfilesStorage.ts` around
lines 632 - 649, mergeConfig currently coerces the read config into `local` and
then calls notifyExternalReadApiProfilesStorageChanged(local, merged), causing
subscribers to see the coerced state as the "old" value; instead, capture the
raw on-disk result returned by readConfig() (like importConfig does) before
calling coerceApiCredentialProfilesConfig, and pass that raw snapshot as the
`oldValue` to notifyExternalReadApiProfilesStorageChanged while keeping `merged`
as the new value so subscribers get the true pre-merge on-disk state; update
mergeConfig to read and store the rawRead (from readConfig()), use
coerceApiCredentialProfilesConfig(rawRead, { now }) to produce `local`, and call
notifyExternalReadApiProfilesStorageChanged(rawRead, merged).
🧹 Nitpick comments (9)
src/entrypoints/content/messageHandlers/handlers/externalReadApi.ts (2)

14-20: Type the sendResponse parameter precisely instead of any.

The handler signature uses (res: any) => void, which loses type safety on the response shape and is inconsistent with the typed sendResponse callbacks used across src/entrypoints/background/runtimeMessages.ts. Consider a small typed alias.

♻️ Proposed change
+type ExternalReadApiNotifyResponse =
+  | { success: true }
+  | { success: false; error: string }
+
 export function handleExternalReadApiNotify(
   request: { payload?: ExternalReadApiNotifyPayload },
-  sendResponse: (res: any) => void,
+  sendResponse: (response: ExternalReadApiNotifyResponse) => void,
 ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/entrypoints/content/messageHandlers/handlers/externalReadApi.ts` around
lines 14 - 20, The sendResponse parameter of handleExternalReadApiNotify is
currently typed as (res: any) => void which loses type safety; replace this with
a precise callback type (e.g., a shared SendResponse<T> alias used in
runtimeMessages.ts) and import/use that alias so the response shape is enforced
— update the function signature in handleExternalReadApiNotify to use the typed
SendResponse<ResponsePayloadType> (or the existing alias from
src/entrypoints/background/runtimeMessages.ts) and adjust any call sites to
satisfy the new type.

50-50: return true is unnecessary here — sendResponse is invoked synchronously.

return true from a runtime.onMessage listener is only required to keep the message channel open for an asynchronous reply. Since sendResponse is already called in both branches (success and catch) before return, returning true has no effect. Returning undefined is fine; if the call site relies on a truthy return value to signal "handled", consider returning false or a more meaningful flag.

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

In `@src/entrypoints/content/messageHandlers/handlers/externalReadApi.ts` at line
50, The listener currently ends with an unnecessary "return true" in the
runtime.onMessage handler even though sendResponse is invoked synchronously in
both success and catch branches; remove the "return true" (or replace with an
explicit "return false" if you need a falsy handled flag) from the handler to
avoid implying an async response, and keep the existing synchronous sendResponse
calls in the same function (the runtime.onMessage listener and sendResponse
references will help you locate the code).
src/contexts/UserPreferencesContext.tsx (1)

1219-1245: Consider aligning fallback pattern with sibling update handlers.

prev.externalReadApi ?? DEFAULT_PREFERENCES.externalReadApi! relies on a non-null assertion, while sibling handlers like updateRedemptionAssist (lines 1123-1138) and updateWebAiApiCheck (lines 1179-1193) use a ?? ?? ({...} satisfies ...) chain that survives a DEFAULT_PREFERENCES shape change without crashing at runtime. Worth using the same defensive pattern for consistency.

♻️ Suggested adjustment
-          const merged = deepOverride(
-            prev.externalReadApi ?? DEFAULT_PREFERENCES.externalReadApi!,
-            updates,
-          )
+          const base =
+            prev.externalReadApi ??
+            DEFAULT_PREFERENCES.externalReadApi ??
+            ({
+              enabled: false,
+              notificationsEnabled: true,
+              tokens: [],
+            } satisfies ExternalReadApiPreferences)
+          const merged = deepOverride(base, updates)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/contexts/UserPreferencesContext.tsx` around lines 1219 - 1245, The
updateExternalReadApi handler uses a non-null assertion on DEFAULT_PREFERENCES
which can crash if the default shape changes; change the fallback to the same
defensive pattern used by siblings by replacing prev.externalReadApi ??
DEFAULT_PREFERENCES.externalReadApi! with a chained fallback like
prev.externalReadApi ?? DEFAULT_PREFERENCES.externalReadApi ?? ({ /* default
ExternalReadApiPreferences fields */ } satisfies ExternalReadApiPreferences) so
deepOverride always receives a concrete object; update references in
updateExternalReadApi (and keep deepOverride, ExternalReadApiPreferences,
userPreferences.savePreferences, and setPreferences usage intact).
tests/entrypoints/content/messageHandlers/handlers/externalReadApi.test.ts (1)

13-71: Consider adding a negative-path test for sanitization/error branches.

This single test only covers the happy path. Per the AI summary, handleExternalReadApiNotify "sanitizes/coerces the incoming payload" and has an error fallback that responds with a non-success result. A targeted test for at least one malformed payload (e.g., missing/invalid topics, throwing postMessage) would strengthen coverage of those branches.

As per coding guidelines: "For new executable files, functions, branches, listeners/controllers, or error fallback paths, include at least one targeted test covering the added behavior."

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

In `@tests/entrypoints/content/messageHandlers/handlers/externalReadApi.test.ts`
around lines 13 - 71, Add a negative-path unit test for
handleExternalReadApiNotify to cover sanitization and error fallback: create a
test that calls handleExternalReadApiNotify with a malformed payload (e.g.,
missing or invalid topics) and assert it returns the non-success response via
the sendResponse mock, does not dispatch the EXTERNAL_READ_API_EVENT_NAME event,
and handles postMessage failures (spy on window.postMessage to throw and assert
sendResponse receives the error path). Use the existing test's patterns
(sendResponse mock, eventListener registration, postMessage spy) to keep
setup/teardown consistent and verify both sanitization rejection and the
postMessage error branch.
src/features/BasicSettings/components/tabs/General/ExternalReadApiSettings.tsx (1)

241-241: Token value rendered in plain text.

The token Input is readOnly but always visible, so anyone glancing at the screen during a screen-share/screenshot can read every active token. Given the privacy warning explicitly notes these tokens grant access to access_token, sessionCookie, refreshToken, and apiKey data, consider matching the New API/Done Hub patterns in this same settings screen which use hideToken/showToken toggles for sensitive credentials.

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

In
`@src/features/BasicSettings/components/tabs/General/ExternalReadApiSettings.tsx`
at line 241, The token is currently rendered plain in ExternalReadApiSettings by
<Input value={entry.token} readOnly disabled={isSaving} />, exposing sensitive
credentials; change this to support a show/hide toggle like the New API/Done Hub
patterns: add local state (e.g., showToken) within ExternalReadApiSettings,
render the Input as masked when showToken is false (use input type="password" or
a masked display) and reveal the token only when toggled, and add a small toggle
button/icon next to the Input that flips showToken while respecting isSaving and
readOnly semantics so tokens remain hidden by default.
src/services/integrations/externalReadApi/index.ts (3)

81-103: Drop the empty JSDoc placeholders.

There are ~30 empty /**\n *\n */ blocks scattered through this file (e.g., lines 81–83, 92–94, 105–107, 116–118, 148–150, 158–160, 168–170, 182–184, …, 744–746). They add noise without explaining intent. Either delete them, or replace the ones above non-obvious helpers (e.g., buildTrackedChangeSignature, filterTrackedStorageChanges, handleTrackedStorageChange) with a one-liner that documents the invariant. As per coding guidelines: "Add brief inline comments or short code-block comments when non-obvious intent, invariants, edge cases, or protocol/browser constraints need clarification; do not narrate obvious code".

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

In `@src/services/integrations/externalReadApi/index.ts` around lines 81 - 103,
Remove the empty JSDoc comment blocks that add noise (e.g., the blank /** */
above createErrorResponse and createSuccessResponse) by deleting them; for
helper functions with non-obvious behavior (e.g., buildTrackedChangeSignature,
filterTrackedStorageChanges, handleTrackedStorageChange) replace the blank JSDoc
with a concise one-line comment describing the invariant or intent (not a full
narrative). Ensure any retained comment is a short inline description
immediately above the function declaration and remove all other purely empty
JSDoc blocks throughout the file.

372-372: JSON.stringify is unreliable for deep equality.

Comparing entries with JSON.stringify(previous) !== JSON.stringify(account) is sensitive to key ordering and (silently) drops undefined values, functions, and NaN. In practice this means two semantically equal account objects produced by different write paths can stringify to different strings (false positives → spurious account.updated events broadcast to all tabs), and conversely a change that only replaces value: 1 with value: undefined is invisible (false negative). It's also O(size) per entity on a hot path.

Prefer a small structural-equality helper (e.g., dequal if a dep is acceptable, or a hand-rolled deep-equal restricted to the known account/bookmark/profile shapes). Same applies to diffBookmarkChanges (line 406) and diffProfileChanges (line 440).

Also applies to: 406-406, 440-440

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

In `@src/services/integrations/externalReadApi/index.ts` at line 372, The current
equality check uses JSON.stringify(previous) !== JSON.stringify(account), which
is unreliable and slow; replace this brittle comparison in the account change
detection with a structural deep-equality helper that handles key ordering,
undefined/NaN, and only the known shapes used by your domain (accounts,
bookmarks, profiles). Implement or import a small deterministic deepEqual
function and use it where the code currently compares previous vs account, and
similarly replace JSON.stringify checks inside diffBookmarkChanges and
diffProfileChanges with the same deepEqual (or a shape-restricted comparator) to
avoid false positives/negatives and reduce overhead; reference the
variables/functions previous, account, diffBookmarkChanges, and
diffProfileChanges when making the changes.

302-338: Diff events drop entity identifiers — consumers can't tell which entity changed.

buildAccountEvent / buildBookmarkEvent / buildProfileEvent emit { type, topic } with no id (or other identifier) of the affected entity. As a result, when N accounts are created in a single batch, the broadcast contains N identical { type: "account.created", topic: "accounts" } entries — and external consumers must always re-pull getSnapshot to find out what changed. Two consequences worth confirming:

  1. The intent appears to be "coarse" notifications (the test name "broadcasts coarse events" suggests this is by design). If so, the duplicate entries are pure overhead — a Set-like collapse to one event per (type, topic) would be cheaper and equally informative.
  2. If finer granularity is intended, the per-entity id should be included in the event detail (and the account.updated branch at line 372 would also benefit from changedKeys so consumers know which fields changed).

Could you clarify which model is intended, and update the events / collapse duplicates accordingly?

Also applies to: 355-452

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

In `@src/services/integrations/externalReadApi/index.ts` around lines 302 - 338,
The change events currently emitted by buildAccountEvent, buildBookmarkEvent,
buildProfileEvent (and buildAccountConfigEvent) lack entity identifiers which
makes per-entity consumers impossible to use; decide between two fixes: (A) for
fine-grained events, update these functions to accept an entity identifier
(e.g., id: string) and include id in the returned ExternalReadApiChangeDetail
(and ensure account.updated builders also include changedKeys when available),
then update callers that create batches to pass each entity's id; or (B) for
coarse notifications, collapse duplicate events before broadcasting by
deduplicating the out-going list by (type, topic) — e.g., produce one event per
unique (type, topic) via a Set/map — and keep the existing payload shape; pick
one model and apply the corresponding change across buildAccountEvent,
buildBookmarkEvent, buildProfileEvent, buildAccountConfigEvent and the code path
that assembles/broadcasts the array so all consumers receive consistent event
granularity.
tests/services/integrations/externalReadApi.test.ts (1)

198-447: Add coverage for the remaining branches in externalReadApi/index.ts.

The suite exercises the happy paths plus disabled-feature and bad-token, but several added branches still have no targeted test:

  • Invalid namespacecreateErrorResponse("Invalid namespace.") (line 640–642).
  • Unsupported methodcreateErrorResponse("Unsupported method.") (line 651–653).
  • notificationsEnabled: falsenotifyAllTabs early-returns and sendTabMessageWithRetry is not called (line 500–502). Currently nothing pins this contract.
  • The exported runtime-message helper handleExternalReadApiStorageChangedMessage (line 668) — both the unsupported-storage-key branch and the success path.
  • The dedup path in filterTrackedStorageChanges / handleTrackedStorageChange (line 537–611): firing the same storage change twice with the same last_updated should only broadcast once.

As per coding guidelines: "For new executable files, functions, branches, listeners/controllers, or error fallback paths, include at least one targeted test covering the added behavior".

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

In `@tests/services/integrations/externalReadApi.test.ts` around lines 198 - 447,
Add unit tests to cover the remaining branches in externalReadApi/index.ts: (1)
callExternal with an invalid namespace and assert it resolves to
createErrorResponse("Invalid namespace."); (2) callExternal with a valid
namespace but unsupported method and assert createErrorResponse("Unsupported
method."); (3) set preferences.externalReadApi.notificationsEnabled = false,
trigger the storageChangedListener path that would normally call notifyAllTabs
and assert sendTabMessageWithRetry is NOT called; (4) import and call
handleExternalReadApiStorageChangedMessage directly to exercise the
unsupported-storage-key branch (pass a payload with an unknown key) and the
success path (pass a valid ACCOUNT_STORAGE_KEYS.ACCOUNTS change) asserting
expected return values and side effects; (5) test the deduplication by invoking
storageChangedListener/handleTrackedStorageChange twice with the same
last_updated and assert sendTabMessageWithRetry only broadcast once (covers
filterTrackedStorageChanges dedupe). Use existing helpers (callExternal,
setupIntegration, storageChangedListener, sendTabMessageWithRetryMock,
ACCOUNT_STORAGE_KEYS, EXTERNAL_READ_API_NAMESPACE) to implement each test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/entrypoints/content/messageHandlers/handlers/externalReadApi.ts`:
- Around line 32-40: The current broadcasting via window.dispatchEvent and
window.postMessage (using eventName and detail) exposes sensitive change events
to any page script; fix by gating broadcasts: add an allowlist check (e.g.,
allowedOrigins array) and only call window.postMessage when
window.location.origin or the target origin is in allowedOrigins, and include a
per-session token field in the posted message (e.g., add token) so receivers
must perform a handshake/validation before acting; also add a clear inline
comment in externalReadApi.ts explaining the security model, why dispatchEvent
is limited/unsafe for cross-origin listeners, and reference the handshake
validator function (e.g., validateHandshake) that listeners must call.

In
`@src/features/BasicSettings/components/tabs/General/ExternalReadApiSettings.tsx`:
- Around line 136-140: The handleDeleteToken function currently deletes a token
immediately; change it to first show a confirmation dialog (using the app's
existing modal/confirm utility used elsewhere) and only call saveSettings({
tokens: config.tokens.filter(entry => entry.id !== tokenId) }) if the user
confirms; ensure cancellation leaves state unchanged, disable the delete control
while awaiting the confirmation/save to avoid double clicks, and surface any
save errors to the user.

In `@src/locales/ja/settings.json`:
- Around line 147-164: The Japanese locale contains untranslated strings in the
externalReadApi block; update the keys copyToken, deleteToken, generateToken,
regenerateToken, and tokenManagement to Japanese (e.g., "copyToken" → "コピー",
"deleteToken" → "削除", "generateToken" → "Token を生成", "regenerateToken" → "Token
を再生成", "tokenManagement" → "アクセストークン") so they match the rest of the block and
zh-CN; also confirm whether "title" ("External Read API / Hook") should be
localized or intentionally kept as a brand and apply the chosen approach
consistently.

In `@src/services/accounts/accountStorage.ts`:
- Around line 138-148: The notification currently sends an oldValue (`previous`)
produced by getStorageConfig() (which uses the read normalizer) and a newValue
(`next`) produced by normalizeAccountStorageConfigForWrite(current), causing
mismatched shapes; fix by normalizing `previous` with the same write normalizer
before calling notifyExternalReadApiAccountStorageChanged so both `oldValue` and
`newValue` share the write-normalized shape (use
normalizeAccountStorageConfigForWrite(previous) or otherwise ensure both use the
same normalizer) in the mutation block where `previous`, `current`, `next`, and
`result` are computed.
- Around line 60-79: notifyExternalReadApiAccountStorageChanged currently awaits
sendMessageWithRetry and can block writers; change it to avoid blocking
background-side storage writes by making the notification fire-and-forget or
reduce retry attempts: either (A) call sendMessageWithRetry without awaiting
(void sendMessageWithRetry(...)) when in background context so
browser.storage.onChanged (already wired) provides delivery, and only await when
called from non-background contexts (detect via your existing context check or
add an isBackgroundContext helper), or (B) set sendMessageWithRetry options to
maxAttempts: 1 for background calls to avoid retries; keep the same payload
(RuntimeActionIds.ExternalReadApiStorageChanged, ACCOUNT_STORAGE_KEYS.ACCOUNTS,
oldValue, newValue) and retain the catch/log path for awaited calls.

In `@src/services/apiCredentialProfiles/apiCredentialProfilesStorage.ts`:
- Around line 44-63: The notifyExternalReadApiProfilesStorageChanged function
currently awaits sendMessageWithRetry inside write-locked code paths (called by
importConfig, mergeConfig, createProfile, updateProfile,
updateTelemetrySnapshot, removeTagIdFromAllProfiles, deleteProfile) which
serializes profile writes when no receiver is present; change it to be
non-blocking: do not await retries inside the lock—either call
sendMessageWithRetry in a fire-and-forget fashion (spawn async task and
swallow/log errors) or use a single-attempt notify API variant and log failures
immediately, or move the notification call out of the write lock to run after
the lock is released; update notifyExternalReadApiProfilesStorageChanged (and/or
its callers) to implement one of these approaches while preserving error logging
via getErrorMessage.

In `@tests/entrypoints/options/ExternalReadApiSettings.test.tsx`:
- Around line 177-189: Add an afterEach that undoes the global stubs to avoid
cross-test pollution: call vi.unstubAllGlobals() to revert
vi.stubGlobal("crypto") and restore navigator.clipboard to its prior value (save
the original clipboard in the surrounding scope before you stub it in
beforeEach, then use Object.defineProperty(globalThis.navigator, "clipboard", {
value: originalClipboard, configurable: true }) in afterEach). Ensure the
afterEach runs in the same test scope where vi.stubGlobal("crypto") and
Object.defineProperty(... "clipboard") are set so crypto and navigator.clipboard
are restored between tests.

In `@tests/services/integrations/externalReadApi.test.ts`:
- Around line 145-160: The test currently replaces globalThis.browser with an
ad-hoc stub (setting messageExternalListener/storageChangedListener) which
breaks the project-wide fake WebExtension behavior; instead import or use the
shared fake browser from wxt/testing/fake-browser (the one wired in
tests/setup.ts) and only add or augment runtime.onMessageExternal if missing—do
not overwrite globalThis.browser; locate the stub in this file referencing
globalThis.browser, messageExternalListener, storageChangedListener and replace
the assignment with code that obtains the fake from wxt/testing/fake-browser and
attaches an addListener wrapper for runtime.onMessageExternal and
storage.onChanged (or augment the existing fake with those properties) so
listener arrays, async response semantics, and sender plumbing remain consistent
with other tests.

In `@wxt.config.ts`:
- Around line 56-58: The externally_connectable block currently allows matches
["http://*/*", "https://*/*"], which exposes the extension API to every visited
origin; either narrow the match patterns to only the known sites/userscripts you
intend to support (update the externally_connectable.matches array) or, if the
wide scope is intentional (e.g., to support user-installed userscripts), add a
clear inline comment next to externally_connectable explaining that this broad
allowlist is deliberate and reference the service-level guard
ensureExternalAccessAllowed in
src/services/integrations/externalReadApi/index.ts so future maintainers
understand the rationale.
- Around line 56-58: The manifest's externally_connectable entry is
Firefox-unsupported and should be included only for non-Firefox builds; update
the manifest generation in wxt.config.ts to conditionally add the
externally_connectable object (the matches: ["http://*/*","https://*/*"] block)
only when env.browser (or the existing browser build variable used in that file)
is not 'firefox', so Firefox manifests omit the key and avoid validation
warnings while other browsers keep the entry.

---

Outside diff comments:
In `@src/services/apiCredentialProfiles/apiCredentialProfilesStorage.ts`:
- Around line 632-649: mergeConfig currently coerces the read config into
`local` and then calls notifyExternalReadApiProfilesStorageChanged(local,
merged), causing subscribers to see the coerced state as the "old" value;
instead, capture the raw on-disk result returned by readConfig() (like
importConfig does) before calling coerceApiCredentialProfilesConfig, and pass
that raw snapshot as the `oldValue` to
notifyExternalReadApiProfilesStorageChanged while keeping `merged` as the new
value so subscribers get the true pre-merge on-disk state; update mergeConfig to
read and store the rawRead (from readConfig()), use
coerceApiCredentialProfilesConfig(rawRead, { now }) to produce `local`, and call
notifyExternalReadApiProfilesStorageChanged(rawRead, merged).

---

Nitpick comments:
In `@src/contexts/UserPreferencesContext.tsx`:
- Around line 1219-1245: The updateExternalReadApi handler uses a non-null
assertion on DEFAULT_PREFERENCES which can crash if the default shape changes;
change the fallback to the same defensive pattern used by siblings by replacing
prev.externalReadApi ?? DEFAULT_PREFERENCES.externalReadApi! with a chained
fallback like prev.externalReadApi ?? DEFAULT_PREFERENCES.externalReadApi ?? ({
/* default ExternalReadApiPreferences fields */ } satisfies
ExternalReadApiPreferences) so deepOverride always receives a concrete object;
update references in updateExternalReadApi (and keep deepOverride,
ExternalReadApiPreferences, userPreferences.savePreferences, and setPreferences
usage intact).

In `@src/entrypoints/content/messageHandlers/handlers/externalReadApi.ts`:
- Around line 14-20: The sendResponse parameter of handleExternalReadApiNotify
is currently typed as (res: any) => void which loses type safety; replace this
with a precise callback type (e.g., a shared SendResponse<T> alias used in
runtimeMessages.ts) and import/use that alias so the response shape is enforced
— update the function signature in handleExternalReadApiNotify to use the typed
SendResponse<ResponsePayloadType> (or the existing alias from
src/entrypoints/background/runtimeMessages.ts) and adjust any call sites to
satisfy the new type.
- Line 50: The listener currently ends with an unnecessary "return true" in the
runtime.onMessage handler even though sendResponse is invoked synchronously in
both success and catch branches; remove the "return true" (or replace with an
explicit "return false" if you need a falsy handled flag) from the handler to
avoid implying an async response, and keep the existing synchronous sendResponse
calls in the same function (the runtime.onMessage listener and sendResponse
references will help you locate the code).

In
`@src/features/BasicSettings/components/tabs/General/ExternalReadApiSettings.tsx`:
- Line 241: The token is currently rendered plain in ExternalReadApiSettings by
<Input value={entry.token} readOnly disabled={isSaving} />, exposing sensitive
credentials; change this to support a show/hide toggle like the New API/Done Hub
patterns: add local state (e.g., showToken) within ExternalReadApiSettings,
render the Input as masked when showToken is false (use input type="password" or
a masked display) and reveal the token only when toggled, and add a small toggle
button/icon next to the Input that flips showToken while respecting isSaving and
readOnly semantics so tokens remain hidden by default.

In `@src/services/integrations/externalReadApi/index.ts`:
- Around line 81-103: Remove the empty JSDoc comment blocks that add noise
(e.g., the blank /** */ above createErrorResponse and createSuccessResponse) by
deleting them; for helper functions with non-obvious behavior (e.g.,
buildTrackedChangeSignature, filterTrackedStorageChanges,
handleTrackedStorageChange) replace the blank JSDoc with a concise one-line
comment describing the invariant or intent (not a full narrative). Ensure any
retained comment is a short inline description immediately above the function
declaration and remove all other purely empty JSDoc blocks throughout the file.
- Line 372: The current equality check uses JSON.stringify(previous) !==
JSON.stringify(account), which is unreliable and slow; replace this brittle
comparison in the account change detection with a structural deep-equality
helper that handles key ordering, undefined/NaN, and only the known shapes used
by your domain (accounts, bookmarks, profiles). Implement or import a small
deterministic deepEqual function and use it where the code currently compares
previous vs account, and similarly replace JSON.stringify checks inside
diffBookmarkChanges and diffProfileChanges with the same deepEqual (or a
shape-restricted comparator) to avoid false positives/negatives and reduce
overhead; reference the variables/functions previous, account,
diffBookmarkChanges, and diffProfileChanges when making the changes.
- Around line 302-338: The change events currently emitted by buildAccountEvent,
buildBookmarkEvent, buildProfileEvent (and buildAccountConfigEvent) lack entity
identifiers which makes per-entity consumers impossible to use; decide between
two fixes: (A) for fine-grained events, update these functions to accept an
entity identifier (e.g., id: string) and include id in the returned
ExternalReadApiChangeDetail (and ensure account.updated builders also include
changedKeys when available), then update callers that create batches to pass
each entity's id; or (B) for coarse notifications, collapse duplicate events
before broadcasting by deduplicating the out-going list by (type, topic) — e.g.,
produce one event per unique (type, topic) via a Set/map — and keep the existing
payload shape; pick one model and apply the corresponding change across
buildAccountEvent, buildBookmarkEvent, buildProfileEvent,
buildAccountConfigEvent and the code path that assembles/broadcasts the array so
all consumers receive consistent event granularity.

In `@tests/entrypoints/content/messageHandlers/handlers/externalReadApi.test.ts`:
- Around line 13-71: Add a negative-path unit test for
handleExternalReadApiNotify to cover sanitization and error fallback: create a
test that calls handleExternalReadApiNotify with a malformed payload (e.g.,
missing or invalid topics) and assert it returns the non-success response via
the sendResponse mock, does not dispatch the EXTERNAL_READ_API_EVENT_NAME event,
and handles postMessage failures (spy on window.postMessage to throw and assert
sendResponse receives the error path). Use the existing test's patterns
(sendResponse mock, eventListener registration, postMessage spy) to keep
setup/teardown consistent and verify both sanitization rejection and the
postMessage error branch.

In `@tests/services/integrations/externalReadApi.test.ts`:
- Around line 198-447: Add unit tests to cover the remaining branches in
externalReadApi/index.ts: (1) callExternal with an invalid namespace and assert
it resolves to createErrorResponse("Invalid namespace."); (2) callExternal with
a valid namespace but unsupported method and assert
createErrorResponse("Unsupported method."); (3) set
preferences.externalReadApi.notificationsEnabled = false, trigger the
storageChangedListener path that would normally call notifyAllTabs and assert
sendTabMessageWithRetry is NOT called; (4) import and call
handleExternalReadApiStorageChangedMessage directly to exercise the
unsupported-storage-key branch (pass a payload with an unknown key) and the
success path (pass a valid ACCOUNT_STORAGE_KEYS.ACCOUNTS change) asserting
expected return values and side effects; (5) test the deduplication by invoking
storageChangedListener/handleTrackedStorageChange twice with the same
last_updated and assert sendTabMessageWithRetry only broadcast once (covers
filterTrackedStorageChanges dedupe). Use existing helpers (callExternal,
setupIntegration, storageChangedListener, sendTabMessageWithRetryMock,
ACCOUNT_STORAGE_KEYS, EXTERNAL_READ_API_NAMESPACE) to implement each test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c451b535-8500-4416-8ca7-6e78ed7514f9

📥 Commits

Reviewing files that changed from the base of the PR and between 57d278b and 33e82ea.

📒 Files selected for processing (27)
  • src/constants/runtimeActions.ts
  • src/contexts/UserPreferencesContext.tsx
  • src/entrypoints/background/index.ts
  • src/entrypoints/background/runtimeMessages.ts
  • src/entrypoints/content/messageHandlers/handlers/externalReadApi.ts
  • src/entrypoints/content/messageHandlers/handlers/index.ts
  • src/entrypoints/content/messageHandlers/index.ts
  • src/features/BasicSettings/components/tabs/General/ExternalReadApiSettings.tsx
  • src/features/BasicSettings/components/tabs/General/GeneralTab.tsx
  • src/locales/en/settings.json
  • src/locales/ja/settings.json
  • src/locales/zh-CN/settings.json
  • src/locales/zh-TW/settings.json
  • src/services/accounts/accountStorage.ts
  • src/services/apiCredentialProfiles/apiCredentialProfilesStorage.ts
  • src/services/integrations/externalReadApi/constants.ts
  • src/services/integrations/externalReadApi/index.ts
  • src/services/preferences/userPreferences.ts
  • tests/entrypoints/background/backgroundSuspendCleanup.test.ts
  • tests/entrypoints/background/runtimeMessages.more.test.ts
  • tests/entrypoints/content/messageHandlers/handlers/externalReadApi.test.ts
  • tests/entrypoints/options/ExternalReadApiSettings.test.tsx
  • tests/entrypoints/options/pages/UsageAnalytics/UsageAnalyticsCharts.test.tsx
  • tests/features/BasicSettings/PermissionOnboardingDialog.languageSelection.test.tsx
  • tests/services/integrations/externalReadApi.test.ts
  • tests/services/userPreferences.test.ts
  • wxt.config.ts

Comment on lines +32 to +40
window.dispatchEvent(new CustomEvent(eventName, { detail }))
window.postMessage(
{
source: "all-api-hub-extension",
type: eventName,
detail,
},
window.location.origin,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify what data ends up in the broadcast `events` payload and whether origin filtering exists.
fd -e ts -e tsx | xargs rg -nP -C3 '\b(diffAccountChanges|diffProfileChanges|diffBookmarkChanges|diffAccountConfigChanges|notifyAllTabs)\b'
fd -e ts externalReadApi --type f | xargs rg -nP -C3 'notifyAllTabs|ExternalReadApiNotifyContent'

Repository: qixing-jk/all-api-hub

Length of output: 4571


🏁 Script executed:

#!/bin/bash
# Get the full context of externalReadApi.ts to understand the data flow
wc -l src/services/integrations/externalReadApi/index.ts
# Read the complete diff functions to see what's being exposed
sed -n '355,385p' src/services/integrations/externalReadApi/index.ts
sed -n '389,419p' src/services/integrations/externalReadApi/index.ts
sed -n '423,461p' src/services/integrations/externalReadApi/index.ts
sed -n '465,496p' src/services/integrations/externalReadApi/index.ts

Repository: qixing-jk/all-api-hub

Length of output: 3880


🏁 Script executed:

#!/bin/bash
# Get the notifyAllTabs function and event building logic
sed -n '500,612p' src/services/integrations/externalReadApi/index.ts

Repository: qixing-jk/all-api-hub

Length of output: 3045


🏁 Script executed:

#!/bin/bash
# Check the content handler file for any filtering/authorization logic
cat -n src/entrypoints/content/messageHandlers/handlers/externalReadApi.ts

Repository: qixing-jk/all-api-hub

Length of output: 1857


🏁 Script executed:

#!/bin/bash
# Search for any comments about security or origin filtering in externalReadApi files
rg -nP '(security|authorization|token|allowlist|filter|origin)' src/services/integrations/externalReadApi/ src/entrypoints/content/messageHandlers/handlers/externalReadApi.ts

Repository: qixing-jk/all-api-hub

Length of output: 1963


Broadcast of sensitive change events to all page scripts needs authorization gating.

The events payload contains diffed account/profile/bookmark changes (IDs and change types: created/updated/deleted), broadcast via window.dispatchEvent and window.postMessage to all scripts in the page context. Once the feature is enabled globally, any 3rd-party script (analytics, ads, embeds) on every page the user visits can passively listen and collect these notifications without ever presenting or validating a token.

Recommended mitigations:

  • Gate dispatch on a per-origin allowlist managed alongside the tokens, or
  • Include a token/handshake challenge in the message routing so the handler only fires after validation.
  • At minimum, add a comment documenting the security model and why any script can read these events once enabled.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/entrypoints/content/messageHandlers/handlers/externalReadApi.ts` around
lines 32 - 40, The current broadcasting via window.dispatchEvent and
window.postMessage (using eventName and detail) exposes sensitive change events
to any page script; fix by gating broadcasts: add an allowlist check (e.g.,
allowedOrigins array) and only call window.postMessage when
window.location.origin or the target origin is in allowedOrigins, and include a
per-session token field in the posted message (e.g., add token) so receivers
must perform a handshake/validation before acting; also add a clear inline
comment in externalReadApi.ts explaining the security model, why dispatchEvent
is limited/unsafe for cross-origin listeners, and reference the handshake
validator function (e.g., validateHandshake) that listeners must call.

Comment on lines +136 to +140
const handleDeleteToken = async (tokenId: string) => {
await saveSettings({
tokens: config.tokens.filter((entry) => entry.id !== tokenId),
})
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing confirmation for irreversible token deletion.

Deleting a token immediately invalidates external access for any client carrying that token (per the privacyWarning copy: "Deleting or disabling a token immediately cuts off that external access."). A single misclick on the trash icon will silently drop the token with no confirmation prompt and no undo.

Consider gating deletion behind a confirmation dialog (e.g., the existing modal/confirm utility used elsewhere in the app), or at least a two-step click-to-confirm pattern.

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

In
`@src/features/BasicSettings/components/tabs/General/ExternalReadApiSettings.tsx`
around lines 136 - 140, The handleDeleteToken function currently deletes a token
immediately; change it to first show a confirmation dialog (using the app's
existing modal/confirm utility used elsewhere) and only call saveSettings({
tokens: config.tokens.filter(entry => entry.id !== tokenId) }) if the user
confirms; ensure cancellation leaves state unchanged, disable the delete control
while awaiting the confirmation/save to avoid double clicks, and surface any
save errors to the user.

Comment on lines +147 to +164
"copyToken": "Copy",
"deleteToken": "Delete",
"description": "有効な access token を持つページやユーザースクリプトに、拡張機能の永続化データの読み取りを明示的に許可し、必要に応じて開いているページコンテキストへ軽量な変更通知を配信します。",
"emptyTokens": "利用可能な access token はまだありません。",
"enable": "外部の読み取り専用アクセスを有効化",
"enableDesc": "有効にすると、有効な token を持つ呼び出し元が All API Hub の永続化された主データ全体を問い合わせできます。",
"generateToken": "Generate Token",
"notifications": "変更通知を有効化",
"notificationsDesc": "有効にすると、開いているページが追跡対象データ変更後の fire-and-forget イベントを受け取れます。下流の配信保証や再試行は対象外です。",
"privacyWarning": "警告: 有効な token を持つ呼び出し元は、access_token、sessionCookie、refreshToken、apiKey などの機密フィールドを含む完全な永続化主データを読み取れます。token を削除または無効化すると、その外部アクセスは直ちに遮断されます。",
"regenerateToken": "Regenerate Token",
"title": "External Read API / Hook",
"tokenCopied": "Access token をコピーしました",
"tokenCopyFailed": "Access token のコピーに失敗しました",
"tokenCreatedAt": "作成日時: {{createdAt}}",
"tokenManagement": "Access tokens",
"tokenManagementDesc": "名前付き token 資格情報を作成します。下流はデータ読み取り時に各リクエストで token を渡すだけでよく、変更通知は上のトグルで別途制御します。",
"tokenNamePlaceholder": "例: metapi-sync"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Untranslated English strings in the Japanese locale.

Several values in the new externalReadApi block are bare English while the rest of the block is properly translated, and the zh-CN counterpart is fully localized. Please translate to Japanese for consistency:

  • copyToken: "Copy" → e.g. "コピー"
  • deleteToken: "Delete" → e.g. "削除"
  • generateToken: "Generate Token" → e.g. "Token を生成"
  • regenerateToken: "Regenerate Token" → e.g. "Token を再生成"
  • tokenManagement: "Access tokens" → e.g. "アクセストークン"

title ("External Read API / Hook") could reasonably stay as a feature brand, but please confirm intent — the zh-CN file translates the prose part. Based on coding guidelines, "Keep locale key shapes stable across languages; do not let one language drift into a pluralized or structurally different key family unless intentionally introduced for every locale" — the spirit applies here to translation completeness as well.

🌐 Suggested diff
   "externalReadApi": {
-    "copyToken": "Copy",
-    "deleteToken": "Delete",
+    "copyToken": "コピー",
+    "deleteToken": "削除",
     "description": "有効な access token を持つページやユーザースクリプトに、拡張機能の永続化データの読み取りを明示的に許可し、必要に応じて開いているページコンテキストへ軽量な変更通知を配信します。",
     "emptyTokens": "利用可能な access token はまだありません。",
     "enable": "外部の読み取り専用アクセスを有効化",
     "enableDesc": "有効にすると、有効な token を持つ呼び出し元が All API Hub の永続化された主データ全体を問い合わせできます。",
-    "generateToken": "Generate Token",
+    "generateToken": "Token を生成",
     "notifications": "変更通知を有効化",
     "notificationsDesc": "有効にすると、開いているページが追跡対象データ変更後の fire-and-forget イベントを受け取れます。下流の配信保証や再試行は対象外です。",
     "privacyWarning": "警告: 有効な token を持つ呼び出し元は、access_token、sessionCookie、refreshToken、apiKey などの機密フィールドを含む完全な永続化主データを読み取れます。token を削除または無効化すると、その外部アクセスは直ちに遮断されます。",
-    "regenerateToken": "Regenerate Token",
+    "regenerateToken": "Token を再生成",
     "title": "External Read API / Hook",
     "tokenCopied": "Access token をコピーしました",
     "tokenCopyFailed": "Access token のコピーに失敗しました",
     "tokenCreatedAt": "作成日時: {{createdAt}}",
-    "tokenManagement": "Access tokens",
+    "tokenManagement": "アクセストークン",
     "tokenManagementDesc": "名前付き token 資格情報を作成します。下流はデータ読み取り時に各リクエストで token を渡すだけでよく、変更通知は上のトグルで別途制御します。",
     "tokenNamePlaceholder": "例: metapi-sync"
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/locales/ja/settings.json` around lines 147 - 164, The Japanese locale
contains untranslated strings in the externalReadApi block; update the keys
copyToken, deleteToken, generateToken, regenerateToken, and tokenManagement to
Japanese (e.g., "copyToken" → "コピー", "deleteToken" → "削除", "generateToken" →
"Token を生成", "regenerateToken" → "Token を再生成", "tokenManagement" → "アクセストークン")
so they match the rest of the block and zh-CN; also confirm whether "title"
("External Read API / Hook") should be localized or intentionally kept as a
brand and apply the chosen approach consistently.

Comment on lines +60 to +79
async function notifyExternalReadApiAccountStorageChanged(
oldValue: AccountStorageConfig,
newValue: AccountStorageConfig,
) {
try {
await sendMessageWithRetry({
action: RuntimeActionIds.ExternalReadApiStorageChanged,
storageKey: ACCOUNT_STORAGE_KEYS.ACCOUNTS,
oldValue,
newValue,
})
} catch (error) {
logger.warn(
"Failed to notify external read API about account storage change",
{
error: getErrorMessage(error),
},
)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether handleExternalReadApiStorageChangedMessage is registered on background, and whether the integration also listens to browser.storage.onChanged for the same keys.
rg -nP -C3 'handleExternalReadApiStorageChangedMessage|ExternalReadApiStorageChanged'
rg -nP -C3 'storage\.onChanged' src/services/integrations/externalReadApi

Repository: qixing-jk/all-api-hub

Length of output: 5111


Review the throughput impact of await-ing sendMessageWithRetry during storage writes.

notifyExternalReadApiAccountStorageChanged awaits sendMessageWithRetry, which retries up to 3 times with exponential backoff on "Receiving end does not exist" errors. When the background context holds the write lock and no popup/options are open, this message fails recoverably and burns through retries (~500ms + 1000ms delay, plus message timeouts) before logging a warning—serializing other writers behind that locked await.

While browser.storage.onChanged (already wired at src/services/integrations/externalReadApi/index.ts:726) provides redundant delivery on the background side, the blocking await still incurs the latency cost:

  • For background-side writes: storage.onChanged will fire regardless, so the await adds unnecessary latency during retry failures.
  • For popup/options-side writes: the await is useful to confirm the message reached the background.

Consider:

  • Removing the await (fire-and-forget) since storage changes are delivered via the listener anyway.
  • Or, if you need to confirm delivery from non-background contexts, split the logic: void the notify on background (where storage.onChanged suffices), and await it elsewhere.
  • Or, set maxAttempts: 1 to avoid blocking on retries.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/accounts/accountStorage.ts` around lines 60 - 79,
notifyExternalReadApiAccountStorageChanged currently awaits sendMessageWithRetry
and can block writers; change it to avoid blocking background-side storage
writes by making the notification fire-and-forget or reduce retry attempts:
either (A) call sendMessageWithRetry without awaiting (void
sendMessageWithRetry(...)) when in background context so
browser.storage.onChanged (already wired) provides delivery, and only await when
called from non-background contexts (detect via your existing context check or
add an isBackgroundContext helper), or (B) set sendMessageWithRetry options to
maxAttempts: 1 for background calls to avoid retries; keep the same payload
(RuntimeActionIds.ExternalReadApiStorageChanged, ACCOUNT_STORAGE_KEYS.ACCOUNTS,
oldValue, newValue) and retain the catch/log path for awaited calls.

Comment on lines +138 to 148
const previous = this.cloneConfig(await this.getStorageConfig())
const current = this.cloneConfig(previous)
const { result, changed } = mutation(current)
if (!changed) {
return result
}

const next = normalizeAccountStorageConfigForWrite(current)
await this.storage.set(ACCOUNT_STORAGE_KEYS.ACCOUNTS, next)
await notifyExternalReadApiAccountStorageChanged(previous, next)
return result
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Notification payload mixes read- and write-normalized shapes.

previous is derived from getStorageConfig() (which applies normalizeAccountStorageConfigForRead), while next is normalizeAccountStorageConfigForWrite(current). External readers receiving oldValue/newValue in the same message will see asymmetric shapes for fields that differ between the read- and write-normalized forms (e.g., legacy fields filled in on read, dropped on write).

If subscribers diff oldValue against newValue, they will see spurious changes on the very first notification after a legacy read. Consider normalizing previous with the write normalizer (or both with the same normalizer) before sending.

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

In `@src/services/accounts/accountStorage.ts` around lines 138 - 148, The
notification currently sends an oldValue (`previous`) produced by
getStorageConfig() (which uses the read normalizer) and a newValue (`next`)
produced by normalizeAccountStorageConfigForWrite(current), causing mismatched
shapes; fix by normalizing `previous` with the same write normalizer before
calling notifyExternalReadApiAccountStorageChanged so both `oldValue` and
`newValue` share the write-normalized shape (use
normalizeAccountStorageConfigForWrite(previous) or otherwise ensure both use the
same normalizer) in the mutation block where `previous`, `current`, `next`, and
`result` are computed.

Comment on lines +44 to +63
async function notifyExternalReadApiProfilesStorageChanged(
oldValue: ApiCredentialProfilesConfig,
newValue: ApiCredentialProfilesConfig,
) {
try {
await sendMessageWithRetry({
action: RuntimeActionIds.ExternalReadApiStorageChanged,
storageKey: API_CREDENTIAL_PROFILES_STORAGE_KEYS.API_CREDENTIAL_PROFILES,
oldValue,
newValue,
})
} catch (error) {
logger.warn(
"Failed to notify external read API about profile storage change",
{
error: getErrorMessage(error),
},
)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Same notification cost-on-write concern as accountStorage.ts.

This helper has the same characteristic as notifyExternalReadApiAccountStorageChanged in src/services/accounts/accountStorage.ts: each call is await-ed inside a write-locked path, and if there is no live message receiver (e.g., only background is running), sendMessageWithRetry will retry up to 3× with exponential backoff before the warning is finally logged. Across importConfig, mergeConfig, createProfile, updateProfile, updateTelemetrySnapshot, removeTagIdFromAllProfiles, and deleteProfile, this serializes all profile writes behind that retry budget when no popup/options is open.

See the corresponding comment on src/services/accounts/accountStorage.ts for proposed mitigations (fire-and-forget, single-attempt config, or rely on browser.storage.onChanged when in-process).

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

In `@src/services/apiCredentialProfiles/apiCredentialProfilesStorage.ts` around
lines 44 - 63, The notifyExternalReadApiProfilesStorageChanged function
currently awaits sendMessageWithRetry inside write-locked code paths (called by
importConfig, mergeConfig, createProfile, updateProfile,
updateTelemetrySnapshot, removeTagIdFromAllProfiles, deleteProfile) which
serializes profile writes when no receiver is present; change it to be
non-blocking: do not await retries inside the lock—either call
sendMessageWithRetry in a fire-and-forget fashion (spawn async task and
swallow/log errors) or use a single-attempt notify API variant and log failures
immediately, or move the notification call out of the write lock to run after
the lock is released; update notifyExternalReadApiProfilesStorageChanged (and/or
its callers) to implement one of these approaches while preserving error logging
via getErrorMessage.

Comment on lines +177 to +189
vi.stubGlobal("crypto", {
randomUUID: vi.fn(() => "uuid-2"),
getRandomValues: vi.fn((values: Uint8Array) => {
values.fill(1)
return values
}),
})
Object.defineProperty(globalThis.navigator, "clipboard", {
value: {
writeText: vi.fn().mockResolvedValue(undefined),
},
configurable: true,
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if vitest is configured with auto-unstub.
fd -t f 'vitest.config' --exec cat

Repository: qixing-jk/all-api-hub

Length of output: 2704


🏁 Script executed:

cat -n tests/entrypoints/options/ExternalReadApiSettings.test.tsx | head -300

Repository: qixing-jk/all-api-hub

Length of output: 8615


Add afterEach to unstub globals and restore navigator.clipboard to prevent cross-test pollution.

vi.stubGlobal("crypto", ...) at line 177 and Object.defineProperty(globalThis.navigator, "clipboard", ...) at line 184 are set in beforeEach but never cleaned up. vi.clearAllMocks() does not revert stubs — only vi.unstubAllGlobals() does. Without explicit unstubGlobals: true in vitest config, these stubs persist across tests and may leak to other test files in the same worker.

Add afterEach cleanup
  beforeEach(() => {
    vi.clearAllMocks()
    mockUpdateExternalReadApi.mockResolvedValue(true)
    mockResetExternalReadApiConfig.mockResolvedValue(true)
    mockedUseUserPreferencesContext.mockReturnValue(createContextValue())
    vi.stubGlobal("crypto", {
      randomUUID: vi.fn(() => "uuid-2"),
      getRandomValues: vi.fn((values: Uint8Array) => {
        values.fill(1)
        return values
      }),
    })
    Object.defineProperty(globalThis.navigator, "clipboard", {
      value: {
        writeText: vi.fn().mockResolvedValue(undefined),
      },
      configurable: true,
    })
  })

+  afterEach(() => {
+    vi.unstubAllGlobals()
+  })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
vi.stubGlobal("crypto", {
randomUUID: vi.fn(() => "uuid-2"),
getRandomValues: vi.fn((values: Uint8Array) => {
values.fill(1)
return values
}),
})
Object.defineProperty(globalThis.navigator, "clipboard", {
value: {
writeText: vi.fn().mockResolvedValue(undefined),
},
configurable: true,
})
beforeEach(() => {
vi.clearAllMocks()
mockUpdateExternalReadApi.mockResolvedValue(true)
mockResetExternalReadApiConfig.mockResolvedValue(true)
mockedUseUserPreferencesContext.mockReturnValue(createContextValue())
vi.stubGlobal("crypto", {
randomUUID: vi.fn(() => "uuid-2"),
getRandomValues: vi.fn((values: Uint8Array) => {
values.fill(1)
return values
}),
})
Object.defineProperty(globalThis.navigator, "clipboard", {
value: {
writeText: vi.fn().mockResolvedValue(undefined),
},
configurable: true,
})
})
afterEach(() => {
vi.unstubAllGlobals()
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/entrypoints/options/ExternalReadApiSettings.test.tsx` around lines 177
- 189, Add an afterEach that undoes the global stubs to avoid cross-test
pollution: call vi.unstubAllGlobals() to revert vi.stubGlobal("crypto") and
restore navigator.clipboard to its prior value (save the original clipboard in
the surrounding scope before you stub it in beforeEach, then use
Object.defineProperty(globalThis.navigator, "clipboard", { value:
originalClipboard, configurable: true }) in afterEach). Ensure the afterEach
runs in the same test scope where vi.stubGlobal("crypto") and
Object.defineProperty(... "clipboard") are set so crypto and navigator.clipboard
are restored between tests.

Comment on lines +145 to +160
;(globalThis as any).browser = {
runtime: {
onMessageExternal: {
addListener: vi.fn((listener: RuntimeMessageExternalListener) => {
messageExternalListener = listener
}),
},
},
storage: {
onChanged: {
addListener: vi.fn((listener: StorageChangedListener) => {
storageChangedListener = listener
}),
},
},
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use wxt/testing/fake-browser instead of hand-rolling the browser global.

The test installs an ad-hoc globalThis.browser with stub addListener capture. The repo's testing convention is to mock WebExtension APIs through wxt/testing/fake-browser (set up in tests/setup.ts); doing it differently here means listener wiring, storage events, and runtime.onMessageExternal won't behave like the real shim used by every other test (e.g., listener arrays, async response semantics, sender plumbing).

If onMessageExternal is missing from fake-browser, augment it locally on top of the fake instance rather than replacing the whole browser object. As per coding guidelines: "WebExtension APIs should be mocked with wxt/testing/fake-browser".

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

In `@tests/services/integrations/externalReadApi.test.ts` around lines 145 - 160,
The test currently replaces globalThis.browser with an ad-hoc stub (setting
messageExternalListener/storageChangedListener) which breaks the project-wide
fake WebExtension behavior; instead import or use the shared fake browser from
wxt/testing/fake-browser (the one wired in tests/setup.ts) and only add or
augment runtime.onMessageExternal if missing—do not overwrite
globalThis.browser; locate the stub in this file referencing globalThis.browser,
messageExternalListener, storageChangedListener and replace the assignment with
code that obtains the fake from wxt/testing/fake-browser and attaches an
addListener wrapper for runtime.onMessageExternal and storage.onChanged (or
augment the existing fake with those properties) so listener arrays, async
response semantics, and sender plumbing remain consistent with other tests.

Comment thread wxt.config.ts
Comment on lines +56 to +58
externally_connectable: {
matches: ["http://*/*", "https://*/*"],
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Broad http://*/* + https://*/* matches expose the external API surface to every site the user visits.

While the service layer enforces per-token authentication (see ensureExternalAccessAllowed in src/services/integrations/externalReadApi/index.ts), this match pattern still lets any origin attempt to connect and probe the API (including timing/error-channel side observation). If the intent is to interoperate with specific known sites/userscripts, consider narrowing the matches list, or at least adding an inline comment explaining why the broadest possible scope is intentional ("user-installed userscripts run on arbitrary origins") so future maintainers know it's deliberate, not an oversight.

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

In `@wxt.config.ts` around lines 56 - 58, The externally_connectable block
currently allows matches ["http://*/*", "https://*/*"], which exposes the
extension API to every visited origin; either narrow the match patterns to only
the known sites/userscripts you intend to support (update the
externally_connectable.matches array) or, if the wide scope is intentional
(e.g., to support user-installed userscripts), add a clear inline comment next
to externally_connectable explaining that this broad allowlist is deliberate and
reference the service-level guard ensureExternalAccessAllowed in
src/services/integrations/externalReadApi/index.ts so future maintainers
understand the rationale.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does Firefox WebExtension manifest support externally_connectable in 2026?

💡 Result:

No, Firefox WebExtensions manifest does not support "externally_connectable" for web page communication in 2026. The key is documented in MDN, but explicitly states that web page communication via runtime.connect/sendMessage from web pages is not supported in Firefox (see bug 1319168). Extension-to-extension communication works without it (equivalent to {"ids": ["*"]}), and the related runtime.onMessageExternal/onConnectExternal APIs are supported since Firefox 54. However, the full feature, especially "matches" for websites, remains unimplemented as of the latest docs (updated 2025) and open bug status. No 2026 changes indicate implementation. Use content scripts or postMessage for web page to extension communication instead.

Citations:


Gate externally_connectable on non-Firefox browsers — this manifest key is not supported in Firefox (MDN docs, bug 1319168) and will produce validation warnings in Firefox builds. Since runtime.onMessageExternal also doesn't fire from web pages on Firefox, declaring this key is both ineffective and noisy. The surrounding manifest already branches on env.browser, so scope this declaration to non-Firefox to keep Firefox manifests clean.

Suggested fix
       // ensure can get site cookies, please refer to https://stackoverflow.com/a/70070106/22460724
       host_permissions: ["<all_urls>"],
-      externally_connectable: {
-        matches: ["http://*/*", "https://*/*"],
-      },
+      ...(env.browser === "firefox"
+        ? {}
+        : {
+            externally_connectable: {
+              matches: ["http://*/*", "https://*/*"],
+            },
+          }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wxt.config.ts` around lines 56 - 58, The manifest's externally_connectable
entry is Firefox-unsupported and should be included only for non-Firefox builds;
update the manifest generation in wxt.config.ts to conditionally add the
externally_connectable object (the matches: ["http://*/*","https://*/*"] block)
only when env.browser (or the existing browser build variable used in that file)
is not 'firefox', so Firefox manifests omit the key and avoid validation
warnings while other browsers keep the entry.

@github-actions
Copy link
Copy Markdown
Contributor

Preview build ready for commit 33e82ea.

Artifacts:

This build is for review only and is not released.

@qixing-jk
Copy link
Copy Markdown
Owner

这个PR和那个issue之间是什么关系?没有太明白

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 26, 2026

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 70.36082% with 115 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/services/integrations/externalReadApi/index.ts 71.31% 70 Missing ⚠️
...omponents/tabs/General/ExternalReadApiSettings.tsx 72.60% 19 Missing and 1 partial ⚠️
src/contexts/UserPreferencesContext.tsx 11.11% 16 Missing ⚠️
...ontent/messageHandlers/handlers/externalReadApi.ts 76.47% 3 Missing and 1 partial ⚠️
src/entrypoints/background/runtimeMessages.ts 33.33% 2 Missing ⚠️
src/services/preferences/userPreferences.ts 0.00% 2 Missing ⚠️
src/entrypoints/content/messageHandlers/index.ts 50.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@chdown
Copy link
Copy Markdown
Author

chdown commented Apr 26, 2026

这个PR和那个issue之间是什么关系?没有太明白

我的理解是外部三方平台太多了,如果都在插件内容去写后续很难解耦,所以直接提供一套对外访问的接口和消息组件,然后再通过中间层去桥接实现,比如我会通过 tampermonkey 脚本实现 通过消息自动或者手动更新新数据到 metapi

@qixing-jk
Copy link
Copy Markdown
Owner

外部API确实是一个很有创意的想法,少有的浏览器扩展暴露API,感谢创意和PR。等我处理完其他部分再来处理这个API的功能

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants