feat: LEXAI-13 API encryption, LEXAI-11 context menu, LEXAI-12 tone analysis
- LEXAI-13: TweetNaCl secretbox encryption for API keys in Options + background
- getOrCreateEncKey() generates per-device 32-byte key stored in chrome.storage.local
- Keys saved as nonce+ciphertext (base64) under apiKeyEnc
- Backward compat: falls back to plaintext apiKey if no encrypted key found
- Lock icon 🔒 shown in Options label when key is encrypted
- LEXAI-11: Right-click context menu with Fix, Rephrase, Shorten, Expand, Tone
- Registered via onInstalled in background service worker
- Sends lexai-context-menu message to content script
- Content script listener added at bottom of main()
- LEXAI-12: Tone analysis button added to floating toolbar (teal #2dd4bf)
- Analysis-only: showModal called with null originalText = no Replace button
- Tone system prompt updated to pure analysis (no rewrite)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { defineBackground } from 'wxt/utils/define-background';
|
||||
import nacl from 'tweetnacl';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -10,6 +11,8 @@ interface AnalyzePayload {
|
||||
interface LexAIConfig {
|
||||
provider?: string;
|
||||
apiKey?: string;
|
||||
apiKeyEnc?: string;
|
||||
encKey?: string;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
@@ -39,13 +42,26 @@ function getSystemPrompt(action: string): string {
|
||||
'Make it richer and more informative while staying on topic. ' +
|
||||
'Return ONLY the expanded text.',
|
||||
tone:
|
||||
'You are a writing coach. Analyze the tone of the provided text (e.g. formal, casual, aggressive, passive) ' +
|
||||
'and rewrite it to be professional and clear. ' +
|
||||
'Return ONLY the improved text.',
|
||||
'You are a writing coach. Analyze the tone of the provided text. ' +
|
||||
'Describe the tone characteristics (e.g. formal, casual, aggressive, passive, confident, etc.) ' +
|
||||
'and note any issues like passive voice, wordiness, or emotional bias. ' +
|
||||
'Return a brief, clear analysis — no rewriting, no extra commentary.',
|
||||
};
|
||||
return prompts[action] ?? prompts.grammar;
|
||||
}
|
||||
|
||||
// ─── Encryption helpers ───────────────────────────────────────────────────────
|
||||
|
||||
async function decryptApiKey(encKeyB64: string, apiKeyEncB64: string): Promise<string | null> {
|
||||
const key = Uint8Array.from(atob(encKeyB64), c => c.charCodeAt(0));
|
||||
const combined = Uint8Array.from(atob(apiKeyEncB64), c => c.charCodeAt(0));
|
||||
const nonce = combined.slice(0, 24);
|
||||
const cipher = combined.slice(24);
|
||||
const decrypted = nacl.secretbox.open(cipher, nonce, key);
|
||||
if (!decrypted) return null;
|
||||
return new TextDecoder().decode(decrypted);
|
||||
}
|
||||
|
||||
// ─── Provider implementations ─────────────────────────────────────────────────
|
||||
|
||||
async function callOpenAI(payload: AnalyzePayload, config: LexAIConfig): Promise<LexAIResponse> {
|
||||
@@ -198,23 +214,34 @@ async function callOpenRouter(payload: AnalyzePayload, config: LexAIConfig): Pro
|
||||
// ─── Main handler ─────────────────────────────────────────────────────────────
|
||||
|
||||
async function handleAnalyzeText(payload: AnalyzePayload): Promise<LexAIResponse> {
|
||||
const config = await chrome.storage.local.get(['provider', 'apiKey', 'model']) as LexAIConfig;
|
||||
const stored = await chrome.storage.local.get(['provider', 'apiKey', 'apiKeyEnc', 'encKey', 'model']);
|
||||
const config = stored as LexAIConfig;
|
||||
|
||||
if (!config.apiKey || config.apiKey.trim() === '') {
|
||||
// Resolve API key — prefer encrypted path, fall back to plaintext for backward compat
|
||||
let apiKey = config.apiKey;
|
||||
if (config.apiKeyEnc && config.encKey) {
|
||||
const decrypted = await decryptApiKey(config.encKey, config.apiKeyEnc);
|
||||
if (decrypted) {
|
||||
apiKey = decrypted;
|
||||
}
|
||||
}
|
||||
|
||||
if (!apiKey || apiKey.trim() === '') {
|
||||
return { error: 'No API key configured. Please open LexAI settings (click the extension icon).' };
|
||||
}
|
||||
|
||||
const resolvedConfig: LexAIConfig = { ...config, apiKey: apiKey.trim() };
|
||||
const provider = config.provider || 'openai';
|
||||
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
return callOpenAI(payload, config);
|
||||
return callOpenAI(payload, resolvedConfig);
|
||||
case 'anthropic':
|
||||
return callAnthropic(payload, config);
|
||||
return callAnthropic(payload, resolvedConfig);
|
||||
case 'groq':
|
||||
return callGroq(payload, config);
|
||||
return callGroq(payload, resolvedConfig);
|
||||
case 'openrouter':
|
||||
return callOpenRouter(payload, config);
|
||||
return callOpenRouter(payload, resolvedConfig);
|
||||
default:
|
||||
return { error: `Unknown provider: "${provider}". Please check LexAI settings.` };
|
||||
}
|
||||
@@ -225,6 +252,46 @@ async function handleAnalyzeText(payload: AnalyzePayload): Promise<LexAIResponse
|
||||
export default defineBackground(() => {
|
||||
console.log('LexAI background service worker started');
|
||||
|
||||
// ─── Context menus ───────────────────────────────────────────────────────
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
chrome.contextMenus.create({
|
||||
id: 'lexai-grammar',
|
||||
title: '⚡ LexAI: Fix Grammar',
|
||||
contexts: ['selection'],
|
||||
});
|
||||
chrome.contextMenus.create({
|
||||
id: 'lexai-rephrase',
|
||||
title: '⚡ LexAI: Rephrase',
|
||||
contexts: ['selection'],
|
||||
});
|
||||
chrome.contextMenus.create({
|
||||
id: 'lexai-shorten',
|
||||
title: '⚡ LexAI: Shorten',
|
||||
contexts: ['selection'],
|
||||
});
|
||||
chrome.contextMenus.create({
|
||||
id: 'lexai-expand',
|
||||
title: '⚡ LexAI: Expand',
|
||||
contexts: ['selection'],
|
||||
});
|
||||
chrome.contextMenus.create({
|
||||
id: 'lexai-tone',
|
||||
title: '⚡ LexAI: Analyze Tone',
|
||||
contexts: ['selection'],
|
||||
});
|
||||
});
|
||||
|
||||
chrome.contextMenus.onClicked.addListener((info, tab) => {
|
||||
if (!info.selectionText || !tab?.id) return;
|
||||
const action = info.menuItemId.toString().replace('lexai-', '');
|
||||
chrome.tabs.sendMessage(tab.id, {
|
||||
type: 'lexai-context-menu',
|
||||
action,
|
||||
text: info.selectionText,
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Message handler ─────────────────────────────────────────────────────
|
||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
if (message.type === 'ANALYZE_TEXT') {
|
||||
handleAnalyzeText(message.payload as AnalyzePayload)
|
||||
|
||||
Reference in New Issue
Block a user