Game Webview
Security posture
The game <webview> (views/kan-game-wrapper.tsx) runs with contextIsolation: true and
disablewebsecurity. The preload (assets/js/webview-preload.js) runs in the isolated world —
keeping Node and @electron/remote — and pushes page-facing hacks into the page's main world
via contextBridge.executeInMainWorld, communicating through the exposed poiPreloadBridge.
Why disablewebsecurity stays: enabling standard web security in the page's main world
breaks (a) the cross-origin game-iframe traversal used by the canvas screenshot
(window.capture in capture-page.js) and (b) historically file:// asset loads. The canvas
capture is deliberately kept (better raw/resize-correct screenshots), so disablewebsecurity
must remain. contextIsolation still isolates the preload's Node from the untrusted page —
that is the security win.
The main poi window and plugin windows remain contextIsolation: false (plugins run Node
in-page) — out of scope for this skill.
Asset hack — everything serves through poi-cache://
lib/kcs-resource.ts registers a privileged poi-cache:// scheme plus a handler that
net.fetches the local file, sets ACAO: *, and sets an explicit Content-Type by extension.
Two trigger paths:
Scripts — two-tier policy
.hack.*script overrides are served proactively by the main-processwebRequest(override-only, so a mod always applies).- Plain cached scripts are served only on load failure: a page-side capture-phase
errorlistener inresource-hack.jsre-injects a/kcs*script from cache when its network load fails. - Proactive plain-cache serving is forbidden: stale version-pinned scripts
(
main.js?version=...) plus gadget RPC under/gadget_html5/break login withUnexpected identifier 'error'. This was tried both ways — do not re-attempt. - The separate login
onErrorpath (runtimekcsLoginfailure) still re-injects and re-runs the gadget handlers, whichwebRequestcannot do — so the page-side fallback stays regardless.
Images and login scripts — page-side
resource-hack.js: an isolated-world resolver maps to poi-cache://resource<pathname>,
applied by the main-world new Image() hook and login-script reinjection.
Ship art is drawn onto the game's WebGL canvas, and a cross-origin image taints the texture
(texImage2D SecurityError). The fix is a CORS-clean load: the new Image() hook sets
crossOrigin = 'anonymous' on hacked images, paired with the handler's ACAO: *. Only hacked
images get crossOrigin, so same-origin network images are untouched. Images are deliberately
not in the webRequest path (page-side crossOrigin avoids both the WebGL taint and a 400).
stylesheet / media / font / script — main-process webRequest.onBeforeRequest
HACKABLE_RESOURCE_TYPES = { stylesheet, media, font, script }. stylesheet/media/font resolve
.hack.* first, then the plain cached origin. script is in OVERRIDE_ONLY_RESOURCE_TYPES,
so findHackFilePathAsync(..., overrideOnly = true) serves only .hack.*. xhr and documents
are excluded because login evaluates XHR bodies.
Implementation notes
- Content-Type comes from the
mimepackage (v4, ESM-only) via a cached native dynamicimport('mime')plusimport type { Mime }. Babel keepsimport()for poi files; a static import would transpile to a brokenrequireof an ESM-only package. - The webview uses
session.defaultSession(no partition) — confirmed byresource-notifier.tsalso hookingdefaultSession. - Shared path logic is plain JS in
assets/js/kcs-resource-path.js, required from both the main process and the renderer preload, because main only babel-registers.es/.ts/.tsx. - Broadcaster events (
network.on.responseetc.) are consumed inviews/env-parts/data-resolver.ts, filtered to/kcsapiplus the/kcs2/js/main.jsgame-start marker — so the bridge's input validation allows the/kcsprefix.
The window.open popup crash (diagnosed 2026-08-07)
Opening the DMM point-charge page (point.dmm.com/choice/pay -> /choice/point) from the game
killed the renderer: reason: "crashed", exit code -2147483645 (0x80000003
STATUS_BREAKPOINT), twice per attempt, with no CHECK/FATAL line in either the Chromium log or
stderr.
Root cause: disablewebsecurity. A window.open popup keeps an opener relationship with
the game page and therefore shares its renderer process; loading an ordinary secure page into
that web-security-disabled process crashes it, taking the game down too.
Fix: wc.setWindowOpenHandler on the guest webContents in lib/webcontent-utils.ts — deny
http(s) popups and re-open them in a detached BrowserWindow (webSecurity: true,
sandbox: true, no node), which drops the opener link. about:blank popups still take the
normal allow path, since the opener scripts them.
Ruled out — do not re-test: --disable-site-isolation-trials (app.ts), devtools extensions
(poi.devtool.enable), the proxy, Sentry, plugins, @electron/remote, and inherited
webPreferences (Electron's makeWebPreferences does not inherit preload or
disablewebsecurity into child windows). A top-level window loading the same URL never
crashes — the opener relationship is required to reproduce. The original symptom was a
misleading @electron/remote / Sentry error about a disposed WebFrameMain, which is
downstream of the crash, not its cause.
Deferred: WebContentsView migration
Replacing <webview> with WebContentsView has been scoped but deferred — it is blocked on an
Electron per-view click-through API. Do not start it without checking with the user.