# Mintegral Checker — TODO ## Task Overview Build a local VS Code webview equivalent of the Mintegral PlayTurbo checker (`https://www.playturbo.com/review`). Validates a Mintegral playable ad ZIP against PlayTurbo requirements. Two check categories: - **Static** — parse `index.html` + ZIP structure without running the ad. - **Runtime** — load the ad in a sandboxed `srcdoc` iframe, inject a monitoring stub, and detect whether lifecycle callbacks (`gameReady`, `gameEnd`, `gameStart`, `gameClose`, `install`) actually fire. --- ## Finished - [x] Pure-Node ZIP parser (EOCD → Central Directory → Local File Headers, zlib DEFLATE) - [x] All 9 static checks: - HTML Requirements (charset, viewport, single-file) - CTA Method (`window.install` reference in scripts) - Game End / Game Ready / Game Start / Game Close (regex on scripts) - File Handling (no external URLs, base64 assets) - File Spec (ZIP ≤ 5 MB, single `index.html`) - Storage (no `localStorage.setItem` / `sessionStorage.setItem`) - [x] Device-frame simulator (phone mockup around `srcdoc` iframe) - [x] Event log panel showing runtime events as they fire - [x] Runtime timer with 60s timeout and per-check waiting badges - [x] `__ping__` handshake → "Simulator connected" in event log (confirms stub runs) - [x] Dual notification channels: polling `__hplMtgEvents` + `parent.__hplMtgNotify` - [x] `MW_INIT` interceptor (wraps `emitCheckData`, forces `_hasReview`/`_isPreview`) - [x] `window.postMessage` override (catches `previewer:gameReady` self-posts) - [x] `mkAccess` — `Object.defineProperty` accessor with undefined getter so AD's `typeof window.gameReady === 'undefined'` guard passes, setter wraps assignment - [x] Capture-phase `load` safety net — wraps any SDK function declarations that overwrote accessors, fires before `body.onload` - [x] Mute button UI (shows on ZIP load, hidden on reset, toggles Mute/Unmute label) - [x] Diagnostic lifecycle logging (`post-parse`, `DOMContentLoaded`, `load`, `load+500ms`, callback assignment/call traces) - [x] AudioContext construction proxy for mute/unmute of closure-owned engine audio --- ## Bugs ### 1. Callbacks never fire (critical, diagnostics added) **Symptom:** Event log shows "Simulator connected" (`__ping__` fires) but no lifecycle events appear. The same ZIP passes all checks on the real PlayTurbo site. **Root cause candidates (narrowed down but not confirmed):** A. **`body.onload` vs capture `load` ordering on `window`** — When the event target is `window` itself, capture and bubble listeners both run at the "at target" phase in registration order. Our stub is in `` and registers first, so it should fire before the body's `onload`. But this assumes no edge-case behaviour in VS Code's Chromium sandbox / srcdoc context. B. **SDK function declaration closes over a local reference** — If the ad does: ```js var gameReady = function() { ... }; window.gameReady = gameReady; ``` and `body.onload="gameReady()"` resolves to the local var (via closure), then reassigning `window.gameReady` in our load-time wrapper has no effect. C. **CSP in VS Code webview blocks stub execution silently** — The webview panel has an implicit CSP. The stub is injected into `srcdoc`; it might run in a restricted context that prevents property interception or `parent` access for later events (even though `__ping__` works because it runs synchronously at parse time). D. **Ad calls `gameReady()` before load** — If the ad boots via DOMContentLoaded and calls `gameReady()` there (rather than body.onload), our load-event safety net fires too late. The `mkAccess` accessor should still catch it, but only if the ad assigns `window.gameReady` first. **What's been added:** - Injected diagnostic logging for lifecycle function `typeof` at post-parse, DOMContentLoaded, load capture before/after wrapping, and 500 ms after load. - Logs callback assignment/wrapping and wrapper call paths to the event log. - Logs dynamic `document.createElement('script')` calls to help trace script injection. **What's still needed if callbacks still do not fire:** - Check if the ad bundles `preview-util.js` inside `index.html` or references it externally (would fail file-handling check anyway). - Compare the diagnostic timeline against the real PlayTurbo execution path. --- ### 2. Mute button does nothing (fixed in monitor stub) **Symptom:** Clicking Mute/Unmute changes the label but has no audible effect. **Root cause:** The current approach searches `Object.keys(cw)` for an `AudioContext` instance. AudioContexts are almost always created in closures (inside Phaser or the ad IIFE) and are never assigned as window properties — `Object.keys` will never find them. `cw.Phaser.game.sound` also likely fails because Phaser stores the game instance internally, not at `window.Phaser.game`. **Fix applied:** Intercepts `AudioContext`/`webkitAudioContext` construction in the monitoring stub, stores captured contexts/gain nodes on `window.__hplAudioContexts` and `window.__hplMasterGains`, and routes `ctx.destination` through a master gain when possible. The mute button now sets captured master gain values first, then falls back to suspending/resuming captured contexts and exposed Phaser/global game sound objects. --- ## Improvements / Nice-to-Have - [ ] **Inject SDK alongside ad** — The real PlayTurbo checker loads `preview-util.js` next to the ad. Bundling (or downloading once) the 265 KB SDK and injecting it into the srcdoc would let `MW_INIT`/`emitCheckData` fire natively, which is the most reliable callback path and matches what the real checker does. - [ ] **Diagnostic mode** — A "Debug" toggle that logs `typeof` of each lifecycle function at key moments (post-parse, DCL, load, 500ms after load) to the event log, making root-cause analysis faster. - [ ] **Replay button** — Reload only the iframe (not the full ZIP re-parse) to retest without re-picking the file. - [ ] **Portrait / landscape toggle** — Flip the device frame aspect ratio. - [ ] **Multiple HTML entry points** — Some ZIPs have `playable.html` instead of `index.html`; add a fallback search. - [ ] **Orientation lock detection** — Check for `screen.orientation.lock()` calls (Mintegral requires the ad to handle both orientations). - [ ] **Version bump and changelog entry** — Once callbacks are working, bump to 0.1.8 and add a changelog entry for the Mintegral Checker tool. --- ## Known Issues / Constraints - VS Code `WebviewPanel` runs in a sandboxed Chromium process. `srcdoc` iframes are same-origin with the webview, so `parent` access works — but the webview's implicit CSP may still block certain APIs inside nested iframes. - The `Object.defineProperty` accessor approach (`mkAccess`) is the only way to let the AD's `typeof window.X === 'undefined'` guard pass while still intercepting the assignment. Any early concrete assignment (even `undefined`) breaks the guard in some runtimes. - SDK `function` declarations at brace-depth 0 are `[[DefineOwnProperty]]` calls at parse time; they overwrite our configurable accessors unconditionally. The load-time safety net is the only recovery path for that scenario. - The `applyStaticResults` function sets `runtimeStatus = null` for lifecycle checks that already failed statically. `handleRuntimeEvent` only transitions `runtimeStatus === 'waiting'` → `'pass'`; it silently ignores `null`. This means a static-fail check can't be runtime-rescued via the direct lifecycle path (though the `handleSdkCheck` path does handle `null`). Intentional, but worth documenting.