Merge pull request #20 from outis1one/claude/read-repo-wA3y1

Claude/read repo w a3y1
This commit is contained in:
Outis
2026-03-27 00:11:55 -04:00
committed by GitHub
11 changed files with 172 additions and 45 deletions
+18 -8
View File
@@ -67,17 +67,13 @@ A browser extension (Chrome, Firefox, and Safari) that intercepts personal infor
| `123-45-6789` | `[REDACTED-SSN]` | | `123-45-6789` | `[REDACTED-SSN]` |
| `4111 1111 1111 1111` | `[REDACTED-CARD]` | | `4111 1111 1111 1111` | `[REDACTED-CARD]` |
### Proper noun detection (automatic) ### Proper noun detection (opt-in)
The auto-detect scanner also catches capitalized words mid-sentence that might be names, company names, or project names you forgot to configure. For example: The auto-detect scanner can optionally flag capitalized phrases that might be names, company names, or project names you forgot to configure. **Disabled by default** because it can produce false positives on normal phrases like "Getting Started" or "Generate Design".
| You type | What happens | Enable it in the popup → Options tab → **Detect proper nouns**.
|----------|-------------|
| `...talked to Sarah about the deploy` | Flags "Sarah" as a possible name |
| `...the Acme Corp internal API` | Flags "Acme Corp" as a possible organization |
| `...pushed to Project Atlas staging` | Flags "Project Atlas" as a possible project name |
These are flagged as warnings (not auto-redacted) so you can decide whether to add them as mappings. Common English words, programming terms, days, and months are excluded to reduce false positives. When enabled, phrases like "Acme Corp" or "Project Atlas" will be flagged as warnings so you can decide whether to add them as mappings. You can click "ignore" on any false positive to permanently dismiss it.
### Bulk import (speed up setup) ### Bulk import (speed up setup)
@@ -402,6 +398,20 @@ src/
## Sync features ## Sync features
Keep your identities, mappings, and settings in sync across browsers and devices.
### Sync methods
| Method | How it works | Chrome | Firefox | Brave | Safari |
|---|---|---|---|---|---|
| **Sync Code** | Generate a code, paste in another browser | Yes | Yes | Yes | Yes |
| **GitHub Gist** | Store settings in a private Gist (needs a free GitHub PAT) | Yes | Yes | Yes | Yes |
| **Custom URL** | Any endpoint supporting GET + PUT (WebDAV, cloud function, etc.) | Yes | Yes | Yes | Yes |
| **Browser account sync** | Automatic via your Chrome/Firefox account | Yes | Yes | No | No |
| **Auto-Sync Folder** | Pick a cloud-synced folder (Dropbox, OneDrive, etc.) | Yes | No | No | No |
Auto-Sync Folder uses the File System Access API which is only available in Chrome. All other methods work in every browser.
### Auto sync ### Auto sync
When configured, the extension automatically pushes and pulls settings on a configurable interval (5/15/30/60 minutes) using GitHub Gist or a custom URL endpoint. Local changes trigger an immediate push. When configured, the extension automatically pushes and pulls settings on a configurable interval (5/15/30/60 minutes) using GitHub Gist or a custom URL endpoint. Local changes trigger an immediate push.
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Silent Send", "name": "Silent Send",
"version": "2.0.5", "version": "2.0.6",
"description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.", "description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.",
"browser_specific_settings": { "browser_specific_settings": {
"gecko": { "gecko": {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Silent Send", "name": "Silent Send",
"version": "2.0.5", "version": "2.0.6",
"description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.", "description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.",
"permissions": [ "permissions": [
"storage", "storage",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "silent-send", "name": "silent-send",
"version": "2.0.5", "version": "2.0.6",
"private": true, "private": true,
"license": "BSL-1.1", "license": "BSL-1.1",
"description": "Browser extension that substitutes personal data before sending to AI services", "description": "Browser extension that substitutes personal data before sending to AI services",
+51 -3
View File
@@ -604,9 +604,11 @@
} }
// Proper noun heuristic — catch names, company names, project names // Proper noun heuristic — catch names, company names, project names
// that aren't configured in identity // Disabled by default (too many false positives). Enable in Options.
if (settings.detectProperNouns) {
const properNouns = detectProperNouns(text, configured); const properNouns = detectProperNouns(text, configured);
findings.push(...properNouns); findings.push(...properNouns);
}
// Deduplicate by value // Deduplicate by value
const seen = new Set(); const seen = new Set();
@@ -906,14 +908,60 @@
return SKIP_URL_PATTERNS.some(p => p.test(url)); return SKIP_URL_PATTERNS.some(p => p.test(url));
} }
window.fetch = async function (url, options) { window.fetch = async function (input, init) {
if (!settings.enabled || !hasSubstitutions()) { if (!settings.enabled || !hasSubstitutions()) {
return originalFetch.call(this, url, options); return originalFetch.call(this, input, init);
}
// Handle both fetch(url, options) and fetch(Request) signatures
let url, options;
if (input instanceof Request) {
url = input.url;
// Clone the Request so we can read/modify the body
options = {
method: input.method,
headers: input.headers,
body: null, // will read below
mode: input.mode,
credentials: input.credentials,
cache: input.cache,
redirect: input.redirect,
referrer: input.referrer,
signal: input.signal,
};
// Read the body from the Request object
try {
const ct = input.headers.get('content-type') || '';
if (ct.includes('json') || ct.includes('text')) {
options.body = await input.text();
} else {
// Non-text body — pass through unmodified
return originalFetch.call(this, input, init);
}
} catch {
return originalFetch.call(this, input, init);
}
} else {
url = input;
options = init ? { ...init } : {};
} }
const urlStr = typeof url === 'string' ? url : url?.url || ''; const urlStr = typeof url === 'string' ? url : url?.url || '';
const method = (options?.method || 'GET').toUpperCase(); const method = (options?.method || 'GET').toUpperCase();
// Convert non-string bodies to string where possible
if (options.body && typeof options.body !== 'string' && !(options.body instanceof FormData)) {
try {
if (options.body instanceof Blob) {
options.body = await options.body.text();
} else if (options.body instanceof ArrayBuffer || ArrayBuffer.isView(options.body)) {
options.body = new TextDecoder().decode(options.body);
} else if (options.body instanceof URLSearchParams) {
options.body = options.body.toString();
}
} catch { /* leave as-is */ }
}
// Only intercept POST/PUT/PATCH with a body // Only intercept POST/PUT/PATCH with a body
if ( if (
(method === 'POST' || method === 'PUT' || method === 'PATCH') && (method === 'POST' || method === 'PUT' || method === 'PATCH') &&
+4 -1
View File
@@ -136,7 +136,7 @@ const AutoDetect = {
* *
* Returns array of { name, value, hint, category, index } * Returns array of { name, value, hint, category, index }
*/ */
scan(text, identity) { scan(text, identity, options) {
if (!text || text.length < 5) return []; if (!text || text.length < 5) return [];
const hasContext = CONTEXT_WORDS.test(text); const hasContext = CONTEXT_WORDS.test(text);
@@ -194,8 +194,11 @@ const AutoDetect = {
} }
// Proper noun heuristic — catch names, company names, project names // Proper noun heuristic — catch names, company names, project names
// Disabled by default (too many false positives). Pass detectProperNouns: true to enable.
if (options?.detectProperNouns) {
const properNouns = this._detectProperNouns(text, configured); const properNouns = this._detectProperNouns(text, configured);
findings.push(...properNouns); findings.push(...properNouns);
}
// Deduplicate overlapping matches // Deduplicate overlapping matches
findings.sort((a, b) => (a.index || 0) - (b.index || 0)); findings.sort((a, b) => (a.index || 0) - (b.index || 0));
+26 -10
View File
@@ -791,25 +791,41 @@ const SilentSendSync = {
// ---------------------------------------------------------------- // ----------------------------------------------------------------
async _getAllData() { async _getAllData() {
const result = await api.storage.local.get(null); // Use dynamic import to avoid circular dependency
const StorageModule = (await import('./storage.js')).default;
const identity = await StorageModule._readSecure('ss_identity');
const mappings = await StorageModule._readSecure('ss_mappings');
const settings = await StorageModule._readSecure('ss_settings');
const result = await api.storage.local.get('ss_lastModified');
return { return {
version: '1', version: '1',
lastModified: result.ss_lastModified || Date.now(), lastModified: result.ss_lastModified || Date.now(),
identity: result.ss_identity || {}, identity: identity || {},
mappings: result.ss_mappings || [], mappings: mappings || [],
settings: result.ss_settings || {}, settings: settings || {},
}; };
}, },
async _applyData(data, source = 'unknown') { async _applyData(data, source = 'unknown') {
const toSet = { const StorageModule = (await import('./storage.js')).default;
// Write through Storage module so data gets encrypted if at-rest
// encryption is enabled
if (data.identity !== undefined) {
await StorageModule._writeSecure('ss_identity', data.identity);
}
if (data.mappings !== undefined) {
await StorageModule._writeSecure('ss_mappings', data.mappings);
}
if (data.settings !== undefined) {
await StorageModule._writeSecure('ss_settings', data.settings);
}
// Metadata stays plaintext
await api.storage.local.set({
ss_lastModified: data.lastModified, ss_lastModified: data.lastModified,
ss_sync_notification: { source, time: Date.now() }, ss_sync_notification: { source, time: Date.now() },
}; });
if (data.identity !== undefined) toSet.ss_identity = data.identity;
if (data.mappings !== undefined) toSet.ss_mappings = data.mappings;
if (data.settings !== undefined) toSet.ss_settings = data.settings;
await api.storage.local.set(toSet);
}, },
async _getSyncChunkKeys() { async _getSyncChunkKeys() {
+6 -4
View File
@@ -238,7 +238,7 @@
<div id="syncStatus" style="margin-top:8px;font-size:12px;min-height:16px"></div> <div id="syncStatus" style="margin-top:8px;font-size:12px;min-height:16px"></div>
</div> </div>
<div style="margin-top:20px;padding-top:16px;border-top:1px solid #e5e7eb"> <div id="fileSyncSection" style="margin-top:20px;padding-top:16px;border-top:1px solid #e5e7eb">
<h3 style="font-size:13px;font-weight:600;margin:0 0 4px">Auto-Sync Folder — fully automatic, no copy-paste</h3> <h3 style="font-size:13px;font-weight:600;margin:0 0 4px">Auto-Sync Folder — fully automatic, no copy-paste</h3>
<p class="section-desc" style="margin-bottom:8px"> <p class="section-desc" style="margin-bottom:8px">
Pick the same folder in each browser once. Changes are written to Pick the same folder in each browser once. Changes are written to
@@ -278,9 +278,11 @@
<!-- Auto Sync --> <!-- Auto Sync -->
<div style="margin-top:20px;padding-top:16px;border-top:1px solid #e5e7eb"> <div style="margin-top:20px;padding-top:16px;border-top:1px solid #e5e7eb">
<h3 style="font-size:13px;font-weight:600;margin:0 0 4px">Auto Sync — background polling</h3> <h3 style="font-size:13px;font-weight:600;margin:0 0 4px">Auto Sync — works in all browsers</h3>
<p class="section-desc" style="margin-bottom:8px"> <p class="section-desc" style="margin-bottom:8px">
Automatically push and pull settings on an interval. Uses the Gist or URL method configured above. Automatically push and pull settings in the background on a schedule.
Uses GitHub Gist or Custom URL — configure one of those above first, then enable auto sync here.
Changes you make locally are pushed immediately; remote changes are pulled on the interval.
</p> </p>
<div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;margin-bottom:8px"> <div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;margin-bottom:8px">
<label class="toggle" title="Enable auto sync"> <label class="toggle" title="Enable auto sync">
@@ -589,7 +591,7 @@
</section> </section>
<footer> <footer>
<p>Silent Send v2.0.5</p> <p>Silent Send v2.0.6</p>
</footer> </footer>
</div> </div>
+49 -11
View File
@@ -721,6 +721,13 @@ let syncDirHandle = null;
const SYNC_FILE_NAME = 'silent-send-sync.json'; const SYNC_FILE_NAME = 'silent-send-sync.json';
async function initFileSync() { async function initFileSync() {
// Hide the entire section if File System Access API isn't supported
if (!window.showDirectoryPicker) {
const section = $('#fileSyncSection');
if (section) section.style.display = 'none';
return;
}
syncDirHandle = await SilentSendSync.loadSyncDirHandle(); syncDirHandle = await SilentSendSync.loadSyncDirHandle();
updateFileSyncUI(); updateFileSyncUI();
if (syncDirHandle) { if (syncDirHandle) {
@@ -1183,9 +1190,7 @@ async function initAutoSyncUI() {
$('#autoSyncEnabled').checked = config.enabled || false; $('#autoSyncEnabled').checked = config.enabled || false;
$('#autoSyncMethod').value = config.method || 'gist'; $('#autoSyncMethod').value = config.method || 'gist';
$('#autoSyncInterval').value = String(config.interval || 15); $('#autoSyncInterval').value = String(config.interval || 15);
if (config.lastPull) { updateAutoSyncStatus(config);
setAutoSyncStatus(`Last pull: ${new Date(config.lastPull).toLocaleString()}`, 'ok');
}
} }
const saveAutoSync = async () => { const saveAutoSync = async () => {
@@ -1194,26 +1199,59 @@ async function initAutoSyncUI() {
config.method = $('#autoSyncMethod').value; config.method = $('#autoSyncMethod').value;
config.interval = parseInt($('#autoSyncInterval').value, 10) || 15; config.interval = parseInt($('#autoSyncInterval').value, 10) || 15;
// Inherit token/URL from existing fields // Always grab the latest token/URL from the page fields
if (config.method === 'gist') { // AND persist them so they survive page reloads
const token = $('#gistToken').value.trim(); const gistToken = $('#gistToken').value.trim();
if (token) config.gistToken = token; if (gistToken) config.gistToken = gistToken;
} else { const customUrl = $('#customSyncUrl').value.trim();
config.url = $('#customSyncUrl').value.trim(); if (customUrl) config.url = customUrl;
config.headers = parseHeadersField($('#customSyncHeaders').value); config.headers = parseHeadersField($('#customSyncHeaders').value);
// Validate: need credentials for the chosen method
if (config.enabled) {
if (config.method === 'gist' && !config.gistToken) {
setAutoSyncStatus('Enter your GitHub PAT in the Gist section above first.', 'warn');
config.enabled = false;
$('#autoSyncEnabled').checked = false;
} else if (config.method === 'url' && !config.url) {
setAutoSyncStatus('Enter a URL in the Custom URL section above first.', 'warn');
config.enabled = false;
$('#autoSyncEnabled').checked = false;
}
} }
await SilentSendSync.saveAutoSyncConfig(config); await SilentSendSync.saveAutoSyncConfig(config);
// Tell service worker to reconfigure alarm
api.runtime.sendMessage({ type: 'autosync:config-changed' }).catch(() => {}); api.runtime.sendMessage({ type: 'autosync:config-changed' }).catch(() => {});
setAutoSyncStatus(config.enabled ? 'Auto sync enabled.' : 'Auto sync disabled.', config.enabled ? 'ok' : 'neutral'); updateAutoSyncStatus(config);
}; };
// Also save token when the Gist token field changes
$('#gistToken').addEventListener('change', async () => {
const config = (await SilentSendSync.getAutoSyncConfig()) || {};
const token = $('#gistToken').value.trim();
if (token) {
config.gistToken = token;
await SilentSendSync.saveAutoSyncConfig(config);
}
});
$('#autoSyncEnabled').addEventListener('change', saveAutoSync); $('#autoSyncEnabled').addEventListener('change', saveAutoSync);
$('#autoSyncMethod').addEventListener('change', saveAutoSync); $('#autoSyncMethod').addEventListener('change', saveAutoSync);
$('#autoSyncInterval').addEventListener('change', saveAutoSync); $('#autoSyncInterval').addEventListener('change', saveAutoSync);
} }
function updateAutoSyncStatus(config) {
if (!config?.enabled) {
setAutoSyncStatus('Auto sync disabled.', 'neutral');
return;
}
const parts = [];
parts.push(`${config.method === 'gist' ? 'GitHub Gist' : 'Custom URL'} every ${config.interval}min`);
if (config.lastPull) parts.push(`last pull: ${new Date(config.lastPull).toLocaleString()}`);
if (config.lastPush) parts.push(`last push: ${new Date(config.lastPush).toLocaleString()}`);
setAutoSyncStatus(parts.join(' · '), 'ok');
}
function setAutoSyncStatus(msg, type) { function setAutoSyncStatus(msg, type) {
const el = $('#autoSyncStatus'); const el = $('#autoSyncStatus');
if (!el) return; if (!el) return;
+8
View File
@@ -236,6 +236,14 @@
<label class="toggle"><input type="checkbox" id="optDocPreview" checked><span class="toggle-slider"></span></label> <label class="toggle"><input type="checkbox" id="optDocPreview" checked><span class="toggle-slider"></span></label>
</div> </div>
<div class="setting-item">
<div class="setting-label">
<strong>Detect proper nouns</strong>
<span class="setting-desc">Flag capitalized phrases (names, companies) — may produce false positives</span>
</div>
<label class="toggle"><input type="checkbox" id="optProperNouns"><span class="toggle-slider"></span></label>
</div>
<div style="margin-top:12px;padding-top:10px;border-top:1px solid #e5e7eb"> <div style="margin-top:12px;padding-top:10px;border-top:1px solid #e5e7eb">
<button class="btn" id="btnOpenFullOptions" style="width:100%;font-size:12px">Open Full Options Page</button> <button class="btn" id="btnOpenFullOptions" style="width:100%;font-size:12px">Open Full Options Page</button>
<p class="help-text" style="margin-top:6px;text-align:center">Sync, encryption, org, version history, import/export, and more</p> <p class="help-text" style="margin-top:6px;text-align:center">Sync, encryption, org, version history, import/export, and more</p>
+2
View File
@@ -225,6 +225,7 @@ async function initUnlockedUI() {
$('#optAutoRedact').checked = settings.autoRedactDetected !== false; $('#optAutoRedact').checked = settings.autoRedactDetected !== false;
$('#optHighlights').checked = settings.showHighlights || false; $('#optHighlights').checked = settings.showHighlights || false;
$('#optDocPreview').checked = settings.docScanPreview !== false; $('#optDocPreview').checked = settings.docScanPreview !== false;
$('#optProperNouns').checked = settings.detectProperNouns || false;
// Options tab change handlers // Options tab change handlers
const optHandlers = [ const optHandlers = [
@@ -233,6 +234,7 @@ async function initUnlockedUI() {
['optAutoRedact', 'autoRedactDetected'], ['optAutoRedact', 'autoRedactDetected'],
['optHighlights', 'showHighlights'], ['optHighlights', 'showHighlights'],
['optDocPreview', 'docScanPreview'], ['optDocPreview', 'docScanPreview'],
['optProperNouns', 'detectProperNouns'],
]; ];
for (const [id, key] of optHandlers) { for (const [id, key] of optHandlers) {
$(`#${id}`).addEventListener('change', async (e) => { $(`#${id}`).addEventListener('change', async (e) => {