Veridex browser extension: risk signals for e-commerce listings, in Brave/Chromium (MV3)
  • TypeScript 79.4%
  • JavaScript 13.9%
  • HTML 3.7%
  • CSS 2%
  • Makefile 0.6%
  • Other 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
kroshtan 8b80171e62
All checks were successful
CI / check (push) Successful in 1m6s
CI / publish-dev (push) Successful in 44s
Merge pull request 'Firefox: package it for addons.mozilla.org' (#18) from amo into main
2026-11-12 13:28:15 +00:00
.forgejo/workflows Firefox: package it for addons.mozilla.org 2026-11-12 14:26:55 +01:00
brand Extension 0.7.0: Unfret name and icon, softer labels, photo thumbnails, store package 2026-11-09 11:58:27 +01:00
icons Extension 0.7.0: Unfret name and icon, softer labels, photo thumbnails, store package 2026-11-09 11:58:27 +01:00
icons-dev Extension 0.8.0: Unfret Dev, a separate extension for the dev deployment 2026-11-09 15:50:04 +01:00
release Extension 0.8.0: Unfret Dev, a separate extension for the dev deployment 2026-11-09 15:50:04 +01:00
scripts Firefox: package it for addons.mozilla.org 2026-11-12 14:26:55 +01:00
src Extension 0.9.1: Unfret moves to unfret.net 2026-11-10 20:48:40 +01:00
store Firefox: package it for addons.mozilla.org 2026-11-12 14:26:55 +01:00
test Extension 0.9.1: Unfret moves to unfret.net 2026-11-10 20:48:40 +01:00
.gitignore release: self-hosted Chromium channel with a signed, reproducible CRX 2026-11-08 15:44:33 +01:00
.nvmrc Firefox: package it for addons.mozilla.org 2026-11-12 14:26:55 +01:00
.prettierignore chore: scaffold the extension (TypeScript, esbuild, vitest, eslint, prettier) 2026-11-08 12:37:09 +01:00
.prettierrc.json chore: scaffold the extension (TypeScript, esbuild, vitest, eslint, prettier) 2026-11-08 12:37:09 +01:00
CHANGELOG.md Firefox: package it for addons.mozilla.org 2026-11-12 14:26:55 +01:00
eslint.config.js chore: scaffold the extension (TypeScript, esbuild, vitest, eslint, prettier) 2026-11-08 12:37:09 +01:00
INSTALL.md Extension 0.9.1: Unfret moves to unfret.net 2026-11-10 20:48:40 +01:00
LICENSE docs, ci: README, privacy policy draft, MIT license, Makefile, Forgejo CI 2026-11-08 15:07:26 +01:00
Makefile Firefox: package it for addons.mozilla.org 2026-11-12 14:26:55 +01:00
package-lock.json Extension 0.9.1: Unfret moves to unfret.net 2026-11-10 20:48:40 +01:00
package.json Firefox: package it for addons.mozilla.org 2026-11-12 14:26:55 +01:00
PRIVACY.md Extension 0.9.1: Unfret moves to unfret.net 2026-11-10 20:48:40 +01:00
README.md Firefox: package it for addons.mozilla.org 2026-11-12 14:26:55 +01:00
tsconfig.json test: smoke-test the built extension in headless Chromium 2026-11-08 15:07:26 +01:00
vitest.config.ts test: smoke-test the built extension in headless Chromium 2026-11-08 15:07:26 +01:00
vitest.smoke.config.ts test: smoke-test the built extension in headless Chromium 2026-11-08 15:07:26 +01:00

Unfret browser extension

Risk signals for the product page you're looking at: Temu-grade listings on Amazon dressed up with thousands of five-star reviews, dropship stores, fake countdowns, pre-ticked add-ons and fees that only appear at checkout. Unfret reports evidence and a plain-words verdict; it never calls a shop a scam.

Chrome (Manifest V3) and Firefox, from one source. Vanilla TypeScript, no UI framework, no remote code, no third-party requests: the extension talks to the Unfret API and nothing else.

Install: from the Chrome Web Store (Chrome, Brave, Edge and other Chromium browsers); Firefox is being submitted to addons.mozilla.org (store/AMO.md). Both are built byte for byte from this repository. The older self-hosted install is described in INSTALL.md, but no longer offered.

How it works

toolbar click ─► popup ─► background ─► inject extract.js into the tab ─► PageFacts + cleaned text
                                   └─► POST /v1/analyze/stream (Bearer session) ─► live steps ─► score, flags
  • Nothing runs until you click "Analyse this page". Each analysis costs one check. The extension has no standing access to any website: it reads a page only through activeTab, which the browser grants for that one tab when you click the toolbar button. On supported marketplace product pages (Amazon, all countries; bol.com) the toolbar icon gets a small amber marker; in Chromium the browser sets it itself from the page address (declarativeContent), without the extension seeing the page.
  • Extraction happens in the browser. src/extract/ is a TypeScript port of the backend's PageFacts extractors (facts/*.py). It also adds what only the rendered page shows: text hidden by stylesheet CSS, the current state of checkboxes, and countdowns read twice a few seconds apart.
  • Only facts and cleaned product text are sent, never the raw page: account menus, delivery addresses, greetings, forms and inputs are stripped from a copy of the page first.
  • Sign-in is Sign-In With Solana. "Sign in" opens https://unfret.net/connect; after you sign a message in your wallet, that page posts the session to itself and the extension's content script (injected only there) relays it. The relay accepts it only from that window on the configured Unfret origin, and the background checks the sender again. Signing grants no spending rights. "Buy checks" opens /checkout, which also shows the invite codes a pack comes with, once, after payment.
  • "Have an invite?" (popup and settings, when signed in) redeems a code onto the account in use (POST /v1/invites/redeem, with its own sign-in); only a new wallet or a never-funded key can. The code isn't kept.
  • "Share this result" asks the API for a preview of the public page (POST /v1/shares/preview), and only on "Share" makes it (POST /v1/shares), both without the sign-in, so the page names no account. The link and its delete code stay with the tab's result in session memory.
  • English only. The extension's own text is English; the verdict's wording is the server's, shown as it comes.

The API contract is in the backend repository (credits door, contract v1).

Layout

src/
  background/   service worker / event page: session relay, icon hint, badge, analysis runner
  content/      extract-entry.ts (injected on demand), connect-relay.ts (connect page only), relay.ts (checks)
  extract/      PageFacts extraction: parse, dom, generic, amazon, bolcom (ports); live, clean (browser-only)
  api/          API client and SSE parser
  popup/        popup UI
  options/      options page (API base URL, sign out)
  shared/       facts types + JSON schema, settings, sites, messages, CSS
scripts/        build (esbuild, per-browser manifests, zips), icons, gen_parity.py
test/           unit, parity (jsdom vs the Python extractors), mock API; smoke/ (headless Chromium)

Permissions

Permission Why
activeTab Read the page you clicked on (any shop, marketplaces too), only then
scripting Inject the extractor into that tab; register the relay for a dev API origin
storage Your session (token, wallet address, expiry) and settings; nothing else
declarativeContent (Chromium) The browser marks the icon on marketplace product pages; no page access
https://unfret.net/connect* The sign-in relay on the Unfret sign-in page (the API itself is CORS)
optional http://localhost/*, http://127.0.0.1/* Only if you point the extension at a local dev server

Development

make install          # npm ci
make test             # unit + parity tests
make lint typecheck
make build-dev        # dist/chrome, dist/firefox (unminified, source maps)
make smoke            # build + headless Chromium run (first: npx playwright install chromium)
make parity           # regenerate expectations from the backend's Python extractors (BACKEND=~/repos/unfret_backend)

Load unpacked:

  • Chrome: chrome://extensions → Developer mode → Load unpacked → dist/chrome.
  • Firefox: about:debugging#/runtime/this-firefox → Load Temporary Add-on → dist/firefox/manifest.json. Firefox has no declarativeContent, so it shows no marketplace marker; analysis works the same.

To use a local backend, open the extension's settings and set the API base to e.g. http://localhost:8080 (the browser asks for access to that address).

make build writes four builds: dist/chrome (the store variant, zipped as dist/unfret-chrome-<version>.zip), dist/chrome-selfhosted (with the update URL and public key, signed into a CRX by make release), dist/chrome-dev and dist/firefox (built, not published).

dist/chrome-dev is Unfret Dev: a separate extension (ID phokojgbhkfcejnnocokabdjoljdfgpl, grey icon with a DEV label) that talks only to the dev deployment (dev.unfret.net, devnet) and updates from there. Install it next to Unfret to test; the production extension never talks to dev. The version comes from package.json. make release produces the signed self-hosted release in dist/release/ (see INSTALL.md).

Releases (CI/CD)

  • Merge to main ⇒ Unfret Dev. CI (.forgejo/workflows/ci.yaml, job publish-dev) builds Unfret Dev as <version>.<run number>, signs it and opens a self-merging pull request on the backend, whose pipeline deploys it to dev.unfret.net.
  • Production ⇒ by hand only. Actions → "Release to the extension stores" (release-store.yaml) builds and checks the store packages (Chrome, Firefox and the source zip addons.mozilla.org asks for) and attaches them to the release v<version>; the uploads are manual (store/SUBMIT.md, store/AMO.md).

Parity with the backend

The same HTML must give the same PageFacts in the browser as on the server, or the scorer sees different features than it was trained on. test/parity.test.ts runs the TypeScript extractors over test/fixtures/*.html and compares them field by field with the Python extractors' output (*.expected.json), and validates against the backend's PageFacts JSON schema. When the backend's extractors change, run make parity and port the change.

The live-only additions (stylesheet-hidden text, live checkbox state, observed countdowns) deliberately go beyond the server's view; they are conservative so as not to skew the scorer, and the scorer should eventually be retrained on extension-sourced facts.

License

MIT. See PRIVACY.md for what is sent and stored.