From 2496f130550b4145156d561f82c787d5a1036627 Mon Sep 17 00:00:00 2001 From: Pandipipas Date: Thu, 4 Jun 2026 14:42:32 +0200 Subject: [PATCH] refactor: update security policies in window creation and enhance loading page CSP --- README.md | 54 +++++++++++------------ docs/architecture.md | 44 +++++++++---------- docs/troubleshooting.md | 70 +++++++++++++++--------------- src/main/windows/window-service.ts | 23 ++++++++-- static/loading.html | 1 + 5 files changed, 102 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 715bd59..b3bd041 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,41 @@ -# scoreko-electron +# Scoreko Desktop -Windows desktop installer for Scoreko. The packaged app includes Electron, NodeCG, the compiled `scoreko-dev` bundle, and the production modules needed to run it, so end users do not need Node.js, pnpm, or a cloned repository. +This is the Windows desktop wrapper for Scoreko. It bundles Electron, NodeCG, and our custom `scoreko-dev` bundle into a single standalone executable. Users just double-click the installer and everything works—no Node.js, pnpm, or command line required. -## Build on a development machine +## Local Development -From the repository root: +If you're working on the app locally, start by installing dependencies at the repository root: ```powershell pnpm install ``` -Then from `scoreko-electron-dev`: +Then, move into the wrapper folder: ```powershell +cd scoreko-electron-dev npm install -npm run dist:win ``` -The installer is written to `scoreko-electron-dev/release/Scoreko-setup-0.1.0.exe`. +### Useful Commands -## What the build does +- `npm run start`: Builds the bundle and launches Electron locally for testing. +- `npm run dist:win`: Packages everything and creates the `.exe` Windows installer in the `release/` folder. +- `npm run prepare:runtime`: Extracts a fresh NodeCG runtime from the parent bundle (useful if you changed dependencies). +- `npm run rebuild:native`: Rebuilds native Node modules (like SQLite) specifically for Electron's V8 engine. +- `npm run doctor`: Runs a quick sanity check to verify your local configuration and port availability. -- Builds the parent `scoreko-dev` bundle with `pnpm build`. -- Creates `scoreko-electron-dev/lib/nodecg` with a small NodeCG runtime. -- Installs production runtime modules into that runtime. -- Rebuilds `better-sqlite3` for Electron before creating the installer. -- Packages the runtime as an Electron extra resource outside the app archive. +## How it works under the hood -## Runtime behavior +When you build the installer, the script automatically compiles the main `scoreko-dev` bundle, provisions a lightweight NodeCG runtime in `lib/nodecg`, and packages it as an external asset alongside the Electron app. -On first launch, Scoreko copies the packaged NodeCG runtime to the user's app data folder and then relaunches itself before starting NodeCG. This keeps `cfg`, `db`, and `logs` writable on Windows even when the app is installed under `Program Files`, and avoids transient startup failures caused by freshly copied runtime files. +When a user runs Scoreko for the first time, the app copies this NodeCG runtime directly into their local AppData folder. This is a deliberate choice: it ensures that databases, configs, and logs remain fully writable, even if the user installed the app in restricted directories like `Program Files`. -## Useful scripts +## Auto-Updates via Gitea -- `npm run start`: build everything and run Electron locally. -- `npm run prepare:runtime`: recreate `lib/nodecg` from the parent bundle. -- `npm run rebuild:native`: rebuild NodeCG native modules for Electron. -- `npm run dist:win`: create the Windows installer. -- `npm run doctor`: check the prepared runtime and the configured port. +Scoreko supports seamless, opt-in updates through your Gitea instance. -## Updates from Gitea - -Scoreko can check a Gitea release feed without forcing the user to update. Edit `static/updates.json` before building: +Before building your production installer, check `static/updates.json`: ```json { @@ -52,11 +46,15 @@ Scoreko can check a Gitea release feed without forcing the user to update. Edit } ``` -For each release, bump `package.json` version, build with `npm run dist:win`, create a Gitea release tagged like `v0.2.0`, and attach `release/Scoreko-setup-0.2.0.exe`. When Scoreko sees a newer tag, it asks whether to download and install it. +**To ship an update:** +1. Bump the version in `package.json`. +2. Run `npm run dist:win` to generate the new installer. +3. Create a new release tag in Gitea (e.g., `v0.2.0`) and attach the `.exe`. +4. The app will detect the new version, notify the user, and handle the installation safely. -## Configuration +## Environment Configuration -The defaults match the parent bundle: +The app ships with sensible defaults that match our development bundle: - `NODECG_BUNDLE_NAME=scoreko-dev` - `NODECG_PORT=9090` @@ -65,4 +63,4 @@ The defaults match the parent bundle: - `SCOREKO_UPDATES_ENABLED=true` - `SCOREKO_UPDATE_ASSET_PATTERN=Scoreko-setup-.*\.exe$` -Copy `.env.example` only if you need local overrides while developing. +You only need to mess with `.env.example` if you want to override these values locally while testing. diff --git a/docs/architecture.md b/docs/architecture.md index 655d985..fa49fd4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,29 +1,27 @@ -# Main process architecture +# Main Process Architecture -## Startup flow +This document breaks down how the Electron main process is structured and what happens when the app launches. -1. `src/main/main.ts` loads `appConfig` from `config/runtime-config.ts`. -2. Installs or refreshes the packaged NodeCG runtime in user data when needed (`nodecg/runtime-provisioner.ts`). -3. Creates windows (`windows/window-factory.ts`). -4. Starts NodeCG with `nodecg/process-manager.ts`. -5. Waits for HTTP readiness and shows loading -> main dashboard. -6. Checks the configured Gitea latest-release endpoint for optional updates. -7. On shutdown, runs a single graceful-stop flow to avoid orphan processes. +## Startup Flow -## Main modules +When a user opens Scoreko, the app goes through a precise sequence to ensure NodeCG starts reliably: -- `config/runtime-config.ts`: read/validate env vars. -- `nodecg/runtime-provisioner.ts`: install/refresh the managed runtime in the writable user data folder and report whether it changed. -- `nodecg/process-manager.ts`: start, readiness, and stop for NodeCG; install/permission/port validation. -- `updates/update-manager.ts`: optional Gitea release checks, installer download, and user-controlled install. -- `updates/update-utils.ts`: release version comparison and installer asset selection. -- `windows/window-factory.ts`: window creation and navigation policy. -- `windows/navigation-security.ts`: internal navigation allowlist and safe external schemes. -- `errors/error-presenter.ts`: fatal error presentation. -- `errors/logger.ts`: structured logging (`info/warn/error/debug`). +1. **Configuration:** `src/main/main.ts` kicks things off by loading `appConfig` via `config/runtime-config.ts`. +2. **Runtime Provisioning:** The app checks the user's AppData directory. If the packaged NodeCG runtime is missing or outdated, it extracts a fresh copy (`nodecg/runtime-provisioner.ts`). +3. **Window Creation:** The initial windows (like the loading screen) are instantiated via `windows/window-factory.ts`. +4. **NodeCG Boot:** `nodecg/process-manager.ts` spawns the NodeCG process in the background. +5. **Readiness Check:** The app continuously polls NodeCG until the HTTP server responds. Once ready, it transitions the UI from the loading screen to the main dashboard. +6. **Update Check:** If updates are enabled, the app checks the configured Gitea endpoint in the background to see if a newer version is available. +7. **Graceful Shutdown:** When the user closes the app, it triggers a unified teardown sequence to cleanly kill the NodeCG child process, preventing zombie processes from lingering in the background. -## Principles +## Core Modules -- Mechanical refactors first. -- Incremental hardening with conservative fallback. -- Automated validation via `typecheck`, `build`, `test`, `doctor`, `lint`. +Here is where the heavy lifting happens: + +- **`config/runtime-config.ts`**: Handles environment variables and defaults. +- **`nodecg/runtime-provisioner.ts`**: Manages copying the NodeCG runtime out of the read-only Electron package into the writable user data folder. +- **`nodecg/process-manager.ts`**: Handles starting, polling, and killing the NodeCG server. It also validates ports and permissions before launching. +- **`updates/update-manager.ts`**: Coordinates the Gitea update flow (checking versions, downloading installers, prompting the user). +- **`windows/window-factory.ts`**: Centralizes window configuration and security defaults. +- **`windows/navigation-security.ts`**: Intercepts navigation events to block unauthorized domains and safely hand off external links (like docs or emails) to the user's default browser. +- **`errors/error-presenter.ts` & `errors/logger.ts`**: Manages structured logging (`electron-log`) and displaying the fallback error screen if boot fails. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index a2cb290..924473f 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,45 +1,43 @@ -# Troubleshooting +# Troubleshooting Guide -## `The packaged NodeCG runtime is incomplete` +Here are some common issues you might run into while developing or using the Scoreko desktop app, along with quick fixes. -- Run `npm run prepare:runtime` from `scoreko-electron-dev`. -- If the parent bundle is not installed yet, run `pnpm install` from the repository root first. +## The app says the NodeCG runtime is incomplete +This usually means you haven't bundled the runtime yet. +- Run `npm run prepare:runtime` in the `scoreko-electron-dev` folder. +- If you haven't even installed the parent bundle, go up to the repository root and run `pnpm install` first. -## `NodeCG is present but internal dependencies are missing` +## NodeCG is present but internal dependencies are missing +This happens if dependencies changed or the initial copy was interrupted. +- Re-run `npm run prepare:runtime` to get a fresh copy. +- If you're seeing SQLite errors when the app launches, you probably need to run `npm run rebuild:native` to compile it for Electron's V8 engine. -- Recreate the runtime with `npm run prepare:runtime`. -- If native SQLite errors appear during launch, run `npm run rebuild:native` before packaging. +## "No read/write permissions on NodeCG" +In production, Scoreko runs NodeCG out of your AppData folder to ensure it has write access. During local development, it runs directly from the repo. +If you see this permission error locally, another process probably has a file locked. Close any zombie Scoreko or Node processes and try `npm run start` again. -## `No read/write permissions on NodeCG` +## Port 9090 is already in use +You have another instance of NodeCG (or another web server) running on port 9090. +- Find and kill the process using the port, or change `NODECG_PORT` in your `.env` file to something else (like 9091). +- You can use `npm run doctor` to quickly test port availability. -- Installed builds run NodeCG from the user's app data folder, so this usually means the local development copy is locked. -- Close any running Scoreko/NodeCG process and run `npm run start` again. +## Timeout while waiting for NodeCG +The app waits for the NodeCG HTTP server to respond. If it times out: +- Check your terminal output. NodeCG might be crashing or hanging on startup due to a bundle error. +- If your machine is just slow, you can increase `NODECG_STARTUP_TIMEOUT_MS` in the `.env` file. -## `Port is already in use` +## The app crashes immediately on a fresh install +Scoreko copies the runtime to `%AppData%\scoreko\nodecg` and relaunches itself on the very first run. +If it gets stuck in a loop or fails immediately: +- Check if your antivirus or Windows Search Indexer is aggressively locking the files in AppData as they are being copied. +- Try running `npm run rebuild:native` and then repackaging the app with `npm run dist:win`. -- Free the port or set `NODECG_PORT` in `.env`. -- Use `npm run doctor` to validate availability before startup. +## macOS builds are failing complaining about an icon +The `electron-builder` config explicitly looks for a Mac icon at `static/icons/icon.icns`. If you don't have one, generate it and place it there before running the macOS build. -## `Timeout while waiting for NodeCG` - -- Check the Electron/NodeCG output in the terminal. -- Increase `NODECG_STARTUP_TIMEOUT_MS` if the environment is slow. -- Recreate the runtime with `npm run prepare:runtime` if the bundle changed. - -## First launch after install fails - -- Scoreko relaunches itself automatically after a fresh runtime install. -- If it still fails, check whether antivirus or file indexing is locking `%AppData%\scoreko\nodecg`. -- Rebuild the installer with `npm run dist:win` after running `npm run rebuild:native`. - -## macOS build fails because of icon - -- The configuration expects `static/icons/icon.icns`. -- Create that file before running macOS packaging. - -## Updates do not appear - -- Check that `static/updates.json` has `"enabled": true` before building the installer. -- The `apiUrl` must point to Gitea's latest release API: `/api/v1/repos///releases/latest`. -- The release tag must be newer than the installed `package.json` version, for example `v0.2.0`. -- The release must include an installer asset matching `assetPattern`, by default `Scoreko-setup-.*\.exe$`. +## Auto-updates aren't triggering +If you published a new release on Gitea but the app ignores it: +- Double check that `static/updates.json` has `"enabled": true` before you build the installer. +- Ensure your `apiUrl` points exactly to the Gitea API: `http://gitea.../api/v1/repos///releases/latest`. +- The git tag you created (e.g., `v0.2.0`) must be semantically higher than the version currently in your `package.json`. +- Make sure the installer `.exe` you uploaded to Gitea actually matches the regex in `assetPattern` (default is `Scoreko-setup-.*\.exe$`). diff --git a/src/main/windows/window-service.ts b/src/main/windows/window-service.ts index 241d121..8a7d8c8 100644 --- a/src/main/windows/window-service.ts +++ b/src/main/windows/window-service.ts @@ -22,7 +22,7 @@ export function createMainWindow({ const windowOptions = createWindowOptions({ allowDevTools, appConfig, rootPath, isLoadingWindow: false }); const window = new BrowserWindow(windowOptions); - denyPermissionsByDefault(window); + applySecurityPolicies(window, allowDevTools); window.setMenuBarVisibility(false); window.webContents.setWindowOpenHandler(({ url }) => { @@ -65,7 +65,7 @@ export function createLoadingWindow({ }: Omit): BrowserWindow { const window = new BrowserWindow(createWindowOptions({ allowDevTools, appConfig, rootPath, isLoadingWindow: true })); - denyPermissionsByDefault(window); + applySecurityPolicies(window, allowDevTools); window.on("page-title-updated", (event) => { event.preventDefault(); @@ -123,8 +123,25 @@ function createWindowOptions({ }; } -function denyPermissionsByDefault(window: BrowserWindow): void { +function applySecurityPolicies(window: BrowserWindow, allowDevTools: boolean): void { window.webContents.session.setPermissionRequestHandler((_webContents, _permission, callback) => { callback(false); }); + + window.webContents.session.webRequest.onHeadersReceived((details, callback) => { + callback({ + responseHeaders: { + ...details.responseHeaders, + "Content-Security-Policy": [ + "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: http://localhost:* http://127.0.0.1:*; connect-src * ws: wss:; img-src * data: blob:; media-src * data: blob:; font-src * data:;" + ] + } + }); + }); + + if (!allowDevTools) { + window.webContents.on("devtools-opened", () => { + window.webContents.closeDevTools(); + }); + } } diff --git a/static/loading.html b/static/loading.html index e5c4487..06a6cb6 100644 --- a/static/loading.html +++ b/static/loading.html @@ -2,6 +2,7 @@ + Scoreko