1. Storage model at a glance
| Persistent store | chrome.storage.local only |
chrome.storage.sync used? | No — nothing synced across devices |
unlimitedStorage? | No — not requested |
| Quota | ~10 MB (chrome.storage.local on Chrome 114+; this extension requires 116+) |
| Session (transient) store | chrome.storage.session — ephemeral cross-worker state only |
| Remote DB / backend account | None. No account, no login, no cloud sync. |
| First-party server contact | Only the optional Deal Feed (tg-forwarded-deals.deals.group); receive-only; opt-in |
chrome.storage.session usage (transient — cleared at end of browser session):
Anonymous analytics session id (30-min GA4 TTL)
Tracks the dynamic DNR mobile-UA rule installed while preview is open
Carries the right-click "Remind Me…" selection to the popup
2. Storage key inventory (every dg.* key)
Top-level persistent keys are defined in src/storage/StorageKeys.ts. Several engine arrays are materialized (derived) from dg.user — the single source of truth the dashboard edits.
| Key | Contents | Sensitivity |
|---|---|---|
| dg.user | The single UserConfig — stores, networks, integrations, settings, credentials, and analyticsEnabled | High — credentials |
| dg.settings | App settings (timeouts, default redirect depth, history cap, theme) | Low |
| dg.domains | Materialized engine domain/store rules | Low |
| dg.excluded | Domains passed through untouched (optional keepOriginal) | Low |
| dg.unwrap | Per-domain rules that extract an embedded destination URL from a query param | Low |
| dg.affiliates | Materialized affiliate networks / trackers / link templates | Low–Medium |
| dg.pipelines | Materialized internal processing pipelines | Low |
| dg.shorteners | Materialized shortener configs ({{token}} pre-substituted) | Medium — token |
| dg.integrations | Materialized Telegram channels + webhooks | High — tokens |
| dg.smartReplacements | User [DG-*] token definitions | Low |
| dg.history | Processing history entries (input/output links, store/network, timestamps) | Medium |
| dg.reminders | Scheduled reminder + scheduled-message entries | Medium |
| dg.analytics.cid | Anonymous analytics client id, random <int>.<unix> | None — no PII |
| dg.analytics.sid | Anonymous analytics session id (session store, 30-min TTL); not exported | None — no PII |
| dg.debug | Diagnostic logging flag; logs stay on-device | Low |
| dg.historyPrunedAt | One-shot timestamp for the "old entries pruned" toast | None |
dg.analytics.cid, dg.debug, and dg.historyPrunedAt are in ALLOWED_IMPORT_KEYS so they round-trip across installs.
3. Credentials — stored locally only
The following credentials are stored only in chrome.storage.local. None is ever uploaded to any DealsGroup server (including the Deal Feed server, which is receive-only):
| Credential | Where stored | Sent to |
|---|---|---|
| Telegram bot token(s) + chat id(s) | dg.user → dg.integrations | api.telegram.org, only on send |
| Webhook token(s) + custom headers | dg.user → dg.integrations | Your webhook URL only, only on broadcast |
| Custom-shortener API token | dg.user → dg.shorteners | Your shortener host only, only when shortening is on |
Credentials are kept out of error messages (scrubbed) and out of backups/shares by default (§7). The Deal Feed never transmits any of these values.
4. Analytics — identifiers, events, and what is never sent
Implementation: src/services/Analytics.ts. GA4 Measurement Protocol, endpoint https://www.google-analytics.com/g/collect. Every payload carries non_personalized_ads: true.
analyticsEnabled is tri-state. The default config leaves it undefined; the first-run wizard shows the consent toggle pre-enabled, and completing onboarding without changing it resolves to enabled. So analytics is on by default. Turn it off anytime in Settings → Behavior. (Chrome data disclosure: "User activity = Yes", event counts only.)Transmitted event params — allowlist ONLY
Hardening (by construction, not convention)
Strings capped at 40 chars (MAX_STRING_LEN).
A denylist regex (FORBIDDEN_VALUE) drops any value resembling a URL, email, or 20+ char token.
Event names must match /^[a-z][a-z0-9_]{0,39}$/; ~38 named events exist across categories.
Events batched up to 25/request; token-bucket rate-limit caps sends at ~30/min.
user_id, or user_properties.5. Complete outbound network table
This is the complete set of outbound destinations. Each occurs only when its inherent feature runs. SSRF guarding (§8) applies to every user-supplied endpoint and every redirect hop.
| # | Destination | When | Data sent | Cookies |
|---|---|---|---|---|
| 1 | The link's own servers | Resolving redirects (on by default; toggleable) | The URL being resolved | credentials:'omit' for fetch hops; JS-redirect detection opens a hidden tab using your normal session |
| 2 | Your shortener API (default ViaDeals via.deals/api/links; or custom; or native amzn.to/fkrt.it) | When shortening is on | The URL to shorten | Cookies only to a hardcoded affiliate-shortener allowlist; all others credentials:'omit'; response capped at 64 KB |
| 3 | Your webhook (Discord / Slack / n8n / custom) | When you broadcast to it | Your templated message/link | credentials:'omit'; SSRF-guarded |
| 4 | api.telegram.org | When you send to Telegram | Message text/photo + bot token | Bot token; no browser cookies |
| 5 | tg-forwarded-deals.deals.group (Socket.io WSS + /media/*) — DealsGroup Deal Feed | Only when you open the Deal Feed and click Connect. Receive-only. Auto-disconnects after 5 min idle | A WebSocket connection (receive-only). Server reports an anonymous online-client count via tg:welcome / tg:clients | None. Images credentials:'omit' |
| 6 | api.qrserver.com | Only when you click to generate a QR code | The URL you are encoding | None |
| 7 | www.google-analytics.com/g/collect | While analytics is enabled | Anonymous event counts + random client id | None |
credentials:'omit'.Content Security Policy (extension pages)
script-src 'self'; object-src 'self'; img-src 'self' data: https://api.qrserver.com https://tg-forwarded-deals.deals.group; connect-src 'self' https: wss:; frame-src https: http:; style-src 'self' 'unsafe-inline'
No remote code, no eval, no unsafe-inline scripts. frame-src permits https:/http: because Mobile Preview renders a user-supplied URL inside an iframe; no DealsGroup-controlled content is ever framed.
6. Link history
| Storage key | dg.history (chrome.storage.local) |
| Soft cap (default) | 500 entries |
| Configurable range | 100–5000 |
| Pruning | Oldest pruned on overflow; one-shot dg.historyPrunedAt flag shows a toast |
7. Export / import, secret & share redaction
Export is produced by StorageService.exportAll() as pretty-printed JSON of the chrome.storage.local snapshot.
Default export (redacted)
redactSecrets() turns credential-shaped fields into "[redacted]"; sensitive headers and secrets embedded in URL query strings are scrubbed.
Full export (opt-in)
Choosing "Include credentials" writes tokens in plain text in the backup file — keep it private.
Single-item / community share
Always strips secrets before sharing — no opt-in to include secrets in a share.
Import
Accepts only ALLOWED_IMPORT_KEYS; drops unknown keys; validates each key's shape; runs the SSRF guard; writes atomically with rollback on failure.
8. SSRF guard
Every user-supplied endpoint and every redirect hop passes through isPublicHttpUrl() (src/utils/url.ts) before any request. It blocks:
non-http(s) schemes
localhost, *.localhost, *.local, *.internal
loopback / link-local / RFC1918 private IPv4 in any inet_aton form (dotted, hex, octal, short)
cloud-metadata 169.254.169.254
IPv6 loopback / unspecified / link-local / ULA and IPv4-mapped private addresses
Outbound fetches are concurrency-capped (6 in flight) with 429/503 backoff. Shortener cookies gated by COOKIE_HOST_ALLOWLIST; responses capped at 64 KB. IDN-homograph hosts are rejected (isSuspiciousIdnHost) so phishing lookalikes are not tagged.
9. Data lifecycle, retention, and deletion
| Action | Effect |
|---|---|
| Writes | saveUserConfig() materializes the user config into engine arrays and performs a delta write — only changed keys are written |
| History retention | Soft-capped (default 500, range 100–5000); oldest pruned on overflow |
| Legacy redirect cache | Removed entirely. Redirects resolved fresh every time. Orphan dg.redirectCache blobs from older installs purged automatically on update |
| Session data | chrome.storage.session values cleared at end of browser session |
| Deal Feed | Live-only; nothing persisted as backlog; socket auto-disconnects after 5 min idle |
| Manual deletion | Clear history, remove individual rules/integrations/reminders, or use the dashboard's clear/reset actions |
| Full deletion | Uninstalling removes all chrome.storage.local and .session data. No server-side copy to delete |
10. What we never collect
We do not collect or upload your configuration, credentials, or history to any DealsGroup server — including the Deal Feed server, which is receive-only.
We do not read or exfiltrate page content. The content script's optional auto-detect/auto-process is off by default and runs only when you enable it and the host matches a configured store rule.
We do not use chrome.storage.sync and do not sync your data across devices.
Analytics never sends URLs, domains, tags, tokens, secrets, message bodies, page content, configuration, user_id, or user_properties.
There is no account, no login, and no advertising/tracking identifier; analytics ids are random and contain no PII.
11. Independent verification
- Inspect storage: in any extension page DevTools console, run
chrome.storage.local.get(null)andchrome.storage.session.get(null). - Watch network calls: DevTools → Network on the dashboard/popup and the service worker. Confirm requests only go to the §5 destinations, only when triggered — and that the Deal Feed only connects after you click Connect.
- Verify analytics scrubbing: read
ALLOWED_PARAM_KEYS,FORBIDDEN_VALUE, and the scrub logic inAnalytics.ts. - Verify SSRF guard: read
isPublicHttpUrl()insrc/utils/url.tsand its call sites. - Verify backup redaction: export with and without "Include credentials" and diff the files.
- Confirm quota: read
getStorageUsage()—QUOTA_BYTESdefaults to10_485_760(10 MB). - Verify the Deal Feed contract: read
FEED_ORIGIN,FEED_IDLE_MS, and thetg:welcome/tg:clientshandlers insrc/dashboard/dashboard.ts.