- TypeScript 92.8%
- Shell 2.3%
- NSIS 1.9%
- Dockerfile 1.6%
- JavaScript 1.2%
- Other 0.2%
| .forgejo/workflows | ||
| assets | ||
| dist | ||
| installer | ||
| scripts | ||
| src | ||
| tools/oxlint/anti-slop | ||
| .gitignore | ||
| .oxlintrc.json | ||
| .releaserc.cjs | ||
| CHANGELOG.md | ||
| Dockerfile | ||
| esbuild.mjs | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| tsconfig.json | ||
skinhub-render
Render worker for SkinHub. Claims capability-typed
tasks from the worker plane (gameplay = the danser clip, screenshots =
the danser panel/thumbnail boots, icons = sharp mod-icon sheets, osk =
zip packing), renders them, and uploads the results through the API. A job
fans out server-side into per-kind chunks of a few skins each, so any number
of workers can share one upload; on one machine the supervisor runs a child
per capability lane (gameplay / screenshots / cpu work), each in its own
toolchain root, so the three proceed at the same time.
Install
Register a worker on the site (/render-workers), have it approved, then:
- Standalone binary (Linux/Windows): download
skinhub-render-worker-<platform>from the latest release and runskinhub-render-worker --token <token>. Everything else (danser, the assets bundle, sharp) downloads on first run into a data folder next to the binary, at the versions the server pins. - Docker:
git.sulej.net/skinhub/skinhub-render:beta(or:latestfor prod), with the toolchain pre-baked. Needs--gpus all,--cap-add SYS_RESOURCE(danser's ffmpeg feed callsF_SETPIPE_SZ) and--add-host osu.ppy.sh:127.0.0.1. - From source:
pnpm install && node dist/index.mjs(Node 22+).
Thin-worker contract
The worker is deliberately dumb. Everything tunable lives in skinhub-api's
/api/render-worker/config: danser version + download URLs, the assets bundle,
danser settings overrides, clip/screenshot timings, the icon-sheet layout, the
sharp package pins, and the minimum worker version. The config is data only:
the worker knows the shape of every invocation itself. The server refuses a
worker older than its minimum at claim time (upgrade_required), and the
worker idles until updated.
Trust model
The worker parses member-uploaded skin bytes with danser, sharp and ffmpeg, so the platform treats it as hostile and gives it as little authority as possible:
- No storage credential. Inputs stream through skinhub-api (only oids confirmed to belong to the task); outputs go to the upload route, which derives the staging key server-side for exactly the slots the task announced.
- No database access, no session secret, no Forgejo token.
- One task per process. It exits when the task finishes and the orchestrator restarts it, so a compromise cannot persist into another user's render.
- Untrusted output. skinhub-api re-validates everything the worker uploads (path shape, size cap, magic bytes) and stores it under a key derived from a hash skinhub-api computes itself.
Config
CLI flags win over env vars.
| Flag | Env | Default | Purpose |
|---|---|---|---|
--token |
RENDER_WORKER_TOKEN |
- | Worker-plane bearer (required) |
--api |
SKINHUB_API_URL |
https://skins.sulej.net |
API origin |
--caps |
WORKER_CAPS |
gameplay,screenshots,icons,osk |
Task kinds to advertise |
--encoder |
RENDER_ENCODER |
nvenc | cpu / nvenc / qsv |
| - | SKINHUB_DATA_DIR |
next to binary / data/ |
danser + assets + sharp cache |
| - | SKINHUB_POLL_SECONDS |
5 |
Idle poll interval |
| - | SKINHUB_LANE_SLOTS |
server lane_slots |
Concurrent tasks per lane, e.g. gameplay=1,osk=4 |
| - | SKINHUB_TASK_TIMEOUT_MS |
server params.task_timeout_secs (600s) |
Hard ceiling on one task; overrides the server value |
The gameplay and screenshots capabilities need a GPU (NVIDIA reservation
with capabilities: [gpu, compute, video, graphics] in Docker); icons and
osk run anywhere. A machine where sharp cannot load automatically drops the
screenshots and icons capabilities.
Development
pnpm install
pnpm run check # typecheck + test + build (dist/ is committed)
semantic-release cuts releases from beta (prereleases) and from main
(stable, starting at the prod promotion); CI attaches the SEA binaries to each
release and bumps the deploy pin.