Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83abb0485d | ||
|
|
b35409ca74 | ||
|
|
700d39c425 | ||
|
|
4272c47ee5 | ||
|
|
c7cb043485 | ||
|
|
4dfe79290e | ||
|
|
d3e3df6d24 | ||
|
|
969570e38b | ||
|
|
b73d646db5 | ||
|
|
8c37ff412c | ||
|
|
06bf359535 | ||
|
|
a4f3415f0f | ||
|
|
c9c9ebe6d4 | ||
|
|
100993445b | ||
|
|
a4f80e7ec1 | ||
|
|
4ad34d3a0a | ||
|
|
c9bae926a5 | ||
|
|
cb3f7e8720 | ||
|
|
eb98ebb682 | ||
|
|
dc7b598558 | ||
|
|
69f3a355c7 |
@@ -2,6 +2,19 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## v1.8.15-alpha (2026-09-13)
|
||||||
|
|
||||||
|
- Cuprate is presented as one user-facing app in My Apps, including its UI launch button; the generated dashboard companion is hidden as an implementation detail instead of appearing under Services.
|
||||||
|
- Added regression coverage for Cuprate install and installed-state grouping.
|
||||||
|
- Release validation was rerun on the corrected tree before OTA and ISO publication.
|
||||||
|
|
||||||
|
## v1.8.14-alpha (2026-09-13)
|
||||||
|
|
||||||
|
- **Cuprate gains a first-party companion dashboard.** The Monero node now has a Bitcoin-style status UI, safe app grouping, a 450 GB disk-safety gate, and a restricted RPC that is never exposed as a launch page.
|
||||||
|
- **Bitcoin Core Tor enrollment uses the correct protocol identity.** `bitcoin-core` is forwarded on port 8333 and resolves to its own hidden-service directory without disturbing legacy Bitcoin aliases.
|
||||||
|
- **GitWorkshop opens Archipelago’s canonical ngit repository by default.** The launcher and registry promotion use the full maintainer/relay/`archy` coordinate, with regression coverage for Companion and browser-tab launches.
|
||||||
|
- **Release validation is stricter.** The registry gate now checks the complete canonical source deep link, and the merged candidate passed the full frontend and focused backend test suites.
|
||||||
|
|
||||||
## v1.8.13-alpha (2026-09-12)
|
## v1.8.13-alpha (2026-09-12)
|
||||||
|
|
||||||
- **GitWorkshop installs reliably on fresh nodes.** The app is classified as a user-facing app while its install placeholder is being created, so it remains visible under My Apps instead of Services.
|
- **GitWorkshop installs reliably on fresh nodes.** The app is classified as a user-facing app while its install placeholder is being created, so it remains visible under My Apps instead of Services.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
app:
|
app:
|
||||||
id: archy-mempool-web
|
id: archy-mempool-web
|
||||||
name: Mempool Web
|
name: Mempool Web
|
||||||
version: 3.0.1
|
version: 3.3.1-archy1
|
||||||
# Where this app comes from, so scripts/check-upstream-releases.py can
|
# Where this app comes from, so scripts/check-upstream-releases.py can
|
||||||
# tell us when the pin below has fallen behind. Without it nothing can:
|
# tell us when the pin below has fallen behind. Without it nothing can:
|
||||||
# container.image names our mirror, not the project it was mirrored from.
|
# container.image names our mirror, not the project it was mirrored from.
|
||||||
@@ -12,7 +12,7 @@ app:
|
|||||||
container_name: mempool
|
container_name: mempool
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.3.1
|
image: source.archipelago-foundation.org/chaum/mempool-frontend:v3.3.1-archy1
|
||||||
pull_policy: if-not-present
|
pull_policy: if-not-present
|
||||||
network: archy-net
|
network: archy-net
|
||||||
|
|
||||||
@@ -45,7 +45,9 @@ app:
|
|||||||
# first, but nginx binds 0.0.0.0:8080 (IPv4) only -> localhost probe gets
|
# first, but nginx binds 0.0.0.0:8080 (IPv4) only -> localhost probe gets
|
||||||
# "connection refused" -> perpetual unhealthy -> health_monitor restart loop.
|
# "connection refused" -> perpetual unhealthy -> health_monitor restart loop.
|
||||||
endpoint: http://127.0.0.1:8080
|
endpoint: http://127.0.0.1:8080
|
||||||
path: /
|
# Probe the backend through nginx: a static page can be healthy while
|
||||||
|
# every API/WebSocket request is stuck on a dead backend address.
|
||||||
|
path: /api/v1/backend-info
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
app:
|
app:
|
||||||
id: mempool
|
id: mempool
|
||||||
name: Mempool Explorer
|
name: Mempool Explorer
|
||||||
version: 3.0.0
|
version: 3.3.1-archy1
|
||||||
# Where this app comes from, so scripts/check-upstream-releases.py can
|
# Where this app comes from, so scripts/check-upstream-releases.py can
|
||||||
# tell us when the pin below has fallen behind. Without it nothing can:
|
# tell us when the pin below has fallen behind. Without it nothing can:
|
||||||
# container.image names our mirror, not the project it was mirrored from.
|
# container.image names our mirror, not the project it was mirrored from.
|
||||||
@@ -11,7 +11,7 @@ app:
|
|||||||
description: Bitcoin mempool and blockchain explorer. Real-time transaction and block visualization.
|
description: Bitcoin mempool and blockchain explorer. Real-time transaction and block visualization.
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.3.1
|
image: source.archipelago-foundation.org/chaum/mempool-frontend:v3.3.1-archy1
|
||||||
image_signature: cosign://...
|
image_signature: cosign://...
|
||||||
pull_policy: if-not-present
|
pull_policy: if-not-present
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -104,7 +104,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.8.13-alpha"
|
version = "1.8.15-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"archipelago-container",
|
"archipelago-container",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.8.13-alpha"
|
version = "1.8.15-alpha"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||||
|
|||||||
@@ -377,6 +377,23 @@ async fn write_staged_torrc(content: &str, staging: &str) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod known_service_tests {
|
||||||
|
use super::{is_protocol_service, known_service_port};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bitcoin_core_is_a_protocol_service_on_the_p2p_port() {
|
||||||
|
// Regression: apps/bitcoin-core/manifest.yml uses id "bitcoin-core",
|
||||||
|
// distinct from the legacy "bitcoin"/"bitcoin-knots" ids. Missing
|
||||||
|
// here means auto-enrollment silently skips it (known_service_port
|
||||||
|
// returns 0) and, separately, regenerate_torrc falls back to the
|
||||||
|
// web-app HiddenServicePort-80 default instead of forwarding 8333
|
||||||
|
// straight through.
|
||||||
|
assert_eq!(known_service_port("bitcoin-core"), 8333);
|
||||||
|
assert!(is_protocol_service("bitcoin-core"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod torrc_tests {
|
mod torrc_tests {
|
||||||
use super::app_hidden_service_port_line;
|
use super::app_hidden_service_port_line;
|
||||||
@@ -594,7 +611,7 @@ fn is_valid_v3_onion(s: &str) -> bool {
|
|||||||
pub(in crate::api::rpc) fn known_service_port(name: &str) -> u16 {
|
pub(in crate::api::rpc) fn known_service_port(name: &str) -> u16 {
|
||||||
match name {
|
match name {
|
||||||
"archipelago" => 80,
|
"archipelago" => 80,
|
||||||
"bitcoin" | "bitcoin-knots" => 8333,
|
"bitcoin" | "bitcoin-core" | "bitcoin-knots" => 8333,
|
||||||
"electrs" | "electrumx" => 50001,
|
"electrs" | "electrumx" => 50001,
|
||||||
"lnd" => 8080,
|
"lnd" => 8080,
|
||||||
"btcpay" | "btcpay-server" | "btcpayserver" => 23000,
|
"btcpay" | "btcpay-server" | "btcpayserver" => 23000,
|
||||||
@@ -619,7 +636,7 @@ pub(in crate::api::rpc) fn known_service_port(name: &str) -> u16 {
|
|||||||
pub(in crate::api::rpc) fn is_protocol_service(name: &str) -> bool {
|
pub(in crate::api::rpc) fn is_protocol_service(name: &str) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
name,
|
name,
|
||||||
"bitcoin" | "bitcoin-knots" | "electrs" | "electrumx" | "lnd"
|
"bitcoin" | "bitcoin-core" | "bitcoin-knots" | "electrs" | "electrumx" | "lnd"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -657,9 +657,19 @@ fn apply_dynamic_metadata(app_id: &str, meta: &mut AppMetadata) {
|
|||||||
/// Map app_id to Tor hidden service directory name.
|
/// Map app_id to Tor hidden service directory name.
|
||||||
/// "archipelago" is the main web UI (nginx port 80).
|
/// "archipelago" is the main web UI (nginx port 80).
|
||||||
/// Supports container names from deploy (archy-*, btcpay-server, etc.).
|
/// Supports container names from deploy (archy-*, btcpay-server, etc.).
|
||||||
|
///
|
||||||
|
/// This must match what enrollment actually names the hidden service dir
|
||||||
|
/// with — both the install-time auto-enroll (`install.rs`) and the manual
|
||||||
|
/// `tor.create-service` RPC write `HiddenServiceDir` using the raw
|
||||||
|
/// `package_id`/`name` verbatim, with no canonicalization. So `bitcoin-core`
|
||||||
|
/// gets its own identity arm rather than folding into the "bitcoin" alias:
|
||||||
|
/// aliasing it here without also canonicalizing the write side would point
|
||||||
|
/// this lookup at `hidden_service_bitcoin`, which never gets created — the
|
||||||
|
/// on-disk dir is always `hidden_service_bitcoin-core` for this app id.
|
||||||
fn tor_service_name(app_id: &str) -> Option<&'static str> {
|
fn tor_service_name(app_id: &str) -> Option<&'static str> {
|
||||||
match app_id {
|
match app_id {
|
||||||
"archipelago" => Some("archipelago"),
|
"archipelago" => Some("archipelago"),
|
||||||
|
"bitcoin-core" => Some("bitcoin-core"),
|
||||||
"bitcoin" | "bitcoin-knots" | "bitcoind" => Some("bitcoin"),
|
"bitcoin" | "bitcoin-knots" | "bitcoind" => Some("bitcoin"),
|
||||||
"electrumx" | "electrs" | "electrum" => Some("electrumx"),
|
"electrumx" | "electrs" | "electrum" => Some("electrumx"),
|
||||||
"lnd" | "lnd-ui" => Some("lnd"),
|
"lnd" | "lnd-ui" => Some("lnd"),
|
||||||
@@ -906,6 +916,28 @@ mod launch_url_port_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tor_service_name_tests {
|
||||||
|
use super::tor_service_name;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bitcoin_core_resolves_to_its_own_hidden_service_dir() {
|
||||||
|
// Regression: enrollment (install.rs, tor.create-service) writes
|
||||||
|
// HiddenServiceDir/tor-hostnames entries using the raw package_id
|
||||||
|
// verbatim, never canonicalized. Aliasing "bitcoin-core" to the
|
||||||
|
// shared "bitcoin" name here would point reads at a directory
|
||||||
|
// enrollment never creates.
|
||||||
|
assert_eq!(tor_service_name("bitcoin-core"), Some("bitcoin-core"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn legacy_bitcoin_ids_share_the_bitcoin_alias() {
|
||||||
|
assert_eq!(tor_service_name("bitcoin"), Some("bitcoin"));
|
||||||
|
assert_eq!(tor_service_name("bitcoin-knots"), Some("bitcoin"));
|
||||||
|
assert_eq!(tor_service_name("bitcoind"), Some("bitcoin"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod extract_lan_address_tests {
|
mod extract_lan_address_tests {
|
||||||
use super::extract_lan_address;
|
use super::extract_lan_address;
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
# Archipelago mempool frontend — adds a resilient nginx backend proxy.
|
# Archipelago mempool frontend — adds a resilient nginx backend proxy.
|
||||||
#
|
#
|
||||||
# The only delta vs the upstream image is /patch/entrypoint.sh, which rewrites
|
# Keep the upstream startup logic; repair its rendered proxy configuration.
|
||||||
# the generated nginx-mempool.conf to use `resolver` + a variable proxy_pass so
|
# Publish this derived image under an Archipelago-specific tag, never the
|
||||||
# the frontend re-resolves the backend (mempool-api) via DNS on every request.
|
# upstream version tag that the registry mirror can overwrite.
|
||||||
# Without this, nginx pins the backend IP at startup and serves 502 / "offline"
|
ARG BASE=source.archipelago-foundation.org/lfg2025/mempool-frontend@sha256:d63498a109622475c913db4e3199d893f2440a451450e542923d2e55a38407a0
|
||||||
# after any backend restart (podman reassigns the IP). See the script header.
|
|
||||||
ARG BASE=source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.0.0
|
|
||||||
FROM ${BASE}
|
FROM ${BASE}
|
||||||
|
|
||||||
# --chmod keeps the exec bit (build runs as USER 1000, plain COPY lands root:0644
|
# --chmod keeps the exec bit (build runs as USER 1000, plain COPY lands root:0644
|
||||||
# → "not executable"). Base USER/ENTRYPOINT/CMD (1000 / /patch/entrypoint.sh /
|
# → "not executable"). Base USER/ENTRYPOINT/CMD (1000 / /patch/entrypoint.sh /
|
||||||
# nginx -g "daemon off;") are inherited unchanged.
|
# nginx -g "daemon off;") are inherited unchanged.
|
||||||
COPY --chmod=0755 entrypoint.sh /patch/entrypoint.sh
|
RUN cp /patch/entrypoint.sh /patch/upstream-entrypoint.sh
|
||||||
|
COPY --chmod=0755 entrypoint.sh start-nginx.sh repair-nginx.sh /patch/
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Mempool frontend DNS recovery
|
||||||
|
|
||||||
|
The stock v3.3.1 nginx configuration resolves `mempool-api` only when workers
|
||||||
|
start. Recreating the backend can change its Podman address while the frontend
|
||||||
|
continues to serve its static page, leaving all API/WebSocket requests offline.
|
||||||
|
|
||||||
|
Build and test the derived image before publishing:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
podman build --pull=never -t source.archipelago-foundation.org/chaum/mempool-frontend:v3.3.1-archy1 docker/mempool-frontend
|
||||||
|
python3 scripts/test-mempool-dns-recovery.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The base is pinned by digest. The wrapper preserves upstream runtime options,
|
||||||
|
then repairs all four local API/WebSocket routes after placeholder rendering.
|
||||||
|
DNS is cached for five seconds using the container network resolver. Explicit
|
||||||
|
rewrites preserve API prefixes and query arguments; backend absence does not
|
||||||
|
prevent nginx startup. An unexpected upstream configuration fails startup
|
||||||
|
instead of silently omitting the fix.
|
||||||
|
|
||||||
|
Use an Archipelago-specific image tag. Do not replace it with a stock upstream
|
||||||
|
mirror when updating mempool. Every upstream update must rebuild this wrapper
|
||||||
|
and pass the recovery test (backend absent, changed IP, HTTP and WebSocket
|
||||||
|
mapping, repeated repair, and frontend restart).
|
||||||
|
|
||||||
|
Publish the tested image before publishing the signed app catalog. Both the
|
||||||
|
mempool umbrella image mapping and the archy-mempool-web embedded manifest must
|
||||||
|
point at the patched image. Keep scripts/image-versions.sh in sync. The frontend
|
||||||
|
health check must reach `/api/v1/backend-info` through nginx, not only `/`.
|
||||||
Regular → Executable
+4
-136
@@ -1,137 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__=${BACKEND_MAINNET_HTTP_HOST:=127.0.0.1}
|
set -eu
|
||||||
__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__=${BACKEND_MAINNET_HTTP_PORT:=8999}
|
# Preserve the pinned upstream entrypoint (including new runtime options).
|
||||||
__MEMPOOL_FRONTEND_HTTP_PORT__=${FRONTEND_HTTP_PORT:=8080}
|
# Apply our DNS repair only after it has rendered the nginx configuration.
|
||||||
|
exec /patch/upstream-entrypoint.sh /patch/start-nginx.sh "$@"
|
||||||
CONF=/etc/nginx/conf.d/nginx-mempool.conf
|
|
||||||
|
|
||||||
# ─── archipelago patch ────────────────────────────────────────────────────
|
|
||||||
# The stock frontend writes `proxy_pass http://<backend>:8999` with a literal
|
|
||||||
# hostname and NO resolver, so nginx resolves the backend IP ONCE at worker
|
|
||||||
# start and caches it for the process lifetime. Podman reassigns the backend
|
|
||||||
# container's IP whenever it is restarted/recreated (gate, OTA, crash, reboot
|
|
||||||
# re-IPAM), after which nginx keeps proxying to the dead IP → /api hangs, the
|
|
||||||
# websocket 502s, and the mempool UI shows "offline" until nginx is reloaded.
|
|
||||||
#
|
|
||||||
# Fix: force per-request DNS re-resolution via `resolver` + a variable in
|
|
||||||
# proxy_pass. Because a variable in proxy_pass disables nginx's automatic
|
|
||||||
# location→URI rewriting, each block is rewritten to preserve its original
|
|
||||||
# path mapping exactly:
|
|
||||||
# /api/v1/ws, /ws → "/" (var + "/" replaces the whole URI)
|
|
||||||
# /api/v1 → identity (no-URI proxy_pass passes $uri unchanged)
|
|
||||||
# /api/ → /api/v1/$1 (explicit rewrite, then no-URI proxy_pass)
|
|
||||||
# Operates on the __PLACEHOLDER__ tokens so the host/port sed below fills in
|
|
||||||
# the concrete values (incl. the `set $mp_backend` line). Idempotent.
|
|
||||||
# Resolver address: podman's aardvark-dns answers on the network gateway
|
|
||||||
# (e.g. 10.89.0.1), NOT Docker's 127.0.0.11. Read it from resolv.conf so this
|
|
||||||
# works on any podman network/subnet (and still falls back for Docker).
|
|
||||||
ARCHY_RESOLVER=$(awk '/^nameserver/ { print $2; exit }' /etc/resolv.conf 2>/dev/null)
|
|
||||||
ARCHY_RESOLVER=${ARCHY_RESOLVER:-127.0.0.11}
|
|
||||||
|
|
||||||
if ! grep -q 'set \$mp_backend' "$CONF"; then
|
|
||||||
awk -v res_addr="$ARCHY_RESOLVER" '
|
|
||||||
BEGIN { res = 0 }
|
|
||||||
/^[[:space:]]*location / && res == 0 {
|
|
||||||
print "\tresolver " res_addr " valid=10s ipv6=off;"
|
|
||||||
res = 1
|
|
||||||
}
|
|
||||||
/proxy_pass http:\/\/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__\/;/ {
|
|
||||||
print "\t\tset $mp_backend __MEMPOOL_BACKEND_MAINNET_HTTP_HOST__;"
|
|
||||||
print "\t\tproxy_pass http://$mp_backend:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/;"
|
|
||||||
next
|
|
||||||
}
|
|
||||||
/proxy_pass http:\/\/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__\/api\/v1\/;/ {
|
|
||||||
print "\t\tset $mp_backend __MEMPOOL_BACKEND_MAINNET_HTTP_HOST__;"
|
|
||||||
print "\t\trewrite ^/api/(.*)$ /api/v1/$1 break;"
|
|
||||||
print "\t\tproxy_pass http://$mp_backend:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__;"
|
|
||||||
next
|
|
||||||
}
|
|
||||||
/proxy_pass http:\/\/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__\/api\/v1;/ {
|
|
||||||
print "\t\tset $mp_backend __MEMPOOL_BACKEND_MAINNET_HTTP_HOST__;"
|
|
||||||
print "\t\tproxy_pass http://$mp_backend:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__;"
|
|
||||||
next
|
|
||||||
}
|
|
||||||
{ print }
|
|
||||||
' "$CONF" > "$CONF.archy" && mv "$CONF.archy" "$CONF"
|
|
||||||
fi
|
|
||||||
# ─── end archipelago patch ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
sed -i "s/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__/${__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__}/g" /etc/nginx/conf.d/nginx-mempool.conf
|
|
||||||
sed -i "s/__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/${__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__}/g" /etc/nginx/conf.d/nginx-mempool.conf
|
|
||||||
|
|
||||||
cp /etc/nginx/nginx.conf /patch/nginx.conf
|
|
||||||
sed -i "s/__MEMPOOL_FRONTEND_HTTP_PORT__/${__MEMPOOL_FRONTEND_HTTP_PORT__}/g" /patch/nginx.conf
|
|
||||||
cat /patch/nginx.conf > /etc/nginx/nginx.conf
|
|
||||||
|
|
||||||
if [ "${LIGHTNING_DETECTED_PORT}" != "" ];then
|
|
||||||
export LIGHTNING=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Runtime overrides - read env vars defined in docker compose
|
|
||||||
|
|
||||||
__MAINNET_ENABLED__=${MAINNET_ENABLED:=true}
|
|
||||||
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
|
|
||||||
__TESTNET4_ENABLED__=${TESTNET_ENABLED:=false}
|
|
||||||
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
|
|
||||||
__LIQUID_ENABLED__=${LIQUID_ENABLED:=false}
|
|
||||||
__LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false}
|
|
||||||
__ITEMS_PER_PAGE__=${ITEMS_PER_PAGE:=10}
|
|
||||||
__KEEP_BLOCKS_AMOUNT__=${KEEP_BLOCKS_AMOUNT:=8}
|
|
||||||
__NGINX_PROTOCOL__=${NGINX_PROTOCOL:=http}
|
|
||||||
__NGINX_HOSTNAME__=${NGINX_HOSTNAME:=localhost}
|
|
||||||
__NGINX_PORT__=${NGINX_PORT:=8999}
|
|
||||||
__BLOCK_WEIGHT_UNITS__=${BLOCK_WEIGHT_UNITS:=4000000}
|
|
||||||
__MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_BLOCKS_AMOUNT:=8}
|
|
||||||
__BASE_MODULE__=${BASE_MODULE:=mempool}
|
|
||||||
__ROOT_NETWORK__=${ROOT_NETWORK:=}
|
|
||||||
__MEMPOOL_WEBSITE_URL__=${MEMPOOL_WEBSITE_URL:=https://mempool.space}
|
|
||||||
__LIQUID_WEBSITE_URL__=${LIQUID_WEBSITE_URL:=https://liquid.network}
|
|
||||||
__MINING_DASHBOARD__=${MINING_DASHBOARD:=true}
|
|
||||||
__LIGHTNING__=${LIGHTNING:=false}
|
|
||||||
__AUDIT__=${AUDIT:=false}
|
|
||||||
__MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
|
||||||
__TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
|
||||||
__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
|
||||||
__ACCELERATOR__=${ACCELERATOR:=false}
|
|
||||||
__ACCELERATOR_BUTTON__=${ACCELERATOR_BUTTON:=true}
|
|
||||||
__SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services}
|
|
||||||
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
|
|
||||||
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
|
|
||||||
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
|
|
||||||
|
|
||||||
# Export as environment variables to be used by envsubst
|
|
||||||
export __MAINNET_ENABLED__
|
|
||||||
export __TESTNET_ENABLED__
|
|
||||||
export __TESTNET4_ENABLED__
|
|
||||||
export __SIGNET_ENABLED__
|
|
||||||
export __LIQUID_ENABLED__
|
|
||||||
export __LIQUID_TESTNET_ENABLED__
|
|
||||||
export __ITEMS_PER_PAGE__
|
|
||||||
export __KEEP_BLOCKS_AMOUNT__
|
|
||||||
export __NGINX_PROTOCOL__
|
|
||||||
export __NGINX_HOSTNAME__
|
|
||||||
export __NGINX_PORT__
|
|
||||||
export __BLOCK_WEIGHT_UNITS__
|
|
||||||
export __MEMPOOL_BLOCKS_AMOUNT__
|
|
||||||
export __BASE_MODULE__
|
|
||||||
export __ROOT_NETWORK__
|
|
||||||
export __MEMPOOL_WEBSITE_URL__
|
|
||||||
export __LIQUID_WEBSITE_URL__
|
|
||||||
export __MINING_DASHBOARD__
|
|
||||||
export __LIGHTNING__
|
|
||||||
export __AUDIT__
|
|
||||||
export __MAINNET_BLOCK_AUDIT_START_HEIGHT__
|
|
||||||
export __TESTNET_BLOCK_AUDIT_START_HEIGHT__
|
|
||||||
export __SIGNET_BLOCK_AUDIT_START_HEIGHT__
|
|
||||||
export __ACCELERATOR__
|
|
||||||
export __ACCELERATOR_BUTTON__
|
|
||||||
export __SERVICES_API__
|
|
||||||
export __PUBLIC_ACCELERATIONS__
|
|
||||||
export __HISTORICAL_PRICE__
|
|
||||||
export __ADDITIONAL_CURRENCIES__
|
|
||||||
|
|
||||||
folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
|
|
||||||
echo ${folder}
|
|
||||||
envsubst < ${folder}/config.template.js > ${folder}/config.js
|
|
||||||
|
|
||||||
exec "$@"
|
|
||||||
|
|||||||
Executable
+56
@@ -0,0 +1,56 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Resolve the backend again after container IP changes. Run after upstream
|
||||||
|
# placeholder substitution, so the repair also works on an existing container.
|
||||||
|
set -eu
|
||||||
|
conf=${1:-/etc/nginx/conf.d/nginx-mempool.conf}
|
||||||
|
resolv=${2:-/etc/resolv.conf}
|
||||||
|
backend=${BACKEND_MAINNET_HTTP_HOST:-127.0.0.1}
|
||||||
|
port=${BACKEND_MAINNET_HTTP_PORT:-8999}
|
||||||
|
resolver=$(awk '/^nameserver/ { print $2; exit }' "$resolv")
|
||||||
|
[ -n "$resolver" ] || { echo 'No DNS resolver configured' >&2; exit 1; }
|
||||||
|
case "$resolver" in *:*) resolver="[$resolver]" ;; esac
|
||||||
|
case "$backend" in *[!a-zA-Z0-9._-]*|'') echo 'Invalid backend hostname' >&2; exit 1 ;; esac
|
||||||
|
case "$port" in *[!0-9]*|'') echo 'Invalid backend port' >&2; exit 1 ;; esac
|
||||||
|
|
||||||
|
tmp=$(mktemp "${conf}.archy.XXXXXX")
|
||||||
|
trap 'rm -f "$tmp"' EXIT HUP INT TERM
|
||||||
|
awk -v backend="$backend" -v port="$port" -v resolver="$resolver" '
|
||||||
|
BEGIN {
|
||||||
|
base = "http://" backend ":" port
|
||||||
|
print "# Archipelago: refresh backend DNS after container replacement."
|
||||||
|
print "resolver " resolver " valid=5s ipv6=off; # archy-dns"
|
||||||
|
print "resolver_timeout 3s; # archy-dns"
|
||||||
|
}
|
||||||
|
/# Archipelago: refresh backend DNS/ || /# archy-dns/ { next }
|
||||||
|
/^[[:space:]]*location[[:space:]]/ { location = $2 }
|
||||||
|
/^[[:space:]]*proxy_pass[[:space:]]/ && index($2, base) == 1 {
|
||||||
|
target = $2
|
||||||
|
sub(/;$/, "", target)
|
||||||
|
path = substr(target, length(base) + 1)
|
||||||
|
if (location != "/api/v1/ws" && location != "/ws" && location != "/api/v1" && location != "/api/") {
|
||||||
|
print "Unexpected backend location: " location > "/dev/stderr"
|
||||||
|
failed = 1; exit 1
|
||||||
|
}
|
||||||
|
if (path != "/" && path != "/api/v1" && path != "/api/v1/") {
|
||||||
|
print "Unexpected backend URI mapping" > "/dev/stderr"
|
||||||
|
failed = 1; exit 1
|
||||||
|
}
|
||||||
|
# Explicitly preserve prefix substitution and query arguments. A variable
|
||||||
|
# proxy_pass without a URI forwards the rewritten URI and original args.
|
||||||
|
print "\t\tset $mp_backend " backend ";"
|
||||||
|
if (path != location)
|
||||||
|
print "\t\trewrite ^" location "(.*)$ " path "$1 break;"
|
||||||
|
print "\t\tproxy_pass http://$mp_backend:" port ";"
|
||||||
|
count++
|
||||||
|
next
|
||||||
|
}
|
||||||
|
/proxy_pass http:\/\/\$mp_backend:/ { count++ }
|
||||||
|
{ print }
|
||||||
|
END {
|
||||||
|
if (failed || count != 4) {
|
||||||
|
print "Expected four backend proxies; refusing an incomplete DNS repair" > "/dev/stderr"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' "$conf" > "$tmp"
|
||||||
|
cat "$tmp" > "$conf"
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
/patch/repair-nginx.sh
|
||||||
|
nginx -t
|
||||||
|
exec "$@"
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.8.13-alpha",
|
"version": "1.8.15-alpha",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.8.13-alpha",
|
"version": "1.8.15-alpha",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scure/bip39": "^2.2.0",
|
"@scure/bip39": "^2.2.0",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.8.13-alpha",
|
"version": "1.8.15-alpha",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "./start-dev.sh",
|
"start": "./start-dev.sh",
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ describe('useAppLauncherStore', () => {
|
|||||||
const store = useAppLauncherStore()
|
const store = useAppLauncherStore()
|
||||||
store.openSession('archipelago-source')
|
store.openSession('archipelago-source')
|
||||||
expect(openInApp).toHaveBeenCalledWith(
|
expect(openInApp).toHaveBeenCalledWith(
|
||||||
'http://192.0.2.10/app/archipelago-source/',
|
'http://192.0.2.10/app/archipelago-source/npub1w3sqdkrhn0gyuvsex32effzgnfpyde6qrrc4u467flg5e9txh4wsfn5vjg/relay.ngit.dev/archy',
|
||||||
)
|
)
|
||||||
expect(store.panelAppId).toBeNull()
|
expect(store.panelAppId).toBeNull()
|
||||||
expect(store.isOpen).toBe(false)
|
expect(store.isOpen).toBe(false)
|
||||||
|
|||||||
@@ -78,12 +78,11 @@ describe('appsConfig service filtering', () => {
|
|||||||
|
|
||||||
it('shows Cuprate as one My Apps entry while hiding its daemon dependency', () => {
|
it('shows Cuprate as one My Apps entry while hiding its daemon dependency', () => {
|
||||||
const entries: Array<[string, PackageDataEntry]> = [
|
const entries: Array<[string, PackageDataEntry]> = [
|
||||||
['cuprate-ui', makePkg('cuprate-ui', 'Cuprate UI', 'money')],
|
['cuprate', makePkg('cuprate', 'Cuprate', 'money')],
|
||||||
['cuprate', makePkg('cuprate', 'Cuprate daemon', 'money')],
|
['archy-cuprate-ui', makePkg('archy-cuprate-ui', 'Cuprate UI companion', 'money')],
|
||||||
]
|
]
|
||||||
;(entries[0][1].manifest as unknown as Record<string, unknown>).interfaces = { main: { ui: 'http://localhost:18091' } }
|
expect(filterEntriesForTab(entries, 'apps', 'all').map(([id]) => id)).toEqual(['cuprate'])
|
||||||
expect(filterEntriesForTab(entries, 'apps', 'all').map(([id]) => id)).toEqual(['cuprate-ui'])
|
expect(filterEntriesForTab(entries, 'services', 'all').map(([id]) => id)).toEqual([])
|
||||||
expect(filterEntriesForTab(entries, 'services', 'all').map(([id]) => id)).toEqual(['cuprate'])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('falls back to packaged app icon when static icon token is not a path', () => {
|
it('falls back to packaged app icon when static icon token is not a path', () => {
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ export const isServiceContainer = sharedIsServiceContainer
|
|||||||
|
|
||||||
const INTERNAL_TOOLING_NAMES = new Set([
|
const INTERNAL_TOOLING_NAMES = new Set([
|
||||||
'buildx_buildkit_default',
|
'buildx_buildkit_default',
|
||||||
|
// Cuprate's dashboard is bundled as a companion of the primary cuprate
|
||||||
|
// package; showing the generated container as a second Services entry
|
||||||
|
// defeats the one-app presentation.
|
||||||
|
'archy-cuprate-ui',
|
||||||
])
|
])
|
||||||
|
|
||||||
export function isInternalToolingPackage(id: string, pkg?: PackageDataEntry): boolean {
|
export function isInternalToolingPackage(id: string, pkg?: PackageDataEntry): boolean {
|
||||||
@@ -37,7 +41,7 @@ export function isServicePackage(id: string, pkg?: PackageDataEntry): boolean {
|
|||||||
|
|
||||||
// Known app -> category mappings (matches App Store categorisation)
|
// Known app -> category mappings (matches App Store categorisation)
|
||||||
export const APP_CATEGORY_MAP: Record<string, string> = {
|
export const APP_CATEGORY_MAP: Record<string, string> = {
|
||||||
'bitcoin-core': 'money', 'bitcoin-knots': 'money', 'bitcoin-ui': 'money', 'cuprate-ui': 'money', 'electrumx': 'money', 'electrs': 'money',
|
'bitcoin-core': 'money', 'bitcoin-knots': 'money', 'bitcoin-ui': 'money', 'cuprate': 'money', 'cuprate-ui': 'money', 'electrumx': 'money', 'electrs': 'money',
|
||||||
'lnd': 'money', 'mempool': 'money', 'mempool-web': 'money', 'btcpay-server': 'commerce',
|
'lnd': 'money', 'mempool': 'money', 'mempool-web': 'money', 'btcpay-server': 'commerce',
|
||||||
'fedimint': 'money', 'fedimint-gateway': 'money',
|
'fedimint': 'money', 'fedimint-gateway': 'money',
|
||||||
'indeedhub': 'media', 'jellyfin': 'media', 'photoprism': 'media', 'immich': 'media',
|
'indeedhub': 'media', 'jellyfin': 'media', 'photoprism': 'media', 'immich': 'media',
|
||||||
|
|||||||
@@ -14,8 +14,10 @@
|
|||||||
// SERVICE_NAMES set that used to live in appsConfig.ts verbatim.
|
// SERVICE_NAMES set that used to live in appsConfig.ts verbatim.
|
||||||
export const SERVICE_NAMES = new Set([
|
export const SERVICE_NAMES = new Set([
|
||||||
'dwn', 'archy-mempool-db', 'archy-btcpay-db', 'archy-nbxplorer', 'archy-tor',
|
'dwn', 'archy-mempool-db', 'archy-btcpay-db', 'archy-nbxplorer', 'archy-tor',
|
||||||
// Cuprate's daemon is the backend dependency of the Cuprate UI app.
|
// Cuprate is presented as one user-facing app. Its companion container
|
||||||
'cuprate',
|
// (archy-cuprate-ui) is the implementation detail; do not classify the
|
||||||
|
// primary package as a Service or it disappears from My Apps and loses its
|
||||||
|
// launch button during/after install.
|
||||||
// Headless backends with no user-facing UI: the Fedimint ecash client daemon,
|
// Headless backends with no user-facing UI: the Fedimint ecash client daemon,
|
||||||
// the Nostr relay, and the Meshtastic LoRa daemon (its chat UI lives in the
|
// the Nostr relay, and the Meshtastic LoRa daemon (its chat UI lives in the
|
||||||
// built-in Mesh tab) belong in Services, not My Apps.
|
// built-in Mesh tab) belong in Services, not My Apps.
|
||||||
|
|||||||
@@ -362,6 +362,30 @@ init()
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
||||||
|
<!-- v1.8.15-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.8.15-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">September 13, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p>Cuprate is presented as one user-facing app in My Apps, including its UI launch button; the generated dashboard companion is hidden as an implementation detail instead of appearing under Services.</p>
|
||||||
|
<p>Added regression coverage for Cuprate install and installed-state grouping.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- v1.8.14-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.8.14-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">September 13, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p><strong>Cuprate gains a first-party companion dashboard.</strong> The Monero node now has a Bitcoin-style status UI, safe app grouping, a 450 GB disk-safety gate, and a restricted RPC that is never exposed as a launch page.</p>
|
||||||
|
<p><strong>Bitcoin Core Tor enrollment uses the correct protocol identity.</strong> bitcoin-core is forwarded on port 8333 and resolves to its own hidden-service directory without disturbing legacy Bitcoin aliases.</p>
|
||||||
|
<p><strong>GitWorkshop opens Archipelago’s canonical ngit repository by default.</strong> The launcher and registry promotion use the full maintainer/relay/archy coordinate, with regression coverage for Companion and browser-tab launches.</p>
|
||||||
|
<p><strong>Release validation is stricter.</strong> The registry gate now checks the complete canonical source deep link, and the merged candidate passed the full frontend and focused backend test suites.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- v1.8.13-alpha -->
|
<!-- v1.8.13-alpha -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
@@ -369,7 +393,9 @@ init()
|
|||||||
<span class="text-xs text-white/40">September 12, 2026</span>
|
<span class="text-xs text-white/40">September 12, 2026</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
<p><strong>GitWorkshop installs reliably on fresh nodes.</strong> The app is classified as a user-facing app while its install placeholder is being created, so it remains visible under My Apps instead of Services. Fresh installs now use the production orchestrator to build the bundled GitWorkshop image rather than sending its local image reference through the legacy registry-pull path. Regression coverage now protects all curated app classifications.</p>
|
<p><strong>GitWorkshop installs reliably on fresh nodes.</strong> The app is classified as a user-facing app while its install placeholder is being created, so it remains visible under My Apps instead of Services.</p>
|
||||||
|
<p><strong>Fresh GitWorkshop installs build the correct image.</strong> The production orchestrator handles its bundled build context instead of sending the local image reference through the legacy registry-pull path.</p>
|
||||||
|
<p><strong>Curated app classification is regression-tested.</strong> Every user-facing app remains in My Apps during installation, while headless services stay in Services.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- v1.8.12-alpha -->
|
<!-- v1.8.12-alpha -->
|
||||||
|
|||||||
+17
-17
@@ -1,29 +1,29 @@
|
|||||||
{
|
{
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"**GitWorkshop installs reliably on fresh nodes.** The app is classified as a user-facing app while its install placeholder is being created, so it remains visible under My Apps instead of Services.",
|
"Cuprate is presented as one user-facing app in My Apps, including its UI launch button; the generated dashboard companion is hidden as an implementation detail instead of appearing under Services.",
|
||||||
"**Fresh GitWorkshop installs build the correct image.** The production orchestrator handles its bundled build context instead of sending the local image reference through the legacy registry-pull path.",
|
"Added regression coverage for Cuprate install and installed-state grouping.",
|
||||||
"**Curated app classification is regression-tested.** Every user-facing app remains in My Apps during installation, while headless services stay in Services."
|
"Release validation was rerun on the corrected tree before OTA and ISO publication."
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"current_version": "1.8.13-alpha",
|
"current_version": "1.8.15-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.13-alpha/archipelago",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.15-alpha/archipelago",
|
||||||
"name": "archipelago",
|
"name": "archipelago",
|
||||||
"new_version": "1.8.13-alpha",
|
"new_version": "1.8.15-alpha",
|
||||||
"sha256": "832c7e75b395f94f919a21d1ad7d32d367e1ef84a0e0995b4e8fe87268aa21a4",
|
"sha256": "3eee71563337f20cb348529925c58b8227afec796783a69176e0b59b9e113c91",
|
||||||
"size_bytes": 64585424
|
"size_bytes": 64571544
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"current_version": "1.8.13-alpha",
|
"current_version": "1.8.15-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.13-alpha/archipelago-frontend-1.8.13-alpha.tar.gz",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.15-alpha/archipelago-frontend-1.8.15-alpha.tar.gz",
|
||||||
"name": "archipelago-frontend-1.8.13-alpha.tar.gz",
|
"name": "archipelago-frontend-1.8.15-alpha.tar.gz",
|
||||||
"new_version": "1.8.13-alpha",
|
"new_version": "1.8.15-alpha",
|
||||||
"sha256": "d0159b61f84eb30013634a97177801474376dddb1189024b94f16236ce7ff538",
|
"sha256": "86a32ef3334b03c197e47d9d28f4435c6f2fa7c4ccacc839a8fc4a0a749495dc",
|
||||||
"size_bytes": 97915796
|
"size_bytes": 98797600
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"release_date": "2026-09-12",
|
"release_date": "2026-09-13",
|
||||||
"signature": "45a322e793a2fc7565cc148f14efbbcf03e0c934183590375368091368ffe49ed2a7e9962692caf54b3432c71976a6c1b0f06a48cf8c4634c7b2e573e5782a0f",
|
"signature": "5e13396e2f33571f136bb1a9ea0356486b3d42c6ba99ee5d33990cd565d9b1178f61eaf6a70a937a006e7de3fd388bcebd63f2daca4bb28accc1b810d8455d03",
|
||||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||||
"version": "1.8.13-alpha"
|
"version": "1.8.15-alpha"
|
||||||
}
|
}
|
||||||
|
|||||||
+3251
-3249
File diff suppressed because one or more lines are too long
+17
-17
@@ -1,29 +1,29 @@
|
|||||||
{
|
{
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"**GitWorkshop installs reliably on fresh nodes.** The app is classified as a user-facing app while its install placeholder is being created, so it remains visible under My Apps instead of Services.",
|
"Cuprate is presented as one user-facing app in My Apps, including its UI launch button; the generated dashboard companion is hidden as an implementation detail instead of appearing under Services.",
|
||||||
"**Fresh GitWorkshop installs build the correct image.** The production orchestrator handles its bundled build context instead of sending the local image reference through the legacy registry-pull path.",
|
"Added regression coverage for Cuprate install and installed-state grouping.",
|
||||||
"**Curated app classification is regression-tested.** Every user-facing app remains in My Apps during installation, while headless services stay in Services."
|
"Release validation was rerun on the corrected tree before OTA and ISO publication."
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"current_version": "1.8.13-alpha",
|
"current_version": "1.8.15-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.13-alpha/archipelago",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.15-alpha/archipelago",
|
||||||
"name": "archipelago",
|
"name": "archipelago",
|
||||||
"new_version": "1.8.13-alpha",
|
"new_version": "1.8.15-alpha",
|
||||||
"sha256": "832c7e75b395f94f919a21d1ad7d32d367e1ef84a0e0995b4e8fe87268aa21a4",
|
"sha256": "3eee71563337f20cb348529925c58b8227afec796783a69176e0b59b9e113c91",
|
||||||
"size_bytes": 64585424
|
"size_bytes": 64571544
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"current_version": "1.8.13-alpha",
|
"current_version": "1.8.15-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.13-alpha/archipelago-frontend-1.8.13-alpha.tar.gz",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.15-alpha/archipelago-frontend-1.8.15-alpha.tar.gz",
|
||||||
"name": "archipelago-frontend-1.8.13-alpha.tar.gz",
|
"name": "archipelago-frontend-1.8.15-alpha.tar.gz",
|
||||||
"new_version": "1.8.13-alpha",
|
"new_version": "1.8.15-alpha",
|
||||||
"sha256": "d0159b61f84eb30013634a97177801474376dddb1189024b94f16236ce7ff538",
|
"sha256": "86a32ef3334b03c197e47d9d28f4435c6f2fa7c4ccacc839a8fc4a0a749495dc",
|
||||||
"size_bytes": 97915796
|
"size_bytes": 98797600
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"release_date": "2026-09-12",
|
"release_date": "2026-09-13",
|
||||||
"signature": "45a322e793a2fc7565cc148f14efbbcf03e0c934183590375368091368ffe49ed2a7e9962692caf54b3432c71976a6c1b0f06a48cf8c4634c7b2e573e5782a0f",
|
"signature": "5e13396e2f33571f136bb1a9ea0356486b3d42c6ba99ee5d33990cd565d9b1178f61eaf6a70a937a006e7de3fd388bcebd63f2daca4bb28accc1b810d8455d03",
|
||||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||||
"version": "1.8.13-alpha"
|
"version": "1.8.15-alpha"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ ELECTRUMX_IMAGE="$ARCHY_REGISTRY/electrumx:v1.18.0"
|
|||||||
|
|
||||||
# Mempool stack
|
# Mempool stack
|
||||||
MEMPOOL_BACKEND_IMAGE="$ARCHY_REGISTRY/mempool-backend:v3.3.1"
|
MEMPOOL_BACKEND_IMAGE="$ARCHY_REGISTRY/mempool-backend:v3.3.1"
|
||||||
MEMPOOL_WEB_IMAGE="$ARCHY_REGISTRY/mempool-frontend:v3.3.1"
|
# The patched frontend is published by chaum on the same trusted registry.
|
||||||
|
MEMPOOL_WEB_IMAGE="${MEMPOOL_WEB_IMAGE:-source.archipelago-foundation.org/chaum/mempool-frontend:v3.3.1-archy1}"
|
||||||
MARIADB_IMAGE="$ARCHY_REGISTRY/mariadb:11.4.10"
|
MARIADB_IMAGE="$ARCHY_REGISTRY/mariadb:11.4.10"
|
||||||
|
|
||||||
# BTCPay
|
# BTCPay
|
||||||
|
|||||||
@@ -53,24 +53,33 @@ if [ -x "$PROJECT_ROOT/core/target/release/archipelago" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
remote_url=$(git -C "$PROJECT_ROOT" remote get-url "$REMOTE")
|
remote_url=$(git -C "$PROJECT_ROOT" remote get-url "$REMOTE")
|
||||||
# https is accepted as well as http. Requiring http:// meant the only remote
|
# Remote URLs are public metadata: ngit can include them in repository
|
||||||
# whose credential actually works for git push (the https one) was rejected,
|
# announcements. Keep credentials in Git's credential helper, never in URLs.
|
||||||
# while the http remote it forced you to use had a dead token — so publishing
|
|
||||||
# failed on auth after the manifest had already passed every check
|
|
||||||
# (v1.7.121-alpha, 2026-08-04). The scheme is carried through to the API URL
|
|
||||||
# rather than assumed.
|
|
||||||
case "$remote_url" in
|
case "$remote_url" in
|
||||||
http://*@*|https://*@*) ;;
|
http://*@*|https://*@*) fail "$REMOTE embeds credentials; move them to a Git credential helper and remove them from the remote URL" ;;
|
||||||
*) fail "$REMOTE must be an authenticated http(s):// Gitea remote URL for API uploads" ;;
|
http://*|https://*) ;;
|
||||||
|
*) fail "$REMOTE must be an http(s):// Gitea remote URL for API uploads" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
scheme=${remote_url%%://*}
|
scheme=${remote_url%%://*}
|
||||||
rest=${remote_url#*://}
|
host_path=${remote_url#*://}
|
||||||
auth=${rest%%@*}
|
|
||||||
host_path=${rest#*@}
|
|
||||||
host=${host_path%%/*}
|
host=${host_path%%/*}
|
||||||
repo_path=${host_path#*/}
|
repo_path=${host_path#*/}
|
||||||
repo_path=${repo_path%.git}
|
repo_path=${repo_path%.git}
|
||||||
|
credential=$(printf 'url=%s\n\n' "$remote_url" | GIT_TERMINAL_PROMPT=0 git -C "$PROJECT_ROOT" credential fill) \
|
||||||
|
|| fail "no Git credential available for $REMOTE; configure a credential helper first"
|
||||||
|
auth_user=""
|
||||||
|
auth_password=""
|
||||||
|
while IFS= read -r field; do
|
||||||
|
case "$field" in
|
||||||
|
username=*) auth_user=${field#username=} ;;
|
||||||
|
password=*) auth_password=${field#password=} ;;
|
||||||
|
esac
|
||||||
|
done <<< "$credential"
|
||||||
|
[ -n "$auth_user" ] && [ -n "$auth_password" ] \
|
||||||
|
|| fail "Git credential helper did not provide a username and password for $REMOTE"
|
||||||
|
auth="$auth_user:$auth_password"
|
||||||
|
unset credential auth_user auth_password
|
||||||
api="$scheme://$host/api/v1/repos/$repo_path"
|
api="$scheme://$host/api/v1/repos/$repo_path"
|
||||||
release_url="$api/releases/tags/v${VERSION}"
|
release_url="$api/releases/tags/v${VERSION}"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Exercise the built frontend against a backend that disappears and changes IP.
|
||||||
|
|
||||||
|
Uses an isolated Podman network and disposable containers, never the node stack.
|
||||||
|
Usage: python3 scripts/test-mempool-dns-recovery.py [frontend-image]
|
||||||
|
"""
|
||||||
|
import ipaddress
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
IMAGE = sys.argv[1] if len(sys.argv) > 1 else (
|
||||||
|
"source.archipelago-foundation.org/chaum/mempool-frontend:v3.3.1-archy1"
|
||||||
|
)
|
||||||
|
BACKEND = "source.archipelago-foundation.org/lfg2025/mempool-backend:v3.3.1"
|
||||||
|
prefix = "mempool-dns-test-" + uuid.uuid4().hex[:8]
|
||||||
|
network, frontend, backend = prefix, prefix + "-web", prefix + "-api"
|
||||||
|
|
||||||
|
|
||||||
|
def podman(*args, check=True):
|
||||||
|
return subprocess.run(["podman", *args], capture_output=True, text=True,
|
||||||
|
check=check, timeout=60).stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def eventually(check, timeout=25):
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return check()
|
||||||
|
except (AssertionError, OSError, urllib.error.URLError):
|
||||||
|
if time.monotonic() >= deadline:
|
||||||
|
raise
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
server = r"""
|
||||||
|
const http = require('http'), crypto = require('crypto');
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.end(JSON.stringify({url: req.url, instance: process.env.INSTANCE}));
|
||||||
|
});
|
||||||
|
server.on('upgrade', (req, socket) => {
|
||||||
|
const key = crypto.createHash('sha1')
|
||||||
|
.update(req.headers['sec-websocket-key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
|
||||||
|
.digest('base64');
|
||||||
|
socket.end('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n' +
|
||||||
|
'Connection: Upgrade\r\nSec-WebSocket-Accept: ' + key + '\r\n' +
|
||||||
|
'X-Upstream-Url: ' + req.url + '\r\nX-Instance: ' + process.env.INSTANCE + '\r\n\r\n');
|
||||||
|
});
|
||||||
|
server.listen(8999, '0.0.0.0');
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
podman("network", "create", network)
|
||||||
|
subnet = ipaddress.ip_network(json.loads(podman("network", "inspect", network))[0]["subnets"][0]["subnet"])
|
||||||
|
podman("run", "-d", "--name", frontend, "--network", network,
|
||||||
|
"-p", "127.0.0.1::8080", "-e", "BACKEND_MAINNET_HTTP_HOST=mempool-api",
|
||||||
|
"-e", "FRONTEND_HTTP_PORT=8080", IMAGE)
|
||||||
|
port = int(podman("port", frontend, "8080/tcp").rsplit(":", 1)[1])
|
||||||
|
url = f"http://127.0.0.1:{port}"
|
||||||
|
|
||||||
|
def static_ready():
|
||||||
|
assert urllib.request.urlopen(url, timeout=4).status == 200
|
||||||
|
|
||||||
|
eventually(static_ready)
|
||||||
|
started = podman("inspect", frontend, "--format", "{{.State.StartedAt}}")
|
||||||
|
try:
|
||||||
|
urllib.request.urlopen(url + "/api/v1/backend-info", timeout=6)
|
||||||
|
raise AssertionError("An absent backend must not appear healthy")
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
assert error.code == 502
|
||||||
|
print("PASS: frontend starts while backend DNS is absent", flush=True)
|
||||||
|
|
||||||
|
for instance, offset in [("first", 10), ("replacement", 11)]:
|
||||||
|
if instance == "replacement":
|
||||||
|
podman("rm", "-f", backend)
|
||||||
|
# Ensure the cached address has expired while the backend is absent.
|
||||||
|
time.sleep(6)
|
||||||
|
podman("run", "-d", "--name", backend, "--network", network,
|
||||||
|
"--network-alias", "mempool-api", "--ip", str(subnet[offset]),
|
||||||
|
"-e", "INSTANCE=" + instance, "--entrypoint", "node", BACKEND,
|
||||||
|
"-e", server)
|
||||||
|
|
||||||
|
for path, expected in [
|
||||||
|
("/api/blocks/tip/height?probe=one", "/api/v1/blocks/tip/height?probe=one"),
|
||||||
|
("/api/v1/fees/recommended?probe=two", "/api/v1/fees/recommended?probe=two"),
|
||||||
|
]:
|
||||||
|
def check_http():
|
||||||
|
with urllib.request.urlopen(url + path, timeout=4) as response:
|
||||||
|
result = json.load(response)
|
||||||
|
assert result == {"url": expected, "instance": instance}, result
|
||||||
|
eventually(check_http)
|
||||||
|
|
||||||
|
for path in ["/api/v1/ws?probe=ws", "/ws?probe=ws"]:
|
||||||
|
def check_ws():
|
||||||
|
with socket.create_connection(("127.0.0.1", port), timeout=4) as sock:
|
||||||
|
sock.sendall((f"GET {path} HTTP/1.1\r\nHost: localhost\r\n"
|
||||||
|
"Upgrade: websocket\r\nConnection: Upgrade\r\n"
|
||||||
|
"Sec-WebSocket-Version: 13\r\n"
|
||||||
|
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n").encode())
|
||||||
|
response = b""
|
||||||
|
while b"\r\n\r\n" not in response:
|
||||||
|
part = sock.recv(4096)
|
||||||
|
assert part, response
|
||||||
|
response += part
|
||||||
|
assert b"101 Switching Protocols" in response, response
|
||||||
|
assert b"X-Upstream-Url: /?probe=ws" in response, response
|
||||||
|
assert ("X-Instance: " + instance).encode() in response, response
|
||||||
|
eventually(check_ws)
|
||||||
|
assert podman("inspect", frontend, "--format", "{{.State.StartedAt}}") == started
|
||||||
|
print(f"PASS: {instance} backend at {subnet[offset]}: HTTP paths, query strings, both WebSocket routes; frontend never restarted", flush=True)
|
||||||
|
|
||||||
|
before = podman("exec", frontend, "cat", "/etc/nginx/conf.d/nginx-mempool.conf")
|
||||||
|
podman("exec", frontend, "/patch/repair-nginx.sh")
|
||||||
|
assert podman("exec", frontend, "cat", "/etc/nginx/conf.d/nginx-mempool.conf") == before
|
||||||
|
podman("exec", frontend, "nginx", "-t")
|
||||||
|
print("PASS: repeated repair is idempotent and nginx configuration is valid", flush=True)
|
||||||
|
podman("restart", frontend)
|
||||||
|
eventually(static_ready)
|
||||||
|
|
||||||
|
def after_restart():
|
||||||
|
with urllib.request.urlopen(url + "/api/blocks/tip/height?restart=1", timeout=4) as response:
|
||||||
|
assert json.load(response) == {
|
||||||
|
"url": "/api/v1/blocks/tip/height?restart=1", "instance": "replacement"
|
||||||
|
}
|
||||||
|
eventually(after_restart)
|
||||||
|
eventually(check_ws)
|
||||||
|
print("PASS: frontend restart preserves DNS recovery and HTTP/WebSocket routing", flush=True)
|
||||||
|
finally:
|
||||||
|
podman("rm", "-f", frontend, backend, check=False)
|
||||||
|
podman("network", "rm", network, check=False)
|
||||||
@@ -104,7 +104,7 @@ if "archipelago-source" not in apps:
|
|||||||
source_promotions = [item for item in promotions if item.get("id") == "archipelago-source"]
|
source_promotions = [item for item in promotions if item.get("id") == "archipelago-source"]
|
||||||
if not source_promotions:
|
if not source_promotions:
|
||||||
raise SystemExit("registry candidate omits the Archipelago source promotion")
|
raise SystemExit("registry candidate omits the Archipelago source promotion")
|
||||||
expected_path = "/npub1w3sqdkrhn0gyuvsex32effzgnfpyde6qrrc4u467flg5e9txh4wsfn5vjg/archy"
|
expected_path = "/npub1w3sqdkrhn0gyuvsex32effzgnfpyde6qrrc4u467flg5e9txh4wsfn5vjg/relay.ngit.dev/archy"
|
||||||
if source_promotions[0].get("path") != expected_path:
|
if source_promotions[0].get("path") != expected_path:
|
||||||
raise SystemExit("source promotion does not open the canonical Archipelago repository")
|
raise SystemExit("source promotion does not open the canonical Archipelago repository")
|
||||||
print("registry candidate includes GitWorkshop and its source promotion")
|
print("registry candidate includes GitWorkshop and its source promotion")
|
||||||
|
|||||||
Reference in New Issue
Block a user