Repository navigation
Releases: NiKrause/relay-button
Release list
0.12.4 — a 2n6 wait that says how long it takes, and Svelte tabs that switch
While the widget waits for a relay's 2n6.me HTTPS route, it used to show
2n6 activation 15/120: still pending, a counter that read like a countdown
toward a failure. It now shows the time spent so far and says that activation
is usually done within 4 minutes, can take up to 10 when Aleph's 2n6 service
is busy, and that the deploy continues on its own; once active it reports how
long it took (#213). The wait itself is unchanged (120 × 5 s).
In the Svelte build, a click on a deployment card's Multiaddresses or Health
tab did nothing until the next state update happened to redraw the card: the
selected tab lived in a let that the markup only read through a helper, and
Svelte's legacy mode does not track that. The template reads it directly now.
The panel's max-height also left out its padding and border, so it ran 34px
past its budget — above the window in inline mode, under the launcher in
floating mode. It uses border-box like the React build, and dvh so mobile
browser bars do not cover its bottom (#215).
@le-space/node's relay probe can retry required addresses that failed:
PROBE_RETRY_WINDOW_MS and PROBE_RETRY_INTERVAL_MS (default 30 s) probe them
again until the window closes. Without the window, one round decides, as
before (#214). The daily relay smoke test uses a 10-minute window, and before
probing it waits until api.2n6.me reports the new instance's route active,
because Aleph's 2n6.me gateway picks up new instances on a periodic sync of up
to about 5 minutes (#216).
0.12.3 — api3.aleph.im is a fallback again
On 2026-11-01 every site publish of Le-Space/landing failed in the CAR upload:
api2.aleph.im answered HTTP 502 Failed to import CAR into IPFS: malformed NDJSON line for a 1 KB site as for a 10 MB one — pyaleph could not read what
its own kubo returned from dag/import. api.aleph.im is a load balancer in
front of the same backend and failed the same way; api3.aleph.im, a core
channel node of its own, took the same signed CAR in 52 s and processed it,
and both Aleph gateways served the result. The node package had rejected api3
since July (#31, without a stated reason); it is accepted again, and the site
runner's default host list is api2, api.aleph.im, api3 — one endpoint pair per
host (the legacy pairing used to keep only the first host next to a single
gateway). Retention, the STORE wait and the website aggregate follow the host
that accepted the upload, as before.
0.12.2 — a slow domain is a warning, not a failed deploy
site-domain-link published the site, linked the domain, and then failed
when Aleph still served the previous build five minutes later – measured on
Le-Space/landing on 2026-10-25 and 2026-10-27, with the link in place each
time. The custom-domain wait now has its own budget of 20 minutes
(ALEPH_SITE_DOMAIN_WAIT_ATTEMPTS × ALEPH_SITE_DOMAIN_WAIT_DELAY_MS; the
DNSLink wait's default is 20 minutes too), and running out of it is a
::warning:: with domain_verified=false in the outputs and a note in the
summary. ALEPH_SITE_DOMAIN_VERIFY_ON_TIMEOUT=fail restores the failure.
0.12.1 — confidential rootfs images publish again
Since 2026-10-26 Aleph's IPFS hosts (ipfs.aleph.cloud, ipfs-2.aleph.im,
ipfs.aleph.im) answer every /api/v0/ call with 404, so
aleph-confidential-rootfs could not publish an image. It now sends the image
and its signed STORE message in one request to an API node's
/api/v0/ipfs/add_file, which takes up to 1 GiB that way and keeps the file
in the same call. The CID is computed first, the way ipfs add does, because
the node checks it (#203). Tested with an 867 MB confidential rootfs: the CIDs
matched and the STORE was processed at once.
The places that still name the dead hosts (default upload URLs, the rootfs
manifest upload in the reusable workflow, the profiles' build scripts) are
listed in #204.
0.12.0 — a GitHub runner that removes itself after one job
Private repositories on GitHub Free get 2000 hosted Actions minutes a month;
once they are used up, every job stays queued. Self-hosted runners are not
affected. 0.12.0 runs them on plain Aleph VMs: one command starts a VM, it
takes one job, and then it forgets its own INSTANCE (#191, #192, #195).
pnpm relay-button gh-runner up --repo owner/nameSetup, step by step: Run GitHub Actions jobs on Aleph (#193).
The image
@le-space/rootfs gains the github-runner profile:
- Ubuntu 24.04, the system of GitHub's own
ubuntu-24.04runners, with
actions-runner2.338.0 (pinned, sha256-checked, updates off), Node.js 24,
Docker and rootfs build tools. It carries no credential. - The published STORE is
9ddeabdb5a6ec23f5b6676f034a84d73a2bb3b2c912d3f736bab932fe33951cd
(963 MiB, close to Aleph's 1 GiB upload limit). - Debian 12 lacked the glibc (2.38) the aleph CLI release binary needs.
Debian 13's cloud image looped in GRUB on Aleph. Growing the root partition
withvirt-resizeproduced an image that never came up, so it is grown in
place withgrowpart. The published image was booted under QEMU to a
login prompt before it was uploaded; the build workflow does not do that
yet.
One job, then gone
- The launcher asks GitHub for a just-in-time config: one runner, one
job, unable to register another. The guest deletes it as soon as the
runner has read it. A just-in-time runner gets only the labels it is
given, so the launcher always addsself-hosted,LinuxandX64. - After the job the VM sends FORGET for its own INSTANCE on behalf of the
runner wallet and powers off. Measured: the FORGET came 8 seconds after the
job, and the scheduler showed the allocation gone. - The poweroff always comes, also when FORGET fails. A TTL timer runs from
boot, so a VM that never gets a config still powers off. janitorerases and forgets what is left, and deregisters offline runners
on GitHub. A failedupremoves INSTANCEs of its name. Aleph can still
place one later, once credits free up, which the janitor then catches.
What Aleph requires, measured
- The forget key needs
INSTANCEas well asFORGET. Aleph lets a
delegate forget only messages it could have created itself; withFORGET
alone the VM's FORGET was rejected. Job code can read the key, so the
runner VMs belong to a dedicated wallet with a small balance. - A day of credits per running VM. Aleph accepts a new VM only while the
wallet could pay a day of runtime for every VM it runs. A default runner
costs about 28,500 credits an hour, so each concurrent runner needs about
684,000 credits in reserve. Only the time a VM runs is charged.
Proven on a real workflow
A private repository's backup-and-restore round trip ran on two of these
runners while its hosted minutes were blocked. It started two confidential
VMs, backed up to Filecoin Calibration and restored with a piece-CID check.
Both runner VMs forgot themselves and nothing was left.
Docs
- Social preview cards for shared links: a site card, and one for the runner
pages (#194). - The launcher is the repository CLI (
pnpm relay-button gh-runner, in
scripts/aleph-github-runner.mjs), used from a checkout. It is not an npm
binary, and@le-space/nodedoes not export its helpers. On npm, the
github-runnerprofile ships in@le-space/rootfsunderreference/.
0.11.0 — a deploy check that checks each transport
The relay probe that decides whether a deployment is healthy only ever
dialed the first address it was given (#187). It probed every address on one
libp2p node, and dial() hands back an existing connection to the same peer.
In two real universal-connectivity deploys, IPv4 TCP was dialed in 388 and
465 ms. Everything after it reported success in 0–1 ms over that same TCP
connection: IPv6 TCP, both proxy-wss addresses, both webtransport addresses
and both webrtc-direct addresses. Several of this autumn's relay defects
(#135, #137, #140) lived in exactly the transports that were never tested.
Each address is now judged on its own connection (#188). Connections to the
peer are closed before and after every dial, and the connection that comes
back must run over the transport that was requested. Against 32 registered
addresses, every family that used to report 0–1 ms is now dialed in 220–593
ms over its own transport.
Two things a Node-based probe cannot check get explicit handling instead of
passing by accident:
- webtransport. Node has no WebTransport transport. uc-go-peer serves
quic-v1andquic-v1/webtransportfrom the same QUIC listener, so the
probe dials that port over libp2p QUIC. That proves the port is forwarded
and the listener answers. It does not exercise HTTP/3 or compare the
certhash with the served certificate. The result says this in a newnote
field. - IPv6. When the probing host has no global IPv6 address,
ip6and
dns6addresses are reported as not checked and not required, rather than
failing a deploy because of the runner.PROBE_IPV6=true|falseoverrides
the detection.
For consumers: universal-connectivity's required families
(relay-probe-policy.json) are now actually tested. A deploy can fail on a
real defect that used to pass, so the first deploy after upgrading deserves
attention.
Not changed: when a required address dials but then fails its ping, the
result is still ok: true, with the failure recorded in error.
One libp2p stack in @le-space/node
@le-space/node carried libp2p 2 for the relay probe next to libp2p 3 for
Helia, which it reached through npm aliases (@le-space/libp2p-*,
libp2p-helia). The probe now runs on libp2p 3, the aliases are gone, and
68 package versions with them (#186, which was merged into a stale branch
and re-landed in #188). multiaddr 13 removed protoNames() and
getPeerId(). Their replacements match multiaddr 12 on 35 addresses,
including relayed ones. A relay's peer id is derived from its EVM key, and
that derivation is unchanged; a test pins the value computed on the old
stack.
No React Native in @le-space/node
@libp2p/webrtc depends on react-native-webrtc, whose react-native peer
is not optional, so pnpm installed React Native into a Node deploy tool:
98 package versions, including Metro and Hermes (#185). Node never loads
that code. A packageExtensions entry now marks the peer optional.
Projects that install @le-space/node from npm still get React Native,
because npm installs non-optional peers too. --legacy-peer-deps avoids it.
Dependencies and advisories
- 38 npm advisories closed through override floors, every target inside its
current major (#184), plus@libp2p/peer-store11 through the stack change. - All overrides now live in
pnpm-workspace.yaml. 16 of them used to be
only inpackage.json, which pnpm 11 no longer reads. - Four
rustls-webpkiadvisories inattest-wasmwere dismissed as
not reachable. The flagged copy is pulled in for algorithm constants only,
and the RA-TLS path uses its own certificate verifier (#183). - 9 advisories remain, each either without a published fix or needing a
major bump (tinypool,katex,postcss-selector-parser6). core,nodeandshared-typesno longer carry"private": true.
They are published, and the flag never reached npm.
0.10.0 — a confidential VM the browser checks for itself
Relay Button can now start an AMD SEV-SNP confidential VM on Aleph. The person
deploying doesn't have to trust the node operator, a server of ours or GitHub
Actions with the image's passphrase. Their own browser checks the VM against
AMD before anything secret leaves it (#154, #169).
How a start from the widget runs:
- The browser fetches the network's SNP runtime, checks it against its
manifest, and computes the launch measurements for the connected
wallet. The INSTANCE message carries them in
environment.trusted_executionand points at an encrypted rootfs. - Once the node runs it, the browser opens an RA-TLS connection to the
guest through an attest relay. The relay forwards TLS records it cannot
read. The browser verifies the guest's report against AMD's certificate
chain, the pinned measurements and policy, and a fresh nonce. The
verifier is aleph-rs's, compiled to WebAssembly. - Only then does the wallet sign the unlock. The rootfs passphrase and the
SSH key carrying the deployment's bootstrap token go to the guest over
that attested channel.
A failed attestation counts as a failed attempt on that node: the instance is
forgotten and the next node is tried. A node whose chip AMD has no certificate
for is tried last on later starts (#179).
What's new, by package:
@le-space/attest-wasm(new on npm). Report verification, the RA-TLS
client, the owner-signed secret injection and the launch measurement
computation, all in WASM with pure-Rust crypto.unlockInstanceand
confidentialTrustedExecutionare the two calls the widget makes
(#170, #171, #173, #175).@le-space/ui. The confidential switch shows only SEV-SNP nodes with a
recent aleph-vm, quotes the confidential price, and starts a confidential
VM when the site setsconfidentialStart(rootfsItemHash,relayUrl,
loadWasm). It refuses when the site has not set it (#156, #176, #177).- The attest relay. Every orbitdb-relay image serves it at
wss://<proxy hostname>/attest, behind its Caddy. There is no default
relay: a site names its own. It runs on its own too, see the Attest relay
page (#172, #178). aleph-vm-deployand the reusable workflow. They get a confidential
mode: one encrypted image per version, a measured launch, attestation and
unlock with Aleph's CLI, then the usual setup. A failed start forgets its
instance. Phase timings show where the time goes (#162–#167).- Confidential rootfs. The measured init chroots into the image and runs
no systemd. A small systemctl replacement starts the units. Caddy runs as
Type=simplewith low ports opened, and the filesystem grows to the
disk (#162, #177).
Verified on real SEV-SNP nodes:
- The CLI path.
- Debian with Docker and a multi-container web stack in a confidential VM (#159, #160).
- The relay profile.
- A Playwright E2E that deploys from the widget with an injected test wallet,
attests, unlocks, waits for the relay's registration, and attests once more
through that relay's own/attest(Aleph Confidential Widget E2E).
Also in this release:
- The wallet helpers moved into
@le-space/browser(#147). - Bootstrap posts are published, awaited and reconciled in one channel
(#165). - undici 6.28.1 (#152).
Encrypted images are built per version with the operator's passphrase.
Images per visitor are still open (#169).
0.9.9 — a site keeps its newest uploads, and lets the rest go
Every static-site publish adds a STORE message, and Aleph bills each one for
as long as it is kept. Nothing ever forgot the old ones. Retention existed,
but it had three problems:
- It matched STOREs on a
refthat uploads have not carried since the
authenticated CAR upload. - It was off unless configured.
- It read a single page of 100 messages.
Measured on 2026-09-17, on the address that publishes the le-space.de sites:
569 STOREs for 13 websites, 557 of them stale, billed at 2,845 credits an
hour. When the credits ran low, add_car answered HTTP 402 Payment Required, and a production deploy failed twice. A second address held 342
STOREs for four live sites.
Retention now follows ALEPH_SITE_NAME (#144). The runner already records
each upload under websites[name], together with the history of the earlier
ones. From that history retention:
- keeps the newest
ALEPH_SITE_RETENTION_KEEP_COUNTuploads, three unless
configured, and forgets the rest in batches of 50 - never forgets an upload that the address's
websitesordomains
aggregate still points to, so a domain whose link job did not run keeps its
site - skips uploads Aleph has already forgotten
A failure shows in the step summary and in the new retention_forgotten
output instead of failing the publish. ALEPH_SITE_REF is ignored, the
action's site_ref input is deprecated, and retention_keep_count now
defaults to 3.
The first publish after upgrading forgets every older upload of that site
beyond the newest three. Without ALEPH_SITE_NAME, retention does nothing.
Also in this release:
ALEPH_SITE_VERIFY=libp2p(#142). It checks a published site by
fetching every block over libp2p into an empty Helia node, instead of
requesting it from Aleph's HTTP gateway.ALEPH_SITE_DOMAIN_VERIFY=dnslink
checks a linked domain through its_dnslinkrecord.heliais now a
dependency of@le-space/nodeand loads when the runner is imported. The
built packages were installed with npm into an empty prefix, as consumer
workflows install them, and the runner imported cleanly on Node 22.21.- The AutoTLS certificate is checked on the port the listener is bound to
(#140). A relay advertises an AutoTLS address only after a TLS handshake
proves the certificate serves. That loopback handshake dialled the announced
port, which exists only on the host's NAT, so the address was never
advertised. - The
qsoverride is raised to^6.16.0(#139), past the advisory.
0.9.8 — a relay publishes its websocket address the moment it has one
A relay refreshes its bootstrap registration on a timer: once about twenty
minutes after boot, then every six hours. AutoTLS issues the certificate on
its own schedule. Nothing connected the two. A certificate that arrived after
that first refresh sat unpublished for up to six hours.
Measured on the live relay 65.108.233.158: booted 07:02:24, AutoTLS ready
07:04:14, registration refreshed 07:30:27, next refresh 13:36. A relay
deployed three and a half hours earlier had published two webtransport
addresses and one webrtc-direct, and no websocket at all, because it had none
to publish when its turn came.
That set cannot be verified by the thing that consumes it.
select_compact_multiaddrs keeps the three best browser-dialable addresses,
ranking proxy wss first, AutoTLS wss second, webtransport and webrtc-direct
after. With neither of the top two ranks available, all three slots go to
transports Node cannot dial, and js-peer's bootstrap:resolve — which proves
a peer is alive by connecting to it over a websocket — reports "no
Node-dialable address proved this peer is live" and refuses to bake a list it
could not verify.
Both relay profiles now start their bootstrap refresh from the AutoTLS
refresh's ExecStartPost. --no-block is not optional: both units are
Type=oneshot, and starting one synchronously from inside the other's
ExecStartPost deadlocks systemd. ucan-store is deliberately untouched —
its AutoTLS unit is a /bin/true placeholder and it has no periodic refresh
to trigger.
How thin the margin was: a run at 07:50 discovered six relays and exactly one
of them offered a websocket address that answered. One other had a proxy
address whose host no longer connects. The build that depends on these relays
was passing on the health of a single peer.
0.9.7 — the relay's websocket address points at a port that is open
Four of the five address families a uc-go-peer relay announces used the port
Aleph published it on. The AutoTLS websocket used the port the relay binds
inside the guest. Measured on the live relay 65.108.233.158: 9097 filtered
from the internet, 24013 open, both the same listener — and 9097 was the one
being advertised.
Browsers discard an address they cannot reach, so the published registration
carried no wss entry at all. The direct-wss probe family then had nothing to
try, its required probe failed, and bootstrap:resolve in js-peer refused to
bake a bootstrap list it could not verify. One wrong port number, and the
build that consumes these relays could not go green.
uc-go-peer-configure.sh had translated the port correctly all along. The
address is rewritten a second time by the AutoTLS refresh, which waits for the
certificate, scans the journal for the concrete SNI hostname, and re-emitted
the matched log line verbatim — carrying the internal port back out with it.
It only ever looked right because the log is where the true hostname is.
orbitdb-relay reads EXTERNAL_RELAY_WS_PORT and raises when it is missing,
which is why its relays have always published a reachable wss address and
uc-go-peer's never did. The two profiles now agree, though uc-go-peer falls
back to the backend port with a line on stderr rather than refusing to boot.
The logic sat inside main(), between a journalctl call and a systemctl restart, where no test could reach it — which is the reason it survived this
long. It now lives in two pure functions, and three of the six tests covering
them fail against the previous release.
Also: a stale .pyc had been committed before __pycache__ was ignored, so
every published tarball shipped bytecode for a script it no longer matched.