For privacy-conscious users · Data

Data handling, byte by byte

A complete, source-accurate inventory of every byte the extension reads, writes, retains, and transmits — table-heavy so a reviewer can map each claim to a concrete storage key, network destination, or API.

Version 1.0.12 Manifest V3 Min Chrome 116
The network model in one sentence

The extension is local-first: all configuration, credentials, and history stay on your device in chrome.storage.local; there is no account and your config/credentials/browsing are never uploaded. It has one first-party network feature — the optional, opt-in, receive-only Deal Feed, which connects to https://tg-forwarded-deals.deals.group to stream deal messages. Every other outbound call goes either to endpoints you configure or to a small set of fixed third parties listed in §5. "Local-first" does not mean "no server" — the Deal Feed server is disclosed in full below.

1. Storage model at a glance

Persistent storechrome.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) storechrome.storage.session — ephemeral cross-worker state only
Remote DB / backend accountNone. No account, no login, no cloud sync.
First-party server contactOnly the optional Deal Feed (tg-forwarded-deals.deals.group); receive-only; opt-in

chrome.storage.session usage (transient — cleared at end of browser session):

dg.analytics.sid

Anonymous analytics session id (30-min GA4 TTL)

Mobile-preview UA rule

Tracks the dynamic DNR mobile-UA rule installed while preview is open

Remind-me payload

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.

KeyContentsSensitivity
dg.userThe single UserConfig — stores, networks, integrations, settings, credentials, and analyticsEnabledHigh — credentials
dg.settingsApp settings (timeouts, default redirect depth, history cap, theme)Low
dg.domainsMaterialized engine domain/store rulesLow
dg.excludedDomains passed through untouched (optional keepOriginal)Low
dg.unwrapPer-domain rules that extract an embedded destination URL from a query paramLow
dg.affiliatesMaterialized affiliate networks / trackers / link templatesLow–Medium
dg.pipelinesMaterialized internal processing pipelinesLow
dg.shortenersMaterialized shortener configs ({{token}} pre-substituted)Medium — token
dg.integrationsMaterialized Telegram channels + webhooksHigh — tokens
dg.smartReplacementsUser [DG-*] token definitionsLow
dg.historyProcessing history entries (input/output links, store/network, timestamps)Medium
dg.remindersScheduled reminder + scheduled-message entriesMedium
dg.analytics.cidAnonymous analytics client id, random <int>.<unix>None — no PII
dg.analytics.sidAnonymous analytics session id (session store, 30-min TTL); not exportedNone — no PII
dg.debugDiagnostic logging flag; logs stay on-deviceLow
dg.historyPrunedAtOne-shot timestamp for the "old entries pruned" toastNone

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):

CredentialWhere storedSent to
Telegram bot token(s) + chat id(s)dg.user → dg.integrationsapi.telegram.org, only on send
Webhook token(s) + custom headersdg.user → dg.integrationsYour webhook URL only, only on broadcast
Custom-shortener API tokendg.user → dg.shortenersYour 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.

Consent / default: 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

tabtoolvia reasonkindsuccess countengagement_time_msecapp_version

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.

Never sent: URLs, domains, affiliate tags/sub-IDs, bot tokens, webhook secrets, message bodies, page content, your configuration, 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.

#DestinationWhenData sentCookies
1The link's own serversResolving redirects (on by default; toggleable)The URL being resolvedcredentials:'omit' for fetch hops; JS-redirect detection opens a hidden tab using your normal session
2Your shortener API (default ViaDeals via.deals/api/links; or custom; or native amzn.to/fkrt.it)When shortening is onThe URL to shortenCookies only to a hardcoded affiliate-shortener allowlist; all others credentials:'omit'; response capped at 64 KB
3Your webhook (Discord / Slack / n8n / custom)When you broadcast to itYour templated message/linkcredentials:'omit'; SSRF-guarded
4api.telegram.orgWhen you send to TelegramMessage text/photo + bot tokenBot token; no browser cookies
5tg-forwarded-deals.deals.group (Socket.io WSS + /media/*) — DealsGroup Deal FeedOnly when you open the Deal Feed and click Connect. Receive-only. Auto-disconnects after 5 min idleA WebSocket connection (receive-only). Server reports an anonymous online-client count via tg:welcome / tg:clientsNone. Images credentials:'omit'
6api.qrserver.comOnly when you click to generate a QR codeThe URL you are encodingNone
7www.google-analytics.com/g/collectWhile analytics is enabledAnonymous event counts + random client idNone
Deal Feed disclosure (explicit): a first-party DealsGroup server. Opt-in (click Connect + confirm), receive-only (deals flow in; your data does not flow out), and live-only (no history/backlog). Auto-disconnects after 5 min idle. Incoming HTML/Markdown is re-sanitized client-side against an allow-list of tags before display (XSS-safe). Feed images fetched with 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 keydg.history (chrome.storage.local)
Soft cap (default)500 entries
Configurable range100–5000
PruningOldest 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

ActionEffect
WritessaveUserConfig() materializes the user config into engine arrays and performs a delta write — only changed keys are written
History retentionSoft-capped (default 500, range 100–5000); oldest pruned on overflow
Legacy redirect cacheRemoved entirely. Redirects resolved fresh every time. Orphan dg.redirectCache blobs from older installs purged automatically on update
Session datachrome.storage.session values cleared at end of browser session
Deal FeedLive-only; nothing persisted as backlog; socket auto-disconnects after 5 min idle
Manual deletionClear history, remove individual rules/integrations/reminders, or use the dashboard's clear/reset actions
Full deletionUninstalling 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

  1. Inspect storage: in any extension page DevTools console, run chrome.storage.local.get(null) and chrome.storage.session.get(null).
  2. 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.
  3. Verify analytics scrubbing: read ALLOWED_PARAM_KEYS, FORBIDDEN_VALUE, and the scrub logic in Analytics.ts.
  4. Verify SSRF guard: read isPublicHttpUrl() in src/utils/url.ts and its call sites.
  5. Verify backup redaction: export with and without "Include credentials" and diff the files.
  6. Confirm quota: read getStorageUsage() — QUOTA_BYTES defaults to 10_485_760 (10 MB).
  7. Verify the Deal Feed contract: read FEED_ORIGIN, FEED_IDLE_MS, and the tg:welcome / tg:clients handlers in src/dashboard/dashboard.ts.