Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Read README.md first; this file is the short list of things that bite.
- Security page URLs (~36k) live in `security/urls.json` and are checked by the worker;
as DNR rules they would blow the 30k guarantee.
- Icons are generated: edit `scripts/icons.mjs` and run it, never hand-edit the PNGs.
- Edge gets `dist/edge` (Chrome build + `src/icons-edge`, no middle finger). Edge rejected the
middle finger under policy 2.10; anything Edge sees, package or listing image, uses the polite set.

## Tests
- `npm test` for the compiler and settings; `npm run test:e2e` for the real build in Chromium.
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
- run: npm run build
- run: npm test
- run: node scripts/verify-load.mjs chrome
- run: node scripts/verify-load.mjs edge
- run: npm run test:e2e
- name: Firefox smoke test
run: npm run test:firefox
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## [Unreleased]

## [2.0.3] - 2026-10-07

### Changed
- Edge gets its own build with a different hand on the icon (every finger up except the
middle one), after Edge Add-ons certification rejected the store logo. Chrome and Firefox
are unchanged.

## [2.0.2] - 2026-09-29

### Changed
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ src/
scriptlets/library.js MAIN-world scriptlets, embedded into each bucket by the build
resources/ redirect stubs (GPT, gtag/GTM, analytics.js, noop files)
icons/ toolbar icons per state, rendered by scripts/icons.mjs from assets/icons/
icons-edge/ the same for the Edge build, without the middle finger (Edge policy 2.10)
popup/, options/, warning/, shared/
test/unit, test/e2e, test/fixtures
```
Expand Down Expand Up @@ -153,6 +154,12 @@ The same scripts run locally: `npm run package`, then
`publish:edge` with the variables above. `npm run store:status` shows what Chrome has.
Listing text and images: `docs/store-listing.md` and `npm run store:art`.

Edge gets its own zip (`SilentBlock-<v>-edge.zip`, built from `dist/edge`) and its own images
(`release/store-art/edge/`). It is the Chrome build with a different hand on the icon: Edge
Add-ons rejected the middle finger under policy 2.10 on 2026-10-01. Edge also has no API for
listing images, and its publish API submits whatever listing is in the Partner Center draft,
so new Edge images go into the draft by hand BEFORE the tag is pushed.

## Filter lists and licences

| List | Licence |
Expand Down
1 change: 1 addition & 0 deletions assets/icons/edge/caution.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/icons/edge/danger.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/icons/edge/idle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/icons/edge/paused.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/icons/edge/protected.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion docs/store-listing.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
One source for all three stores. The Firefox listing text is updated automatically on
release (`scripts/publish/firefox.mjs`). Chrome and Edge have no API for listing text, so
paste these in by hand when they change. Store images: `npm run store:art` renders them into
`release/store-art/`.
`release/store-art/` (Chrome and Firefox) and `release/store-art/edge/` (Edge, polite icon:
never upload the middle-finger set to Edge, it fails certification under policy 2.10).

Store items:
- Chrome Web Store: `accdpphckockpflaggbplikpmaknioao` (publisher b7b011da-ecfd-4ec1-8d99-c600f9c31543)
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "silentblock",
"version": "2.0.2",
"version": "2.0.3",
"private": true,
"description": "Free, silent ad blocker for Chrome (MV3). No ads, no nags.",
"type": "module",
Expand Down
8 changes: 5 additions & 3 deletions scripts/build.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Builds dist/chrome and dist/firefox from src/ and the filter lists.
// Builds dist/chrome, dist/firefox and dist/edge from src/ and the filter lists.
//
// node scripts/build.mjs full build, selectors validated in headless Chromium
// node scripts/build.mjs --no-validate skip browser validation (faster, CI without a browser)
Expand Down Expand Up @@ -272,12 +272,14 @@ function manifestFor(target) {
}

const report = [];
for (const target of ['chrome', 'firefox']) {
// Edge is the Chrome build with the polite icons (src/icons-edge, see scripts/icons.mjs).
for (const target of ['chrome', 'firefox', 'edge']) {
const dist = path.join(root, 'dist', target);
await rm(dist, { recursive: true, force: true });
await mkdir(dist, { recursive: true });
for (const dir of ['background', 'popup', 'options', 'warning', 'content', 'shared', 'icons', 'resources']) {
await cp(path.join(root, 'src', dir), path.join(dist, dir), { recursive: true });
const from = dir === 'icons' && target === 'edge' ? 'icons-edge' : dir;
await cp(path.join(root, 'src', from), path.join(dist, dir), { recursive: true });
}
await rm(path.join(dist, 'shared/unwall-core.js'), { force: true });
for (const [rel, data] of Object.entries(out)) {
Expand Down
75 changes: 50 additions & 25 deletions scripts/icons.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
// Renders the toolbar icons: one shield, five states.
// Renders the toolbar icons: one shield, five states, two hands.
// node scripts/icons.mjs
// Writes assets/icons/<state>.svg (sources) and src/icons/<state>-<size>.png (shipped).
// Writes assets/icons/<state>.svg + src/icons/<state>-<size>.png (Chrome and Firefox), and
// assets/icons/edge/<state>.svg + src/icons-edge/<state>-<size>.png (Edge).
//
// The shield is "Tidewater": faceted, with the hand cut clean through so the toolbar itself
// makes the gesture. That only reads if the shield colour is mid-tone (a white finger on a
// light toolbar, a dark one on a dark toolbar), so keep new colours in that band.
//
// Edge Add-ons rejected the middle finger (policy 2.10, store logo, 2026-10-01), so the Edge
// build gets the "polite" hand: every finger up except that one.
import { writeFile, mkdir } from 'node:fs/promises';
import path from 'node:path';
import { chromium } from '@playwright/test';
Expand All @@ -20,43 +24,64 @@ export const STATES = {
};
const SIZES = [16, 32, 48, 64, 128];

// Hand in local coordinates, middle finger centred on x=32. The thumb grows out of the
// palm's lower-left along the index finger so it reads as one fist at 16px.
const HAND = [
'<rect x="23" y="0" width="18" height="54" rx="9"/>',
'<rect x="7" y="29" width="16.5" height="30" rx="8.25"/>',
'<rect x="40.5" y="31" width="15" height="28" rx="7.5"/>',
'<rect x="55" y="37" width="11.5" height="22" rx="5.75"/>',
'<path d="M7 46 H66.5 V60 C66.5 70 60 77 50 77 H23 C13 77 7 70 7 60 Z"/>',
'<rect x="3" y="40" width="14.5" height="38" rx="7.25" transform="rotate(-20 10.25 59)"/>',
'<path d="M5 56 C4 68 9 77 20 77 L20 52 Z"/>',
].join('');
// Hands in local coordinates, middle finger centred on x=32.
export const HANDS = {
// The thumb grows out of the palm's lower-left along the index finger so it reads as one
// fist at 16px.
classic: [
'<rect x="23" y="0" width="18" height="54" rx="9"/>',
'<rect x="7" y="29" width="16.5" height="30" rx="8.25"/>',
'<rect x="40.5" y="31" width="15" height="28" rx="7.5"/>',
'<rect x="55" y="37" width="11.5" height="22" rx="5.75"/>',
'<path d="M7 46 H66.5 V60 C66.5 70 60 77 50 77 H23 C13 77 7 70 7 60 Z"/>',
'<rect x="3" y="40" width="14.5" height="38" rx="7.25" transform="rotate(-20 10.25 59)"/>',
'<path d="M5 56 C4 68 9 77 20 77 L20 52 Z"/>',
].join(''),
// The classic fist with the fingers swapped: index, ring and pinky up, middle folded down.
// Same finger columns, palm and thumb as classic so the two read as one family.
polite: [
'<rect x="7" y="6" width="16.5" height="53" rx="8.25"/>',
'<rect x="23" y="31" width="18" height="28" rx="9"/>',
'<rect x="40.5" y="4" width="15" height="55" rx="7.5"/>',
'<rect x="55" y="16" width="11.5" height="43" rx="5.75"/>',
'<path d="M7 46 H66.5 V60 C66.5 70 60 77 50 77 H23 C13 77 7 70 7 60 Z"/>',
'<rect x="3" y="40" width="14.5" height="38" rx="7.25" transform="rotate(-20 10.25 59)"/>',
'<path d="M5 56 C4 68 9 77 20 77 L20 52 Z"/>',
].join(''),
};
// Which hand each icon set uses, and where it goes.
const SETS = [
{ hand: 'classic', svgDir: 'assets/icons', pngDir: 'src/icons' },
{ hand: 'polite', svgDir: 'assets/icons/edge', pngDir: 'src/icons-edge' },
];
const SHIELD = 'M64 10 L110 22 L104 80 L64 118 L24 80 L18 22 Z';

export function shieldSvg({ body, facet }, title) {
export function shieldSvg({ body, facet }, title, hand = 'classic') {
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128"><title>${title}</title>` +
`<defs><mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="128" height="128">` +
`<rect width="128" height="128" fill="#000"/>` +
`<path d="${SHIELD}" fill="#fff" stroke="#fff" stroke-width="10" stroke-linejoin="round"/>` +
`<g fill="#000" transform="translate(64 26) scale(0.8) translate(-32 0)">${HAND}</g></mask></defs>` +
`<g fill="#000" transform="translate(64 26) scale(0.8) translate(-32 0)">${HANDS[hand]}</g></mask></defs>` +
`<g mask="url(#m)"><rect width="128" height="128" fill="${body}"/>` +
`<path d="M64 4 L116 18 L110 84 L64 126 Z" fill="${facet}"/></g></svg>\n`;
}

if (import.meta.url === `file:///${process.argv[1].replace(/\\/g, '/')}` || process.argv[1].endsWith('icons.mjs')) {
await mkdir(path.join(root, 'assets/icons'), { recursive: true });
await mkdir(path.join(root, 'src/icons'), { recursive: true });
const browser = await chromium.launch();
const page = await browser.newPage();
for (const [state, colours] of Object.entries(STATES)) {
const svg = shieldSvg(colours, `SilentBlock: ${colours.meaning}`);
await writeFile(path.join(root, `assets/icons/${state}.svg`), svg);
for (const size of SIZES) {
await page.setViewportSize({ width: size, height: size });
await page.setContent(`<html><body style="margin:0;background:transparent"><img src="data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}" width="${size}" height="${size}" style="display:block"></body></html>`);
await page.locator('img').screenshot({ path: path.join(root, `src/icons/${state}-${size}.png`), omitBackground: true });
for (const { hand, svgDir, pngDir } of SETS) {
await mkdir(path.join(root, svgDir), { recursive: true });
await mkdir(path.join(root, pngDir), { recursive: true });
for (const [state, colours] of Object.entries(STATES)) {
const svg = shieldSvg(colours, `SilentBlock: ${colours.meaning}`, hand);
await writeFile(path.join(root, svgDir, `${state}.svg`), svg);
for (const size of SIZES) {
await page.setViewportSize({ width: size, height: size });
await page.setContent(`<html><body style="margin:0;background:transparent"><img src="data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}" width="${size}" height="${size}" style="display:block"></body></html>`);
await page.locator('img').screenshot({ path: path.join(root, pngDir, `${state}-${size}.png`), omitBackground: true });
}
console.log(`${hand.padEnd(8)} ${state.padEnd(10)} ${colours.body} ${colours.meaning}`);
}
console.log(`${state.padEnd(10)} ${colours.body} ${colours.meaning}`);
}
await browser.close();
}
4 changes: 2 additions & 2 deletions scripts/package.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Zips dist/chrome and dist/firefox into release/. manifest.json sits at the zip root,
// Zips dist/chrome, dist/firefox and dist/edge into release/. manifest.json sits at the zip root,
// which is what the Chrome Web Store and addons.mozilla.org expect.
// node scripts/package.mjs
import { readdir, readFile, writeFile, mkdir, stat } from 'node:fs/promises';
Expand Down Expand Up @@ -77,7 +77,7 @@ function zip(files) {

const outDir = path.join(root, 'release');
await mkdir(outDir, { recursive: true });
for (const target of ['chrome', 'firefox']) {
for (const target of ['chrome', 'firefox', 'edge']) {
const dir = path.join(root, 'dist', target);
try { await stat(path.join(dir, 'manifest.json')); } catch { throw new Error(`dist/${target} missing: run npm run build first`); }
const manifest = JSON.parse(await readFile(path.join(dir, 'manifest.json'), 'utf8'));
Expand Down
4 changes: 2 additions & 2 deletions scripts/publish/edge.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Microsoft Edge Add-ons: upload the Chrome zip (Edge runs the same build) to the draft
// Microsoft Edge Add-ons: upload the Edge zip (the Chrome build with the polite icons) to the draft
// submission, then publish it for certification.
// EDGE_PRODUCT_ID=... EDGE_CLIENT_ID=... EDGE_API_KEY=... node scripts/publish/edge.mjs
// Credentials: Partner Center > Microsoft Edge > Publish API. The product ID is the Partner
Expand All @@ -24,7 +24,7 @@ async function waitFor(url, what) {
const v = await version();
console.log(`edge: uploading v${v}`);
const { res: upRes } = await call(`${API}/draft/package`, {
method: 'POST', headers: { ...auth, 'content-type': 'application/zip' }, body: await readFile(await zipFor('chrome')),
method: 'POST', headers: { ...auth, 'content-type': 'application/zip' }, body: await readFile(await zipFor('edge')),
}, 'upload');
await waitFor(`${API}/draft/package/operations/${upRes.headers.get('location')}`, 'upload');

Expand Down
Loading
Loading