Skip to content

Commit 33a37fb

Browse files
clay-goodclaude
andcommitted
fix: SDK auto-update now actually proxies new names + CI Node 24 opt-in
Phase 4.4 wired the daily ad-patterns.json refresh into chrome.storage.local.cached_patterns and merged the snapshot into the isolated-world ai-ad-blocker.js. But the MAIN-world sdk-blocker.js — the script that actually installs the proxies on the page's window — only ever saw the bundled patterns. So if the daily refresh added a new SDK function name (e.g. ChatGPT decides to ship as "OpenAIAds" tomorrow), DOM selectors picked it up but the actual interceptor never did. Half-implemented auto-update. Fix: - sdk-blocker.js now tracks installed proxies in a Set, exposes installProxiesFor(names), and listens for {source: 'armorly', type: 'update-sdk-list', functions: [...]} messages on the window. The bundled list installs synchronously at document_start; the cached list arrives later via the bridge. - ai-ad-blocker.js, on a successful mergeCachedPatterns(), postMessages the merged function list to MAIN world. Same channel ai-ad-blocker uses to receive `sdk-blocked` notifications, opposite direction. - New Playwright test seeds cached_patterns with a synthetic "ArmorlyAutoUpdateProbe" function name, opens fake-ads.html, and asserts the proxy is installed. Cleans up after itself so other tests aren't affected. CI hygiene - Switched `npm install` -> `npm ci`. We've been committing package-lock.json since v2.8.0, so npm ci is the deterministic install that matches it. - Set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true at the workflow level on both build.yml and release.yml. Silences the Node 20 deprecation banner GitHub annotated on every previous run. All 6 Playwright tests pass. Six-target build still clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 286dc5e commit 33a37fb

7 files changed

Lines changed: 106 additions & 13 deletions

File tree

.github/workflows/build.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ on:
99
permissions:
1010
contents: read
1111

12+
# Opt JavaScript actions into the Node 24 runtime ahead of GitHub's June 2026
13+
# default flip. Silences the deprecation warning we hit on every run.
14+
env:
15+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
16+
1217
jobs:
1318
build:
1419
runs-on: ubuntu-latest
@@ -87,7 +92,7 @@ jobs:
8792
path: ~/.cache/ms-playwright
8893
key: pw-${{ runner.os }}-${{ hashFiles('package.json') }}
8994

90-
- run: npm install
95+
- run: npm ci
9196

9297
- run: npx playwright install --with-deps chromium
9398

.github/workflows/release.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ on:
4545
permissions:
4646
contents: write # needed to create the GitHub Release
4747

48+
# Opt JavaScript actions into the Node 24 runtime ahead of GitHub's June 2026
49+
# default flip.
50+
env:
51+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
52+
4853
jobs:
4954
build:
5055
runs-on: ubuntu-latest

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ All notable user-facing changes to Armorly are documented here. Format loosely f
44

55
## [Unreleased]
66

7+
### Fixed
8+
- **Auto-update now actually reaches the SDK interceptor.** Phase 4.4 wired the daily refresh into `chrome.storage.local.cached_patterns` and merged the snapshot into the isolated-world script's patterns object — but the MAIN-world [sdk-blocker.js](extension/content/sdk-blocker.js) only saw the bundled list, so any *newly-named* SDK from the daily refresh was never proxied. Fix: when the isolated-world script merges a newer snapshot, it postMessages the updated function list to the MAIN world; sdk-blocker listens and installs proxies for any names it hasn't seen yet (deduped via a Set). DOM selectors and affiliate params were always being auto-updated correctly; this gap was specific to SDK function-name coverage. New Playwright test seeds a synthetic name in `cached_patterns` and asserts the proxy lands.
9+
710
### Added
811
- **Automated demo recording (Phase 3).** New ChatGPT-styled fixture page ([tests/fixtures/chatgpt-mock.html](tests/fixtures/chatgpt-mock.html)) plus an off→on storyboard in the `@demo` Playwright test produce a deterministic 7.6-second walkthrough. `./scripts/make-demo-gif.sh` converts the webm to a 900 KB GIF via ffmpeg + gifsicle. Output is committed at [docs/demo.gif](docs/demo.gif) and embedded at the top of the README — what used to require a manual screen recording now lives in the repo and replays exactly the same every build.
912

1013
### Changed
14+
- CI: switched `npm install``npm ci` (we have a committed lock file) and set `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true` at the workflow level to opt into Node 24 ahead of GitHub's June 2026 default flip. Silences the deprecation banner on every run.
1115
- **README restructured** along the lines of SPEC.md Phase 1.1: install table moved to the top, "Why ads will destroy AI" essay shifted below the practical sections, developer content consolidated under a single "For developers" heading. Stale facts corrected — limitation #4 now reflects the v2.5.0 pattern auto-update, limitation #12's "no network-level blocking" claim removed (we added that in v2.3.0), the project-structure tree now lists `background.js`, `sdk-blocker.js`, `rules/`, and `ad-patterns.json`. Added a build-status badge linking to the [build workflow](.github/workflows/build.yml).
1216
- **`npm run demo`** now actually produces a webm. It sets `ARMORLY_DEMO=1`, which flips `recordVideo` on the persistent context and runs only the `@demo`-tagged walkthrough — the existing fixture pages, end to end, at 1280×720. Output lands in `test-results/demo/`.
1317

armorly-extension.zip

663 Bytes
Binary file not shown.

extension/content/ai-ad-blocker.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,20 @@
369369
// service worker. Bundled patterns are the floor; this can only ADD.
370370
if (data.cached_patterns && typeof patterns.mergeCachedPatterns === 'function') {
371371
const applied = patterns.mergeCachedPatterns(data.cached_patterns);
372-
if (applied) console.log('[Armorly] Using cached patterns', patterns.version);
372+
if (applied) {
373+
console.log('[Armorly] Using cached patterns', patterns.version);
374+
// Push the updated SDK function list to the MAIN-world sdk-blocker
375+
// so newly-named SDKs from the daily refresh also get proxied.
376+
// Without this hop, auto-update only covers DOM selectors and
377+
// affiliate params — the actual interceptor stayed bundled-only.
378+
try {
379+
window.postMessage({
380+
source: 'armorly',
381+
type: 'update-sdk-list',
382+
functions: patterns.getAllSDKFunctions()
383+
}, '*');
384+
} catch (_) { /* noop */ }
385+
}
373386
}
374387
init();
375388
});

extension/content/sdk-blocker.js

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919
'use strict';
2020

2121
if (!window.ArmorlyAdPatterns) return;
22-
const sdkFunctions = window.ArmorlyAdPatterns.getAllSDKFunctions();
22+
23+
// Names we've already installed proxies for. Used to ignore duplicates
24+
// when the isolated-world script later pushes an updated list pulled
25+
// from cached_patterns (chrome.storage.local). See message listener below.
26+
const installed = new Set();
2327

2428
function createProxy() {
2529
return new Proxy({}, {
@@ -37,16 +41,38 @@
3741
});
3842
}
3943

40-
sdkFunctions.forEach(function (name) {
41-
try {
42-
const proxy = createProxy();
43-
Object.defineProperty(window, name, {
44-
get: function () { return proxy; },
45-
set: function () { return true; },
46-
configurable: false
47-
});
48-
} catch (_) {
49-
// Property may already be defined non-configurable. Skip.
44+
function installProxiesFor(names) {
45+
if (!Array.isArray(names)) return;
46+
for (const name of names) {
47+
if (typeof name !== 'string' || installed.has(name)) continue;
48+
try {
49+
const proxy = createProxy();
50+
Object.defineProperty(window, name, {
51+
get: function () { return proxy; },
52+
set: function () { return true; },
53+
configurable: false
54+
});
55+
installed.add(name);
56+
} catch (_) {
57+
// Property may already be defined non-configurable on the page; skip.
58+
}
59+
}
60+
}
61+
62+
// First pass: install from the bundled patterns. Runs synchronously at
63+
// document_start so the proxy lands before any inline ad-SDK call.
64+
installProxiesFor(window.ArmorlyAdPatterns.getAllSDKFunctions());
65+
66+
// Second pass: when ai-ad-blocker.js (isolated world) finishes its
67+
// chrome.storage.local lookup and merges cached_patterns, it postMessages
68+
// the updated function list to us so we can proxy any names the daily
69+
// background refresh added since the bundle was packaged.
70+
window.addEventListener('message', function (event) {
71+
if (event.source !== window) return;
72+
const data = event.data;
73+
if (!data || data.source !== 'armorly') return;
74+
if (data.type === 'update-sdk-list' && Array.isArray(data.functions)) {
75+
installProxiesFor(data.functions);
5076
}
5177
});
5278
})();

tests/extension.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,46 @@ test('@demo end-to-end ChatGPT-mock walkthrough for the hero video', async () =>
151151
await worker.evaluate(() => chrome.storage.local.set({ disabled_domains: [] }));
152152
});
153153

154+
test('auto-update bridge: cached pattern SDK names reach the MAIN-world interceptor', async () => {
155+
// SPEC.md Phase 4.4 was originally only wired to the isolated-world script,
156+
// so newly-added SDK function names from the daily refresh never got
157+
// proxied. This test seeds a synthetic name in `cached_patterns`, opens a
158+
// page, and asserts the MAIN-world sdk-blocker installed a proxy for it.
159+
let [worker] = context.serviceWorkers();
160+
if (!worker) worker = await context.waitForEvent('serviceworker');
161+
162+
await worker.evaluate(() =>
163+
chrome.storage.local.set({
164+
cached_patterns: {
165+
version: '9999-12-31', // strictly newer than BUNDLED_VERSION
166+
adSDKs: {
167+
future_network: {
168+
functions: ['ArmorlyAutoUpdateProbe'],
169+
methods: ['init'],
170+
domainPatterns: []
171+
}
172+
}
173+
}
174+
})
175+
);
176+
177+
const page = await context.newPage();
178+
await page.goto(`${FIXTURES}/fake-ads.html`);
179+
180+
// The bridge is async: ai-ad-blocker's storage callback resolves, it
181+
// postMessages the new function list, sdk-blocker proxies it. Allow up
182+
// to 5s for the round trip — typically lands in well under 100ms.
183+
await page.waitForFunction(
184+
() => typeof (window as any).ArmorlyAutoUpdateProbe !== 'undefined',
185+
undefined,
186+
{ timeout: 5000 }
187+
);
188+
expect(await page.evaluate(() => typeof (window as any).ArmorlyAutoUpdateProbe)).toBe('object');
189+
190+
// Cleanup so the rest of the suite isn't influenced by a fake cached snapshot.
191+
await worker.evaluate(() => chrome.storage.local.set({ cached_patterns: null }));
192+
});
193+
154194
test('hidden-content shield empties white-on-white injection but leaves visible content', async () => {
155195
const page = await context.newPage();
156196
await page.goto(`${FIXTURES}/hidden-injection.html`);

0 commit comments

Comments
 (0)