Hack Any Webapp

Model A vs Model B

The heart of the comparison: Gizmo v2.0.1 (file-pull) versus v2.2.x (rules + local apply).

Model A: file-pull

The v2.0.1 extension is a strict subset of the v2.2.x one. There is no bridge, no background patch handler, no cache, no ISOLATED world. A single MAIN-world content script fetches the already-patched bundle straight from the repo and injects it via the onreset trick.

v2.0.1 contents/gizmo-patch.ts
// v2.0.1 lib/patch-config.ts
export const PATCHED_URL =
"https://raw.githubusercontent.com/alexey-max-fedorov/gizmo-ai-unlimited/main/patcher/dist/entry.min.js";

// v2.0.1 contents/gizmo-patch.ts → injectPatchedBundle()
fetch(PATCHED_URL, { cache: "no-cache" })
.then(r => r.text())
.then(js => {
  const payload = "var URL=window.URL;\n" + js;
  document.documentElement.setAttribute("onreset", payload);
  document.documentElement.dispatchEvent(new CustomEvent("reset"));
  document.documentElement.removeAttribute("onreset");
});

The sequence is cache-less and maximally simple: one content script, one fetch, one injection.

Model A: sequence (cache-less)
PageExtension (MAIN)Our repoinsert <script entry>DNR block + neutralizeGET entry.min.js (19 MB)200 OKonreset(payload)patched bundle runs

Model B: rules + local apply

Instead of treating the 19 MB bundle as the product, the patcher publishes patches.json, 756 bytes of rules. The client downloads the rules and the pristine bundle from the vendor, applies the rules in the background service worker, and caches the result. The onreset injection at the end is identical; everything before it is new architecture.

Model B: sequence (rules + local apply)
PageMAINISOLATEDBackgroundOur repoVendorinsert <script>block + neutralize, capture URLevt __request__ {url}sendMessageGET patches.json (756 B)cache {file, hash}? missGET entry.min.js (19 MB)200 OKapplyRules + wrap + cachepatched JSevt __response__onreset(payload)patched bundle runs

The cache key is the tuple {bundleFilename, patchesHash}. A vendor redeploy changes bundleFilename; a rule edit changes patchesHash. Either axis independently forces exactly one rebuild, then steady-state cache hits. Both storage and unlimitedStorage permissions are required. unlimitedStorage alone leaves chrome.storage itself undefined at runtime with no compile-time warning.

The trade-off

DimensionModel A (file-pull)Model B (rules + local apply)
Primary artifact19 MB patched bundle756 B rules recipe
Repo growth per vendor deploycommits a 19 MB binarycommits a sub-1 KB JSON
Client bandwidth (cold)19 MB from our repo756 B (ours) + 19 MB (vendor)
Client bandwidth (warm)19 MB every load (or HTTP cache)756 B + cache hit (no 19 MB)
Who serves the heavy bytesour GitHub raw CDNthe vendor's own CDN
Remote-code posture (store review)extension executes a remote file we host; strongest remote-code-execution signalextension executes the vendor's own bytes, transformed locally by a tiny declarative rule set; easier to justify
Freshness without extension updateyes (repo file changes)yes (rules file changes)
Self-heal latency≤2 h (CI republish)≤2 h (CI republish)
Tamper surfacewhoever controls our repo controls the executed code wholesaleour repo controls only the diff; the bulk is the vendor's signed-by-delivery bytes
Local computenoneone regex pass over ~19 MB per (bundle, rules) change
Storagenone required~18 MB cached; needs unlimitedStorage
Failure containmenta bad publish ships a fully-broken bundlea bad rule degrades gracefully; original structure still present
Implementation complexityone MAIN content scriptMAIN + ISOLATED + background + cache lib
Offline/repo-down resiliencebreaks if our repo is unreachablebreaks if either our rules or the vendor bundle is unreachable

Model B trades client-side complexity (a background worker, a bridge, a cache, ~18 MB of storage) for three strategic wins: (1) the repo stops storing a multi-megabyte binary that changes constantly; (2) the heavy bytes are served by the vendor, not us; and (3) the extension's executed payload is mostly the vendor's own code, with our contribution reduced to an auditable, sub-kilobyte declarative diff, a meaningfully better answer to a store reviewer asking "what remote code does this run?" Model A's advantage is sheer simplicity and zero local storage/compute, at the cost of hosting and executing a wholesale remote binary.

The exact transition (git archaeology)

The working repo was a shallow clone (50 commits). Un-shallowing via git fetch --unshallow origin main restored the full 225-commit history, enabling a precise version-to-commit mapping on origin/main:

VersionCommitDateNote
2.0.0caa28b42026-05-13declarativeNetRequest manifest
2.0.1c24cec32026-05-13Model A baseline; Gizmo AI Unlimited rename
2.1.06fa4a692026-05-18broaden reload to all gizmo.ai subdomains
2.2.05edea202026-05-18version bump; still byte-identical Model A
2.2.1bdedade2026-05-20current HEAD; Model B

A precise finding from the archaeology: the v2.2.0 bump commit is byte-for-byte identical to v2.0.1's content script. It still fetches entry.min.js directly. The Model-A-to-Model-B refactor landed as a tight commit sequence immediately after the 2.2.0 tag:

git log 2.2.0..2.2.1 --oneline
2711e44  refactor(lib): replace PATCHED_URL with PATCHES_URL + filename helper
5ea7243  feat(lib): browser-side applyPatchRules + wrapWithMarkers
32782b9  feat(lib): bundle cache wrapper around chrome.storage.local
e64040b  feat(extension): ISOLATED-world bridge to background
88158dd  feat(extension): MAIN-world script requests patched bundle via bridge
66b0f58  fix(extension): omit world: ISOLATED from bridge PlasmoCSConfig
f1957d6  feat(patcher): emit patches.json; throw on any rule degradation

So "v2.2.0 = Model B" is true of the 2.2.x line as shipped (HEAD), with the mechanism introduced in the commits bridging 2.2.0 to 2.2.1. Both models remain viable; v2.2.x simply chose the rules path.

Where Play Origin sits

Play Origin is the instructive hybrid. P-NP's patcher already emits a Model-B manifest.json (rules plus prefix plus suffix plus defaultMenuUrl, declared the "primary artifact"), yet the shipping ProdigyOrigin extension still file-pulls the whole patched game.min.js:

ProdigyOrigin extension/contents/origin-bridge.ts
const defaultGameUrl =
"https://raw.githubusercontent.com/ProdigyPXP/P-NP/master/dist/game.min.js";
// ProdigyOrigin extension/contents/prodigy.ts -> injectGameViaFetch(gameUrl)
fetch(gameUrl).then(r => r.text()).then(js => { /* onreset(js) */ });

That is Model A in practice, even though the server side is already provisioned for Model B. The manifest.json (rules) is staged for a future client migration; until then game.min.js (nominally a "verification copy") is what actually loads. This is a textbook example of decoupling server and client release trains: the harder, riskier client rewrite can land later without blocking the patcher's evolution.