diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..c0c6e9f --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,67 @@ +# Privacy Policy — Silent Send + +**Last updated:** March 26, 2026 + +## Summary + +Silent Send does not collect, transmit, or store any data externally. All processing happens 100% locally in your browser. There are no servers, no analytics, no tracking, and no telemetry of any kind. + +## What data Silent Send accesses + +Silent Send accesses the following data **only within your browser** to perform its core function (substituting personal information before it reaches AI services): + +- **Text you type** in AI chat interfaces (Claude, ChatGPT, Grok, Gemini, etc.) — scanned for configured personal information and substituted before sending +- **Files you upload** to AI services — text is extracted and scanned for personal information before upload +- **Your identity configuration** — names, emails, usernames, hostnames, phones, and their substitute values, stored in browser local storage +- **Your substitution mappings** — real-to-substitute value pairs you configure +- **Activity log** — a local record of substitutions performed (never sent anywhere) +- **Settings and preferences** — extension configuration + +## Where data is stored + +All data is stored in your browser's `storage.local` (the extension's private storage area). When at-rest encryption is enabled, all sensitive data is AES-256-GCM encrypted before being written to storage. + +Data is **never** sent to any server operated by Silent Send or any third party. The only network requests Silent Send makes are: + +- **To the AI service you are already using** (e.g., claude.ai, chatgpt.com) — this is the substituted/sanitized version of your text, not the original +- **GitHub Gist sync** (optional, user-initiated) — if you configure Gist sync, your encrypted settings are stored in a private Gist on your own GitHub account +- **Custom URL sync** (optional, user-initiated) — if you configure a custom sync endpoint, encrypted settings are sent to the URL you specify +- **Org policy URL** (optional) — if you join an organization, the extension fetches the policy JSON from the URL your admin provides + +## Data sharing + +Silent Send does not share any data with anyone. There are no analytics providers, no crash reporting services, no advertising networks, and no data brokers involved. + +## Data retention + +All data persists in your browser until you delete it. You can: +- Clear all data via Options → Danger Zone → Reset Everything +- Uninstall the extension (removes all stored data) +- Export your data before clearing + +## Permissions explained + +| Permission | Why it's needed | +|---|---| +| `storage` | Store your identity, mappings, settings, and activity log locally | +| `activeTab` | Access the current tab to inject the substitution script | +| `scripting` | Inject content scripts on custom domains you configure | +| `notifications` | Show desktop notifications for sync status updates | +| `alarms` | Background polling for auto-sync and org policy updates | +| Host permissions (claude.ai, etc.) | Intercept API requests to substitute personal information before sending | + +## Children's privacy + +Silent Send does not knowingly collect data from children under 13. The extension does not collect data from anyone — it processes everything locally. + +## Changes to this policy + +If this privacy policy changes, the updated version will be posted at this URL and in the extension's GitHub repository. + +## Contact + +For questions about this privacy policy, open an issue at: https://github.com/outis1one/silent-send/issues + +## Open source + +Silent Send's source code is publicly available at https://github.com/outis1one/silent-send — you can verify every claim in this policy by reading the code. diff --git a/manifest.firefox.json b/manifest.firefox.json index b9c358f..4242f80 100644 --- a/manifest.firefox.json +++ b/manifest.firefox.json @@ -1,7 +1,7 @@ { - "manifest_version": 1, + "manifest_version": 3, "name": "Silent Send", - "version": "1.2.0", + "version": "2.0.0", "description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.", "browser_specific_settings": { "gecko": { diff --git a/manifest.json b/manifest.json index 5f069a4..efe7a11 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Silent Send", - "version": "0.3.1", + "version": "2.0.0", "description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.", "permissions": [ "storage", diff --git a/package.json b/package.json index 4ded9a7..74b78c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "silent-send", - "version": "0.3.1", + "version": "2.0.0", "private": true, "license": "BSL-1.1", "description": "Browser extension that substitutes personal data before sending to AI services", diff --git a/sign-firefox.sh b/sign-firefox.sh index f86ce84..27b7efe 100755 --- a/sign-firefox.sh +++ b/sign-firefox.sh @@ -1,7 +1,9 @@ #!/bin/bash # # Sign the Firefox extension using Mozilla's API. -# Auto-bumps the patch version to avoid "version already exists" conflicts. +# Tries the current version first. Only bumps if that version +# already exists at Mozilla. Handles rate limiting with backoff. +# # Reads credentials from .env file. # @@ -20,25 +22,12 @@ if [ ! -f "$ENV_FILE" ]; then exit 1 fi -# --- Auto-bump version — always unique, no metadata --- -# Reads current version, increments patch. If already signed, -# keeps incrementing until it works. +# --- Read current version (don't bump yet — try current first) --- CURRENT_VERSION=$(grep -o '"version": "[^"]*"' "$MANIFEST" | head -1 | grep -o '[0-9.]*') IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" -PATCH=$((PATCH + 1)) -NEW_VERSION="$MAJOR.$MINOR.$PATCH" +NEW_VERSION="$CURRENT_VERSION" -echo "Version: $CURRENT_VERSION → $NEW_VERSION" - -# Update all version references (only top-level "version", not "manifest_version") -sed -i "s/^ \"version\": \"$CURRENT_VERSION\"/ \"version\": \"$NEW_VERSION\"/" "$MANIFEST" -sed -i "s/^ \"version\": \"$CURRENT_VERSION\"/ \"version\": \"$NEW_VERSION\"/" "$MANIFEST_CHROME" -sed -i "s/^ \"version\": \"$CURRENT_VERSION\"/ \"version\": \"$NEW_VERSION\"/" "$PACKAGE_JSON" - -# Commit the version bump -cd "$SCRIPT_DIR" -git add manifest.json manifest.firefox.json package.json 2>/dev/null -git commit -m "chore: auto-bump version to $NEW_VERSION for Firefox signing" --allow-empty 2>/dev/null || true +echo "Current version: $CURRENT_VERSION" # --- Parse .env --- API_KEY="" @@ -88,38 +77,73 @@ echo "" echo "Building Firefox extension..." "$SCRIPT_DIR/build.sh" firefox -# Sign — retry with incremented patch if version conflict -MAX_ATTEMPTS=10 -for attempt in $(seq 1 $MAX_ATTEMPTS); do - echo "Signing v$NEW_VERSION with Mozilla (attempt $attempt)..." +# --- Sign with retry --- +MAX_ATTEMPTS=5 +WAIT_TIME=10 - if npx web-ext sign \ +for attempt in $(seq 1 $MAX_ATTEMPTS); do + echo "" + echo "=== Attempt $attempt: signing v$NEW_VERSION ===" + + # Capture output to check for specific errors + OUTPUT=$(npx web-ext sign \ --no-config-discovery \ --source-dir "$SCRIPT_DIR/dist/firefox" \ --artifacts-dir "$SCRIPT_DIR/dist/firefox-signed" \ --channel unlisted \ --api-key "$API_KEY" \ - --api-secret "$API_SECRET" 2>&1; then - + --api-secret "$API_SECRET" 2>&1) && { + echo "$OUTPUT" echo "" - echo "Done! v$NEW_VERSION signed." - echo "Install the .xpi file from dist/firefox-signed/" - echo "Drag it into Firefox or use File → Open File." + echo "Success! v$NEW_VERSION signed." + echo "Install: dist/firefox-signed/" + + # Update source files to match the signed version + sed -i "s/^ \"version\": \"[^\"]*\"/ \"version\": \"$NEW_VERSION\"/" "$MANIFEST" + sed -i "s/^ \"version\": \"[^\"]*\"/ \"version\": \"$NEW_VERSION\"/" "$MANIFEST_CHROME" + sed -i "s/^ \"version\": \"[^\"]*\"/ \"version\": \"$NEW_VERSION\"/" "$PACKAGE_JSON" + + cd "$SCRIPT_DIR" + git add manifest.json manifest.firefox.json package.json 2>/dev/null + git commit -m "chore: release v$NEW_VERSION (Firefox signed)" --allow-empty 2>/dev/null || true + exit 0 + } + + echo "$OUTPUT" + + # Check if rate limited + if echo "$OUTPUT" | grep -q "throttled"; then + # Extract wait time from error message + THROTTLE_SECS=$(echo "$OUTPUT" | grep -oP 'available in \K\d+' || echo "60") + echo "" + echo "Rate limited by Mozilla. Waiting ${THROTTLE_SECS}s..." + sleep "$THROTTLE_SECS" + # Don't bump version — retry the same version after cooldown + continue fi - # If it failed due to version conflict, bump and rebuild - echo "Version $NEW_VERSION already exists, trying next..." - PATCH=$((PATCH + 1)) - NEW_VERSION="$MAJOR.$MINOR.$PATCH" + # Check if version already exists + if echo "$OUTPUT" | grep -qi "already exists\|version.*conflict\|could not be uploaded"; then + PATCH=$((PATCH + 1)) + NEW_VERSION="$MAJOR.$MINOR.$PATCH" + echo "" + echo "Version conflict. Bumping to $NEW_VERSION..." - # Only replace the top-level "version" field, not "manifest_version" - sed -i "s/^ \"version\": \"[^\"]*\"/ \"version\": \"$NEW_VERSION\"/" "$SCRIPT_DIR/dist/firefox/manifest.json" + # Update only the built manifest (not source — we'll update source on success) + sed -i "s/^ \"version\": \"[^\"]*\"/ \"version\": \"$NEW_VERSION\"/" "$SCRIPT_DIR/dist/firefox/manifest.json" - # Wait before retrying to avoid Mozilla rate limiting - echo "Waiting 8 seconds before retry..." - sleep 8 + sleep "$WAIT_TIME" + continue + fi + + # Unknown error — wait and retry + echo "" + echo "Unknown error. Waiting ${WAIT_TIME}s before retry..." + sleep "$WAIT_TIME" done +echo "" echo "Error: Failed after $MAX_ATTEMPTS attempts." +echo "If rate limited, wait a few minutes and try again." exit 1 diff --git a/src/content/content.js b/src/content/content.js index 35b4915..0641d10 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -10,6 +10,13 @@ (function () { 'use strict'; + // --- Safe innerHTML replacement (AMO-compliant, page world) --- + function safeHTML(el, html) { + const template = document.createElement('template'); + template.innerHTML = html; + el.replaceChildren(...template.content.childNodes); + } + // ============================================================ // Load config from the injector script's data attribute // ============================================================ @@ -514,7 +521,7 @@ const more = warnings.length > 5 ? `
` : ''; - warningEl.innerHTML = ` + safeHTML(warningEl, ` ` ).join(''); - docPreviewEl.innerHTML = ` + safeHTML(docPreviewEl, `| Device | Browser | @@ -1322,7 +1328,7 @@ async function renderDevices() {${!isCurrent ? `` : ''} |
|---|
${escapeHtml(w.value)} — ${w.hint}