Skip to content

Releases: NiKrause/relay-button

0.12.4 — a 2n6 wait that says how long it takes, and Svelte tabs that switch

Choose a tag to compare

@github-actions github-actions released this 02 Nov 13:52
f05e14b

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

Choose a tag to compare

@github-actions github-actions released this 01 Nov 18:20
bb9edb4

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

Choose a tag to compare

@github-actions github-actions released this 29 Oct 05:57
22de671

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

Choose a tag to compare

@github-actions github-actions released this 29 Oct 05:11
28e4e09

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

Choose a tag to compare

@github-actions github-actions released this 21 Oct 14:02
9b9513c

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/name

Setup, 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.04 runners, with
    actions-runner 2.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
    with virt-resize produced an image that never came up, so it is grown in
    place with growpart. 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 adds self-hosted, Linux and X64.
  • 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.
  • janitor erases and forgets what is left, and deregisters offline runners
    on GitHub. A failed up removes 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 INSTANCE as well as FORGET. Aleph lets a
    delegate forget only messages it could have created itself; with FORGET
    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/node does not export its helpers. On npm, the
    github-runner profile ships in @le-space/rootfs under reference/.

0.11.0 — a deploy check that checks each transport

Choose a tag to compare

@github-actions github-actions released this 19 Oct 12:01
1fe88ef

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-v1 and quic-v1/webtransport from 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 new note
    field.
  • IPv6. When the probing host has no global IPv6 address, ip6 and
    dns6 addresses are reported as not checked and not required, rather than
    failing a deploy because of the runner. PROBE_IPV6=true|false overrides
    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-store 11 through the stack change.
  • All overrides now live in pnpm-workspace.yaml. 16 of them used to be
    only in package.json, which pnpm 11 no longer reads.
  • Four rustls-webpki advisories in attest-wasm were 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-parser 6).
  • core, node and shared-types no longer carry "private": true.
    They are published, and the flag never reached npm.

0.10.0 — a confidential VM the browser checks for itself

Choose a tag to compare

@github-actions github-actions released this 17 Oct 08:31
e128772

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:

  1. 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_execution and points at an encrypted rootfs.
  2. 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.
  3. 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. unlockInstance and
    confidentialTrustedExecution are 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 sets confidentialStart (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-deploy and 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=simple with 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

Choose a tag to compare

@github-actions github-actions released this 17 Sep 18:32
8cd470c

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 ref that 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_COUNT uploads, three unless
    configured, and forgets the rest in batches of 50
  • never forgets an upload that the address's websites or domains
    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 _dnslink record. helia is now a
    dependency of @le-space/node and 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 qs override is raised to ^6.16.0 (#139), past the advisory.

0.9.8 — a relay publishes its websocket address the moment it has one

Choose a tag to compare

@github-actions github-actions released this 02 Sep 07:58
c048f87

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

Choose a tag to compare

@github-actions github-actions released this 02 Sep 07:37
6ab50fc

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.