A StartOS package for satd, built with Start9's TypeScript SDK. It is
published from its own repository (epochbtc/satd-startos) — Start9's
registry expects one repository per package — and lives here so it is
reviewed and versioned with satd.
Contents: satd only — the daemon, sat-cli, sat-tui and the MCP server.
No Lightning, no BTCPay, no wallets: StartOS users compose those from their
own marketplace, and a package that bundled a second copy of software the
store already offers would be worse than useless.
Image: ghcr.io/epochbtc/satd, unmodified. It already carries satd-init
and mkca.sh, so this package's first run is the same one the reference stack
and the appliance perform and cannot drift from them.
satd serves TLS itself on 8336 / 50002 / 3001, from a CA it generates per install. That is the right answer for the reference stack and the appliance, where nothing else can issue a certificate. It is the wrong answer here.
StartOS already terminates TLS at its reverse proxy, with a certificate chaining to the server's root CA — the one the user's browser trusts on that box. Exporting satd's own listeners would ask every user to import a second certificate authority for a single service.
So this package binds satd's plain listeners and lets the OS wrap them.
satd's TLS listeners still run, unexported, which leaves them on lo and
lxcbr0 and off the LAN. satd-init is used unmodified.
MCP is the exception: satd refuses to start with MCP bound off-loopback unless
TLS and auth are both configured, so that listener speaks TLS from satd's own
certificate. The OS re-wraps it — terminating the client's connection with the
server's certificate and opening a fresh one inward — with
upstreamCertValidation: 'disable', because the inward leg presents a
certificate from satd's per-install CA that the OS has no way to be taught.
This is a deliberate departure from the interface table this file used to carry, which specified satd's own TLS ports. That table was written without reference to how StartOS handles TLS.
Requires Node 22+, Docker, jq, and:
start-cli2.0+ — fromStart9Labs/start-technologiesreleases (start-cli_x86_64-linux). Note thatStart9Labs/shared-workflowsis the legacy build line and pins start-cliv0.4.0-beta.9; the SDK 2.0 line this package targets usesstart-technologiesinstead.squashfs-tools-ngandsquashfs-tools— two separate projects, andpackneeds a binary from each:tar2sqfsfrom the former to turn each image layer set into a squashfs,mksquashfsfrom the latter for the.s9pkitself. A build with only one of them fails partway with a bareNo such file or directorynaming the missing binary. Homebrew has nosquashfs-tools-ngformula, so on macOS this step wants alinux/arm64container rather than a host toolchain.- A container runtime.
packresolves the image pinned in the manifest and embeds its layers.start-clireaches forpodmanfirst and reportsDocker Error: podman: No such file or directorywhen it is absent, even with Docker working — setSTARTOS_USE_PODMAN=falseto use Docker. - A packaging workspace in the parent directory.
start-clilooks for a.startos/marker in the directory containing the package repo, socontrib/packaging/.startos/has to exist. Create it withcd contrib/packaging && start-cli s9pk init-workspace— note that also clones the wholestart-technologiesmonorepo beside it, which is not wanted here. An empty.startos/is not enough: start-cli 2.0.0 refuses to pack withUninitialized: No packaging workspace foundunless it holds theconfig.yamlandbuild.key.pemthatinit-workspacegenerates. Run it in a scratch directory and copy those two files across to avoid the clone..startos/is gitignored because it holds a per-machine signing key.
Then:
make # typecheck, test, lint, bundle, and pack every arch
make x86 # just x86_64
make install # sideload to the server in ~/.startos/config.yamlmake runs tsc --noEmit, the tests, the SDK's lint pass and ncc before it
packs, so a type error or a failing test stops the build.
The SDK ships the entire build as s9pk.mk; the Makefile here is one
include line.
make itself cannot run in CI — packing wants start-cli, tar2sqfs and a
signing workspace, and make install wants a server. The parts that can are
gated by the app-store packages job in .github/workflows/appliance.yml:
npm ci, tsc --noEmit, the tests, and the ncc bundle. It runs on any PR
touching this directory, and also on one touching
contrib/stack/satd/satd-init, because test/networks.test.ts reads that
file — gating only on this directory would skip the drift check on the very
change that causes drift.
npm audit reports high-severity DoS advisories against brace-expansion
and js-yaml, and they cannot be fixed here. @start9labs/start-sdk
declares bundleDependencies: [@start9labs/start-core, eslint, typescript-eslint], which makes 127 of the 158 entries in
package-lock.json inBundle: true — files inside the SDK's tarball rather
than edges npm resolves. overrides regenerates the lockfile and leaves
those versions exactly as they were, and 2.0.9 is the newest SDK published.
Nine alerts over five instances: three advisory ranges each against
brace-expansion 1.1.15 (at three paths), brace-expansion 5.0.6, and
js-yaml 4.2.0. Every one sits under the SDK's bundled eslint,
@eslint/eslintrc, @eslint/config-array or
@typescript-eslint/typescript-estree. GitHub scopes them runtime, which
reflects their sitting inside a runtime package, not anything at runtime
reaching them.
That toolchain does run. s9pk.mk calls the SDK's lint.mjs as part of the
javascript/index.js build gate, so eslint executes on every make — the
reachability argument cannot rest on nothing invoking it. What does not run
is the vulnerable code. Instrumenting all five instances and running the
whole gate (lint.mjs, npm run check, npm run build):
brace-expansionis required byminimatch— eight loads — and its export is called zero times. The advisories are all about expanding{}groups, and the only glob in play isstartos/**/*.ts, which has no braces.eslint.config.base.mjssupplies exactly that one pattern.js-yamlis never loaded.lint.mjspassesoverrideConfigFile: truewith an inline config, soeslintrcnever searches for a YAML config file to parse.
None of it ships, either. javascript/index.js requires nothing outside
Node's builtins, and carries no brace-expansion or js-yaml code — its
only matches for eslint are eslint-disable comments in vendored source.
One false lead worth recording: the bundle does contain
tag:yaml.org,2002:, 102 times. That is yaml 2.9.0, a different library
under no advisory here — YAMLParseError and LineCounter are present,
YAMLException and DEFAULT_SCHEMA are not.
The nine alerts are dismissed as not_used on that evidence.
.github/dependabot.yml scopes an ignore to those two package names, so
an advisory against something this package really does resolve still
surfaces.
Installed and run on StartOS 0.4.0.1 (x86_64), sideloaded with
start-cli package install -s. What that proved:
- satd-init runs unmodified from the image and produces this install's CA,
certificate, MCP token,
authfile.tomlandbitcoin.conf, all owned bysatdwith the right modes. - The node syncs, and both health checks report as documented — Node "satd is ready", Blockchain Sync "Syncing blocks: …%".
- Every exported interface answers through StartOS's reverse proxy with a
certificate chaining to the server's root CA: Esplora
GET /api/blocks/tip/height→ 200, Electrumserver.version→satd-electrs-compatible, both verifying against that CA withVerify return code: 0 (ok). MCP is 401 without a token and, once the MCP Hostnames action names the address the client uses, returns a fullinitializeresult with the token the MCP Token action prints. - The Network action moves a running node between chains, re-rendering the config and rebinding the P2P port each time.
Three defects came out of it, none of them visible to a typecheck: the ready
gate probed /readyz and so never went green during a sync; the Network
action wrote the store without restarting the node; and the manifest pinned
an image tag that predates satd-init, so the package as first written could
not have started at all.
bridgeSubnet is now checked rather than assumed — the rpcallowip range it
feeds is what admits the OS proxy on the real bridge, and the RPC interface
answers.
Also checked, now on every PR that touches this directory: the package
typechecks against @start9labs/start-sdk 2.0.9, test/networks.test.ts
verifies the network list and every P2P port against
contrib/stack/satd/satd-init itself so the two cannot drift (which is why
a change to that file runs this job too), and
test/reactivity.test.ts guards the two defects above that a typecheck
cannot see.
The aarch64 package has since been built and installed the same way, on an
arm64 machine, against StartOS 0.4.0.1. Everything above holds there: the
image's binaries are genuinely aarch64 (e_machine 0xb7, not an emulated
x86_64), satd-init produces the same artefacts, the node syncs, and every
interface answers through the proxy. Nothing failed for a reason that had
anything to do with the architecture.
That install is also what surfaced the fourth defect, which x86_64 would have
shown just as readily had anything reached MCP by name: satd left the MCP
transport's Host allowlist at its loopback-only default, so every request
arriving by hostname was answered 403 before authentication ran. The StartOS
proxy forwards the client's Host unchanged and performs no validation of
its own, which makes satd's check the only DNS-rebinding defence on that
path — so it is kept, and the MCP Hostnames action is how the names
clients use reach it. The package cannot derive them: getHostInfo carries
only operator-added custom domains, the .local name comes from the server's
own hostname, which no effect exposes, and the container's hostname is a
generated id.
Still unverified: backup and restore. The exclusions were corrected against a
datadir satd 0.5.2 actually wrote: they named Bitcoin Core's indexes/ and
debug.log, which satd never creates, and missed chainstate_background/,
which it does. test/backups.test.ts checks the names against satd's storage
code. Whether a restore brings back a working node has not been tried.
The Blockchain Sync check used to print verificationprogress as a
percentage. satd 0.5.2 computes that field from timestamps, so a node at
genesis reported roughly 69%. It now shows the block count against the header
count.
Start9's community registry takes packages by email and then owns them:
- The package goes in a public repository,
epochbtc/satd-startos, withmainas its default branch.contrib/packaging/sync-store.sh startosfills a clean clone of it from this directory. - Email submissions@start9.com with the link.
- Start9 forks it into
Start9-Communityand reviews it as a pull request on the fork. The fork is the upstream from then on: every later version is a pull request against it, synced from here the same way. - A merged pull request builds, tags and publishes to community-beta.
- Promotion to community production is ours to ask for, by email.
.github/workflows/ holds the four workflows accepted packages use. They are
inert here, since GitHub only runs workflows at a repository's root, and live
once synced. build.yml runs on pull requests with no signing key. The other
three publish, need Start9's key, and run only in the Start9-Community fork.
The version tag is the package version with : replaced by _ and no
prefix: 0.5.2:0 is tagged v0.5.2_0.
Two tests read satd's own files. Outside this repository test/upstream.ts
falls back to copies the sync vendors into test/upstream/, from the same satd
commit; the storage-layout test has no copy and skips.
For each release:
-
Bump the image tag and digest in
startos/manifest/index.tsand the version and release notes instartos/versions/current.ts. A new upstream version resets the revision to0; a package-only change bumps it. -
Install it on a StartOS box and confirm every interface answers.
-
Sync, review the diff, and push, or open the pull request against the fork:
contrib/packaging/sync-store.sh startos ../satd-startos