From e3f55b10fc06d169bf0190fbf73577a87925cecd Mon Sep 17 00:00:00 2001 From: Vayun Godara Date: Tue, 4 Aug 2026 13:08:02 +0200 Subject: [PATCH 1/3] gates: run the client and join gates on every OS with a display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The verification client no longer requires Linux + xvfb. Display modes: - xvfb (headless Linux) — unchanged, invisible virtual framebuffer - visible-window (macOS / Windows / Linux desktop) — the real client opens as a small window for a few minutes, announced in the panel log; same client, same verdict, purely cosmetic cost - no display at all — the honest "not verified" skip stays Windows groundwork that the modes stand on: sandbox assembly uses junctions and hardlinks (file symlinks need admin there), the join sandbox picks the per-OS Forge argfile, the HMC gamedir survives .properties backslash escaping, and the launcher JDK is discovered per-OS instead of hardcoded. ENV_SKIP now applies only under Xvfb/Mesa — window mode runs on a real GPU and boots the full mod set. macOS gets -XstartOnFirstThread for LWJGL. Rejected on principle: stubbed-GL headless mode everywhere — a stub can mask or invent render-init crashes, and a gate that can lie is worse than a window on the desktop. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KfRjm8SZCtXnoSB1z6hdfB --- ARCHITECTURE.md | 19 +++- README.md | 8 +- server/src/services/launchgate.ts | 156 ++++++++++++++++++++++-------- server/src/services/platform.ts | 12 ++- server/src/services/preflight.ts | 10 +- 5 files changed, 152 insertions(+), 53 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9eed00a..ceda1b6 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -101,7 +101,18 @@ remember). Without them, servers are LAN/VPN-only and everything else works. ## Cross-platform honesty -Linux is the full experience. On macOS/Windows the panel and the server-side -dry-boot run fine; the headless-client and join gates require Linux + -`xvfb-run` and **skip with an honest "not verified" verdict** rather than -pretending to pass. +The gates need a display, and there are three answers to that: + +- **Headless Linux + `xvfb-run`** — the client boots invisibly in a virtual + framebuffer. +- **macOS / Windows / a Linux desktop** — *visible-window mode*: the real + verification client opens as a small window on the desktop for a few + minutes, announced in the panel log so nobody closes it mid-verdict. Same + client, same fidelity, purely cosmetic cost. +- **A display-less box that isn't Linux** — the client and join gates **skip + with an honest "not verified" verdict** rather than pretending to pass. + The static scan and server dry-boot run everywhere regardless. + +A stubbed-GL "headless anywhere" mode was considered and rejected: stubbed +rendering can mask or invent render-init crashes, which makes the test lie — +the same reason the boot gates refuse `-Xverify:none`. diff --git a/README.md b/README.md index a531801..59f1d7e 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,11 @@ restarts, and a storage cleaner. - Node.js 22+ - [Crafty Controller](https://craftycontrol.com/) managing your servers -- Linux is the full experience; on macOS/Windows the panel and server-side - preflight run fine, while the client/join gates need Linux + `xvfb` and - will skip with an honest verdict elsewhere. +- Any OS. On headless Linux the client/join gates run invisibly under `xvfb`; + on macOS/Windows (or a Linux desktop) the verification client opens as a + small window for a few minutes instead — same real client, same verdict. + A box with no display at all skips those two gates with an honest + "not verified" verdict; the server-side dry-boot always runs. ## Quick start diff --git a/server/src/services/launchgate.ts b/server/src/services/launchgate.ts index 7f33fbd..a5786e0 100644 --- a/server/src/services/launchgate.ts +++ b/server/src/services/launchgate.ts @@ -6,43 +6,102 @@ import { join, basename, dirname } from 'node:path'; import { PATHS } from '../config.js'; import { serverDir, javaFor } from './servers.js'; import { detect } from './detect.js'; -import { killTree, IS_WIN } from './platform.js'; -import { symlinkSync } from 'node:fs'; +import { killTree, IS_WIN, sandboxLink, KILLABLE_SPAWN_OPTS } from './platform.js'; +import { FORGE_ARGFILE } from './preflight.js'; import { spawnSync } from 'node:child_process'; -// the client gate needs a virtual display — Linux + xvfb-run. Elsewhere the -// gate degrades HONESTLY: verdict says why it skipped, the dry-boot preflight -// still protects the server, and nothing pretends to have tested a client. -let xvfbState: boolean | null = null; -function clientGateAvailable(): boolean { - if (xvfbState !== null) return xvfbState; - xvfbState = !IS_WIN && process.platform === 'linux' - && spawnSync('which', ['xvfb-run'], { stdio: 'ignore' }).status === 0; - return xvfbState; +// the client gate needs a DISPLAY. Three ways to get one, checked in order: +// 'xvfb' — Linux + xvfb-run: invisible virtual framebuffer. +// 'window' — macOS/Windows (or a Linux desktop without xvfb): the REAL +// client opens as a small window on the desktop for a few +// minutes. Full fidelity, zero extra dependencies — a visible +// window beats a stubbed-GL headless hack, because stubbed GL +// can mask or invent render-init crashes and the verdict lies. +// null — headless box that isn't Linux-with-xvfb: the gate degrades +// HONESTLY — the verdict says why it skipped, the dry-boot +// preflight still protects the server, and nothing pretends to +// have tested a client. +type GateMode = 'xvfb' | 'window'; +let modeState: GateMode | null | undefined; +export function gateMode(): GateMode | null { + if (modeState !== undefined) return modeState; + if (process.platform === 'linux' && spawnSync('which', ['xvfb-run'], { stdio: 'ignore' }).status === 0) { + modeState = 'xvfb'; + } else if (IS_WIN || process.platform === 'darwin' || process.env.DISPLAY || process.env.WAYLAND_DISPLAY) { + modeState = 'window'; + } else { + modeState = null; + } + return modeState; } // TIER-2 LAUNCH GATE — AutoModpack is a mirror, not a validator (its own // README: users must ensure the modpack works before it distributes it). So // after every pack regenerate, this boots the EXACT client set friends will -// receive — a real Minecraft client under Xvfb with mc-runtime-test driving -// it into a world — and records PASS/FAIL. A client-side conflict is caught +// receive — a real Minecraft client (under Xvfb on headless Linux, as a small +// desktop window elsewhere) with mc-runtime-test driving it into a world — +// and records PASS/FAIL. A client-side conflict is caught // on this box instead of on a friend's screen. // // Proven live 2026-07-20: main's 43-mod set (sodium+iris+shaders present) // boots and joins in ~2min; the harness caught its first real finding the // same evening (ImmediatelyFast's font-atlas patch NPEs under Xvfb/Mesa). // -// ENV_SKIP: mods that crash ONLY in this headless environment while being -// demonstrably fine on real clients (owner plays with them daily). They ship -// to players but are excluded from gate boots. Keep this list SHORT and -// documented — every entry is a hole in the gate. +// ENV_SKIP: mods that crash ONLY in the Xvfb/Mesa software-GL environment +// while being demonstrably fine on real clients (owner plays with them +// daily). They ship to players but are excluded from xvfb-mode boots — in +// window mode the client runs on a real GPU, so the FULL set is tested. +// Keep this list SHORT and documented — every entry is a hole in the gate. const ENV_SKIP = [ 'immediatelyfast', // FontSet.resetTextures NPE under Xvfb/Mesa only — bisected 2026-07-20 ]; const HMC = join(PATHS.root, 'Tools', 'headlessmc'); const LAUNCHER = join(HMC, 'headlessmc-launcher-2.10.0.jar'); -const JAVA25 = join(PATHS.root, 'Tools', 'jdk-25.0.3+9', 'bin', 'java'); + +// the launcher JVM takes the newest JDK under Tools/ (jdk-25 on the home +// box, java.exe on Windows); which java the GAME runs under is HMC's own +// affair, chosen at version-install time via `--java`. +let gateJavaState: string | null | undefined; +function gateJava(): string | null { + if (gateJavaState !== undefined) return gateJavaState; + gateJavaState = null; + const tools = join(PATHS.root, 'Tools'); + if (existsSync(tools)) { + // numeric-major sort: a legacy jdk-8 must not beat jdk-25 lexically + const jdks = readdirSync(tools).filter((x) => x.startsWith('jdk-')) + .sort((a, b) => (parseInt(b.slice(4), 10) || 0) - (parseInt(a.slice(4), 10) || 0)); + for (const d of jdks) { + const p = join(tools, d, 'bin', IS_WIN ? 'java.exe' : 'java'); + if (existsSync(p)) { gateJavaState = p; break; } + } + } + return gateJavaState; +} + +// low-priority wrappers: Linux gets nice+ionice (the bulk-ops law), macOS +// has nice but no ionice, Windows has neither — and gates only run when zero +// players are online, so priority matters least exactly where it's missing. +const NICE = IS_WIN ? [] : ['nice', '-n', '19']; +const NICE_IO = IS_WIN ? [] : process.platform === 'linux' + ? ['nice', '-n', '19', 'ionice', '-c3'] : ['nice', '-n', '19']; + +/** Full argv for a gate CLIENT boot, per display mode. extraProps are -D + JVM props (gameargs etc.), launcherArgs everything after the launcher jar. */ +function clientArgv(extraProps: string[], launcherArgs: string[]): string[] { + const argv = [...NICE_IO]; + if (gateMode() === 'xvfb') argv.push('xvfb-run', '-a'); + argv.push(gateJava()!); + if (gateMode() === 'xvfb') argv.push('-Dhmc.check.xvfb=true'); + // LWJGL on macOS must create its window on the process's first thread + if (process.platform === 'darwin') argv.push('-XstartOnFirstThread'); + argv.push(...extraProps, '-jar', LAUNCHER, ...launcherArgs); + return argv; +} + +// window mode keeps the visible client small and out of the way; xvfb mode +// leaves geometry alone (the virtual framebuffer nobody sees) +const WINDOW_SIZE_ARGS = '--width 640 --height 360'; // known HMC hang modes — timeout is a FAIL. 15 min: a 70-mod pack with a // 183MB physics mod under software GL genuinely needs >10 (live 2026-07-21) const RUN_TIMEOUT_MS = 15 * 60_000; @@ -196,7 +255,8 @@ const execFileP = promisify(execFile); // for its whole duration. Live 2026-07-21: "panel isnt loading" while the // horrorFabric gate downloaded 1.20.1. async function hmcCmd(args: string[]): Promise { - await execFileP('nice', ['-n', '19', JAVA25, '-jar', LAUNCHER, '--command', ...args], { + const argv = [...NICE, gateJava()!, '-jar', LAUNCHER, '--command', ...args]; + await execFileP(argv[0], argv.slice(1), { cwd: HMC, timeout: 8 * 60_000, maxBuffer: 32 * 1024 * 1024, }); } @@ -286,7 +346,7 @@ async function runJoinGate( const ns = det.loader === 'forge' ? 'minecraftforge' : 'neoforge'; const fdir = join(sdir, 'libraries', 'net', ns, det.loader === 'forge' ? 'forge' : 'neoforge'); const ver = existsSync(fdir) ? readdirSync(fdir)[0] : null; - const rel = ver ? `libraries/net/${ns}/${det.loader === 'forge' ? 'forge' : 'neoforge'}/${ver}/unix_args.txt` : null; + const rel = ver ? `libraries/net/${ns}/${det.loader === 'forge' ? 'forge' : 'neoforge'}/${ver}/${FORGE_ARGFILE}` : null; if (rel && existsSync(join(sdir, rel))) launchArgs = [heap, ...fast, `@${rel}`, 'nogui']; } const java = javaFor(det.mc, det.loader); @@ -297,11 +357,11 @@ async function runJoinGate( mkdirSync(join(srv, 'mods'), { recursive: true }); for (const item of ['fabric.jar', 'libraries', 'versions', '.fabric']) { const src = join(sdir, item); - if (existsSync(src)) symlinkSync(src, join(srv, item)); + if (existsSync(src)) sandboxLink(src, join(srv, item)); } for (const jar of readdirSync(join(sdir, 'mods')).filter((f) => f.endsWith('.jar'))) { if (/automodpack/i.test(jar)) continue; - symlinkSync(join(sdir, 'mods', jar), join(srv, 'mods', jar)); + sandboxLink(join(sdir, 'mods', jar), join(srv, 'mods', jar)); } writeFileSync(join(srv, 'eula.txt'), 'eula=true\n', 'utf8'); writeFileSync(join(srv, 'server.properties'), [ @@ -328,8 +388,9 @@ async function runJoinGate( let cliChild: ReturnType | null = null; let settled = false; let dwellTimer: NodeJS.Timeout | null = null; - const srvChild = spawn('nice', ['-n', '19', java, ...launchArgs], { - cwd: srv, stdio: ['ignore', 'pipe', 'pipe'], detached: true, + const srvArgv = [...NICE, java, ...launchArgs]; + const srvChild = spawn(srvArgv[0], srvArgv.slice(1), { + cwd: srv, stdio: ['ignore', 'pipe', 'pipe'], ...KILLABLE_SPAWN_OPTS, }); const finish = (ok: boolean, detail: string): void => { if (settled) return; @@ -390,10 +451,11 @@ async function runJoinGate( const escaped = (det.mc ?? '').replace(/\./g, '\\.'); const versionRegex = det.loader === 'fabric' ? `fabric-loader-.*-${escaped}` : `${escaped}-forge-.*`; log(`launchgate: join test — sandbox server up, quick-playing client into 127.0.0.1:${JOIN_PORT}`); - cliChild = spawn('nice', ['-n', '19', 'ionice', '-c3', 'xvfb-run', '-a', - JAVA25, '-Dhmc.check.xvfb=true', `-Dhmc.gameargs=--quickPlayMultiplayer 127.0.0.1:${JOIN_PORT}`, - '-jar', LAUNCHER, '--command', 'launch', versionRegex, '-regex', '--jvm', '-Xmx4G', - ], { cwd: HMC, stdio: ['ignore', 'pipe', 'pipe'], detached: true }); + const gameArgs = `--quickPlayMultiplayer 127.0.0.1:${JOIN_PORT}` + + (gateMode() === 'window' ? ` ${WINDOW_SIZE_ARGS}` : ''); + const cliArgv = clientArgv([`-Dhmc.gameargs=${gameArgs}`], + ['--command', 'launch', versionRegex, '-regex', '--jvm', '-Xmx4G']); + cliChild = spawn(cliArgv[0], cliArgv.slice(1), { cwd: HMC, stdio: ['ignore', 'pipe', 'pipe'], ...KILLABLE_SPAWN_OPTS }); cliChild.on('exit', (code) => { if (!joined) inconclusive(`client exited (code ${code}) before ever joining the server`); }); @@ -470,8 +532,11 @@ async function runGate(serverId: string, log: (m: string) => void): Promise void): Promise basename(rel).toLowerCase().includes(s))) { skippedEnv++; continue; } + if (envSkip.some((s) => basename(rel).toLowerCase().includes(s))) { skippedEnv++; continue; } for (const base of [join(sdir, 'automodpack', 'host-modpack', 'main'), sdir]) { const src = join(base, rel); if (existsSync(src)) { @@ -506,21 +574,29 @@ async function runGate(serverId: string, log: (m: string) => void): Promise((resolve) => { - const child = spawn('nice', ['-n', '19', 'ionice', '-c3', 'xvfb-run', '-a', - JAVA25, '-Dhmc.check.xvfb=true', '-jar', LAUNCHER, - '--command', 'launch', versionRegex, '-regex', '--jvm', '-Xmx4G', - // detached: the child owns a process group, so a timeout kill takes the - // WHOLE tree (nice→ionice→xvfb-run→java). child.kill alone only shot the - // nice wrapper and the Minecraft client lived on as an orphan, wedging - // every later gate run (live 2026-07-21, twice). - ], { cwd: HMC, stdio: ['ignore', 'pipe', 'pipe'], detached: true }); + const argv = clientArgv( + gateMode() === 'window' ? [`-Dhmc.gameargs=${WINDOW_SIZE_ARGS}`] : [], + ['--command', 'launch', versionRegex, '-regex', '--jvm', '-Xmx4G'], + ); + // KILLABLE_SPAWN_OPTS: on POSIX the child owns a process group, so a + // timeout kill takes the WHOLE tree (nice→ionice→xvfb-run→java). + // child.kill alone only shot the nice wrapper and the Minecraft client + // lived on as an orphan, wedging every later gate run (live 2026-07-21, + // twice). On Windows killTree's taskkill /T does the same job. + const child = spawn(argv[0], argv.slice(1), { cwd: HMC, stdio: ['ignore', 'pipe', 'pipe'], ...KILLABLE_SPAWN_OPTS }); let out = ''; child.stdout.on('data', (d: Buffer) => { out += d.toString(); }); child.stderr.on('data', (d: Buffer) => { out += d.toString(); }); diff --git a/server/src/services/platform.ts b/server/src/services/platform.ts index ec09f54..688358d 100644 --- a/server/src/services/platform.ts +++ b/server/src/services/platform.ts @@ -1,6 +1,6 @@ import { execFile, spawn } from 'node:child_process'; import { promisify } from 'node:util'; -import { renameSync, statSync } from 'node:fs'; +import { renameSync, statSync, symlinkSync, linkSync, copyFileSync } from 'node:fs'; import { mkdir } from 'node:fs/promises'; import { basename, dirname } from 'node:path'; @@ -134,6 +134,16 @@ export async function chownToDirOwner(destDir: string): Promise { } catch { /* never fail the caller over ownership */ } } +/** Borrow src into a sandbox at dst without copying. POSIX symlinks; Windows + can't symlink without admin/developer-mode, so directories get junctions + (always allowed) and files get hardlinks, falling back to a real copy when + the sandbox sits on a different volume. */ +export function sandboxLink(src: string, dst: string): void { + if (!IS_WIN) { symlinkSync(src, dst); return; } + if (statSync(src).isDirectory()) { symlinkSync(src, dst, 'junction'); return; } + try { linkSync(src, dst); } catch { copyFileSync(src, dst); } +} + /** Kill a spawned child AND its whole process tree. On Windows, spawn's own `timeout`/kill() only signals the cmd.exe wrapper and the real grandchild lives on — taskkill /T tears down the tree. On Linux the caller must have diff --git a/server/src/services/preflight.ts b/server/src/services/preflight.ts index 29cfdf7..fce978a 100644 --- a/server/src/services/preflight.ts +++ b/server/src/services/preflight.ts @@ -1,10 +1,10 @@ -import { existsSync, mkdirSync, renameSync, rmSync, symlinkSync, writeFileSync, readdirSync, readFileSync } from 'node:fs'; +import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync, readdirSync, readFileSync } from 'node:fs'; import { spawn } from 'node:child_process'; import { join, basename } from 'node:path'; import { PATHS } from '../config.js'; import { serverDir, javaFor } from './servers.js'; import { detect } from './detect.js'; -import { IS_WIN } from './platform.js'; +import { IS_WIN, sandboxLink } from './platform.js'; // cross-platform launch: `nice` is a Linux nicety, not a requirement; Forge // ships win_args.txt beside unix_args.txt @@ -97,14 +97,14 @@ async function runPreflightInner( // mods regenerate defaults in the sandbox instead of writing to the real one for (const item of ['fabric.jar', 'libraries', 'versions', '.fabric', 'server.properties']) { const src = join(dir, item); - if (existsSync(src)) symlinkSync(src, join(sandbox, item)); + if (existsSync(src)) sandboxLink(src, join(sandbox, item)); } const skip = new Set((opts.withoutJars ?? []).map((f) => basename(f))); for (const jar of readdirSync(join(dir, 'mods')).filter((f) => f.endsWith('.jar'))) { - if (!skip.has(jar)) symlinkSync(join(dir, 'mods', jar), join(sandbox, 'mods', jar)); + if (!skip.has(jar)) sandboxLink(join(dir, 'mods', jar), join(sandbox, 'mods', jar)); } for (const extra of opts.extraJars ?? []) { - if (existsSync(extra)) symlinkSync(extra, join(sandbox, 'mods', basename(extra))); + if (existsSync(extra)) sandboxLink(extra, join(sandbox, 'mods', basename(extra))); } writeFileSync(join(sandbox, 'eula.txt'), 'eula=false\n', 'utf8'); From da8d58a184892c69eaff9bb5af62012d2ef8111b Mon Sep 17 00:00:00 2001 From: Vayun Godara Date: Tue, 4 Aug 2026 13:18:58 +0200 Subject: [PATCH 2/3] ci: boot-smoke the panel on all three OSes, scan every push for secrets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - smoke-boot.mjs boots the built server against an empty temp layout and requires /api/health to answer within 90s — the shape of a first-run install, on ubuntu/windows/macos alike - gitleaks job with full history on every push (the repo is public) - dependabot for npm and actions, weekly Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KfRjm8SZCtXnoSB1z6hdfB --- .github/dependabot.yml | 11 +++++++++++ .github/smoke-boot.mjs | 42 ++++++++++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 27 +++++++++++++++++++------- 3 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/smoke-boot.mjs diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8ae7a74 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 5 + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly diff --git a/.github/smoke-boot.mjs b/.github/smoke-boot.mjs new file mode 100644 index 0000000..21edd7c --- /dev/null +++ b/.github/smoke-boot.mjs @@ -0,0 +1,42 @@ +// CI boot smoke: the built panel must BOOT and serve /api/health on an EMPTY +// layout on every OS — no Crafty, no JDK, no servers, no token. Watchers are +// expected to log errors on the way up; the bar is "starts, serves, and +// degrades honestly", which is exactly what a first-run install looks like. +import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { spawn } from 'node:child_process'; + +const PORT = 25571; +const root = mkdtempSync(join(tmpdir(), 'spawnpoint-smoke-')); +mkdirSync(join(root, 'Spawnpoint', 'data'), { recursive: true }); +writeFileSync(join(root, 'Spawnpoint', 'data', 'settings.json'), JSON.stringify({ port: PORT })); + +let exited = false; +const child = spawn(process.execPath, ['server/dist/index.js'], { + env: { ...process.env, SPAWNPOINT_ROOT: root }, + stdio: 'inherit', +}); +child.on('exit', (code) => { + exited = true; + console.error(`panel exited before the health check passed (code ${code})`); + process.exit(1); +}); + +const deadline = Date.now() + 90_000; +while (Date.now() < deadline && !exited) { + try { + const res = await fetch(`http://127.0.0.1:${PORT}/api/health`); + if (res.ok) { + console.log('panel boots and serves /api/health on', process.platform); + child.removeAllListeners('exit'); + child.kill(); + process.exit(0); + } + } catch { /* not up yet */ } + await new Promise((r) => setTimeout(r, 1000)); +} +console.error('panel did not serve /api/health within 90s'); +child.removeAllListeners('exit'); +child.kill(); +process.exit(1); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5052354..9365f1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,9 @@ -# Cross-platform build proof: the panel must COMPILE and BOOT on every OS a -# self-hoster might own. The Linux leg is the full experience; mac/windows -# legs prove the panel + preflight degrade gracefully (the client launch gate -# needs Linux + xvfb and says so honestly at runtime). +# Cross-platform proof: the panel must COMPILE and BOOT on every OS a +# self-hoster might own. All three legs now run the same bar — build, then +# boot the real server against an empty layout and serve /api/health (the +# shape of a first-run install). The client/join gates pick their display +# mode at runtime: xvfb on headless Linux, a small visible window on +# desktop OSes, an honest skip where no display exists. name: ci on: push: @@ -25,7 +27,18 @@ jobs: - run: npm ci - run: npm run build -w server - run: npm run build -w web - # smoke: the built server must at least load its entrypoint without a - # crash on this OS (no Crafty/Java present in CI — a clean import and - # config bootstrap is the bar here) + # smoke 1: the built server must load its entrypoint cleanly - run: node -e "import('./server/dist/config.js').then(m => { if (!m.PATHS) throw new Error('config failed'); console.log('config loads on', process.platform); })" + # smoke 2: boot the real panel on an empty layout and hit /api/health + - run: node .github/smoke-boot.mjs + + # the repo is public — every push gets a full-history secret scan + gitleaks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 00eee003a7f72ac54aa9b1899ca377512e2892af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 11:20:32 +0000 Subject: [PATCH 3/3] build(deps): Bump @fastify/cookie from 11.1.1 to 11.1.2 Bumps [@fastify/cookie](https://github.com/fastify/fastify-cookie) from 11.1.1 to 11.1.2. - [Release notes](https://github.com/fastify/fastify-cookie/releases) - [Commits](https://github.com/fastify/fastify-cookie/compare/v11.1.1...v11.1.2) --- updated-dependencies: - dependency-name: "@fastify/cookie" dependency-version: 11.1.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 35 +++++------------------------------ server/package.json | 2 +- 2 files changed, 6 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1a4c1af..55f966c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "spawnpoint", "version": "0.1.0", + "license": "MIT", "workspaces": [ "server", "web" @@ -59,7 +60,6 @@ "os": [ "aix" ], - "peer": true, "engines": { "node": ">=18" } @@ -77,7 +77,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -95,7 +94,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -113,7 +111,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -131,7 +128,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -149,7 +145,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -167,7 +162,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -185,7 +179,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -203,7 +196,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -221,7 +213,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -239,7 +230,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -257,7 +247,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -275,7 +264,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -293,7 +281,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -311,7 +298,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -329,7 +315,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -347,7 +332,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -365,7 +349,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -383,7 +366,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -401,7 +383,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -419,7 +400,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -437,7 +417,6 @@ "os": [ "openharmony" ], - "peer": true, "engines": { "node": ">=18" } @@ -455,7 +434,6 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=18" } @@ -473,7 +451,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -491,7 +468,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -509,7 +485,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -552,9 +527,9 @@ } }, "node_modules/@fastify/cookie": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.1.1.tgz", - "integrity": "sha512-sJ0NXzGVYjUB4OynPZRsIcQ1mKSP4rW45xLCN0aelRq5Vl37xVVbz5kJ6Y0a9m2T0mCUjYCuvlUA9QlTafrZWw==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.1.2.tgz", + "integrity": "sha512-Dtrpk/YOGUsbRMvP/8ZqPpwnMRv0qSqodFdoQ2B589Obc7jw4s4Qla+cV72Bsm7WsZJnqlYFX/i7uSBq0xzg6g==", "funding": [ { "type": "github", @@ -3521,7 +3496,7 @@ "server": { "version": "0.1.0", "dependencies": { - "@fastify/cookie": "^11.0.0", + "@fastify/cookie": "^11.1.2", "@fastify/static": "^8.0.0", "adm-zip": "^0.5.16", "fastify": "^5.0.0", diff --git a/server/package.json b/server/package.json index 6635b86..dd9e04e 100644 --- a/server/package.json +++ b/server/package.json @@ -9,7 +9,7 @@ "start": "node dist/index.js" }, "dependencies": { - "@fastify/cookie": "^11.0.0", + "@fastify/cookie": "^11.1.2", "@fastify/static": "^8.0.0", "adm-zip": "^0.5.16", "fastify": "^5.0.0",