Where Are User Preferences and API Credentials Stored in FluentRead?

FluentRead stores general user preferences in Chrome extension storage under the key local:config via @wxt-dev/storage, while API credentials are isolated in Greasemonkey/Tampermonkey storage (GM_setValue/GM_getValue) with a token_ prefix to prevent exposure in plain text configuration files.

The open-source FluentRead project (available at bistutu/fluentread) implements a dual-layer storage architecture that separates user preferences from sensitive API credentials. Understanding where this data lives is critical for security auditing, backup procedures, and debugging configuration issues.

Storage Architecture Overview

FluentRead employs two distinct persistence mechanisms:

  1. Chrome Extension Storage (@wxt-dev/storage): Handles non-sensitive user preferences like language pairs, UI themes, and hotkeys.
  2. Userscript Storage (GM_setValue/GM_getValue): Isolates sensitive API credentials (OpenAI, ZhiPu, DeepL keys) in the userscript manager's secure storage.

This separation ensures that API keys never appear in the extension's exported JSON configuration, reducing the risk of accidental credential leakage.

Where User Preferences Are Stored

The Config Model and Schema

All user preferences are defined by the Config class in entrypoints/utils/model.ts. This TypeScript interface dictates the shape of the persisted JSON, including fields for sourceLanguage, targetLanguage, hotkey, theme, and selectedService.

// entrypoints/utils/model.ts
export class Config {
  sourceLanguage: string = 'auto';
  targetLanguage: string = 'en';
  hotkey: string = 'Alt+T';
  theme: string = 'light';
  selectedService: string = 'openai';
  // ... additional fields
}

Persistence Layer in Chrome Extension Storage

The actual persistence logic resides in entrypoints/utils/config.ts. During extension initialization, the loadConfig() function retrieves the JSON string stored under the key local:config using the @wxt-dev/storage API.

// entrypoints/utils/config.ts
import { Config } from "@/entrypoints/utils/model";

export let config: Config = new Config();
export const configReady = loadConfig();

async function loadConfig() {
  const value = await storage.getItem('local:config');
  if (typeof value === 'string' && value.trim().length > 0) {
    const parsedConfig = JSON.parse(value);
    if (isConfigObjectValid(parsedConfig)) {
      Object.assign(config, parsedConfig);
      return;
    }
  }
  // Initialize with defaults if no valid config exists
  await storage.setItem('local:config', JSON.stringify(config));
}

When users modify settings through the UI, settingManager.setSetting() in userscripts.js triggers an immediate writeback to local:config:

// Inside settingManager.setSetting() – userscripts.js
util.setValue('hotkey', util.getElementValue('fluent-read-hotkey'));
await storage.setItem('local:config', JSON.stringify(config));

Where API Credentials Are Stored

Isolation in Userscript Storage

API credentials follow a strict isolation policy. They are stored using the Greasemonkey/Tampermonkey storage API (GM_setValue and GM_getValue) inside userscripts.js. The tokenManager object prefixes each credential with token_ followed by the service identifier (e.g., token_openai, token_zhipu).

// tokenManager in userscripts.js
const tokenManager = {
  setToken: (model, value) => {
    // model is e.g. "openai", "zhipu", "yiyan"
    GM_setValue("token_" + model, value);
  },
  getToken: model => GM_getValue("token_" + model, '')
};

When a user saves a token in the settings panel, settingManager.setSetting() delegates to tokenManager.setToken(), which executes GM_setValue to persist the secret in the userscript manager's isolated storage.

Handling the New API Service

The "New API" service represents an exception to the isolated storage rule. Its API key is stored inside the config.token map (config.token[services.newapi]) and persists alongside general preferences in local:config. This occurs in entrypoints/utils/newApi.ts after user confirmation.

// entrypoints/utils/newApi.ts
export async function setNewApiKey(payload) {
  const apiKey = payload.apiKey || '';
  config.token[services.newapi] = apiKey;          // Stored in config
  await storage.setItem('local:config', JSON.stringify(config));
}

AK/SK Pairs for Baidu and Similar Services

For services requiring Access Key/Secret Key pairs (such as Baidu Yiyan), the system stores an object containing both values under the same token_<model> key in Greasemonkey storage.

// Storing AK/SK pair in userscripts.js
tokenManager.setToken(transModel.yiyan, {ak: accessKey, sk: secretKey});

Retrieval occurs through tokenManager.getToken(transModel.yiyan), which returns the object for use in API authentication headers.

Security Implications of the Storage Design

The architectural decision to separate general preferences from API credentials provides several security benefits:

  • Credential Isolation: Sensitive tokens never appear in the local:config JSON export, preventing accidental exposure when users share configuration backups.
  • Browser Sandbox: Greasemonkey storage operates within the userscript manager's isolated context, adding an additional access layer beyond standard extension storage.
  • No Hardcoded Secrets: As confirmed by the source analysis, no API keys exist in the repository; all credentials are runtime-generated and user-specific.

Summary

  • General Preferences: Stored in Chrome extension storage under local:config as JSON, managed by entrypoints/utils/config.ts using @wxt-dev/storage.
  • API Credentials: Isolated in Greasemonkey/Tampermonkey storage with token_ prefixes, handled by tokenManager in userscripts.js.
  • New API Exception: Stored within config.token[services.newapi] and persisted with general preferences in local:config via entrypoints/utils/newApi.ts.
  • AK/SK Pairs: Stored as objects ({ak, sk}) in Greasemonkey storage under their respective token_<model> keys.

Frequently Asked Questions

Does FluentRead store API keys in plain text within the extension files?

No. According to the source code in bistutu/fluentread, API keys are never hardcoded or stored as plain text within the extension's source files. Instead, they are persisted at runtime using the Greasemonkey/Tampermonkey storage API (GM_setValue/GM_getValue), which keeps them isolated from the extension's standard configuration JSON.

What happens to my settings if I uninstall the Chrome extension?

General user preferences stored under local:config in Chrome extension storage will be removed by the browser when the extension is uninstalled. However, API credentials stored via Greasemonkey/Tampermonkey storage may persist depending on your userscript manager's settings, as that storage is managed separately from the Chrome extension lifecycle.

Can I manually back up my FluentRead configuration?

Yes, but you must handle two separate storage locations. You can export the local:config JSON from Chrome's extension storage (via developer tools or storage inspection utilities) to back up general preferences. API credentials must be backed up separately from your userscript manager's storage interface, as they are not included in the local:config export for security reasons.

Why are API credentials stored separately from other preferences?

The separation exists to prevent accidental credential exposure. By storing API tokens in Greasemonkey storage with a token_ prefix rather than in the main local:config JSON, FluentRead ensures that sensitive keys are not included when users export or share their general configuration settings. This architectural choice adds a security boundary between user preferences and authentication secrets.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →