mindflayer-server

Mind Flayer

Mind Flayer server

Docker CI and release Docker Pulls GitHub Release

The Docker workflow runs tests, creates semantic releases, and publishes multi-platform images to Docker Hub. Release builds publish the full version, minor, major, and latest tags; main-branch builds also publish edge.

The server has two deliberately separate endpoints:

The reverse proxy remains outside this repository. Configure the Foundry module with its external reverse-proxy host, port and /ws path; it already constructs the appropriate external wss:// URL.

Container topology

services:
  mindflayer-server:
    build: .
    user: node
    environment:
      MINDFLAYER_DATA_DIR: /data
      MINDFLAYER_FIRMWARE_DIR: /firmware
      FOUNDRY_PORT: 8080
      DEVICE_PORT: 10443
    ports:
      - "10443:10443"
    expose:
      - "8080"
    volumes:
      - mindflayer-data:/data
      - ./firmware:/firmware:ro
volumes:
  mindflayer-data:

The image runs as the unprivileged node user. Ensure bind-mounted directories are writable by that UID where required. The image contains neither PlatformIO nor any firmware signing private key.

Published container images support linux/amd64, linux/arm64, and linux/s390x. Node.js 24 does not provide the Alpine base image for 32-bit ARM, so linux/arm/v6 and linux/arm/v7 are not published.

The image defaults persistent server identity, credentials, and the verified firmware cache to /data, accepts optional manually managed firmware at /firmware, and declares /data as a volume. The read-only /firmware mount is unnecessary for automatic updates. Its Docker health check verifies both /healthz on the Foundry HTTP listener and /healthz on the device HTTPS listener.

Device TLS bootstrap

On first start the server creates /data/tls/device-key.pem (mode 0600) and a self-signed /data/tls/device-cert.pem. The certificate can be recreated while retaining the key; the private key is the durable server identity and must be backed up with the data volume. Extract the pinned public key with:

openssl pkey -in /data/tls/device-key.pem -pubout -out device-public.pem

The host bundle tool exports this public key as DER/SPKI into each keypad’s serial provisioning envelope. Losing the private key changes server identity; every keypad then needs trusted serial reprovisioning. Corrupt or mismatched key/certificate state fails startup instead of silently changing identity.

Device provisioning and rollout

Create a unique 256-bit device secret:

MINDFLAYER_DATA_DIR=/data npm run device:provision -- controller1

The command stores /data/devices.json with restrictive permissions. Create a mode-0600 ignored serial bundle without printing its secret fields:

MINDFLAYER_DATA_DIR=/data MINDFLAYER_WIFI_SSID='ssid' \
MINDFLAYER_WIFI_PASSWORD='password' MINDFLAYER_SERVER_HOST='10.42.0.1' \
npm run device:bundle -- controller1 provisioning/controller1.provisioning.bin
npm run device:serial-provision -- provisioning/controller1.provisioning.bin /dev/serial/by-path/...

The serial sender automatically performs the keypad’s double-reset recovery sequence through FTDI RTS. Recovery boot leaves the physically shared GPIO3/RXD0 out of NeoPixel DMA mode, sends the validated envelope, waits for acknowledgement, and requires no button press.

Serial runtime diagnostics default to disabled. Set MINDFLAYER_SERIAL_DEBUG=true while creating a bundle to enable them for that device; use false or omit the variable for silent normal operation. Provisioning acknowledgements remain available regardless of this setting.

Connected keypads automatically install newer stable verified releases by default, including releases discovered while they are already connected. Installation reboots the keypad and preserves provisioning. See firmware updates for verification, polling, opt-outs, and offline operation.

To pin a keypad instead, add rollout metadata to its entry in devices.json and restart the server:

{
  "secret": "<existing secret>",
  "targetVersion": "1.2.3",
  "allowDowngrade": false
}

Without a target, the newest verified stable release is selected unless the entry has "autoUpdate": false. A target takes precedence over automatic selection and must exist in the verified cache or manual repository. Equal versions mean no update. Automatic downgrades are never offered; only an explicit target with "allowDowngrade": true permits one. Authenticated devices receive short-lived, device-bound opaque download grants in an HTTP Authorization: Bearer header.

Firmware repository

The server downloads the keypad release archive from GitHub and verifies its RSA-2048/SHA-256 signature against the bundled production public key before caching or offering it. It never compiles or signs firmware and needs no signing private key. The keypad independently verifies the signature before installation.

For an explicit manual/offline rollout, copy a trusted signed binary into the read-only firmware mount and create manifest.json following firmware/manifest.example.json. Each release declares schema version, hardware ID, semantic version, relative artifact path, byte size and SHA-256. Startup rejects invalid versions, missing or outside files, traversal, and size/hash mismatches. Unlike automatic imports, this operator-managed path checks repository integrity, not the RSA signature; installation still requires the keypad’s independent signature verification. Manual releases require an explicit device target and are never automatically selected as the latest release.

After HMAC authentication, an unpinned registration receives restricted-CBOR FIRMWARE_ACCEPTED for its reported version; a pinned device receives it only for the available configured target. This is distinct from authentication: it allows the temporary rBoot candidate’s complete health gate to promote it. Acceptance precedes any newer update offer, so discovering another release cannot prevent the returning candidate’s health handshake. The server never knows or controls rBoot slot numbers.

Development

License

The server is licensed under GPL-3.0-only. Third-party dependencies retain their own licenses. Published containers include the project license and credits and declare the GPL license in their OCI metadata.

The server’s published branches and tags were rewritten on 2026-09-10 to make the project license consistently GPLv3 throughout history. Existing clones should be replaced with fresh clones after preserving local work; do not merge the old history back into the repository.

Commands

npm ci
npm test
npm audit
npm start

Commits on main are released with semantic-release. Conventional Commit types determine the next version, update package.json, package-lock.json, and CHANGELOG.md, create the Git tag and GitHub release, and supply matching semantic Docker tags and OCI version metadata.