feat: custom domains for OpenWebUI + fix README issues
- Add custom domain support in Options page so users can add self-hosted AI services (e.g. https://ai.myserver.com) - Background worker dynamically injects content scripts on custom domains using scripting.executeScript - Add optional_host_permissions so Chrome can grant per-domain access - Rewrite README: add clone step to Firefox instructions, clarify what "credentials" means in step 3, add Windows commands alongside Mac/Linux for every terminal step - Bump version to 0.2.0 https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw
This commit is contained in:
@@ -10,7 +10,7 @@ A browser extension (Chrome + Firefox) that intercepts personal information and
|
|||||||
| ChatGPT | chatgpt.com, chat.openai.com | Untested |
|
| ChatGPT | chatgpt.com, chat.openai.com | Untested |
|
||||||
| Grok | grok.x.ai, x.com/i/grok | Untested |
|
| Grok | grok.x.ai, x.com/i/grok | Untested |
|
||||||
| Gemini | gemini.google.com | Untested |
|
| Gemini | gemini.google.com | Untested |
|
||||||
| OpenWebUI | localhost, 127.0.0.1 (self-hosted) | Untested |
|
| OpenWebUI | localhost, 127.0.0.1, or custom domain | Untested |
|
||||||
|
|
||||||
> **Note:** Only Claude has been tested so far. The other services have API interception patterns defined but may need adjustments. PRs welcome.
|
> **Note:** Only Claude has been tested so far. The other services have API interception patterns defined but may need adjustments. PRs welcome.
|
||||||
|
|
||||||
@@ -51,83 +51,123 @@ A browser extension (Chrome + Firefox) that intercepts personal information and
|
|||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
You need [Git](https://git-scm.com/downloads) and [Node.js](https://nodejs.org/) (v18+) installed.
|
- [Git](https://git-scm.com/downloads)
|
||||||
|
- [Node.js](https://nodejs.org/) v18 or newer (needed for Firefox signing only)
|
||||||
|
|
||||||
|
### Step 1: Get the code
|
||||||
|
|
||||||
|
Open a terminal (Terminal on Mac, Command Prompt or PowerShell on Windows, any terminal on Linux) and run:
|
||||||
|
|
||||||
Clone the repo:
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/outis1one/silent-send.git
|
git clone https://github.com/outis1one/silent-send.git
|
||||||
cd silent-send
|
cd silent-send
|
||||||
```
|
```
|
||||||
|
|
||||||
### Chrome (Developer Mode)
|
This downloads the extension code to a `silent-send` folder on your computer.
|
||||||
|
|
||||||
1. Run `./build.sh chrome` (or just use the root directory directly)
|
### Chrome
|
||||||
2. Open `chrome://extensions/` in your browser
|
|
||||||
3. Enable **Developer mode** (toggle in the top right corner)
|
1. Open `chrome://extensions/` in Chrome
|
||||||
4. Click **Load unpacked** → navigate to and select the `dist/chrome/` folder (or the repo root)
|
2. Enable **Developer mode** (toggle in the top right corner)
|
||||||
|
3. Click **Load unpacked**
|
||||||
|
4. Navigate to the `silent-send` folder you cloned and select it
|
||||||
5. Navigate to any supported AI site — the extension icon appears in your toolbar
|
5. Navigate to any supported AI site — the extension icon appears in your toolbar
|
||||||
|
|
||||||
That's it for Chrome. No account, no store, no fees.
|
That's it for Chrome. No build step, no account, no store, no fees.
|
||||||
|
|
||||||
### Firefox (signed, persistent)
|
### Firefox (signed, persistent)
|
||||||
|
|
||||||
Firefox requires extensions to be cryptographically signed before it will install them permanently. Mozilla provides free self-hosted signing — no store listing, no review process, no fees.
|
Firefox requires extensions to be cryptographically signed before it will permanently install them. Mozilla provides free signing — no store listing, no review process, no fees. You just need a free Firefox account.
|
||||||
|
|
||||||
#### Step 1: Create a free Firefox account
|
#### Step 1: Create a free Firefox account
|
||||||
|
|
||||||
1. Go to https://accounts.firefox.com/ and create an account (or sign in if you have one)
|
1. Go to https://accounts.firefox.com/ and sign up (or sign in if you have one already)
|
||||||
2. This is the same account used for Firefox Sync — you may already have one
|
2. This is the same account used for Firefox Sync — you may already have one
|
||||||
|
|
||||||
#### Step 2: Get your Mozilla API credentials
|
#### Step 2: Generate your signing keys
|
||||||
|
|
||||||
1. Go to https://addons.mozilla.org/developers/addon/api/key/
|
1. Go to https://addons.mozilla.org/developers/addon/api/key/
|
||||||
2. Sign in with your Firefox account from step 1
|
2. Sign in with your Firefox account
|
||||||
3. On that page you'll see two values:
|
3. You'll see two values on that page:
|
||||||
- **JWT issuer** — this is your API key, looks like `user:12345678:901`
|
- **JWT issuer** — looks like `user:12345678:901`
|
||||||
- **JWT secret** — a long alphanumeric string, this is your API secret
|
- **JWT secret** — a long string of random characters
|
||||||
4. Keep this page open — you'll need both values in the next step
|
4. You need both of these. Copy them or keep the page open.
|
||||||
|
|
||||||
#### Step 3: Configure your credentials
|
#### Step 3: Install dependencies and save your signing keys
|
||||||
|
|
||||||
|
In your terminal, inside the `silent-send` folder:
|
||||||
|
|
||||||
|
**Mac / Linux:**
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Copy the env template
|
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
Now open `.env` in any text editor and paste in your values from step 2:
|
**Windows (Command Prompt):**
|
||||||
|
```cmd
|
||||||
|
npm install
|
||||||
|
copy .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Now open the `.env` file in any text editor (Notepad, VS Code, etc.) and replace the placeholder values with the two values from step 2:
|
||||||
|
|
||||||
```
|
```
|
||||||
WEB_EXT_API_KEY="user:12345678:901"
|
WEB_EXT_API_KEY="user:12345678:901"
|
||||||
WEB_EXT_API_SECRET="your-jwt-secret-here"
|
WEB_EXT_API_SECRET="your-jwt-secret-here"
|
||||||
```
|
```
|
||||||
|
|
||||||
Save the file.
|
Save and close the file.
|
||||||
|
|
||||||
#### Step 4: Build and sign
|
#### Step 4: Build and sign
|
||||||
|
|
||||||
|
**Mac / Linux:**
|
||||||
```bash
|
```bash
|
||||||
source .env && npm run sign:firefox
|
source .env && npm run sign:firefox
|
||||||
```
|
```
|
||||||
|
|
||||||
This builds the Firefox version and submits it to Mozilla for signing. It takes 10-30 seconds. When done, you'll see a signed `.xpi` file in `dist/firefox-signed/`.
|
**Windows (Command Prompt):**
|
||||||
|
```cmd
|
||||||
|
set /p x= < nul & for /f "tokens=1,* delims==" %a in (.env) do @set %a=%~b
|
||||||
|
npm run sign:firefox
|
||||||
|
```
|
||||||
|
|
||||||
#### Step 5: Install the signed extension
|
**Windows (PowerShell):**
|
||||||
|
```powershell
|
||||||
|
Get-Content .env | ForEach-Object { if ($_ -match '^(.+?)=(.*)$') { [Environment]::SetEnvironmentVariable($matches[1], $matches[2].Trim('"')) } }
|
||||||
|
npm run sign:firefox
|
||||||
|
```
|
||||||
|
|
||||||
|
This submits the extension to Mozilla for signing (takes 10-30 seconds). When done, you'll find a signed `.xpi` file in `dist/firefox-signed/`.
|
||||||
|
|
||||||
|
#### Step 5: Install
|
||||||
|
|
||||||
- Drag the `.xpi` file into any Firefox window, **or**
|
- Drag the `.xpi` file into any Firefox window, **or**
|
||||||
- Firefox menu → File → Open File → select the `.xpi`
|
- Firefox menu → File → Open File → select the `.xpi`
|
||||||
- Click **Add** when prompted
|
- Click **Add** when prompted
|
||||||
|
|
||||||
The signed `.xpi` is **permanent** — it survives browser restarts, updates, everything. No store listing, no review process, no fees. You only need to re-sign when you update to a new version.
|
Done. The extension is **permanently installed** — survives restarts, updates, everything. You only need to re-sign if you update to a newer version of Silent Send.
|
||||||
|
|
||||||
### Firefox (temporary, for development)
|
### Firefox (temporary, no signing needed)
|
||||||
|
|
||||||
If you just want to try it out without signing:
|
If you just want to try it out quickly:
|
||||||
|
|
||||||
|
**Mac / Linux:**
|
||||||
```bash
|
```bash
|
||||||
npm install && npm run run:firefox
|
npm install && npm run run:firefox
|
||||||
```
|
```
|
||||||
This opens a fresh Firefox with the extension pre-loaded. Auto-reloads on file changes. Resets when Firefox closes — useful for testing, not for daily use.
|
|
||||||
|
**Windows:**
|
||||||
|
```cmd
|
||||||
|
npm install && npm run run:firefox
|
||||||
|
```
|
||||||
|
|
||||||
|
This opens Firefox with the extension pre-loaded. Resets when Firefox closes — useful for testing.
|
||||||
|
|
||||||
|
## Custom domains (OpenWebUI, etc.)
|
||||||
|
|
||||||
|
If you run OpenWebUI or another AI service on a custom domain (not localhost), go to **Options** → **Custom Domains** and add your domain (e.g. `https://ai.myserver.com`). The extension will activate on those domains too.
|
||||||
|
|
||||||
|
For Chrome, you'll need to also grant the extension permission to access the new domain via `chrome://extensions/` → Silent Send → Details → Site access.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -145,7 +185,7 @@ src/
|
|||||||
popup/
|
popup/
|
||||||
popup.html/css/js — Quick access: identity, mappings, activity, test mode
|
popup.html/css/js — Quick access: identity, mappings, activity, test mode
|
||||||
options/
|
options/
|
||||||
options.html/css/js — Full mapping management, import/export, settings
|
options.html/css/js — Full mapping management, import/export, settings, custom domains
|
||||||
lib/
|
lib/
|
||||||
substitution-engine.js — Core explicit find/replace logic
|
substitution-engine.js — Core explicit find/replace logic
|
||||||
smart-patterns.js — Auto-detection of emails, names, usernames, hostnames, phones, paths
|
smart-patterns.js — Auto-detection of emails, names, usernames, hostnames, phones, paths
|
||||||
@@ -157,5 +197,5 @@ src/
|
|||||||
|
|
||||||
- All data stays local in browser storage
|
- All data stays local in browser storage
|
||||||
- No external servers, no telemetry, no analytics
|
- No external servers, no telemetry, no analytics
|
||||||
- The extension only activates on claude.ai
|
- The extension only activates on supported AI sites (and any custom domains you add)
|
||||||
- Your real identity data never leaves your machine
|
- Your real identity data never leaves your machine
|
||||||
|
|||||||
+4
-10
@@ -63,19 +63,13 @@
|
|||||||
"48": "icons/icon48.svg",
|
"48": "icons/icon48.svg",
|
||||||
"128": "icons/icon128.svg"
|
"128": "icons/icon128.svg"
|
||||||
},
|
},
|
||||||
|
"optional_permissions": [
|
||||||
|
"<all_urls>"
|
||||||
|
],
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
{
|
{
|
||||||
"resources": ["src/content/content.js"],
|
"resources": ["src/content/content.js"],
|
||||||
"matches": [
|
"matches": ["<all_urls>"]
|
||||||
"https://claude.ai/*",
|
|
||||||
"https://chatgpt.com/*",
|
|
||||||
"https://chat.openai.com/*",
|
|
||||||
"https://grok.x.ai/*",
|
|
||||||
"https://x.com/*",
|
|
||||||
"https://gemini.google.com/*",
|
|
||||||
"http://localhost/*",
|
|
||||||
"http://127.0.0.1/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-10
@@ -54,19 +54,14 @@
|
|||||||
"48": "icons/icon48.svg",
|
"48": "icons/icon48.svg",
|
||||||
"128": "icons/icon128.svg"
|
"128": "icons/icon128.svg"
|
||||||
},
|
},
|
||||||
|
"optional_host_permissions": [
|
||||||
|
"https://*/*",
|
||||||
|
"http://*/*"
|
||||||
|
],
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
{
|
{
|
||||||
"resources": ["src/content/content.js"],
|
"resources": ["src/content/content.js"],
|
||||||
"matches": [
|
"matches": ["<all_urls>"]
|
||||||
"https://claude.ai/*",
|
|
||||||
"https://chatgpt.com/*",
|
|
||||||
"https://chat.openai.com/*",
|
|
||||||
"https://grok.x.ai/*",
|
|
||||||
"https://x.com/*",
|
|
||||||
"https://gemini.google.com/*",
|
|
||||||
"http://localhost/*",
|
|
||||||
"http://127.0.0.1/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* Silent Send - Background Service Worker
|
* Silent Send - Background Service Worker
|
||||||
*
|
*
|
||||||
* Manages badge count, coordinates between popup and content scripts.
|
* Manages badge count, coordinates between popup and content scripts.
|
||||||
|
* Injects content scripts on custom domains dynamically.
|
||||||
* Uses `api` alias for cross-browser compatibility (Chrome + Firefox).
|
* Uses `api` alias for cross-browser compatibility (Chrome + Firefox).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -11,6 +12,18 @@ import api from '../lib/browser-polyfill.js';
|
|||||||
// Track substitution counts per tab
|
// Track substitution counts per tab
|
||||||
const tabCounts = new Map();
|
const tabCounts = new Map();
|
||||||
|
|
||||||
|
// Built-in URL patterns
|
||||||
|
const BUILTIN_URL_PATTERNS = [
|
||||||
|
'https://claude.ai/*',
|
||||||
|
'https://chatgpt.com/*',
|
||||||
|
'https://chat.openai.com/*',
|
||||||
|
'https://grok.x.ai/*',
|
||||||
|
'https://x.com/i/grok*',
|
||||||
|
'https://gemini.google.com/*',
|
||||||
|
'http://localhost/*',
|
||||||
|
'http://127.0.0.1/*',
|
||||||
|
];
|
||||||
|
|
||||||
// --- Badge Management ---
|
// --- Badge Management ---
|
||||||
|
|
||||||
function updateBadge(tabId) {
|
function updateBadge(tabId) {
|
||||||
@@ -22,11 +35,16 @@ function updateBadge(tabId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reset count when tab navigates
|
// Reset count when tab navigates
|
||||||
api.tabs.onUpdated.addListener((tabId, changeInfo) => {
|
api.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
|
||||||
if (changeInfo.status === 'loading') {
|
if (changeInfo.status === 'loading') {
|
||||||
tabCounts.set(tabId, 0);
|
tabCounts.set(tabId, 0);
|
||||||
updateBadge(tabId);
|
updateBadge(tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject content script on custom domains when page loads
|
||||||
|
if (changeInfo.status === 'complete' && tab.url) {
|
||||||
|
await injectOnCustomDomain(tabId, tab.url);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cleanup when tab closes
|
// Cleanup when tab closes
|
||||||
@@ -34,6 +52,47 @@ api.tabs.onRemoved.addListener((tabId) => {
|
|||||||
tabCounts.delete(tabId);
|
tabCounts.delete(tabId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Dynamic injection for custom domains ---
|
||||||
|
|
||||||
|
async function injectOnCustomDomain(tabId, tabUrl) {
|
||||||
|
const settings = await Storage.getSettings();
|
||||||
|
const customDomains = settings.customDomains || [];
|
||||||
|
if (customDomains.length === 0) return;
|
||||||
|
|
||||||
|
const matches = customDomains.some((domain) => tabUrl.startsWith(domain));
|
||||||
|
if (!matches) return;
|
||||||
|
|
||||||
|
// Check if already injected (avoid double-injection)
|
||||||
|
try {
|
||||||
|
const results = await api.scripting.executeScript({
|
||||||
|
target: { tabId },
|
||||||
|
func: () => !!window.__silentSendInjected,
|
||||||
|
});
|
||||||
|
if (results?.[0]?.result) return;
|
||||||
|
} catch (e) {
|
||||||
|
// Permission denied — user hasn't granted access to this domain
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject CSS
|
||||||
|
try {
|
||||||
|
await api.scripting.insertCSS({
|
||||||
|
target: { tabId },
|
||||||
|
files: ['src/content/content.css'],
|
||||||
|
});
|
||||||
|
} catch (e) { /* non-fatal */ }
|
||||||
|
|
||||||
|
// Inject content script
|
||||||
|
try {
|
||||||
|
await api.scripting.executeScript({
|
||||||
|
target: { tabId },
|
||||||
|
files: ['src/content/injector.js'],
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Silent Send] Failed to inject on custom domain:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Message Handling ---
|
// --- Message Handling ---
|
||||||
|
|
||||||
api.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
api.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
@@ -88,18 +147,16 @@ const messageHandlers = {
|
|||||||
|
|
||||||
async 'update:settings'(message) {
|
async 'update:settings'(message) {
|
||||||
await Storage.saveSettings(message.settings);
|
await Storage.saveSettings(message.settings);
|
||||||
// Broadcast to content scripts on all supported sites
|
|
||||||
const SUPPORTED_URLS = [
|
// Build list of all URL patterns (built-in + custom)
|
||||||
'https://claude.ai/*',
|
const allPatterns = [...BUILTIN_URL_PATTERNS];
|
||||||
'https://chatgpt.com/*',
|
const customDomains = message.settings.customDomains || [];
|
||||||
'https://chat.openai.com/*',
|
for (const domain of customDomains) {
|
||||||
'https://grok.x.ai/*',
|
allPatterns.push(domain + '/*');
|
||||||
'https://x.com/i/grok*',
|
}
|
||||||
'https://gemini.google.com/*',
|
|
||||||
'http://localhost/*',
|
// Broadcast to content scripts
|
||||||
'http://127.0.0.1/*',
|
for (const urlPattern of allPatterns) {
|
||||||
];
|
|
||||||
for (const urlPattern of SUPPORTED_URLS) {
|
|
||||||
const tabs = await api.tabs.query({ url: urlPattern }).catch(() => []);
|
const tabs = await api.tabs.query({ url: urlPattern }).catch(() => []);
|
||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
api.tabs.sendMessage(tab.id, {
|
api.tabs.sendMessage(tab.id, {
|
||||||
|
|||||||
+59
-53
@@ -8,63 +8,69 @@
|
|||||||
* Communication: page script <-> content script via window.postMessage
|
* Communication: page script <-> content script via window.postMessage
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
// Cross-browser API
|
// Prevent double-injection on custom domains
|
||||||
const api =
|
if (window.__silentSendInjected) return;
|
||||||
typeof browser !== 'undefined' && browser.runtime
|
window.__silentSendInjected = true;
|
||||||
? browser
|
|
||||||
: typeof chrome !== 'undefined'
|
|
||||||
? chrome
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Load mappings and settings, then inject into page
|
// Cross-browser API
|
||||||
async function init() {
|
const api =
|
||||||
const result = await api.storage.local.get(['ss_mappings', 'ss_identity', 'ss_settings']);
|
typeof browser !== 'undefined' && browser.runtime
|
||||||
const mappings = result.ss_mappings || [];
|
? browser
|
||||||
const identity = result.ss_identity || {};
|
: typeof chrome !== 'undefined'
|
||||||
const settings = result.ss_settings || { enabled: true };
|
? chrome
|
||||||
|
: null;
|
||||||
|
|
||||||
// Inject the main interception script into the page's world
|
// Load mappings and settings, then inject into page
|
||||||
const script = document.createElement('script');
|
async function init() {
|
||||||
script.setAttribute('data-ss-config', JSON.stringify({ mappings, identity, settings }));
|
const result = await api.storage.local.get(['ss_mappings', 'ss_identity', 'ss_settings']);
|
||||||
script.src = api.runtime.getURL('src/content/content.js');
|
const mappings = result.ss_mappings || [];
|
||||||
(document.head || document.documentElement).appendChild(script);
|
const identity = result.ss_identity || {};
|
||||||
script.onload = () => script.remove();
|
const settings = result.ss_settings || { enabled: true };
|
||||||
|
|
||||||
// Listen for substitution events from the page script
|
// Inject the main interception script into the page's world
|
||||||
window.addEventListener('message', (event) => {
|
const script = document.createElement('script');
|
||||||
if (event.source !== window) return;
|
script.setAttribute('data-ss-config', JSON.stringify({ mappings, identity, settings }));
|
||||||
if (event.data?.type === 'ss:substitution-performed') {
|
script.src = api.runtime.getURL('src/content/content.js');
|
||||||
api.runtime.sendMessage({
|
(document.head || document.documentElement).appendChild(script);
|
||||||
type: 'substitution:performed',
|
script.onload = () => script.remove();
|
||||||
count: event.data.count,
|
|
||||||
replacements: event.data.replacements,
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Forward storage changes to the page script
|
// Listen for substitution events from the page script
|
||||||
api.storage.onChanged.addListener((changes) => {
|
window.addEventListener('message', (event) => {
|
||||||
if (changes.ss_mappings || changes.ss_identity || changes.ss_settings) {
|
if (event.source !== window) return;
|
||||||
window.postMessage({
|
if (event.data?.type === 'ss:substitution-performed') {
|
||||||
type: 'ss:config-updated',
|
api.runtime.sendMessage({
|
||||||
mappings: changes.ss_mappings?.newValue,
|
type: 'substitution:performed',
|
||||||
identity: changes.ss_identity?.newValue,
|
count: event.data.count,
|
||||||
settings: changes.ss_settings?.newValue,
|
replacements: event.data.replacements,
|
||||||
}, '*');
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for settings updates from popup via runtime messages
|
// Forward storage changes to the page script
|
||||||
api.runtime.onMessage.addListener((message) => {
|
api.storage.onChanged.addListener((changes) => {
|
||||||
if (message.type === 'settings:updated') {
|
if (changes.ss_mappings || changes.ss_identity || changes.ss_settings) {
|
||||||
window.postMessage({
|
window.postMessage({
|
||||||
type: 'ss:config-updated',
|
type: 'ss:config-updated',
|
||||||
settings: message.settings,
|
mappings: changes.ss_mappings?.newValue,
|
||||||
}, '*');
|
identity: changes.ss_identity?.newValue,
|
||||||
}
|
settings: changes.ss_settings?.newValue,
|
||||||
});
|
}, '*');
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
init();
|
// Listen for settings updates from popup via runtime messages
|
||||||
|
api.runtime.onMessage.addListener((message) => {
|
||||||
|
if (message.type === 'settings:updated') {
|
||||||
|
window.postMessage({
|
||||||
|
type: 'ss:config-updated',
|
||||||
|
settings: message.settings,
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
})();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
showHighlights: false,
|
showHighlights: false,
|
||||||
revealMode: false,
|
revealMode: false,
|
||||||
maxLogEntries: 200,
|
maxLogEntries: 200,
|
||||||
|
customDomains: [],
|
||||||
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'general'],
|
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'general'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Custom Domains</h2>
|
||||||
|
<p class="section-desc">Add domains for self-hosted AI services (like OpenWebUI). The extension will activate on these domains in addition to the built-in ones.</p>
|
||||||
|
<div class="domain-list" id="domainList"></div>
|
||||||
|
<div class="add-row">
|
||||||
|
<input type="text" id="newDomain" placeholder="https://ai.myserver.com" class="input" style="flex:2">
|
||||||
|
<button class="btn btn-primary" id="btnAddDomain">Add Domain</button>
|
||||||
|
</div>
|
||||||
|
<p class="section-desc" style="margin-top:8px;margin-bottom:0">
|
||||||
|
After adding a domain in Chrome, you also need to grant permission: <code>chrome://extensions/</code> → Silent Send → Details → Site access → add the domain.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h2>Activity Log</h2>
|
<h2>Activity Log</h2>
|
||||||
<div class="log-actions">
|
<div class="log-actions">
|
||||||
@@ -89,7 +102,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>Silent Send v0.1.0</p>
|
<p>Silent Send v0.2.0</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,15 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
$('#maxLogEntries').value = settings.maxLogEntries || 200;
|
$('#maxLogEntries').value = settings.maxLogEntries || 200;
|
||||||
|
|
||||||
renderMappings();
|
renderMappings();
|
||||||
|
renderDomains();
|
||||||
renderLog();
|
renderLog();
|
||||||
|
|
||||||
|
// Custom domains
|
||||||
|
$('#btnAddDomain').addEventListener('click', addDomain);
|
||||||
|
$('#newDomain').addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') addDomain();
|
||||||
|
});
|
||||||
|
|
||||||
// Settings listeners
|
// Settings listeners
|
||||||
$('#showHighlights').addEventListener('change', async (e) => {
|
$('#showHighlights').addEventListener('change', async (e) => {
|
||||||
await Storage.saveSettings({ showHighlights: e.target.checked });
|
await Storage.saveSettings({ showHighlights: e.target.checked });
|
||||||
@@ -176,6 +183,61 @@ async function renderLog() {
|
|||||||
.join('');
|
.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Custom Domains ---
|
||||||
|
async function addDomain() {
|
||||||
|
let domain = $('#newDomain').value.trim();
|
||||||
|
if (!domain) return;
|
||||||
|
|
||||||
|
// Normalize: ensure it has a protocol
|
||||||
|
if (!domain.startsWith('http://') && !domain.startsWith('https://')) {
|
||||||
|
domain = 'https://' + domain;
|
||||||
|
}
|
||||||
|
// Strip trailing slashes
|
||||||
|
domain = domain.replace(/\/+$/, '');
|
||||||
|
|
||||||
|
const domains = settings.customDomains || [];
|
||||||
|
if (domains.includes(domain)) {
|
||||||
|
alert('Domain already added.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
domains.push(domain);
|
||||||
|
settings.customDomains = domains;
|
||||||
|
await Storage.saveSettings({ customDomains: domains });
|
||||||
|
renderDomains();
|
||||||
|
$('#newDomain').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDomains() {
|
||||||
|
const list = $('#domainList');
|
||||||
|
const domains = settings.customDomains || [];
|
||||||
|
|
||||||
|
if (domains.length === 0) {
|
||||||
|
list.innerHTML = '<div style="text-align:center;color:#9ca3af;padding:12px;font-size:13px">No custom domains. Built-in sites (Claude, ChatGPT, Grok, Gemini, localhost) are always active.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.innerHTML = domains
|
||||||
|
.map((d, i) => `
|
||||||
|
<div class="domain-item" style="display:flex;align-items:center;justify-content:space-between;padding:8px;background:#f9fafb;border-radius:6px;margin-bottom:4px">
|
||||||
|
<span style="font-size:13px;font-family:monospace">${escapeHtml(d)}</span>
|
||||||
|
<button class="btn btn-sm btn-danger btn-remove-domain" data-index="${i}">×</button>
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
list.querySelectorAll('.btn-remove-domain').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const idx = parseInt(btn.dataset.index, 10);
|
||||||
|
const domains = settings.customDomains || [];
|
||||||
|
domains.splice(idx, 1);
|
||||||
|
settings.customDomains = domains;
|
||||||
|
await Storage.saveSettings({ customDomains: domains });
|
||||||
|
renderDomains();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = str;
|
div.textContent = str;
|
||||||
|
|||||||
Reference in New Issue
Block a user