headscale leaves the platform. 45 files move to plugins/offscale/ and the
platform stops knowing it exists.
api/router.ts the thin auth-gated proxy, now at /api/offscale
sidecar/ 18 files, the whole headscale contract and its admin keys
db/ schema + queries, offscale_servers
web/ 26 files as panels and a layout — no screen, per the rule
removed from the platform: the hono mount, the `headscale` capability, the
App.tsx route pair, the screen and its barrel, the AppRegistry spread, the
officerdev re-exports, the dock tile, the page-title rule, and both database
barrels. tsgo is clean and nothing references it.
the imports tell the story of what the plugin↔host API actually is. the sidecar
takes @@/sidecar/protocol, @@/sidecar/connect, @@/data-path and
@@/officer-url.mjs; the queries take officerdb/db and officerdb/crypto; the
schema takes officerdb/auth/schema for the one reference a plugin may make; the
web half takes useClient, copyToClipboard, WorkspaceView and TerminalView from
the officerdev barrel. all of it resolves because a plugin lives inside the repo
— no publishing, no version negotiation.
AND IT FOUND A REAL BUG IN THE INSTALLER. createSidecarProxy learns its port
from a one-shot `<name>:server` event and subscribes when the plugin's router is
first imported — at mount. install started the sidecar BEFORE mounting, so the
announcement fired into a void: process online, routes mounted, every request
answering `503 sidecar not available` until something forced a reconnect. it
would have hit every plugin with an http sidecar. `example` never caught it
because it has no listener to announce.
install and enable now mount before starting; disable still unmounts before
stopping. neither direction leaves a mounted route in front of a sidecar that
cannot be reached.
verified live: /api/offscale/_officer/servers answers {"servers":[]}, /offscale
and /offscale/nodes serve, the old /api/headscale is 404, the offscale
capability is registered from the manifest, and officer-offscale is online.
757 pass, same 10 pre-existing failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gap at the other end of create-user.ts: the owner could set a password once, at
creation, and never again. Losing it meant a hand-written UPDATE with an argon2
hash — the same "edit Postgres by hand" hole that creating accounts used to have.
POST /api/users/:id/password, owner-gated, with a button on the row.
GENERATED, not typed. The failure this exists for is "I created the account and
forgot to copy the password down", and an owner typing a replacement can lose it the
same way on the second go. Shown once in a dialog built to be copied — a dialog and
not a toast, because a toast that times out while somebody finds a pen loses the one
thing they came for.
The generator satisfies validatePassword BY CONSTRUCTION rather than by luck: one
character drawn from each of the four required classes, the rest from the union,
then Fisher-Yates shuffled so the first four positions are not always
lower/upper/digit/special. Rejection sampling throughout — `% n` on a byte biases
the early characters. Then it runs validatePassword on its own output, so if the
rules ever gain a requirement the alphabets do not cover it throws at the one call
site instead of minting passwords the login form rejects. Measured: 20,000
generations, all four classes present every time.
l, I, 1, O and 0 are absent from the alphabets. This gets read off a screen and
typed somewhere else.
Signs them out everywhere, as asked: passwordChangedAt = now, and userMiddleware
already refuses any token whose iat predates it. That overwrites the null
create-user leaves to mean "the owner chose this, not them" — checked, nothing reads
that column except the token check.
The Linux account is deliberately untouched, and the dialog says so. Members have no
Linux password and never had one: ensureOsUser runs useradd with no -p, so it is
created locked. Their terminal goes through setpriv, which does not authenticate;
their SSH is the key the owner pasted; `su - <member>` as root does not ask. And
machine-setup sets PasswordAuthentication no — verified on this host — so one could
not be used to log in even if it existed. Setting one would be a new way in, not a
repair.
The owner is excluded: they have change-password, which asks for the current one,
and resetting themselves here would end the session doing it.
Verified: transpiles, all lucide icons exist, 20k generator runs. tsgo next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>