v1.2.0
Minor Changes
- #53
2363908Thanks @haydenbleasel! - Add Left 4 Dead 2 as a supported game. Uses theleft4devops/l4d2image with a docker-compose inlineserver.cfgfor cvars that need to survive map loads (z_difficulty,sv_allow_lobby_connect_only). Join passwords are disabled — L4D2's coop lobby flow drops clients aftersv_passwordverification on direct connect, so servers stay private via unlisted IP/port instead.
Patch Changes
-
#67
f1e0609Thanks @haydenbleasel! - Return 400 instead of an unhandled 500 when the heartbeat, events, or command-ack agent routes receive a signed request with a malformed JSON body.JSON.parsesat outside the routes' error handling, unlike the other agent endpoints. -
#67
f1e0609Thanks @haydenbleasel! - Block symlink path traversal in agent file operations.resolveInRootonly normalized the path string, so a symlink planted inside the bind-mounted data root by a compromised game container could redirectFILES_INSTALL_FROM_URL/FILES_DELETEwrites outside the root (the agent runs as root on the host). Paths are now resolved throughrealpathon their deepest existing ancestor and re-verified against the real data root before any read, write, or delete. -
#67
f1e0609Thanks @haydenbleasel! - Always validate game settings when creating a server. A request that omitted thesettingsfield skipped validation entirely, bypassing required fields like Don't Starve Together's cluster token and provisioning a paid VM that could never start. -
#67
f1e0609Thanks @haydenbleasel! - Prevent double-delivery of agent commands.claimPendingCommandsdid a find-then-update, so two overlapping long-polls (e.g. an agent retry racing a still-running handler) could both read the same pending rows and execute a command — including destructive ones likeFILES_DELETE— twice. Each row is now claimed with a conditional update onstatus: "pending", so exactly one caller wins it. -
#67
f1e0609Thanks @haydenbleasel! - Write the agent's state file atomically.saveStatewrote a temp file but then copied it into place with a truncate-and-write, so a crash mid-save could leave a corruptstate.jsonthat permanently bricked the agent on next boot. The temp file is now swapped in withrename. -
#67
f1e0609Thanks @haydenbleasel! - Fetch all pages when listing a server's backups and snapshots.listImagesForServerrequested a single page of 50 images for the whole account and filtered client-side, so servers on accounts with more than 50 images silently lost backups from the UI. Backups are now filtered server-side via Hetzner'sbound_toparameter and both listings followmeta.pagination.next_pageto exhaustion. -
#67
f1e0609Thanks @haydenbleasel! - Make snapshot builder VM creation idempotent.stepCreateBuilderVmnow returns the already-persistedproviderBuilderIdon retry instead of creating another VM, and the builder name is derived deterministically from the build id so a retried create whose first response was lost is rejected by the provider's unique-name constraint rather than silently billing a second orphaned VM. -
#67
f1e0609Thanks @haydenbleasel! - Harden docker-compose value escaping.escapeComposeValueonly escaped double quotes, but values are emitted inside YAML double-quoted scalars where\is the escape character — a backslash in a server name or game setting (e.g. world name, MOTD) produced invalid YAML or terminated the scalar early, and embedded newlines could inject additional YAML lines into the root-run compose file. Backslashes are now escaped before quotes and control characters are stripped. -
#67
f1e0609Thanks @haydenbleasel! - Stopdocker composeinvocations from hanging the agent. The subprocess was spawned with piped stdout/stderr that were never read, so any compose command producing more than the OS pipe buffer (~64KB, easily hit by image-pull progress) blocked forever with no timeout. Both pipes are now drained while awaiting exit, and compose failures include the last stderr line for easier debugging. -
#67
f1e0609Thanks @haydenbleasel! - Make agent enrollment atomic. The bootstrap token was burned in a transaction but the agent row was created after it committed, so a transient create failure left the token burned with no agent — the VM's retries then hit "Token already used" forever and the server had to be re-provisioned. The burn is now a conditional update inside the same transaction as the create, which also closes the race where two concurrent requests could both pass the burned-token check. -
#67
f1e0609Thanks @haydenbleasel! - Deduplicate retried agent event batches. When the agent re-sent a batch after a lost response, the server assigned fresh ids and seqs, duplicating activity timeline entries and log lines. Agent-sourced rows now use deterministic ids derived from the protocol'sclientEventId(activity) andagentSeq(logs), scoped by agent id, so replayed inserts are dropped. -
#53
2363908Thanks @haydenbleasel! - Regenerate the Hetzner OpenAPI types. Picks up@deprecatedmarkers on the singularget_<resource>_actionendpoints (per the 2026-04-30 changelog), the Load Balancer service/target type split, the primary/secondary Zone schema split, and per-location pricing breakdowns. None of the endpoints we call were affected. -
#67
f1e0609Thanks @haydenbleasel! - Close the console log stream on unmount.LogsStream's effect cleanup only set a flag and never closed the liveEventSourceor cleared the pending reconnect timer, so navigating away from the Console tab left the SSE connection (and the server's 500ms log poll loop) running for up to the route's 300smaxDuration, with stale listeners still updating state. -
#67
f1e0609Thanks @haydenbleasel! - Size the Minecraft JVM heap from the actual machine instead of a hardcoded6G. Rescaling a Minecraft server previously had no effect on the game (and on machines smaller than the default the fixed heap could exceed total RAM). The compose builder now receives the provider-reported machine memory and gives the JVM ~75% of it; the heap is applied whenever a new compose is pushed (install and settings updates). -
#67
f1e0609Thanks @haydenbleasel! - Stop reporting transient provisioning errors as "Owner has not configured provider credentials". The catch instepCreateProviderServerswallowed every error from the provider lookup (including DB blips) into a permanentFatalError; it now only convertsMissingProviderCredentialsErrorand lets everything else retry. -
5c50664Thanks @haydenbleasel! - Refactor the Hetzner integration behind a provider-agnosticProviderinterface so a future AWS or GCP backend can be added by writing a new implementation rather than rewriting call sites.lib/hetzner/is replaced bylib/providers/(interface + types + per-user factory) withlib/providers/hetzner/as the only implementation today; all 23 call sites (workflow steps, route handlers, server actions, page loaders) now consume the interface and pass string IDs end-to-end (no moreNumber()round-trips). Workflow steps are renamed to drop theHetznerprefix (stepCreateHetznerServer→stepCreateProviderServer, etc.). The Prisma schema renameshetznerToken/hetznerImageId/hetznerServerId/hetznerBuilderIdtoprovider*and adds aprovider String @default("hetzner")column toUser,Server,UserSnapshot, andSnapshotBuild; the migration usesALTER TABLE … RENAME COLUMNso existing data is preserved. UI copy and the/api/account/hetznerroute path remain Hetzner-named — those are deferred until a second provider lands. -
#67
f1e0609Thanks @haydenbleasel! - Fix races between provisioning and deletion. Deleting a server while its VM create was in flight could leak the Hetzner VM forever (teardown saw noproviderServerId, and the provision step's retry path skipped cleanup) and could mark the deleted server as "failed". Now the cancelled paths ofstepCreateProviderServerdelete the VM themselves, the post-create state update no longer clobbers teardown's "deleted" phase,stepMarkFailedskips deleted servers, andwaitForServerRunningchecks for cancellation before treating a vanished VM as a fatal provisioning error. -
#67
f1e0609Thanks @haydenbleasel! - Don't mark a successful snapshot build as failed when post-save cleanup hiccups.stepDeleteBuilderruns after the snapshot id is saved and the build is "ready", so provider errors while deleting the builder VM are now swallowed (matchingstepDeletePreviousSnapshot) instead of landing in the catch handler and flipping the build to "failed". -
#53
2363908Thanks @haydenbleasel! - Show the docker image string on the new-server summary card and the Details tab so it's clear which image will be (or is) running. -
#67
f1e0609Thanks @haydenbleasel! - Stop leaking snapshot images on failed builds. The image id is now persisted to the build row immediately aftercreate_image(making the step retry-safe), and the build failure handler deletes any image that never reached "ready" instead of leaving a per-GB-billed orphan snapshot behind. -
#67
f1e0609Thanks @haydenbleasel! - Don't permanently block snapshot builds when the workflow fails to start. The build row was committed as "pending" beforestart(buildSnapshot, …); if the start threw, the row stayed non-terminal and the "already running" guard rejected every future build for that user. The row is now marked "failed" when the workflow can't be started. -
#67
f1e0609Thanks @haydenbleasel! - Validate thecursorquery parameter on the activity and log SSE streams. A non-numeric cursor becameNaN, flowed into Prisma asseq: { gt: NaN }, and errored the stream immediately; it now falls back to 0. -
#67
f1e0609Thanks @haydenbleasel! - Ack unimplemented agent commands as failed instead of succeeded.UPLOAD_BACKUPandFETCH_LOGSexist in the protocol but have no agent implementation; they previously fell through the switch and were acked as "succeeded", which would falsely report a backup or log fetch as done if one were ever enqueued.