尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Know what your team's Chrome extensions can do, and hear about it the day an update asks for more. Built by Michael Lip.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗Know what your team's Chrome extensions can do, and hear about it when that changes. Built by Michael Lip (github.com/theluckystrike), whose Chrome extensions have 36,021 store users across 29 listings. Live at https://extension-watch.lipmichal.workers.dev
Every company runs dozens of browser extensions that nobody reviewed. Extensions update themselves silently. An extension that only needed one site when someone installed it can, after an update, ask to "read and change all your data on all websites", and the browser shows no warning when an update keeps the extension enabled under policy. Security teams find out after an incident, if at all.
The grade measures access, not intent. A password manager needs to read every page, so it grades high, and that is fine when you trust its publisher. What matters is knowing it, and being told when an extension you trusted asks for more.
The scan results show each extension's access grade, its highest finding, and a details drawer with every rule that fired.
A watchlist shows the private link (blurred here), the change log, the extensions and the alert webhook.
Browser ── /api/scan ──────────► Cloudflare Worker ──► Google update service (CRX package, streamed)
── /api/watchlists ───► │ find manifest + locale, grade, diff
◄── /w/<private link> ── ├─► Workers KV, one record per watchlist + one shared record per extension per day
Cron Trigger every 10 min ──── re-checks lists not checked for 23 h, oldest first ──► webhook alerts
manifest.json or _locales/<x>/messages.json. It then reads manifest.json at the offset the ZIP index gives and checks that the local header there carries the same name, so a decoy manifest hidden inside another file cannot change the grade. A package with two index entries for manifest.json is refused, and a package that could be read before but not after an update is logged and alerted as a high-severity change. Memory follows the size of the wanted entries, not the package. Captures end at the next local header, and entries that inflate past 4 MB are refused. In production, Keeper (a 143 MB package) and RoboForm were read together in 5.5 s. When Google does honour ranges, only the needed parts are fetched. If the name is still a locale key, the display name comes from the extension's Web Store page. The ZIP and DEFLATE decoding is written in the Worker, with no zip library. It handles CRX2, CRX3, data-descriptor entries, comments and a BOM in manifests.| Key | Value | Expiry |
|---|---|---|
wl:<admin token> | { name, ids[], snapshots{id: {manifest, assessment, error, checkedAt}}, changes[], createdAt, lastCheck, viewedAt, webhook, viewToken, plan, trialEndsAt }, with { lastCheck } as key metadata so the scheduler can pick due lists from a listing alone. plan is free, team or trial, and a record with no plan counts as Team | 90 days, renewed by every save |
au:<admin token>:<id> | One admin action as { at, action } in the key metadata, one key per action so concurrent actions cannot overwrite each other, newest 100 kept, moved to the new prefix on rotate and deleted with the list | 90 days |
vw:<read-only token> | The admin token it belongs to | 400 days |
ext:<extension id> | The day's snapshot of one extension, shared by every watchlist | 3 days |
rl:<action>:<day>:<salted address hash> | Daily counter for scans, new watchlists and test alerts, keyed by a salted hash of the address. IPv4 is hashed whole and IPv6 is hashed by its /64 prefix | 2 days |
tri:<salted sha256 of client address>:<attempt id> | One marker for each Team trial attempt, so a fourth new list from the same address inside 30 days starts on Free. The key holds a salted SHA-256 hash of the address, never the address. IPv4 is hashed whole and IPv6 is hashed by its /64 prefix. Each marker expires at the end of the 30 day window | 30 days from the first trial |
del:<admin token> | Tombstone for a deleted list or a rotated old admin token, so a check that was running when the list was deleted or rotated re-reads it before saving and refuses to write the old record back | 400 days |
A change is { kind, value, severity, id, at }. The change log keeps the last 200 entries.
| Method and path | Purpose |
|---|---|
POST /api/scan { ids } | Grade up to 25 extensions |
POST /api/watchlists { name, ids, plan? } | Create a watchlist (up to 50 extensions) and get back the private token. plan is optional and only free is meaningful, which starts the list on Free and skips the Team trial |
GET /api/watchlists/<token> | Read a watchlist. The admin token returns role: admin, the read-only token and the webhook host. The read-only token returns role: viewer and no webhook |
PUT /api/watchlists/<admin token> { webhook } | Set or clear the alert webhook |
PUT /api/watchlists/<admin token> { name } | Rename a watchlist |
DELETE /api/watchlists/<admin token> | Delete a watchlist and its read-only link |
PUT /api/watchlists/<admin token>/plan { plan } | Set the plan to free or team. Needs the admin link and the operator code in an x-operator-code header, so a list owner cannot upgrade their own list while billing is not live |
POST /api/watchlists/<admin token>/rotate | Issue a new admin token and a new read-only token, move the list to the new key, delete the old keys and return the new links. Admin link only, and both old links return 404 afterwards |
POST /api/test-alert { token: <admin token> } | Send a labelled test message to the saved webhook |
GET /api/rules | The full points table |
GET /api/watchlists/<token>/export.csv | Inventory as CSV, for the admin and read-only links |
GET /api/watchlists/<token>/changes.csv | Change log as CSV, for the admin and read-only links |
GET /api/watchlists/<admin token>/audit.csv | Admin action log as CSV, admin link only |
GET /api/watchlists/<admin token>/policy.json?threshold=D&mode=threshold | Chrome ExtensionSettings policy, admin link only. Mode is threshold (default) or default-deny |
The full points table is public at /api/rules and on the Methodology page. Some examples follow.
| Finding | Points |
|---|---|
Access to all websites (<all_urls>, *://*/*, https://*/) | 30 |
debugger | 30 |
| Content scripts on all sites | 20 |
nativeMessaging, userScripts, blocking webRequest | 20 each |
Script policy that allows 'unsafe-eval' or remote script | 20 |
cookies, management, proxy | 15 each |
| Updates from a server other than Google | 15 |
| Manifest V2 (deprecated) | 10 |
notifications, readingList, tabGroups, tts | 2 each |
Rules that describe the same power are grouped, and a group counts once at its highest value. For example, broad host access, content scripts on all sites and script injection all mean "can run code on every site", so together they count 30, not 58. The grade bands are 0 to 19 A, 20 to 39 B, 40 to 59 C, 60 to 79 D, 80 and above F. Optional permissions are listed as "can request later" with 0 points, because the browser does not grant them at install. Every rule is deterministic. The same manifest always gives the same grade, and no model is involved.
Chrome shows an install warning for several permissions that now carry points. They include webNavigation, topSites, bookmarks, identity.email, contentSettings, browsingData, pageCapture, sessions, tabCapture, desktopCapture, downloads, management, privacy, proxy, geolocation, clipboardRead, nativeMessaging and declarativeNetRequestFeedback. An update that adds webNavigation, topSites and identity.email reaches 30 points and raises an alert.
Broad host access is decided with the ICANN public suffix list. "https://publicsuffix.org/list/public_suffix_list.dat" is downloaded and stored as a generated module, so *://*.co.uk/* and *://*.com/* grant every site, while *://*.bit.ly/*, *://*.x.ai/* and *://*.t.co/* grant one domain. Host permissions and content script matches on mail, single sign-on, password, banking, CRM, collaboration and cloud console sites add 15 points as Sensitive site access, and optional permissions on those sites are listed with 0 points. A wildcard for a parent of a listed host counts, so *://*.google.com/*, *://*.slack.com/*, *://*.office.com/* and *://*.microsoft.com/* are sensitive because they cover mail.google.com, accounts.google.com, app.slack.com, outlook.office.com and login.microsoft.com. A wildcard for a bare parent such as *.amazon.com is not sensitive, and neither is a single host such as docs.github.com. When a host permission already reaches every site, the all-sites rule scores it once and the sensitive rule adds 0 there and keeps its label, so *://*/* and <all_urls> are not counted twice.
The first production check ran on 18 October 2026 with the shared cache cleared so only the deployed code ran. The grading is deterministic, so the same manifests always give the same grades, and no 'wasm-unsafe-eval' policy was mistaken for 'unsafe-eval'.
localhost, .internal or lvh.me are refused, and redirects are not followed. Alerts always carry the read-only link, never the admin link, because chat channels are shared.application/json (the MIME type is parsed, not substring-matched), so neither a form nor a simple cross-site request from another page can start work in a visitor's browser.X-Frame-Options: DENY and Referrer-Policy: no-referrer. Text from manifests is rendered with textContent, never as HTML.The privacy and terms page lives at /privacy and is linked in the footer. It states exactly what the code stores and for how long, which is the watchlist name, the extension ids, the snapshots and the change log in Workers KV, the full webhook URL stored so alerts can be sent and shown masked as its host only, the 90-day expiry of a list record, no cookies and no analytics, the subprocessors Cloudflare for hosting and storage and Google for the public update service and the Web Store pages, and how to delete a list with the admin delete button or the DELETE route. The contact line is Michael Lip, github.com/theluckystrike.
The admin page has a Rotate links button with a confirm step. It calls POST /api/watchlists/<admin token>/rotate, which issues a new admin token and a new read-only token, moves the list to the new key and deletes the old keys. Both old links return 404 within about a minute, the time Workers KV takes to drop cached reads. Use it after an admin link is shared by mistake.
Each list keeps an audit log of admin actions, which are created, webhook set or removed, test alert sent, policy export, a CSV download, rotate, rename and delete. Each entry is stored under its own KV key (prefix au:<admin token>, a time-sortable id), so two admins acting at the same time both keep their entry and neither overwrites the other, and the newest 100 are read back by listing that prefix. Each entry holds the time and the action only, never a token or a webhook URL. Rotate moves the keys to the new prefix and delete removes them. Admins see it on the watchlist page and can download it as .../audit.csv, which the read-only link cannot reach.
The watchlist page has download buttons for the inventory CSV and the change log CSV. Both links, admin and read-only, can download them. The admin link also builds a Chrome ExtensionSettings policy from a grade threshold and a mode. Threshold mode, the default, keeps the catch-all installable and blocks only the listed extensions graded at or below the threshold. Default deny mode blocks every extension that is not on the list. Both modes put the debugger block and the sensitive host block on the catch-all entry, so extensions that are not on the list are also kept off the debugger permission and off sensitive sites. In the Google Admin console, open Chrome browser, then Custom Configurations under Chrome Enterprise Core, and paste the JSON. The same JSON can be deployed through the Chrome Group Policy templates on Windows, added to the com.google.Chrome configuration profile plist on macOS, or written to a file under /etc/opt/chrome/policies/managed/ on Linux. The policy lists one wildcard host pattern per sensitive domain family, with no path and no port. An allowed extension repeats the debugger and host blocks because Chrome does not inherit them from the catch-all entry. A password manager or sign-on tool, found by its extension id, keeps its own vendor domains and the shared sign-in hosts accounts.google.com, login.microsoftonline.com and okta.com out of runtime_blocked_hosts and reaches them through runtime_allowed_hosts. This holds in both modes. A blocked extension needs only the blocked install mode. CSV and policy export are Team features. A list on the Free plan after its trial answers HTTP 402 on the CSV, policy and audit routes, and billing is not connected in this version.
Every list starts on a 14 day Team trial, so a first visitor sees every feature at once. Three trials are allowed per client address inside a 30 day window, counted in Workers KV under keys that hold only a salted SHA-256 hash of the address, never the address itself, and each marker expires at the end of that window. Workers KV has no compare and set, so each claim writes its own unique marker and then counts the markers, refusing when more than three exist. The cap holds for claims that see each other's markers, and it is best effort across locations because KV is eventually consistent, so it is not a hard guarantee. A fourth new list from the same address inside the window starts on Free, and the creation response and the watchlist page both say why. Free keeps the risk report and up to 25 extension ids per list, and Team raises the limit to 50 and adds the CSV exports, the policy export and the audit log.
Billing is not live yet, and upgrades to Team are granted by the operator with a secret code, so this is a plan and gating model in code, not a paid product. The operator sets a Worker secret named OPERATOR_CODE with npx wrangler secret put OPERATOR_CODE. If it is unset the plan route answers 403 for every caller and no list can move to Team. The route is PUT /api/watchlists/<admin token>/plan, and the operator code travels in the x-operator-code header, so a plan change needs both the operator code and the admin token of the list. The list owner sends the operator their admin link, and the operator takes the token from it and calls the route. The read-only link cannot change a plan. After the trial a Free list answers HTTP 402 with a plain message on the CSV, policy and audit routes, and the watchlist page shows a usage badge with the plan, the number of ids checked of the limit and the trial days left, plus a note when more ids are stored than Free checks. A list saved before plans existed has no plan field and counts as Team, so nothing saved earlier breaks. A Free list that held more than 25 ids on the trial keeps them, but the daily check covers its first 25. The operator also sets a Worker secret named ADDRESS_SALT, a long random string of at least 16 characters, which salts the address hashes so they cannot be reversed by trying every address. If the secret is missing or shorter than 16 characters, the trial and rate limit routes answer 503 with a plain configuration error instead of using an empty salt.
| Plan | Price | Ids per list | Included |
|---|---|---|---|
| Free | 0 | up to 25 | Risk report, daily checks, alerts, change log |
| Team | 29 USD a month | no plan limit, up to the 50 the service holds | Everything in Free, plus CSV exports, the policy export and the audit log |
scripts/economics.ts runs the real scheduled check code against an in-memory fake KV with 300 lists of 20 ids each for one simulated day of 144 runs, and prints the KV reads, KV writes, list calls and subrequests. It is a local simulation with a fake KV, not a measurement on Cloudflare. A day for 300 lists spends 1,560 KV reads, 320 KV writes and 144 list calls, inside the Workers Free limits, the first run reads 560 keys and writes 128, and the busiest run spends 689 KV operations. With 20 shared extensions 980 lists fit the 1,000 KV writes a day of Workers Free, and production runs on a Workers Paid setting with cpu_ms 10000. The details are in docs/ECONOMICS.md.
| Built in this version | Planned |
|---|---|
| Scan and grade up to 25 extensions | Real billing and payments, with a payment provider |
| A plan model with a 14 day Team trial, a Free id limit and 402 gating, upgrades by hand | Email alerts |
| Shared daily cache, one download per extension a day | Single sign-on |
| A scheduler runs every 10 minutes and each watchlist is re-checked about once a day, with a change log | |
| Slack, Discord and Google Chat webhook alerts | |
| Read-only link and admin link for every watchlist | Unlimited extensions |
| CSV inventory, change log and audit log export | |
| Rotate links and a privacy and terms page | |
| Chrome ExtensionSettings policy export in two modes |
npm install
npx wrangler kv namespace create WATCHLISTS # put the id in wrangler.jsonc
npx wrangler secret put OPERATOR_CODE # the code the operator uses to move a list to Team
npx wrangler secret put ADDRESS_SALT # a random string of at least 16 characters
npx wrangler dev
npm test # 245 tests, no network
npm run check # TypeScript with no errors
npm run economics # measured KV and subrequest counts for a day of checks
MIT. Not affiliated with Google. Chrome is a trademark of Google LLC.