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]` |
| `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 |
|----------|-------------|
| `...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 |
Enable it in the popup → Options tab → **Detect proper nouns**.
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)
@@ -402,6 +398,20 @@ src/
## 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
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,
"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.",
"browser_specific_settings": {
"gecko": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"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.",
"permissions": [
"storage",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "silent-send",
"version": "2.0.5",
"version": "2.0.6",
"private": true,
"license": "BSL-1.1",
"description": "Browser extension that substitutes personal data before sending to AI services",
+53 -5
View File
@@ -604,9 +604,11 @@
}
// Proper noun heuristic — catch names, company names, project names
// that aren't configured in identity
const properNouns = detectProperNouns(text, configured);
findings.push(...properNouns);
// Disabled by default (too many false positives). Enable in Options.
if (settings.detectProperNouns) {
const properNouns = detectProperNouns(text, configured);
findings.push(...properNouns);
}
// Deduplicate by value
const seen = new Set();
@@ -906,14 +908,60 @@
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()) {
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 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
if (
(method === 'POST' || method === 'PUT' || method === 'PATCH') &&
+6 -3
View File
@@ -136,7 +136,7 @@ const AutoDetect = {
*
* Returns array of { name, value, hint, category, index }
*/
scan(text, identity) {
scan(text, identity, options) {
if (!text || text.length < 5) return [];
const hasContext = CONTEXT_WORDS.test(text);
@@ -194,8 +194,11 @@ const AutoDetect = {
}
// Proper noun heuristic — catch names, company names, project names
const properNouns = this._detectProperNouns(text, configured);
findings.push(...properNouns);
// Disabled by default (too many false positives). Pass detectProperNouns: true to enable.
if (options?.detectProperNouns) {
const properNouns = this._detectProperNouns(text, configured);
findings.push(...properNouns);
}
// Deduplicate overlapping matches
findings.sort((a, b) => (a.index || 0) - (b.index || 0));
+26 -10
View File
@@ -791,25 +791,41 @@ const SilentSendSync = {
// ----------------------------------------------------------------
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 {
version: '1',
lastModified: result.ss_lastModified || Date.now(),
identity: result.ss_identity || {},
mappings: result.ss_mappings || [],
settings: result.ss_settings || {},
identity: identity || {},
mappings: mappings || [],
settings: settings || {},
};
},
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_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() {
+6 -4
View File
@@ -238,7 +238,7 @@
<div id="syncStatus" style="margin-top:8px;font-size:12px;min-height:16px"></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>
<p class="section-desc" style="margin-bottom:8px">
Pick the same folder in each browser once. Changes are written to
@@ -278,9 +278,11 @@
<!-- Auto Sync -->
<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">
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>
<div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;margin-bottom:8px">
<label class="toggle" title="Enable auto sync">
@@ -589,7 +591,7 @@
</section>
<footer>
<p>Silent Send v2.0.5</p>
<p>Silent Send v2.0.6</p>
</footer>
</div>
+50 -12
View File
@@ -721,6 +721,13 @@ let syncDirHandle = null;
const SYNC_FILE_NAME = 'silent-send-sync.json';
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();
updateFileSyncUI();
if (syncDirHandle) {
@@ -1183,9 +1190,7 @@ async function initAutoSyncUI() {
$('#autoSyncEnabled').checked = config.enabled || false;
$('#autoSyncMethod').value = config.method || 'gist';
$('#autoSyncInterval').value = String(config.interval || 15);
if (config.lastPull) {
setAutoSyncStatus(`Last pull: ${new Date(config.lastPull).toLocaleString()}`, 'ok');
}
updateAutoSyncStatus(config);
}
const saveAutoSync = async () => {
@@ -1194,26 +1199,59 @@ async function initAutoSyncUI() {
config.method = $('#autoSyncMethod').value;
config.interval = parseInt($('#autoSyncInterval').value, 10) || 15;
// Inherit token/URL from existing fields
if (config.method === 'gist') {
const token = $('#gistToken').value.trim();
if (token) config.gistToken = token;
} else {
config.url = $('#customSyncUrl').value.trim();
config.headers = parseHeadersField($('#customSyncHeaders').value);
// Always grab the latest token/URL from the page fields
// AND persist them so they survive page reloads
const gistToken = $('#gistToken').value.trim();
if (gistToken) config.gistToken = gistToken;
const customUrl = $('#customSyncUrl').value.trim();
if (customUrl) config.url = customUrl;
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);
// Tell service worker to reconfigure alarm
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);
$('#autoSyncMethod').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) {
const el = $('#autoSyncStatus');
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>
</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">
<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>
+2
View File
@@ -225,6 +225,7 @@ async function initUnlockedUI() {
$('#optAutoRedact').checked = settings.autoRedactDetected !== false;
$('#optHighlights').checked = settings.showHighlights || false;
$('#optDocPreview').checked = settings.docScanPreview !== false;
$('#optProperNouns').checked = settings.detectProperNouns || false;
// Options tab change handlers
const optHandlers = [
@@ -233,6 +234,7 @@ async function initUnlockedUI() {
['optAutoRedact', 'autoRedactDetected'],
['optHighlights', 'showHighlights'],
['optDocPreview', 'docScanPreview'],
['optProperNouns', 'detectProperNouns'],
];
for (const [id, key] of optHandlers) {
$(`#${id}`).addEventListener('change', async (e) => {