Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/botnadzor/extension/llms.txt

Use this file to discover all available pages before exploring further.

Botnadzor uses @webext-core/proxy-service to enable seamless communication between the background service worker, content scripts, and popup. This architecture allows all extension contexts to access background services as if they were local objects.

Overview

Browser extensions run in multiple isolated contexts:
  • Background — Service worker that manages state and business logic
  • Content Scripts — Scripts injected into web pages (VK.com)
  • Popup — Extension UI shown when clicking the extension icon
These contexts cannot directly access each other’s memory, so @webext-core/proxy-service provides a transparent RPC (Remote Procedure Call) layer.

How It Works

The proxy service library uses browser extension messaging APIs to forward method calls from content scripts and popup to the background service worker.

In Background Context

Services are registered in src/entrypoints/background.ts:
src/entrypoints/background.ts
import { registerService } from "@webext-core/proxy-service";
import { AuthService } from "./background/@services/auth-service";
import { StaticListsService } from "./background/@services/static-lists-service";

// Create service instances
const authService = new AuthService({
  aliasManagerForDynamicApi,
});

const staticListsService = new StaticListsService({
  aliasManagerForStaticApi,
  rootConfigService,
});

// Register services with unique keys
registerService(authServiceKey, authService);
registerService(staticListsServiceKey, staticListsService);

In Content Script / Popup

Services are accessed using getService() from @webext-core/proxy-service:
import { getService } from "@webext-core/proxy-service";
import { authServiceKey } from "@/shared/proxy-service-keys";

// Get a proxy to the background service
const authService = getService(authServiceKey);

// Call methods as if it were a local object
const authStatus = await authService.getAuthStatus();

Type-Safe Service Keys

Service keys are defined in src/shared/proxy-service-keys.ts with full TypeScript type safety:
src/shared/proxy-service-keys.ts
import type { ProxyServiceKey } from "@webext-core/proxy-service";
import type { AuthService } from "../entrypoints/background/@services/auth-service";
import type { StaticListsService } from "../entrypoints/background/@services/static-lists-service";

export const authServiceKey: ProxyServiceKey<AuthService> = "auth-service";

export const staticListsServiceKey: ProxyServiceKey<StaticListsService> =
  "static-lists-service";

// ... 11 more service keys
The ProxyServiceKey<T> type ensures that:
  • Method calls are type-checked
  • Return types are inferred correctly
  • Only public methods are accessible

Registered Services

All 13 services are registered in src/entrypoints/background.ts:
src/entrypoints/background.ts
registerService(affiliationServiceKey, affiliationService);
registerService(authServiceKey, authService);
registerService(collectingServiceKey, collectingService);
registerService(dxConfigServiceKey, dxConfigService);
registerService(extensionVersionServiceKey, extensionVersionService);
registerService(frontendServiceKey, frontendService);
registerService(inspectorServiceKey, inspectorService);
registerService(notificationServiceKey, notificationService);
registerService(popupServiceKey, popupService);
registerService(regDateServiceKey, regDateService);
registerService(rootConfigServiceKey, rootConfigService);
registerService(staticListsServiceKey, staticListsService);
registerService(userConfigServiceKey, userConfigService);

Service Descriptions

Checks if accounts are affiliated with bot/spam lists.Key Methods:
  • checkAccount(accountIdentifier) — Check account affiliation
  • pollAffiliation() — Poll for affiliation changes
Manages user authentication with access codes.Key Methods:
  • getAuthStatus() — Get current auth status
  • setAccessCode(code) — Set access code
  • checkAuth() — Validate access code
  • fetchFromDynamicApiWithAccessCode() — Make authenticated API calls
Example:
const authStatus = await authService.getAuthStatus();
if (authStatus.state === "valid") {
  console.log("Authenticated with level:", authStatus.accessLevel);
}
Collects comment data for bot detection analysis.Key Methods:
  • collectCommentIfNeeded() — Register comment for collection
  • persistRegisteredCommentsIfNeeded() — Save collected data
Developer experience configuration for debugging.Key Methods:
  • get() — Get DX config
  • patch() — Update DX config
Tracks extension version and update notifications.Key Methods:
  • pollVersionInfo() — Get version information
Manages the frontend base URL for the Botnadzor web app.Key Methods:
  • getBaseUrl() — Get frontend base URL
Provides comment inspection tools for detailed analysis.Key Methods:
  • trigger() — Trigger inspector for a comment
  • pollState() — Poll inspector state
Browser notification management.Key Methods:
  • show() — Show browser notification
Popup state management.Key Methods:
  • pollState() — Poll popup state
  • setState() — Update popup state
Fetches VK account registration dates.Key Methods:
  • fetchRegDate(accountIdentifier) — Fetch registration date
Example:
const result = await regDateService.fetchRegDate({ vkId: 123456 });
if (result.success && result.regDate) {
  console.log("Registered:", result.regDate);
}
Remote configuration management.Key Methods:
  • get() — Get root configuration
  • poll() — Poll for config changes
Manages static lists (bots, insertions, etc.) with local/remote combining.Key Methods:
  • getItems(listId) — Get all items from a list
  • findItem(listId, index, value) — Find specific item
  • getListSummary(listId) — Get list statistics
  • addLocalItem(listId, item) — Add local item
  • removeLocalItem(listId, index, value) — Remove item
  • updateIfNeeded() — Update lists from remote
See Static Lists for detailed documentation.
User preferences and configuration.Key Methods:
  • get() — Get user config
  • patch() — Update user config

Service Architecture

Services follow consistent patterns:

Constructor Dependencies

Services declare dependencies in their constructor:
src/entrypoints/background/@services/auth-service.ts
export class AuthService {
  private aliasManagerForDynamicApi: AliasManager;
  private pollableAuthStatus: Pollable<AuthStatus>;

  constructor({
    aliasManagerForDynamicApi,
  }: {
    aliasManagerForDynamicApi: AliasManager;
  }) {
    this.aliasManagerForDynamicApi = aliasManagerForDynamicApi;
    this.pollableAuthStatus = new Pollable<AuthStatus>({ state: "unknown" });
    
    void this.checkAuth();
  }
}

Pollable State

Services use the Pollable pattern for reactive state:
private pollableAuthStatus: Pollable<AuthStatus>;

// Poll for changes (blocks until state changes)
async pollAuthStatus(
  lastPollVersion: PollVersion | undefined,
): Promise<PollResult<AuthStatus>> {
  return this.pollableAuthStatus.poll(lastPollVersion);
}

// Get current value (non-blocking)
getAuthStatus(): AuthStatus {
  return this.pollableAuthStatus.getValue();
}
Clients can poll for state changes:
let pollVersion: PollVersion | undefined;
while (true) {
  const result = await authService.pollAuthStatus(pollVersion);
  pollVersion = result.version;
  
  console.log("Auth status changed:", result.value);
}

Async Initialization

Services that need async setup start background tasks in the constructor:
constructor({ ... }) {
  // Synchronous setup
  this.pollableAuthInput = new Pollable(undefined);
  
  // Start async tasks without blocking
  void this.checkAuth();
  void this.startSyncingAuthInputWithStore();
}

Disposal

Services implement Symbol.dispose for cleanup:
private disposed = false;

[Symbol.dispose](): void {
  this.disposed = true;
}

private async someBackgroundTask() {
  while (!this.disposed) {
    // Do work...
    await delay(1000);
  }
}

Usage Examples

In Content Scripts

src/entrypoints/content/insertion-management.ts
import { staticListsService, dxConfigService } from "@/shared/proxy-services";

// Fetch insertion configs
const insertions = await staticListsService.getItems("insertions");
const dxConfig = await dxConfigService.get();

// Filter and apply configs
const configs = dxConfig.insertionsRemoved ? [] : insertions;
mountNewInsertions({ configs, instanceMap, contentId });

In Popup

src/entrypoints/popup/app/tabs/=access.tsx
import { authService } from "@/shared/proxy-services";

function AccessTab() {
  const [authStatus, setAuthStatus] = useState<AuthStatus>();

  useEffect(() => {
    // Poll for auth status changes
    let pollVersion: PollVersion | undefined;
    
    (async () => {
      while (true) {
        const result = await authService.pollAuthStatus(pollVersion);
        pollVersion = result.version;
        setAuthStatus(result.value);
      }
    })();
  }, []);

  const handleAccessCodeSubmit = (code: string) => {
    authService.setAccessCode(code);
  };

  return (
    <div>
      {authStatus?.state === "valid" ? (
        <p>Authenticated as {authStatus.accessLevel}</p>
      ) : (
        <input onChange={(e) => handleAccessCodeSubmit(e.target.value)} />
      )}
    </div>
  );
}

In Insertion Variants

src/entrypoints/content/insertion-variants/=account.ts
getServiceData: async ({ markupData, serviceLookup }) => {
  // serviceLookup contains proxy services
  const [accountAffiliation, frontendBaseUrl] = await Promise.all([
    serviceLookup.affiliationService.checkAccount(markupData.accountIdentifier),
    serviceLookup.frontendService.getBaseUrl(),
  ]);

  return { accountAffiliation, frontendBaseUrl };
}

Limitations

The proxy service has some limitations due to browser extension messaging constraints.

Non-Serializable Values

You cannot pass non-serializable values across contexts:
// ❌ Won't work: Functions cannot be serialized
await service.registerCallback(() => console.log("callback"));

// ❌ Won't work: DOM elements cannot be serialized
await service.processElement(document.body);

// ✅ Works: Plain objects are serializable
await service.updateConfig({ enabled: true, threshold: 0.5 });

Method Return Types

All service methods must return Promise<T> where T is JSON-serializable:
// ✅ Good: Returns serializable data
async getAuthStatus(): Promise<AuthStatus> {
  return this.pollableAuthStatus.getValue();
}

// ❌ Bad: Returns non-serializable function
getCallback(): () => void {
  return () => console.log("callback");
}

Performance Considerations

Call Overhead

Each service call involves:
  1. Serialization of arguments
  2. Browser messaging API call
  3. Deserialization in background context
  4. Method execution
  5. Serialization of result
  6. Response messaging
  7. Deserialization of result
This adds ~1-5ms overhead per call.

Batch Operations

When possible, batch multiple operations:
// ❌ Inefficient: 3 separate RPC calls
const affiliation1 = await affiliationService.checkAccount(id1);
const affiliation2 = await affiliationService.checkAccount(id2);
const affiliation3 = await affiliationService.checkAccount(id3);

// ✅ Better: Single RPC call with Promise.all
const [affiliation1, affiliation2, affiliation3] = await Promise.all([
  affiliationService.checkAccount(id1),
  affiliationService.checkAccount(id2),
  affiliationService.checkAccount(id3),
]);

Next Steps

Static Lists

Deep dive into the StaticListsService implementation

Entrypoints

Learn about the three extension entrypoints

Build docs developers (and LLMs) love