feat: dynamic content script registration for custom domains

Replace tab-load-only injection with scripting.registerContentScripts()
so custom domains inject at document_start (like built-in sites) and
persist across service worker restarts. Scripts re-register automatically
when domains change. Removed domains now revoke browser permissions.

https://claude.ai/code/session_01KF4i7Ra7zCEDskxDBaNtcT
This commit is contained in:
Claude
2026-03-27 04:40:56 +00:00
parent a3904843ba
commit 66b497f94b
2 changed files with 54 additions and 1 deletions
+45
View File
@@ -61,6 +61,44 @@ api.tabs.onRemoved.addListener((tabId) => {
// --- Dynamic injection for custom domains ---
const DYNAMIC_SCRIPT_ID = 'ss-custom-domains';
/**
* Sync the dynamically registered content script with the current
* custom domains list. Uses scripting.registerContentScripts so custom
* domains inject at document_start (like built-in sites) and persist
* across service worker restarts.
*/
async function syncCustomDomainScripts() {
const settings = await Storage.getSettings();
const customDomains = settings.customDomains || [];
// Build match patterns from domains (e.g. "https://my-ai.com" → "https://my-ai.com/*")
const matches = customDomains.map(d => d.replace(/\/$/, '') + '/*');
try {
// Remove existing dynamic script first
await api.scripting.unregisterContentScripts({ ids: [DYNAMIC_SCRIPT_ID] }).catch(() => {});
if (matches.length > 0) {
await api.scripting.registerContentScripts([{
id: DYNAMIC_SCRIPT_ID,
matches,
js: ['src/content/injector.js'],
css: ['src/content/content.css'],
runAt: 'document_start',
allFrames: true,
}]);
}
} catch (e) {
console.warn('[Silent Send] Failed to register custom domain scripts:', e);
}
}
/**
* Inject on an already-open tab for a newly added custom domain.
* Only needed for tabs that were open before the content script was registered.
*/
async function injectOnCustomDomain(tabId, tabUrl) {
const settings = await Storage.getSettings();
const customDomains = settings.customDomains || [];
@@ -362,6 +400,11 @@ api.storage.onChanged.addListener(async (changes, areaName) => {
const settings = await Storage.getSettings();
await updateIcon(settings);
// Re-register dynamic content scripts when custom domains change
if (changes.ss_settings && areaName === 'local') {
await syncCustomDomainScripts();
}
// Push to browser.storage.sync when local data changes (same-browser cross-device)
if (areaName === 'local' && settings.browserSync) {
await SilentSendSync.pushToSyncStorage();
@@ -487,6 +530,7 @@ api.runtime.onInstalled.addListener(async () => {
api.action.setBadgeBackgroundColor({ color: '#6b7280' });
const settings = await Storage.getSettings();
await updateIcon(settings);
await syncCustomDomainScripts();
// Set up alarms on install
await setupAutoSyncAlarm();
await setupOrgPolicyAlarm();
@@ -496,6 +540,7 @@ api.runtime.onInstalled.addListener(async () => {
(async () => {
const settings = await Storage.getSettings();
await updateIcon(settings);
await syncCustomDomainScripts();
// Check if extension is locked (encrypted data, no cached key)
const locked = await Storage.isLocked();
+9 -1
View File
@@ -620,9 +620,17 @@ function renderDomains() {
btn.addEventListener('click', async () => {
const idx = parseInt(btn.dataset.index, 10);
const domains = settings.customDomains || [];
domains.splice(idx, 1);
const removed = domains.splice(idx, 1)[0];
settings.customDomains = domains;
await Storage.saveSettings({ customDomains: domains });
// Revoke browser permission for the removed domain
if (removed) {
try {
await api.permissions.remove({ origins: [removed + '/*'] });
} catch (e) { /* non-fatal */ }
}
renderDomains();
});
});