Reveddit-style browser extension for the .win sites (patriots.win / scored.co / communities.win — same backend, different aliases). It watches your own posts and comments and notifies you when one is removed or shadow-removed, and lets you see whether your content is being hidden from other users.
Detection runs in the background service worker, so it keeps working with no .win tab open (like reveddit real-time). The content script's only jobs are to capture your session and paint in-page highlights — all polling/detection is in the background.
- Session capture (content script). The site keeps per-user API tokens in
localStorage; sent asX-Api-Key/X-Api-Secretheaders, they authenticate independently of cookies. The content script reads them, resolves your username (user/about.json?user=me), and hands{user, origin, apiKey, apiSecret}to the background. It re-captures on every page load so the session stays fresh. - Polling (background). Using those tokens the background polls your own
content/profile.json(authenticated) — one call returns your recent posts and comments withis_removed/removal_sourceflags. The service worker has host permissions, so it makes these cross-origin calls itself, with no tab open. On first run it pages through your whole history once (bounded to ~500 items, ~0.8s between pages) to find existing removals — recorded without per-item notifications, summarized as a single "found N removed" notice. After that, ongoing checks fetch only the first page each minute (~1 request), since new removals of recent items show up there. Achrome.alarmstimer (with a watchdog) drives the cycle. - Confirm + notify. Each observation is folded into stored state; after a couple of consecutive confirmations a removal is recorded, a notification fires, and the badge updates (each id notifies at most once, ever).
- Recovering your text. A removed item's list entry appears immediately (its
permalink is built from the uuid — no lookup needed). The text fills in right
after:
- posts/comments you made while installed already have their text, captured from
the page's own
create_comment/create_posttraffic — shown instantly; - for backlog removals the background recovers your own copy via the ordinary
comment/thread?id=…endpoint (authenticated with your tokens — the same call the site uses to render any permalink, verified to return a removed comment's content to its author). Until it arrives the popup shows a "loading…" indicator.
- posts/comments you made while installed already have their text, captured from
the page's own
Public-view audit: for each removed comment or post, the background also fetches
the logged-out view (the same comment/thread / post.json endpoints, but
without tokens) and compares it to your authored view. This (a) corroborates the
removal independently of the is_removed flag, (b) corrects false positives — if
the public still shows your content, the record is dropped — and (c) classifies the
removal as a visible placeholder vs. hidden from others entirely.
Posts too: the profile feed carries the same removal fields for your posts, so
removed posts are detected, listed, notified and highlighted exactly like comments
(a removed post shows you its title; the public sees it blanked). Highlighting marks
the post block; comments mark the comment block.
In-thread highlighting (content script): on .win pages (threads and your user
page, on both patriots.win and scored.co), the content script outlines any of your
own posts/comments it knows are removed and adds a small attributed banner — an
extension tag (click → options) and a removal-source pill ("community filter" /
"moderator" / …, with a tooltip), plus a "hidden from others" pill when the audit
found the public can't see it at all. A MutationObserver re-marks items loaded
later (SPA navigation, "load more").
The extension only ever reads your own session's normal endpoints and stores text you authored — no undocumented content-recovery endpoint. Default poll interval is 1 minute (one bulk request per check).
See src/background.ts (service-worker orchestration), src/src/poller.ts (the
detection cycle), monitoring.ts (removal transition logic + recording),
win-api.ts (id↔uuid encoding, profile normalization, public-view classifier),
content.ts / inject.ts (session + post capture, highlighting), and storage.ts.
This project uses Yarn 4 (vendored at .yarn/releases/), matching the
reveddit website/extension. The supply-chain settings in .yarnrc.yml are
intentional: npmMinimalAgeGate: 7d (refuse packages younger than 7 days),
enableScripts: false (no dependency install scripts run), and
checksumBehavior: throw. Dependency versions are pinned to releases at least 30
days old.
yarn install # respects the 7-day age gate, runs no install scripts
yarn typecheck
yarn test # vitest: id<->uuid encoding, profile normalization, removal-detection
# threshold, notify-once orchestration, url parsing, public-view classifier
yarn build-chrome # PROD -> dist-chrome/ and chrome.zip
yarn build-firefox # PROD -> dist-firefox/ and firefox.zip (manifest rewritten to MV2)
yarn build-chrome-dev # DEV -> dist-chrome-dev/ (debug breadcrumbs, inline maps)
yarn build-firefox-dev # DEV -> dist-firefox-dev/ — dev and prod never share a dir,
# so a dev build can't be shipped by mistake
yarn test:e2e # Playwright: loads the real built extension in Chromium and
# checks the popup/options/history pages.
# First time: `yarn playwright install chromium`.
yarn e2e:login # one-time: opens Chromium with the extension + a persistent
# profile; log in by hand, press Enter to save the session.
# Then: WIN_E2E_LIVE=1 yarn test:e2e (runs logged-in tests)
yarn test:firefox # headless-Firefox smoke test over Marionette: installs the
# built dist-firefox as a temporary add-on and asserts the
# whole pipeline (background, alarm, content script on the
# live site, messaging, popup, auth). With WIN_USER/WIN_PASS
# in .env it also does a REAL login and asserts live session
# capture. Build with build-firefox-dev first (dev builds
# carry the data-gw-* breadcrumbs the test reads).Load unpacked (dev builds): Chrome → chrome://extensions → Developer mode → Load
unpacked → dist-chrome-dev/. Firefox → about:debugging → Load Temporary Add-on →
any file in dist-firefox-dev/.
One-time setup: fill the publishing block in .env (see .env.example — the
variable names match the reveddit real-time extension, so account-level values
copy over; only CHROME_EXTENSION_ID is per-extension). Credentials guides:
Chrome OAuth client —
https://github.com/fregante/chrome-webstore-upload/blob/main/How%20to%20generate%20Google%20API%20keys.md;
AMO keys — https://addons.mozilla.org/developers/addon/api/key/.
yarn publish:all # preflight → publish Chrome → publish Firefox
yarn publish:chrome # CWS: fresh prod build → upload zip → pauses while you
# paste the description (the CWS API can't update listing
# text; it's copied to your clipboard + the dashboard
# opens) → publish. Expired refresh token? It offers
# `yarn refresh:chrome` (localhost OAuth flow that
# rewrites .env) and retries.
yarn publish:firefox # AMO: fresh prod build → source.zip (git archive HEAD —
# commit first!) → web-ext sign (listed) → updates the
# store description through the AMO API.
yarn publish:firefox:text # only push STORE_DESCRIPTION.txt to the AMO listingpreflight (run by every publish command) is typecheck + unit tests + Chromium
e2e against the dev build + the headless-Firefox live probe (test:firefox).
Dev builds go to dist-*-dev/, prod to dist-*/ — and the publish scripts each
rebuild their production artifact right before uploading, so nothing stale (or
dev) can end up in a store submission.
Both flows refuse to run if STORE_DESCRIPTION.txt has no entry for the current
manifest version. AMO requires human-readable source for webpack-bundled
submissions — reviewers rebuild with the steps in Develop above (Node 20,
yarn install, yarn build-firefox).
CI alternative: tag v<version> (must equal src/manifest.json version) and push —
release.yml builds, publishes both stores (secrets: CWS_*, AMO_JWT_*), and
attaches the zips to the GitHub release. Interactive steps are skipped in CI, so
verify the Chrome listing description in the dashboard after a tagged release.
removal_source: communityFilter/communityFilterPending= automatic filter removal;moderator/nuke= manual. The auto-filter targets certain slurs and terms; new accounts (is_new_user: true) also get held.- A community-filtered comment is present in the anonymous thread with
is_removed: trueand empty content (shown to others as "removed by community filter") → classifiedpublicly_removed. - Author content recovery:
comment/profile.jsonblanks the content even for the author, butcomment/thread?id=…(authenticated with your API tokens) returns the author'sraw_content— this is what backlog recovery uses.
- Backlog recovery + polling fetch
${origin}/api/v2/...from the background with yourX-Api-Key/X-Api-Secrettokens; verified against patriots.win — confirm scored.co / communities.win serve/api/v2on their own origin under header-token auth too (the service worker's host permissions cover all three). - Removal detection trusts
content/profile.json'sis_removedflag, and also treats a comment whose content the feed has blanked (once we'd seen its text) as removed, so it survives the site dropping/renaming that flag. The per-item public-view audit runs only on already-flagged removals (to confirm + classify), not as a primary detector — if the feed ever stopped exposing removal state at all, detection would need to fall back to auditing every tracked item. - Page-size params are ignored by the API (fixed 25/page); pagination uses the
verified
&post=N&comment=Mrunning-count cursor.