Initial StartOS package for Station Master (v0.5.1)

Built from source via a git submodule pinned to a tag, not a published image
— the Dockerfile, main.ts, and manifest should never need to change for an
ordinary version bump, only the submodule pin (see UPDATING.md). One volume,
one interface serving the browser client + lobby/intent API + SSE stream
from a single origin, no dependencies. The server-wide join secret (D14) is
seeded on install, exposed via the Get Join Secret action, and blocks start
behind a critical task until retrieved — the same first-set/rotation pattern
as actual-budget-startos's admin password.

Verified on phoenix.local: installs, the critical task correctly blocks an
ordinary start, force-starting confirms the daemon binds its port and serves
the client, and store.json correctly holds the install-seeded join secret.
Not verified: the Get Join Secret action's execution end-to-end — start-cli's
`package action run` fails with a client-side deserialization error that
reproduces identically against actual-budget's already-shipped equivalent
action, so this looks like a start-cli issue rather than a defect here.

icon.svg is still the scaffold's hello-world placeholder — no real Station
Master icon exists yet to ship in its place.
This commit is contained in:
Jesse
2026-08-21 08:44:07 -04:00
commit de28b30eea
40 changed files with 2540 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
import { utils } from '@start9labs/start-sdk'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { storeJson } from '../fileModels/store.json'
/**
* Mints a fresh join secret on every run and returns it — first-set and rotation are the same
* action (recipe-admin-credentials.md). Writing a new value restarts the daemon with it: main.ts
* reads `joinSecret` reactively via `.const(effects)`, so a rotation here takes effect
* immediately rather than needing a manual restart.
*
* Rotating invalidates the old secret for anyone who hasn't joined yet, but never removes a
* player already seated in a game — D14's secret gates the lobby door only (join-secret ≠
* session token, per `lobby-and-sessions.md` §1).
*/
export const getJoinSecret = sdk.Action.withoutInput(
'get-join-secret',
async () => ({
name: i18n('Get Join Secret'),
description: i18n('Retrieve or rotate the secret players need to create or join a game'),
warning: null,
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
async ({ effects }) => {
const joinSecret = utils.getDefaultString({ charset: 'a-z,A-Z,0-9', len: 24 })
await storeJson.merge(effects, { joinSecret })
return {
version: '1',
title: 'Join Secret',
message:
'Share this with anyone you want to be able to create or join a game on this server — ' +
"it doesn't identify a person or a seat, just who's allowed at the lobby door. Running " +
'this action again generates a new one and restarts the server with it; anyone already ' +
'seated in a game keeps playing, but the old secret stops working for new games and joins.',
result: {
type: 'single',
name: 'Join Secret',
description: null,
value: joinSecret,
masked: true,
copyable: true,
qr: false,
},
}
},
)
+4
View File
@@ -0,0 +1,4 @@
import { sdk } from '../sdk'
import { getJoinSecret } from './getJoinSecret'
export const actions = sdk.Actions.of().addAction(getJoinSecret)