qevo
v1.0.15
Published
Cross-browser extension toolkit - Unified API for Chrome & Firefox extension development with messaging, storage, webRequest, and tab management
Maintainers
Readme
🚀 Qevo
The Ultimate Cross-Browser Extension Toolkit
Pronounced "keh-vo" • Unified API for Chrome & Firefox Extension Development
Stop fighting browser APIs. Start building extensions.
Features • Installation • Quick Start • Documentation • Examples
🎯 Why Qevo?
Building browser extensions is hard. Managing Chrome vs Firefox differences is harder. Qevo makes it effortless.
// ❌ Without Qevo - Manual cross-browser handling
const api = typeof browser !== 'undefined' ? browser : chrome;
api.runtime.sendMessage({ type: 'getData' }, (response) => {
if (api.runtime.lastError) {
console.error(api.runtime.lastError);
} else {
// Handle response
}
});
// ✅ With Qevo - Clean, modern, type-safe
import qevo from 'qevo';
const response = await qevo.sendMessageToBackground('getData', {});✨ Core Features
Qevo provides four powerful pillars for extension development:
📨 Intelligent Messaging
Modern async/await communication between all extension contexts:
- ✅ Background ↔ Content scripts
- ✅ Tab-to-tab messaging
- ✅ Broadcast to all tabs
- ✅ Promise-based with timeout/retry
- ✅ Type-safe payloads
- ✅ Event-driven listeners
🗂️ Advanced Storage
Professional-grade storage with features browsers should have:
- ✅ Automatic TTL (time-to-live)
- ✅ Expiration dates
- ✅ Event listeners (add/update/remove)
- ✅ Batch atomic operations
- ✅ Prefix/suffix key search
- ✅ Cross-environment (Chrome/Firefox/IndexedDB)
🌐 WebRequest Mastery
Complete control over HTTP traffic:
- ✅ All 9 webRequest events
- ✅ Block/redirect requests
- ✅ Modify headers
- ✅ Monitor traffic
- ✅ Authentication handling
- ✅ Simple, unified API
📑 Tab Management
Powerful tab control and queries:
- ✅ Find tabs by URL/title
- ✅ Query all tabs
- ✅ Current window/tab detection
- ✅ Cross-window operations
- ✅ Rich tab metadata
Plus: Auto-detected debug mode, environment detection, context validation, and full TypeScript support.
📦 Installation
npm install qevo🚀 Quick Start
1. Import Qevo
import qevo from 'qevo';
// That's it! No initialization, no setup, no config.
// Qevo is ready to use immediately.2. Send Your First Message
// In background script
qevo.message.on('ping', (data, sender, sendResponse) => {
sendResponse({ success: true, data: 'pong' });
});
// In content script
const response = await qevo.sendMessageToBackground('ping', {});
console.log(response.data); // "pong"3. Store Data with Auto-Expiration
// Store session token that expires in 1 hour
await qevo.storage.put('sessionToken', 'abc123', { ttl: 3600 });
// Retrieve it
const token = await qevo.storage.get<string>('sessionToken');You're now using Qevo! 🎉
📚 Comprehensive API
Qevo's messaging system revolutionizes cross-script communication with a modern, promise-based API that works consistently across Chrome and Firefox.
Core Concepts
Message Flow:
Content Script → Background Script → Another Tab
↓ ↓ ↓
Sender Processor ReceiverMessage Types: Unique string identifiers for different message handlers Payloads: Any JSON-serializable data Responses: Type-safe return values with success/error handling
📤 Sending Messages
qevo.sendMessageToBackground<T, R>(messageType, data, options?)
Send a message to the background script and wait for response.
Use Cases:
- Content script needs data from background
- Request background to perform action
- Query background state
Parameters:
messageType: string // Unique message identifier
data: T // Message payload (any JSON data)
options?: {
timeout?: number // Max wait time (default: 5000ms)
retries?: number // Retry attempts (default: 0)
retryDelay?: number // Delay between retries (default: 1000ms)
expectResponse?: boolean // Wait for response (default: true)
priority?: 'low' | 'normal' | 'high' // Message priority
}Returns: Promise<MessageResponse<R>>
interface MessageResponse<R> {
success: boolean; // Operation succeeded
data?: R; // Response data
error?: string; // Error message if failed
timestamp?: number; // Response timestamp
messageId?: string; // Unique message ID
duration?: number; // Round-trip time in ms
}Example:
// Content script - request user data
const response = await qevo.sendMessageToBackground<
{ userId: number }, // Request type
{ name: string, email: string } // Response type
>('getUserData', { userId: 123 }, {
timeout: 10000,
retries: 2,
priority: 'high'
});
if (response.success) {
console.log('User:', response.data.name);
} else {
console.error('Error:', response.error);
}qevo.sendMessageToTab<T, R>(tabId, messageType, data, options?)
Send a message to a specific tab's content script.
Use Cases:
- Background tells content script to update UI
- Tab-to-tab communication via background
- Trigger actions in specific tabs
Parameters:
tabId: number // Target tab ID
messageType: string // Message identifier
data: T // Message payload
options?: MessageOptions // Same as sendMessageToBackgroundExample:
// Background script - tell tab to show notification
const tabs = await qevo.getAllTabs();
for (const tab of tabs) {
if (tab.url.includes('example.com')) {
await qevo.sendMessageToTab(tab.tabId, 'showNotification', {
title: 'Update Available',
message: 'New version released!',
type: 'info'
});
}
}qevo.sendMessageToTabs<T>(messageType, data, filter?, options?)
Send a message to multiple tabs matching criteria.
Parameters:
messageType: string
data: T
filter?: {
urls?: string[] // URL patterns
active?: boolean // Only active tabs
currentWindow?: boolean // Only current window
}
options?: MessageOptionsExample:
// Notify all tabs on specific domain
await qevo.sendMessageToTabs('dataUpdated', { timestamp: Date.now() }, {
urls: ['https://example.com/*']
});qevo.broadcastMessage<T>(messageType, data, options?)
Broadcast a message to ALL tabs across all windows.
Use Cases:
- Global state changes
- System-wide notifications
- Sync updates across all tabs
Example:
// Background - broadcast theme change to all tabs
await qevo.broadcastMessage('themeChanged', {
theme: 'dark',
timestamp: Date.now()
});📥 Receiving Messages
qevo.message.on<T, R>(messageType, listener)
Register a listener for incoming messages.
Parameters:
messageType: string // Message type to listen for
listener: (
data: T, // Message payload
sender: MessageSender, // Sender info (tab, url, etc.)
sendResponse: (response: MessageResponse<R>) => void
) => void | Promise<void> | boolean | Promise<boolean>Sender Information:
interface MessageSender {
tab?: {
id: number;
windowId: number;
url: string;
title: string;
};
url?: string; // Sender's URL
origin?: string; // Sender's origin
}Response Patterns:
// 1. Synchronous response
qevo.message.on('getData', (data, sender, sendResponse) => {
sendResponse({ success: true, data: { id: 123 } });
});
// 2. Async response (return true!)
qevo.message.on('fetchUser', async (data, sender, sendResponse) => {
const user = await fetchUserFromAPI(data.userId);
sendResponse({ success: true, data: user });
return true; // IMPORTANT: Keeps channel open for async
});
// 3. No response expected
qevo.message.on('logEvent', (data) => {
console.log('Event logged:', data);
// No sendResponse call
});Example - Complex Handler:
interface AuthRequest {
username: string;
password: string;
}
interface AuthResponse {
token: string;
expiresAt: Date;
user: { id: number; name: string };
}
qevo.message.on<AuthRequest, AuthResponse>(
'authenticate',
async (data, sender, sendResponse) => {
try {
// Validate input
if (!data.username || !data.password) {
sendResponse({
success: false,
error: 'Username and password required'
});
return true;
}
// Perform authentication
const authResult = await authenticateUser(
data.username,
data.password
);
// Store token
await qevo.storage.put('authToken', authResult.token, {
ttl: 3600
});
// Send success response
sendResponse({
success: true,
data: authResult,
timestamp: Date.now()
});
} catch (error) {
sendResponse({
success: false,
error: error.message
});
}
return true; // Keep channel open for async
}
);qevo.message.off<T, R>(messageType, listener)
Remove a specific message listener.
Example:
const handler = (data, sender, sendResponse) => { ... };
qevo.message.on('myEvent', handler);
// Later... remove it
qevo.message.off('myEvent', handler);qevo.message.clear(messageType)
Remove ALL listeners for a message type.
Example:
qevo.message.clear('userData'); // Removes all 'userData' listeners🔍 Message API Utilities
qevo.message.getTypes()
Get all registered message types.
Returns: string[]
Example:
const types = qevo.message.getTypes();
console.log('Registered handlers:', types);
// ['getUserData', 'authenticate', 'updateSettings', ...]qevo.message.hasListeners(messageType)
Check if there are listeners for a message type.
Returns: boolean
Example:
if (!qevo.message.hasListeners('getData')) {
console.warn('No handler registered for getData');
}💡 Messaging Best Practices
1. Always Handle Errors
try {
const response = await qevo.sendMessageToTab(tabId, 'getData', {});
if (!response.success) {
console.error('Operation failed:', response.error);
}
} catch (error) {
console.error('Message delivery failed:', error);
}2. Use Timeouts for Unreliable Operations
const response = await qevo.sendMessageToBackground('syncData', data, {
timeout: 30000, // 30 seconds for slow operations
retries: 2 // Retry twice if fails
});3. Return true for Async Handlers
// ✅ Correct
qevo.message.on('asyncOp', async (data, sender, sendResponse) => {
await someAsyncOperation();
sendResponse({ success: true });
return true; // REQUIRED!
});
// ❌ Wrong - response channel closes immediately
qevo.message.on('asyncOp', async (data, sender, sendResponse) => {
await someAsyncOperation();
sendResponse({ success: true });
// Missing return true!
});4. Use TypeScript for Type Safety
interface GetUserRequest {
userId: number;
}
interface GetUserResponse {
id: number;
name: string;
email: string;
}
// Sender
const response = await qevo.sendMessageToBackground<
GetUserRequest,
GetUserResponse
>('getUser', { userId: 123 });
// Receiver
qevo.message.on<GetUserRequest, GetUserResponse>(
'getUser',
async (data, sender, sendResponse) => {
const user = await fetchUser(data.userId);
sendResponse({ success: true, data: user });
return true;
}
);Qevo's storage system (qevo.storage) goes far beyond basic key-value storage. It provides enterprise-grade features that browsers should have had from the start.
Core Concepts
Automatic Backend Selection:
- Chrome Extension:
chrome.storage.local(~10MB) - Firefox Extension:
browser.storage.local(~10MB) - Web/Worker Context: IndexedDB (~50MB+)
Key Features:
- ⏰ TTL (Time-To-Live): Auto-delete after X seconds
- 📅 Expiration Dates: Delete at specific time
- 🔔 Event Listeners: React to add/update/remove
- ⚡ Batch Operations: Atomic multi-operation transactions
- 🔍 Key Search: Prefix/suffix matching
- 🧹 Auto Cleanup: Background cleanup of expired items
💾 Basic Operations
qevo.storage.put<T>(key, value, options?)
Store a value with optional expiration.
Parameters:
key: string // Storage key
value: T // Any JSON-serializable data
options?: {
ttl?: number // Time-to-live in SECONDS
expires?: Date // Absolute expiration date
}Returns: Promise<void>
Examples:
// 1. Simple storage
await qevo.storage.put('username', 'john_doe');
// 2. With TTL (auto-delete after 5 minutes)
await qevo.storage.put('sessionToken', 'abc123', { ttl: 300 });
// 3. With expiration date
await qevo.storage.put('subscription', {
plan: 'premium',
features: ['feature1', 'feature2']
}, {
expires: new Date('2025-12-31T23:59:59Z')
});
// 4. Complex objects
await qevo.storage.put('userSettings', {
theme: 'dark',
language: 'en',
notifications: {
email: true,
push: false,
sms: true
}
}, { ttl: 86400 }); // 24 hoursqevo.storage.get<T>(key, usePrefix?)
Retrieve a value by key.
Parameters:
key: string // Storage key
usePrefix?: boolean // If true, finds first key starting with `key`Returns: Promise<T | null>
- Returns the value if found and not expired
- Returns
nullif not found or expired - Automatically removes expired items
Examples:
// 1. Simple retrieval
const username = await qevo.storage.get<string>('username');
if (username) {
console.log('Welcome back,', username);
}
// 2. Complex objects with type safety
interface UserSettings {
theme: 'light' | 'dark';
language: string;
notifications: {
email: boolean;
push: boolean;
sms: boolean;
};
}
const settings = await qevo.storage.get<UserSettings>('userSettings');
if (settings) {
applyTheme(settings.theme);
setLanguage(settings.language);
}
// 3. Prefix matching - gets first key starting with 'session_'
const firstSession = await qevo.storage.get('session_', true);
// 4. With default fallback
const theme = await qevo.storage.get<string>('theme') || 'light';qevo.storage.has(key)
Check if a key exists (and is not expired).
Returns: Promise<boolean>
Example:
if (await qevo.storage.has('authToken')) {
// User is authenticated
loadDashboard();
} else {
// User needs to login
showLoginForm();
}qevo.storage.remove(key)
Remove a key from storage.
Returns: Promise<void>
Example:
// User logout
await qevo.storage.remove('authToken');
await qevo.storage.remove('userSession');
await qevo.storage.remove('userData');🔍 Advanced Queries
qevo.storage.listKeys(prefix?)
List all keys, optionally filtered by prefix.
Parameters:
prefix?: string // Optional prefix to filter keysReturns: Promise<string[]>
Examples:
// 1. Get all keys
const allKeys = await qevo.storage.listKeys();
console.log('Total items:', allKeys.length);
// 2. Get all session-related keys
const sessionKeys = await qevo.storage.listKeys('session_');
// Returns: ['session_token', 'session_user', 'session_expires']
// 3. Get all cache keys
const cacheKeys = await qevo.storage.listKeys('cache_');
for (const key of cacheKeys) {
await qevo.storage.remove(key); // Clear all cache
}
// 4. Find all temporary data
const tempKeys = await qevo.storage.listKeys('temp_');
console.log('Temporary items:', tempKeys);qevo.storage.getKeyByPrefix(prefix)
Get the first key that starts with prefix.
Returns: Promise<string | null>
Example:
const sessionKey = await qevo.storage.getKeyByPrefix('session_');
if (sessionKey) {
const value = await qevo.storage.get(sessionKey);
}qevo.storage.getKeyBySuffix(suffix)
Get the first key that ends with suffix.
Returns: Promise<string | null>
Example:
const tokenKey = await qevo.storage.getKeyBySuffix('_token');
// Finds: 'auth_token', 'api_token', etc.⚡ Batch Operations
qevo.storage.batch(operations)
Execute multiple operations atomically in a single transaction.
Operations:
type BatchOperation =
| { type: 'set'; key: string; value: any; ttl?: number; expires?: Date }
| { type: 'get'; key: string }
| { type: 'remove'; key: string };Returns: Promise<(any | null | undefined)[]>
- Array of results corresponding to each operation
setoperations returnundefinedgetoperations return the value ornullremoveoperations returnundefined
Example:
// Initialize user session with multiple related items
const results = await qevo.storage.batch([
// Set auth token (expires in 1 hour)
{
type: 'set',
key: 'auth_token',
value: 'abc123xyz',
ttl: 3600
},
// Set user data (expires in 1 hour)
{
type: 'set',
key: 'user_data',
value: {
id: 123,
name: 'John Doe',
email: '[email protected]'
},
ttl: 3600
},
// Set preferences (permanent)
{
type: 'set',
key: 'user_preferences',
value: {
theme: 'dark',
language: 'en'
}
},
// Remove old session if exists
{
type: 'remove',
key: 'old_session'
},
// Get existing cart data
{
type: 'get',
key: 'shopping_cart'
}
]);
const cartData = results[4]; // Result from the get operationBenefits:
- ✅ Atomic - all operations succeed or all fail
- ✅ Faster than individual operations
- ✅ Consistent state - no partial updates
🔔 Event Listeners
Listen to storage changes in real-time across all extension contexts.
qevo.storage.addListener(type, callback)
Register a listener for storage events.
Event Types:
'add'- New item added'update'- Existing item modified'remove'- Item removed
Callbacks:
// Add event
(key: string, value: any) => void
// Update event
(key: string, oldValue: any, newValue: any) => void
// Remove event
(key: string) => voidExamples:
// 1. Monitor authentication changes
qevo.storage.addListener('add', (key, value) => {
if (key === 'authToken') {
console.log('User logged in');
updateUIToAuthenticatedState();
}
});
qevo.storage.addListener('remove', (key) => {
if (key === 'authToken') {
console.log('User logged out');
redirectToLogin();
}
});
qevo.storage.addListener('update', (key, oldValue, newValue) => {
if (key === 'userSettings') {
console.log('Settings changed');
applyNewSettings(newValue);
}
});
// 2. Track all storage changes (debugging)
qevo.storage.addListener('add', (key, value) => {
console.log('Added:', key, value);
});
qevo.storage.addListener('update', (key, old, neu) => {
console.log('Updated:', key, 'from', old, 'to', neu);
});
qevo.storage.addListener('remove', (key) => {
console.log('Removed:', key);
});
// 3. Sync across tabs
qevo.storage.addListener('update', (key, oldValue, newValue) => {
if (key === 'cart_items') {
// Notify other tabs about cart changes
qevo.broadcastMessage('cartUpdated', {
items: newValue,
timestamp: Date.now()
});
}
});qevo.storage.removeListener(type, callback)
Remove a specific listener.
Example:
const handler = (key, value) => console.log(key, value);
qevo.storage.addListener('add', handler);
// Later...
qevo.storage.removeListener('add', handler);📊 Storage Metadata
qevo.storage.length()
Get the number of items in storage.
Returns: Promise<number>
Example:
const count = await qevo.storage.length();
console.log(`You have ${count} items stored`);
if (count > 1000) {
console.warn('Storage is getting full, consider cleanup');
}qevo.storage.getStorageUsage()
Get current storage usage in bytes.
Returns: Promise<number>
Example:
const bytes = await qevo.storage.getStorageUsage();
const mb = (bytes / 1024 / 1024).toFixed(2);
console.log(`Using ${mb} MB of storage`);
// Check quota (Chrome: ~10MB)
const QUOTA_MB = 10;
const usagePercent = (bytes / (QUOTA_MB * 1024 * 1024)) * 100;
if (usagePercent > 80) {
alert(`Storage ${usagePercent.toFixed(1)}% full!`);
}💡 Storage Best Practices
1. Use TTL for Temporary Data
// ✅ Good - auto-cleanup
await qevo.storage.put('apiCache', data, { ttl: 300 });
// ❌ Bad - manual cleanup required
await qevo.storage.put('apiCache', data);
setTimeout(() => qevo.storage.remove('apiCache'), 300000);2. Namespace Your Keys
// ✅ Good - organized
await qevo.storage.put('auth_token', token);
await qevo.storage.put('auth_user', user);
await qevo.storage.put('cache_users', users);
await qevo.storage.put('cache_posts', posts);
// ❌ Bad - hard to manage
await qevo.storage.put('token', token);
await qevo.storage.put('user', user);
await qevo.storage.put('users', users);3. Use Batch for Related Data
// ✅ Good - atomic transaction
await qevo.storage.batch([
{ type: 'set', key: 'user_id', value: 123 },
{ type: 'set', key: 'user_name', value: 'John' },
{ type: 'set', key: 'user_email', value: '[email protected]' }
]);
// ❌ Bad - can result in partial state
await qevo.storage.put('user_id', 123);
await qevo.storage.put('user_name', 'John');
// If fails here, id and name saved but no email
await qevo.storage.put('user_email', '[email protected]');4. Handle Expiration Gracefully
const token = await qevo.storage.get<string>('authToken');
if (!token) {
// Token expired or doesn't exist
// Redirect to login or refresh token
await refreshAuthToken();
}⚠️ Background/Service Worker Only - This API only works in background scripts, not content scripts.
Qevo's WebRequest API provides complete control over HTTP traffic with a simplified, type-safe interface.
Core Concepts
Request Lifecycle:
BeforeRequest → BeforeSendHeaders → SendHeaders → HeadersReceived →
ResponseStarted → Completed/ErrorOccurred
↓ Can redirect/cancel
BeforeRedirect
↓ Auth required?
AuthRequiredEvent Categories:
- Blocking Events: Can modify/cancel requests
- Observing Events: Monitor-only, cannot modify
🎯 Event Types
'BeforeRequest' (Blocking)
Fired before request is sent. Can cancel or redirect.
Use Cases:
- Block ads/trackers
- Redirect requests
- Log request attempts
Example:
qevo.webRequest.on('BeforeRequest',
(details) => {
console.log('Request to:', details.url);
console.log('Method:', details.method);
console.log('Type:', details.type);
// Block tracking scripts
if (details.url.includes('analytics')) {
return { cancel: true };
}
// Redirect old API to new API
if (details.url.includes('api.old.com')) {
return {
redirectUrl: details.url.replace('api.old.com', 'api.new.com')
};
}
},
{ urls: ['<all_urls>'] },
['blocking']
);'BeforeSendHeaders' (Blocking)
Fired before headers are sent. Can modify request headers.
Use Cases:
- Add authentication headers
- Modify user agent
- Add custom headers
Example:
qevo.webRequest.on('BeforeSendHeaders',
(details) => {
const headers = details.requestHeaders || [];
// Add authorization header
headers.push({
name: 'Authorization',
value: 'Bearer abc123xyz'
});
// Add custom header
headers.push({
name: 'X-Custom-Client',
value: 'MyExtension/1.0'
});
// Modify existing header
const userAgentIndex = headers.findIndex(
h => h.name.toLowerCase() === 'user-agent'
);
if (userAgentIndex >= 0) {
headers[userAgentIndex].value = 'CustomBot/1.0';
}
return { requestHeaders: headers };
},
{ urls: ['https://api.example.com/*'] },
['blocking', 'requestHeaders']
);'SendHeaders' (Observing)
Fired when headers are sent. Monitor-only.
Use Cases:
- Log outgoing requests
- Monitor request headers
- Debug network issues
Example:
qevo.webRequest.on('SendHeaders',
(details) => {
console.log('Sending request to:', details.url);
console.log('Headers:', details.requestHeaders);
// Log API calls
if (details.url.includes('/api/')) {
logAPICall({
url: details.url,
method: details.method,
timestamp: details.timeStamp
});
}
},
{ urls: ['<all_urls>'] },
['requestHeaders']
);'HeadersReceived' (Blocking)
Fired when response headers received. Can modify response headers.
Use Cases:
- Modify CORS headers
- Change content type
- Add security headers
Example:
qevo.webRequest.on('HeadersReceived',
(details) => {
const headers = details.responseHeaders || [];
// Add CORS header
headers.push({
name: 'Access-Control-Allow-Origin',
value: '*'
});
// Force content type
const ctIndex = headers.findIndex(
h => h.name.toLowerCase() === 'content-type'
);
if (ctIndex >= 0) {
headers[ctIndex].value = 'application/json';
}
return {
responseHeaders: headers,
statusCode: details.statusCode
};
},
{ urls: ['<all_urls>'] },
['blocking', 'responseHeaders']
);'AuthRequired' (Blocking)
Fired when authentication required.
Use Cases:
- Provide credentials
- Handle proxy auth
- Custom auth flows
Example:
qevo.webRequest.on('AuthRequired',
(details) => {
return {
authCredentials: {
username: 'user',
password: 'pass'
}
};
},
{ urls: ['<all_urls>'] },
['blocking']
);'ResponseStarted' (Observing)
Fired when response starts. Monitor-only.
Example:
qevo.webRequest.on('ResponseStarted',
(details) => {
console.log('Response started:', details.url);
console.log('Status:', details.statusCode);
console.log('Headers:', details.responseHeaders);
},
{ urls: ['<all_urls>'] },
['responseHeaders']
);'BeforeRedirect' (Observing)
Fired before redirect happens. Monitor-only.
Example:
qevo.webRequest.on('BeforeRedirect',
(details) => {
console.log('Redirecting from:', details.url);
console.log('Redirecting to:', details.redirectUrl);
},
{ urls: ['<all_urls>'] }
);'Completed' (Observing)
Fired when request completes successfully.
Example:
const requestTimes = new Map();
qevo.webRequest.on('BeforeRequest', (details) => {
requestTimes.set(details.requestId, Date.now());
}, { urls: ['<all_urls>'] });
qevo.webRequest.on('Completed', (details) => {
const startTime = requestTimes.get(details.requestId);
if (startTime) {
const duration = Date.now() - startTime;
console.log(`Request took ${duration}ms:`, details.url);
requestTimes.delete(details.requestId);
}
}, { urls: ['<all_urls>'] });'ErrorOccurred' (Observing)
Fired when request fails.
Example:
qevo.webRequest.on('ErrorOccurred',
(details) => {
console.error('Request failed:', details.url);
console.error('Error:', details.error);
// Log failed API calls
if (details.url.includes('/api/')) {
logAPIError({
url: details.url,
error: details.error,
timestamp: details.timeStamp
});
}
},
{ urls: ['<all_urls>'] }
);🎛️ Filters
Control which requests trigger your listeners.
interface WebRequestFilter {
urls: string[]; // URL patterns (required)
types?: ResourceType[]; // Request types
tabId?: number; // Specific tab
windowId?: number; // Specific window
}
// Resource types
type ResourceType =
| 'main_frame' // Top-level document
| 'sub_frame' // iframe
| 'stylesheet' // CSS
| 'script' // JavaScript
| 'image' // Images
| 'font' // Fonts
| 'object' // Plugins
| 'xmlhttprequest' // XHR/fetch
| 'ping' // Ping
| 'csp_report' // CSP reports
| 'media' // Audio/video
| 'websocket' // WebSocket
| 'other';Examples:
// 1. All requests
{ urls: ['<all_urls>'] }
// 2. Specific domain
{ urls: ['https://example.com/*'] }
// 3. Multiple domains
{ urls: [
'https://api.example.com/*',
'https://cdn.example.com/*'
]}
// 4. Only images
{
urls: ['<all_urls>'],
types: ['image']
}
// 5. Only XHR/fetch from specific domain
{
urls: ['https://api.example.com/*'],
types: ['xmlhttprequest']
}
// 6. Specific tab
{
urls: ['<all_urls>'],
tabId: 123
}📌 Extra Info Specs
Specify what additional data you need.
type ExtraInfoSpec =
| 'blocking' // Can modify request
| 'requestHeaders' // Include request headers
| 'responseHeaders' // Include response headers
| 'extraHeaders'; // Include extra headersExamples:
// Monitor headers
qevo.webRequest.on('SendHeaders', handler,
{ urls: ['<all_urls>'] },
['requestHeaders']
);
// Modify request
qevo.webRequest.on('BeforeRequest', handler,
{ urls: ['<all_urls>'] },
['blocking']
);
// Modify headers
qevo.webRequest.on('BeforeSendHeaders', handler,
{ urls: ['<all_urls>'] },
['blocking', 'requestHeaders', 'extraHeaders']
);🛠️ WebRequest Utilities
qevo.webRequest.off(eventType, listener)
Remove a listener.
Example:
const handler = (details) => { ... };
qevo.webRequest.on('SendHeaders', handler, { urls: ['<all_urls>'] });
// Later...
qevo.webRequest.off('SendHeaders', handler);qevo.webRequest.clear(eventType)
Remove all listeners for an event type.
Example:
qevo.webRequest.clear('SendHeaders');qevo.webRequest.isAvailable()
Check if WebRequest API is available (background script check).
Returns: boolean
💡 WebRequest Best Practices
1. Use Specific Filters
// ✅ Good - specific
qevo.webRequest.on('SendHeaders', handler, {
urls: ['https://api.example.com/*'],
types: ['xmlhttprequest']
}, ['requestHeaders']);
// ❌ Bad - all requests
qevo.webRequest.on('SendHeaders', handler, {
urls: ['<all_urls>']
}, ['requestHeaders']);2. Only Request Data You Need
// ✅ Good - minimal
qevo.webRequest.on('SendHeaders', handler,
{ urls: ['<all_urls>'] }
);
// ❌ Bad - unnecessary data
qevo.webRequest.on('SendHeaders', handler,
{ urls: ['<all_urls>'] },
['requestHeaders', 'responseHeaders', 'extraHeaders']
);3. Handle Errors in Blocking Listeners
qevo.webRequest.on('BeforeRequest', (details) => {
try {
// Your logic
if (shouldBlock(details.url)) {
return { cancel: true };
}
} catch (error) {
console.error('Error in webRequest handler:', error);
// Don't block on error
return {};
}
}, { urls: ['<all_urls>'] }, ['blocking']);Powerful tab querying and management capabilities.
📋 Query Tabs
qevo.getAllTabs()
Get all tabs across all windows.
Returns: Promise<TabInfo[]>
TabInfo Interface:
interface TabInfo {
tabId: number;
windowId: number;
url: string;
title: string;
active: boolean;
pinned: boolean;
audible: boolean;
muted: boolean;
incognito: boolean;
status: 'loading' | 'complete';
favIconUrl?: string;
index: number;
isInCurrentWindow: boolean;
isCurrentTab: boolean;
}Example:
const tabs = await qevo.getAllTabs();
// Find YouTube tabs
const youtubeTabs = tabs.filter(tab =>
tab.url.includes('youtube.com')
);
// Get all loading tabs
const loadingTabs = tabs.filter(tab =>
tab.status === 'loading'
);
// Count tabs per window
const tabsByWindow = tabs.reduce((acc, tab) => {
acc[tab.windowId] = (acc[tab.windowId] || 0) + 1;
return acc;
}, {});qevo.getTabsInCurrentWindow()
Get all tabs in the current window.
Returns: Promise<TabInfo[]>
Example:
const tabs = await qevo.getTabsInCurrentWindow();
console.log(`Current window has ${tabs.length} tabs`);qevo.getCurrentTab()
Get the currently active tab.
Returns: Promise<TabInfo | null>
Example:
const currentTab = await qevo.getCurrentTab();
if (currentTab) {
console.log('Current tab:', currentTab.title);
console.log('URL:', currentTab.url);
}🔍 Find Tabs
qevo.getTabByUrl(url, exactMatch?)
Find a tab by URL.
Parameters:
url: string // URL to search for
exactMatch?: boolean // true = exact match, false = partial match (default: false)Returns: Promise<TabInfo | null>
Examples:
// Partial match (default)
const githubTab = await qevo.getTabByUrl('github.com');
// Exact match
const specificTab = await qevo.getTabByUrl(
'https://github.com/user/repo',
true
);
// Check if tab exists
if (await qevo.getTabByUrl('example.com')) {
console.log('Example.com is open');
}qevo.getTabById(tabId)
Get tab by ID.
Returns: Promise<TabInfo | null>
Example:
const tab = await qevo.getTabById(123);
if (tab) {
console.log('Tab found:', tab.title);
} else {
console.log('Tab no longer exists');
}qevo.getTabByTitle(title, exactMatch?)
Find a tab by title.
Parameters:
title: string // Title to search for
exactMatch?: boolean // true = exact match, false = partial matchReturns: Promise<TabInfo | null>
Examples:
// Partial match
const tab = await qevo.getTabByTitle('GitHub');
// Exact match
const specificTab = await qevo.getTabByTitle(
'GitHub - Where the world builds software',
true
);💡 Tab Management Best Practices
1. Cache Tab Queries
// ✅ Good
const tabs = await qevo.getAllTabs();
const githubTabs = tabs.filter(t => t.url.includes('github.com'));
const youtubeTabs = tabs.filter(t => t.url.includes('youtube.com'));
// ❌ Bad
const githubTabs = await qevo.getAllTabs().then(tabs =>
tabs.filter(t => t.url.includes('github.com'))
);
const youtubeTabs = await qevo.getAllTabs().then(tabs =>
tabs.filter(t => t.url.includes('youtube.com'))
);2. Check for Null Results
const tab = await qevo.getTabByUrl('example.com');
if (tab) {
await qevo.sendMessageToTab(tab.tabId, 'update', {});
} else {
console.warn('Tab not found');
}Environment Detection
qevo.isBackgroundScript()
Check if code is running in background/service worker.
Returns: boolean
Example:
if (qevo.isBackgroundScript()) {
// Initialize background-only features
qevo.webRequest.on('SendHeaders', ...);
} else {
console.log('Running in content script or popup');
}qevo.isContentScript()
Check if code is running in content script.
Returns: boolean
Example:
if (qevo.isContentScript()) {
// Content script specific code
document.addEventListener('click', handler);
}qevo.getBrowserType()
Get current browser type.
Returns: 'chrome' | 'firefox'
Example:
const browser = qevo.getBrowserType();
if (browser === 'firefox') {
// Firefox-specific workarounds
} else {
// Chrome behavior
}Debug Mode
qevo.debug
Enable/disable verbose logging.
Type: boolean (getter/setter)
Example:
// Enable debug mode
qevo.debug = true;
// Check status
if (qevo.debug) {
console.log('Debug mode enabled');
}
// Disable
qevo.debug = false;Auto-Detection: Qevo automatically detects debug mode from:
process.env.NODE_ENV !== 'production'import.meta.env.DEV === true- Vite/Webpack/bundler dev mode
🎨 Real-World Examples
Example 1: Authentication System
// background.ts
import qevo from 'qevo';
// Listen for login
qevo.message.on('login', async (data: { username: string; password: string }, sender, sendResponse) => {
try {
// Authenticate
const response = await fetch('https://api.example.com/auth', {
method: 'POST',
body: JSON.stringify(data)
});
const { token, user } = await response.json();
// Store token with 1 hour TTL
await qevo.storage.put('authToken', token, { ttl: 3600 });
await qevo.storage.put('currentUser', user, { ttl: 3600 });
// Notify all tabs
await qevo.broadcastMessage('authChanged', { authenticated: true, user });
sendResponse({ success: true, data: user });
} catch (error) {
sendResponse({ success: false, error: error.message });
}
return true;
});
// Auto-logout on token expiration
qevo.storage.addListener('remove', (key) => {
if (key === 'authToken') {
qevo.broadcastMessage('authChanged', { authenticated: false });
}
});// content.ts
import qevo from 'qevo';
// Handle auth state changes
qevo.message.on('authChanged', (data) => {
if (data.authenticated) {
showWelcomeMessage(data.user.name);
} else {
redirectToLogin();
}
});
// Login form handler
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const response = await qevo.sendMessageToBackground('login', {
username: usernameInput.value,
password: passwordInput.value
});
if (response.success) {
console.log('Logged in:', response.data.name);
} else {
showError(response.error);
}
});Example 2: Request Monitoring Dashboard
// background.ts
interface RequestMetrics {
url: string;
method: string;
duration: number;
statusCode: number;
timestamp: number;
}
const activeRequests = new Map<string, number>();
const metrics: RequestMetrics[] = [];
// Track request start
qevo.webRequest.on('BeforeRequest', (details) => {
activeRequests.set(details.requestId, Date.now());
}, { urls: ['<all_urls>'] });
// Track request completion
qevo.webRequest.on('Completed', (details) => {
const startTime = activeRequests.get(details.requestId);
if (startTime) {
const metric: RequestMetrics = {
url: details.url,
method: details.method,
duration: Date.now() - startTime,
statusCode: details.statusCode || 200,
timestamp: Date.now()
};
metrics.push(metric);
// Store last 100 metrics
if (metrics.length > 100) {
metrics.shift();
}
// Update dashboard
qevo.sendMessageToTabs('metricsUpdated', metrics, {
urls: ['*://*/dashboard.html']
});
activeRequests.delete(details.requestId);
}
}, { urls: ['<all_urls>'] });
// Provide metrics to popup
qevo.message.on('getMetrics', (data, sender, sendResponse) => {
sendResponse({ success: true, data: metrics });
});Example 3: Multi-Tab Sync
// Sync form data across tabs
import qevo from 'qevo';
// Save form data with debouncing
let saveTimeout: number;
document.querySelectorAll('input, textarea').forEach(input => {
input.addEventListener('input', () => {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(async () => {
const formData = collectFormData();
await qevo.storage.put('formDraft', formData, { ttl: 86400 });
// Notify other tabs
qevo.broadcastMessage('formDataChanged', formData);
}, 500);
});
});
// Listen for changes from other tabs
qevo.message.on('formDataChanged', (data) => {
if (confirm('Form data changed in another tab. Load latest version?')) {
populateForm(data);
}
});
// Load saved data on page load
window.addEventListener('load', async () => {
const saved = await qevo.storage.get('formDraft');
if (saved) {
populateForm(saved);
}
});🏗️ TypeScript Support
Qevo is built with TypeScript and provides complete type safety.
// Define your message types
interface GetUserRequest {
userId: number;
}
interface GetUserResponse {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
// Type-safe message sending
const response = await qevo.sendMessageToBackground<
GetUserRequest,
GetUserResponse
>('getUser', { userId: 123 });
if (response.success && response.data) {
const user: GetUserResponse = response.data;
console.log(user.name); // ✅ Type-safe
console.log(user.age); // ❌ TypeScript error
}
// Type-safe message handling
qevo.message.on<GetUserRequest, GetUserResponse>(
'getUser',
async (data, sender, sendResponse) => {
const userId = data.userId; // ✅ Type-safe
const user = await fetchUser(userId);
sendResponse({
success: true,
data: user // ✅ Must match GetUserResponse
});
return true;
}
);
// Type-safe storage
interface UserSettings {
theme: 'light' | 'dark';
notifications: boolean;
language: string;
}
await qevo.storage.put<UserSettings>('settings', {
theme: 'dark',
notifications: true,
language: 'en'
});
const settings = await qevo.storage.get<UserSettings>('settings');
if (settings) {
console.log(settings.theme); // ✅ 'light' | 'dark'
}🎯 Best Practices Summary
✅ Do's
- Always use TypeScript types
- Use TTL for temporary data
- Handle errors properly with try/catch
- Use batch operations for related data
- Namespace your storage keys
- Return
truein async message handlers - Use specific filters in webRequest
- Check for null results from queries
- Clean up listeners when done
- Use debug mode during development
❌ Don'ts
- Don't use webRequest in content scripts
- Don't forget to await async operations
- Don't store sensitive data without encryption
- Don't create memory leaks with listeners
- Don't use
<all_urls>without good reason - Don't ignore error responses
- Don't use sync storage APIs manually
- Don't forget TTL for temporary data
- Don't make redundant tab queries
- Don't block main thread with heavy operations
📞 Support & Resources
- 📚 Documentation: Full API reference (this README)
- 🐛 Issues: Report bugs or request features
- 💬 Community: Join discussions
- ⭐ GitHub: Star if you find this useful!
📄 License
MIT License - Free to use in personal and commercial projects
Built with ❤️ for the browser extension community
Making extension development delightful, one API at a time.
Qevo • Cross-Browser • Type-Safe • Production-Ready
