Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82001403b4 | ||
|
|
81ede159ac | ||
|
|
8e988be853 | ||
|
|
210f7f1b12 | ||
|
|
ed49cc974f | ||
|
|
4849186ab9 | ||
|
|
3347b8b8b9 | ||
|
|
e382e679ae | ||
|
|
77d0768a21 | ||
|
|
f133d5555a | ||
|
|
cbd5314dd9 | ||
|
|
9fb2e1ed9e | ||
|
|
7125dea05d | ||
|
|
bcdf2c75be | ||
|
|
e77f60085d | ||
|
|
6c31eb9d4a | ||
|
|
63e6c64c63 | ||
|
|
4d8bb1fd44 | ||
|
|
2b4b60013c | ||
|
|
f0ef410948 | ||
|
|
19467e9b7c | ||
|
|
628ed252b4 |
@@ -1,5 +1,19 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.8.9-alpha (2026-09-01)
|
||||||
|
|
||||||
|
- **Lightning sends work again after the LND 0.21.2 update.** LND 0.21 removed the old synchronous payment route the node's backend paid through (`/v1/channels/transactions`) — every Lightning send answered the literal "Not Found" and the wallet showed "Payment failed: Not Found". The backend now pays through the supported Router.SendPaymentV2 route, keeps the same settle-then-report behaviour (a slow multi-hop payment is still tracked to completion, never falsely declared failed), and translates LND's failure reasons into plain advice. A new gate test speaks the payment route directly against the running LND, so an image/backend skew like this can never ship silently again.
|
||||||
|
|
||||||
|
- **The node no longer pins HSTS — HTTP access is a supported mode, and it stays working.** The HTTPS listener used to send `Strict-Transport-Security: max-age=31536000; includeSubDomains`; browsers that visited HTTPS once cached that and then silently upgraded the still-open HTTP dashboard's calls to HTTPS, which is a scheme change — cross-origin — so every request died as "CORS blocked / Failed to fetch" while the node was perfectly healthy. The HTTPS listener now actively clears the cached policy (`max-age=0`) and port 80 sends no HSTS at all, which is deliberate: the node's certificate is optional and self-signed, and devices that haven't installed the CA must keep plain-HTTP access (that's what Settings → Node certificate is for). If your browser already cached the old policy, visiting the dashboard over HTTPS once after this update clears it; a gate test now refuses any config that reintroduces the pin.
|
||||||
|
|
||||||
|
- **App frames open over HTTPS again — including the ones that "did not connect."** The launcher asked the signed catalog for each app's port policy under the name you click ("Mempool Web", "Bitcoin Knots"), but the catalog declares those ports under the manifest that owns them (the Mempool web container, Bitcoin UI). The lookup missed, the launcher handed the iframe an `http://` address, and the browser blocked it as mixed content — the app tile went blank or spun forever. Port resolution now follows launch aliases (mempool-web, bitcoin-knots/bitcoin-core, lnd, electrs and friends), falls back to a port-wide catalog scan when the id is unknown, and the catalog is warmed as soon as the dashboard loads rather than only in the App Store, so the very first app you open already knows which ports serve TLS.
|
||||||
|
|
||||||
|
- **Signing in to IndeeHub with Nostr works over HTTPS.** The NIP-07 bridge compared the app frame's origin for exact equality with the recorded `http://` app URL — a frame the browser upgraded to HTTPS (or any scheme change) was silently ignored, and replies addressed to the stale origin were refused outright, so Nostr sign-in quietly did nothing. The bridge now matches host and port (scheme intentionally ignored) and always replies to the frame's real origin.
|
||||||
|
|
||||||
|
- **Nginx Proxy Manager starts again.** Converting it to a platform manifest dropped two things its image needs: the `/etc/letsencrypt` mount its boot script hard-requires, and the `NET_BIND_SERVICE` capability its internal nginx needs to bind ports 80/443/81 under the orchestrator's `--cap-drop=ALL`. The result was an endless start/die loop (a node watched it restart 3,176 times). Both are declared in its manifest now, its certs live on unchanged under the same persistent app directory, and the signed catalog carries the fix so installed nodes heal on the next update.
|
||||||
|
|
||||||
|
- **Portainer's first-run token is in the app page, not buried in "server logs."** New Portainer versions mint a one-time setup token on a fresh install and print it only to the container logs — on an appliance that meant telling the user to go read a server log to get into their own app. The token now appears in the same launch interstitial as app login credentials (with a copy button), only while first-run setup is actually pending; once the admin account exists the card disappears on its own.
|
||||||
|
|
||||||
## v1.8.8-alpha (2026-09-01)
|
## v1.8.8-alpha (2026-09-01)
|
||||||
|
|
||||||
- **SSH over the mesh is now a first-class setting.** Settings gains an "SSH over mesh" card: off by default, and when you allow it the node's mesh firewall opens port 22 — either to every mesh peer (behind an explicit "I understand" confirmation, because that's a real exposure) or only to the mesh addresses you list. The rule is owned by the node (the `90-ssh.nft` drop-in), so it survives upgrades and daemon reinstalls, and the card tells you up front whether sshd is running, whether it listens on IPv6 (the mesh is IPv6-only — this is what a broken attempt looks like before it happens), and whether password login is on (keys-only is the recommended pairing). From Termux on your phone, `fipssh <user>@<node-npub>` connects once the toggle is on — the npub is the durable address, and the command is shown with a copy button on the card.
|
- **SSH over the mesh is now a first-class setting.** Settings gains an "SSH over mesh" card: off by default, and when you allow it the node's mesh firewall opens port 22 — either to every mesh peer (behind an explicit "I understand" confirmation, because that's a real exposure) or only to the mesh addresses you list. The rule is owned by the node (the `90-ssh.nft` drop-in), so it survives upgrades and daemon reinstalls, and the card tells you up front whether sshd is running, whether it listens on IPv6 (the mesh is IPv6-only — this is what a broken attempt looks like before it happens), and whether password login is on (keys-only is the recommended pairing). From Termux on your phone, `fipssh <user>@<node-npub>` connects once the toggle is on — the npub is the durable address, and the command is shown with a copy button on the card.
|
||||||
|
|||||||
Submodule aiui/.claude/worktrees/agitated-hofstadter deleted from 10e12a329f
Submodule aiui/.claude/worktrees/funny-hofstadter deleted from 1c5185a15c
Submodule aiui/.claude/worktrees/happy-colden deleted from 666e1232f4
Submodule aiui/.claude/worktrees/hardcore-beaver deleted from a817fa199f
Submodule aiui/.claude/worktrees/heuristic-raman deleted from e8e002debc
Submodule aiui/.claude/worktrees/priceless-colden deleted from aaaef7d710
+20
-20
@@ -13,20 +13,20 @@
|
|||||||
{
|
{
|
||||||
"id": "adguardhome",
|
"id": "adguardhome",
|
||||||
"title": "AdGuard Home",
|
"title": "AdGuard Home",
|
||||||
"version": "v0.107.55",
|
"version": "v0.107.79",
|
||||||
"description": "Network-wide ad and tracker blocking: a DNS server that filters every device on your LAN, with a web console for rules and client management.",
|
"description": "Network-wide ad and tracker blocking: a DNS server that filters every device on your LAN, with a web console for rules and client management.",
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"author": "AdGuard",
|
"author": "AdGuard",
|
||||||
"category": "networking",
|
"category": "networking",
|
||||||
"tier": "optional",
|
"tier": "optional",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.55",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.79",
|
||||||
"repoUrl": "https://github.com/AdguardTeam/AdGuardHome"
|
"repoUrl": "https://github.com/AdguardTeam/AdGuardHome"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "alby-hub",
|
"id": "alby-hub",
|
||||||
"title": "Alby Hub",
|
"title": "Alby Hub",
|
||||||
"version": "1.23.0",
|
"version": "1.23.0",
|
||||||
"description": "Self-custodial Lightning wallet hub. Runs its own Lightning node on your Archipelago and connects your apps to it over Nostr Wallet Connect \u2014 one hub, every app pays through it.",
|
"description": "Self-custodial Lightning wallet hub. Runs its own Lightning node on your Archipelago and connects your apps to it over Nostr Wallet Connect — one hub, every app pays through it.",
|
||||||
"icon": "/assets/img/app-icons/alby-hub.svg",
|
"icon": "/assets/img/app-icons/alby-hub.svg",
|
||||||
"author": "Alby",
|
"author": "Alby",
|
||||||
"category": "money",
|
"category": "money",
|
||||||
@@ -191,13 +191,13 @@
|
|||||||
{
|
{
|
||||||
"id": "filebrowser",
|
"id": "filebrowser",
|
||||||
"title": "File Browser",
|
"title": "File Browser",
|
||||||
"version": "2.27.0",
|
"version": "2.63.23",
|
||||||
"description": "Baseline Archipelago file manager service.",
|
"description": "Baseline Archipelago file manager service.",
|
||||||
"icon": "/assets/img/app-icons/file-browser.webp",
|
"icon": "/assets/img/app-icons/file-browser.webp",
|
||||||
"author": "File Browser",
|
"author": "File Browser",
|
||||||
"category": "data",
|
"category": "data",
|
||||||
"tier": "core",
|
"tier": "core",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/filebrowser:v2.27.0",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/filebrowser:v2.63.23",
|
||||||
"repoUrl": "https://github.com/filebrowser/filebrowser",
|
"repoUrl": "https://github.com/filebrowser/filebrowser",
|
||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"ports": [
|
"ports": [
|
||||||
@@ -218,12 +218,12 @@
|
|||||||
{
|
{
|
||||||
"id": "gitea",
|
"id": "gitea",
|
||||||
"title": "Gitea",
|
"title": "Gitea",
|
||||||
"version": "1.23",
|
"version": "1.27.3",
|
||||||
"description": "Self-hosted Git service with built-in container registry, CI/CD, and package hosting.",
|
"description": "Self-hosted Git service with built-in container registry, CI/CD, and package hosting.",
|
||||||
"icon": "/assets/img/app-icons/gitea.svg",
|
"icon": "/assets/img/app-icons/gitea.svg",
|
||||||
"author": "Gitea",
|
"author": "Gitea",
|
||||||
"category": "development",
|
"category": "development",
|
||||||
"dockerImage": "docker.io/gitea/gitea:1.23",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/gitea:1.27.3",
|
||||||
"repoUrl": "https://gitea.com",
|
"repoUrl": "https://gitea.com",
|
||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"ports": [
|
"ports": [
|
||||||
@@ -274,12 +274,12 @@
|
|||||||
{
|
{
|
||||||
"id": "homeassistant",
|
"id": "homeassistant",
|
||||||
"title": "Home Assistant",
|
"title": "Home Assistant",
|
||||||
"version": "2026.7.3",
|
"version": "2026.8.3",
|
||||||
"description": "Open source home automation platform. Control and monitor your smart home devices.",
|
"description": "Open source home automation platform. Control and monitor your smart home devices.",
|
||||||
"icon": "/assets/img/app-icons/homeassistant.png",
|
"icon": "/assets/img/app-icons/homeassistant.png",
|
||||||
"author": "Home Assistant",
|
"author": "Home Assistant",
|
||||||
"category": "home",
|
"category": "home",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/home-assistant:2026.8.2",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/home-assistant:2026.8.3",
|
||||||
"repoUrl": "https://github.com/home-assistant/core",
|
"repoUrl": "https://github.com/home-assistant/core",
|
||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"ports": [
|
"ports": [
|
||||||
@@ -338,13 +338,13 @@
|
|||||||
{
|
{
|
||||||
"id": "lnd",
|
"id": "lnd",
|
||||||
"title": "LND",
|
"title": "LND",
|
||||||
"version": "0.18.4",
|
"version": "0.21.2",
|
||||||
"description": "Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.",
|
"description": "Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.",
|
||||||
"icon": "/assets/img/app-icons/lnd.png",
|
"icon": "/assets/img/app-icons/lnd.png",
|
||||||
"author": "Lightning Labs",
|
"author": "Lightning Labs",
|
||||||
"category": "money",
|
"category": "money",
|
||||||
"tier": "core",
|
"tier": "core",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/lnd:v0.21.2-beta",
|
||||||
"repoUrl": "https://github.com/lightningnetwork/lnd",
|
"repoUrl": "https://github.com/lightningnetwork/lnd",
|
||||||
"requires": [
|
"requires": [
|
||||||
"bitcoin-knots"
|
"bitcoin-knots"
|
||||||
@@ -370,7 +370,7 @@
|
|||||||
"id": "netbird",
|
"id": "netbird",
|
||||||
"title": "NetBird",
|
"title": "NetBird",
|
||||||
"version": "2.38.0",
|
"version": "2.38.0",
|
||||||
"description": "Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN. The user-facing entry point \u2014 a TLS proxy in front of the dashboard + server.",
|
"description": "Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN. The user-facing entry point — a TLS proxy in front of the dashboard + server.",
|
||||||
"icon": "/assets/img/app-icons/netbird.svg",
|
"icon": "/assets/img/app-icons/netbird.svg",
|
||||||
"author": "NetBird",
|
"author": "NetBird",
|
||||||
"category": "networking",
|
"category": "networking",
|
||||||
@@ -412,7 +412,7 @@
|
|||||||
"id": "nginx-proxy-manager",
|
"id": "nginx-proxy-manager",
|
||||||
"title": "Nginx Proxy Manager",
|
"title": "Nginx Proxy Manager",
|
||||||
"version": "2.12.1",
|
"version": "2.12.1",
|
||||||
"description": "Reverse proxy with SSL. Beautiful web interface for managing proxies. On a node, this manages its admin UI and upstream configuration \u2014 the proxy's own :80/:443 listeners are not published (the node's web server owns those ports).",
|
"description": "Reverse proxy with SSL. Beautiful web interface for managing proxies. On a node, this manages its admin UI and upstream configuration — the proxy's own :80/:443 listeners are not published (the node's web server owns those ports).",
|
||||||
"icon": "/assets/img/app-icons/nginx.svg",
|
"icon": "/assets/img/app-icons/nginx.svg",
|
||||||
"author": "Nginx Proxy Manager",
|
"author": "Nginx Proxy Manager",
|
||||||
"category": "networking",
|
"category": "networking",
|
||||||
@@ -448,7 +448,7 @@
|
|||||||
"id": "ollama",
|
"id": "ollama",
|
||||||
"title": "Ollama",
|
"title": "Ollama",
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
"description": "Run large language models locally. Download and run AI models like Llama, Mistral on your own hardware \u2014 served on the node's loopback for the AI assistant (Settings \u2192 Claude Auth \u2192 model backend), never exposed to the network.",
|
"description": "Run large language models locally. Download and run AI models like Llama, Mistral on your own hardware — served on the node's loopback for the AI assistant (Settings → Claude Auth → model backend), never exposed to the network.",
|
||||||
"icon": "/assets/img/app-icons/ollama.png",
|
"icon": "/assets/img/app-icons/ollama.png",
|
||||||
"author": "Ollama",
|
"author": "Ollama",
|
||||||
"category": "community",
|
"category": "community",
|
||||||
@@ -460,7 +460,7 @@
|
|||||||
"id": "phoenixd",
|
"id": "phoenixd",
|
||||||
"title": "phoenixd",
|
"title": "phoenixd",
|
||||||
"version": "0.9.0",
|
"version": "0.9.0",
|
||||||
"description": "Headless Lightning daemon by ACINQ (the Phoenix wallet team). No screen of its own \u2014 it exposes a small local API that other apps and tools use to send and receive Lightning payments. Channel liquidity is managed automatically for a fee.",
|
"description": "Headless Lightning daemon by ACINQ (the Phoenix wallet team). No screen of its own — it exposes a small local API that other apps and tools use to send and receive Lightning payments. Channel liquidity is managed automatically for a fee.",
|
||||||
"icon": "/assets/img/app-icons/phoenixd.svg",
|
"icon": "/assets/img/app-icons/phoenixd.svg",
|
||||||
"author": "ACINQ",
|
"author": "ACINQ",
|
||||||
"category": "money",
|
"category": "money",
|
||||||
@@ -495,7 +495,7 @@
|
|||||||
"id": "pine",
|
"id": "pine",
|
||||||
"title": "Pine",
|
"title": "Pine",
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"description": "A private voice assistant for your home. Pine runs speech-to-text (Whisper), text-to-speech (Piper) and wake-word detection (openWakeWord) on your own node and pairs with a PineVoice satellite speaker, so Home Assistant Assist works locally with nothing sent to the cloud. Ask it about your node \u2014 block height, sync, peers, Lightning balance \u2014 and, when a Claude API key is set, anything else.",
|
"description": "A private voice assistant for your home. Pine runs speech-to-text (Whisper), text-to-speech (Piper) and wake-word detection (openWakeWord) on your own node and pairs with a PineVoice satellite speaker, so Home Assistant Assist works locally with nothing sent to the cloud. Ask it about your node — block height, sync, peers, Lightning balance — and, when a Claude API key is set, anything else.",
|
||||||
"icon": "/assets/img/app-icons/pine.svg",
|
"icon": "/assets/img/app-icons/pine.svg",
|
||||||
"author": "Archipelago",
|
"author": "Archipelago",
|
||||||
"category": "home",
|
"category": "home",
|
||||||
@@ -505,13 +505,13 @@
|
|||||||
{
|
{
|
||||||
"id": "portainer",
|
"id": "portainer",
|
||||||
"title": "Portainer",
|
"title": "Portainer",
|
||||||
"version": "2.19.4",
|
"version": "2.45.0",
|
||||||
"description": "Container management web UI for the local Podman socket.",
|
"description": "Container management web UI for the local Podman socket.",
|
||||||
"icon": "/assets/img/app-icons/portainer.webp",
|
"icon": "/assets/img/app-icons/portainer.webp",
|
||||||
"author": "Portainer",
|
"author": "Portainer",
|
||||||
"category": "development",
|
"category": "development",
|
||||||
"tier": "optional",
|
"tier": "optional",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/portainer:2.39.6",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/portainer:2.45.0",
|
||||||
"repoUrl": "https://github.com/portainer/portainer",
|
"repoUrl": "https://github.com/portainer/portainer",
|
||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"ports": [
|
"ports": [
|
||||||
@@ -603,13 +603,13 @@
|
|||||||
{
|
{
|
||||||
"id": "vaultwarden",
|
"id": "vaultwarden",
|
||||||
"title": "Vaultwarden",
|
"title": "Vaultwarden",
|
||||||
"version": "1.30.0",
|
"version": "1.37.2",
|
||||||
"description": "Self-hosted password vault with zero-knowledge encryption.",
|
"description": "Self-hosted password vault with zero-knowledge encryption.",
|
||||||
"icon": "/assets/img/app-icons/vaultwarden.webp",
|
"icon": "/assets/img/app-icons/vaultwarden.webp",
|
||||||
"author": "Vaultwarden",
|
"author": "Vaultwarden",
|
||||||
"category": "data",
|
"category": "data",
|
||||||
"tier": "recommended",
|
"tier": "recommended",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/vaultwarden:1.37.1-alpine",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/vaultwarden:1.37.2-alpine",
|
||||||
"repoUrl": "https://github.com/dani-garcia/vaultwarden",
|
"repoUrl": "https://github.com/dani-garcia/vaultwarden",
|
||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"ports": [
|
"ports": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
app:
|
app:
|
||||||
id: adguardhome
|
id: adguardhome
|
||||||
name: AdGuard Home
|
name: AdGuard Home
|
||||||
version: v0.107.55
|
version: v0.107.79
|
||||||
upstream:
|
upstream:
|
||||||
kind: github
|
kind: github
|
||||||
repo: AdguardTeam/AdGuardHome
|
repo: AdguardTeam/AdGuardHome
|
||||||
@@ -10,7 +10,7 @@ app:
|
|||||||
device on your LAN, with a web console for rules and client management.
|
device on your LAN, with a web console for rules and client management.
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.55
|
image: source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.79
|
||||||
pull_policy: if-not-present
|
pull_policy: if-not-present
|
||||||
network: pasta
|
network: pasta
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,19 @@ app:
|
|||||||
# uses for its own RPC port (-rpcbind=0.0.0.0:8332 internally, gate
|
# uses for its own RPC port (-rpcbind=0.0.0.0:8332 internally, gate
|
||||||
# restricts it externally) — not a new risk, the same one already
|
# restricts it externally) — not a new risk, the same one already
|
||||||
# reviewed and accepted for Bitcoin's RPC.
|
# reviewed and accepted for Bitcoin's RPC.
|
||||||
|
# - tracing.stdout.level / tracing.file.{level,max_log_files}: an
|
||||||
|
# operator reading Cuprated.toml on disk should be able to see and
|
||||||
|
# tune the log level directly instead of the file silently omitting
|
||||||
|
# the whole [tracing] table (verified live on amishparadise
|
||||||
|
# 2026-09-01: the deployed file had no [tracing] section at all, and
|
||||||
|
# the level was only discoverable by running `cuprated
|
||||||
|
# --generate-config` and diffing). file.level is set to "info", NOT
|
||||||
|
# cuprated's own raw default of "debug" — matches the reference dev
|
||||||
|
# config this app was built and tested against
|
||||||
|
# (ssmithx@archy-dev-pa:/home/ssmithx/cuprate/Cuprated.toml,
|
||||||
|
# verified 2026-09-01), which deliberately runs file logging quieter
|
||||||
|
# than the binary default. max_log_files similarly follows that
|
||||||
|
# reference (14, not the binary default of 7).
|
||||||
files:
|
files:
|
||||||
- path: /var/lib/archipelago/cuprate/Cuprated.toml
|
- path: /var/lib/archipelago/cuprate/Cuprated.toml
|
||||||
content: |
|
content: |
|
||||||
@@ -133,6 +146,13 @@ app:
|
|||||||
|
|
||||||
[rpc.restricted]
|
[rpc.restricted]
|
||||||
enable = true
|
enable = true
|
||||||
|
|
||||||
|
[tracing.stdout]
|
||||||
|
level = "info"
|
||||||
|
|
||||||
|
[tracing.file]
|
||||||
|
level = "info"
|
||||||
|
max_log_files = 14
|
||||||
overwrite: false
|
overwrite: false
|
||||||
|
|
||||||
health_check:
|
health_check:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
app:
|
app:
|
||||||
id: filebrowser
|
id: filebrowser
|
||||||
name: File Browser
|
name: File Browser
|
||||||
version: 2.27.0
|
version: 2.63.23
|
||||||
# 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: Baseline Archipelago file manager service.
|
description: Baseline Archipelago file manager service.
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: source.archipelago-foundation.org/lfg2025/filebrowser:v2.27.0
|
image: source.archipelago-foundation.org/lfg2025/filebrowser:v2.63.23
|
||||||
pull_policy: if-not-present
|
pull_policy: if-not-present
|
||||||
network: archy-net
|
network: archy-net
|
||||||
custom_args: ["--config", "/data/.filebrowser.json"]
|
custom_args: ["--config", "/data/.filebrowser.json"]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
app:
|
app:
|
||||||
id: gitea
|
id: gitea
|
||||||
name: Gitea
|
name: Gitea
|
||||||
version: "1.23"
|
version: "1.27.3"
|
||||||
# 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:
|
|||||||
category: development
|
category: development
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: docker.io/gitea/gitea:1.23
|
image: source.archipelago-foundation.org/lfg2025/gitea:1.27.3
|
||||||
pull_policy: if-not-present
|
pull_policy: if-not-present
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
app:
|
app:
|
||||||
id: homeassistant
|
id: homeassistant
|
||||||
name: Home Assistant
|
name: Home Assistant
|
||||||
version: 2026.7.3
|
version: 2026.8.3
|
||||||
# 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: Open source home automation platform. Control and monitor your smart home devices.
|
description: Open source home automation platform. Control and monitor your smart home devices.
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: source.archipelago-foundation.org/lfg2025/home-assistant:2026.8.2
|
image: source.archipelago-foundation.org/lfg2025/home-assistant:2026.8.3
|
||||||
pull_policy: if-not-present
|
pull_policy: if-not-present
|
||||||
network: pasta
|
network: pasta
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
app:
|
app:
|
||||||
id: lnd
|
id: lnd
|
||||||
name: LND
|
name: LND
|
||||||
version: 0.18.4
|
version: 0.21.2
|
||||||
# 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: Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.
|
description: Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta
|
image: source.archipelago-foundation.org/lfg2025/lnd:v0.21.2-beta
|
||||||
pull_policy: if-not-present
|
pull_policy: if-not-present
|
||||||
network: archy-net
|
network: archy-net
|
||||||
# BITCOIND_HOST must follow the node's actual Bitcoin container — Knots or
|
# BITCOIND_HOST must follow the node's actual Bitcoin container — Knots or
|
||||||
|
|||||||
@@ -24,7 +24,14 @@ app:
|
|||||||
disk_limit: 1Gi
|
disk_limit: 1Gi
|
||||||
|
|
||||||
security:
|
security:
|
||||||
capabilities: [CHOWN, SETUID, SETGID, DAC_OVERRIDE]
|
# NET_BIND_SERVICE is load-bearing, not decoration: NPM's internal nginx
|
||||||
|
# listens on 80, 443 AND 81, and the orchestrator runs --cap-drop=ALL —
|
||||||
|
# without this cap every start dies with "bind() to 0.0.0.0:80 failed
|
||||||
|
# (13: Permission denied)" and s6 restart-loops forever (shorty-s,
|
||||||
|
# 2026-09-01, restart counter 3176 within hours of the manifest
|
||||||
|
# conversion). The legacy podman-run path defaulted to the full cap set,
|
||||||
|
# which is why it never showed there.
|
||||||
|
capabilities: [CHOWN, SETUID, SETGID, DAC_OVERRIDE, NET_BIND_SERVICE]
|
||||||
readonly_root: false
|
readonly_root: false
|
||||||
no_new_privileges: true
|
no_new_privileges: true
|
||||||
network_policy: isolated
|
network_policy: isolated
|
||||||
@@ -47,6 +54,13 @@ app:
|
|||||||
source: /var/lib/archipelago/nginx-proxy-manager
|
source: /var/lib/archipelago/nginx-proxy-manager
|
||||||
target: /data
|
target: /data
|
||||||
options: [rw]
|
options: [rw]
|
||||||
|
# Current NPM images refuse to start unless /etc/letsencrypt is a mount in
|
||||||
|
# its own right. Keeping the files below the same persistent app directory
|
||||||
|
# preserves existing certificates while satisfying that startup contract.
|
||||||
|
- type: bind
|
||||||
|
source: /var/lib/archipelago/nginx-proxy-manager/letsencrypt
|
||||||
|
target: /etc/letsencrypt
|
||||||
|
options: [rw]
|
||||||
|
|
||||||
environment: []
|
environment: []
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ app:
|
|||||||
# (--beam-size 1). Bumped past the image version so catalog-driven nodes
|
# (--beam-size 1). Bumped past the image version so catalog-driven nodes
|
||||||
# pick up the args change; the pre-release form "3.4.1-1" would compare
|
# pick up the args change; the pre-release form "3.4.1-1" would compare
|
||||||
# LOWER than 3.4.1 under semver and never roll out.
|
# LOWER than 3.4.1 under semver and never roll out.
|
||||||
version: "3.4.2"
|
version: "3.6.0"
|
||||||
# Tracks the rhasspy/wyoming-whisper image we pin (Docker Hub — the
|
# Tracks the rhasspy/wyoming-whisper image we pin (Docker Hub — the
|
||||||
# project's GitHub tags are not the image tags). NOTE: this manifest
|
# project's GitHub tags are not the image tags). NOTE: this manifest
|
||||||
# deliberately ships an args-tuned revision AHEAD of the image tag (see
|
# deliberately ships an args-tuned revision AHEAD of the image tag (see
|
||||||
@@ -24,7 +24,7 @@ app:
|
|||||||
container_name: pine-whisper
|
container_name: pine-whisper
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: docker.io/rhasspy/wyoming-whisper:3.4.1
|
image: docker.io/rhasspy/wyoming-whisper:3.6.0
|
||||||
pull_policy: if-not-present
|
pull_policy: if-not-present
|
||||||
network: archy-net
|
network: archy-net
|
||||||
network_aliases: [pine-whisper]
|
network_aliases: [pine-whisper]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
app:
|
app:
|
||||||
id: portainer
|
id: portainer
|
||||||
name: Portainer
|
name: Portainer
|
||||||
version: 2.19.4
|
version: 2.45.0
|
||||||
# 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:
|
|||||||
category: development
|
category: development
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: source.archipelago-foundation.org/lfg2025/portainer:2.39.6
|
image: source.archipelago-foundation.org/lfg2025/portainer:2.45.0
|
||||||
pull_policy: if-not-present
|
pull_policy: if-not-present
|
||||||
data_uid: "1000:1000"
|
data_uid: "1000:1000"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
app:
|
app:
|
||||||
id: vaultwarden
|
id: vaultwarden
|
||||||
name: Vaultwarden
|
name: Vaultwarden
|
||||||
version: 1.30.0
|
version: 1.37.2
|
||||||
# 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: Self-hosted password vault with zero-knowledge encryption.
|
description: Self-hosted password vault with zero-knowledge encryption.
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: source.archipelago-foundation.org/lfg2025/vaultwarden:1.37.1-alpine
|
image: source.archipelago-foundation.org/lfg2025/vaultwarden:1.37.2-alpine
|
||||||
pull_policy: if-not-present
|
pull_policy: if-not-present
|
||||||
network: pasta
|
network: pasta
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -104,7 +104,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.8.8-alpha"
|
version = "1.8.9-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"archipelago-container",
|
"archipelago-container",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.8.8-alpha"
|
version = "1.8.9-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"
|
||||||
|
|||||||
@@ -4,6 +4,44 @@ use tracing::info;
|
|||||||
|
|
||||||
use super::LND_REST_BASE_URL;
|
use super::LND_REST_BASE_URL;
|
||||||
|
|
||||||
|
fn router_error_message(body: &serde_json::Value) -> Option<&str> {
|
||||||
|
body.get("error")
|
||||||
|
.and_then(|e| e.get("message"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.or_else(|| body.get("message").and_then(|v| v.as_str()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn payment_error(message: &str) -> anyhow::Error {
|
||||||
|
if message.to_ascii_lowercase().contains("invoice expired") {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Payment failed: this invoice has expired ({}). Ask the recipient for a fresh invoice and try again.",
|
||||||
|
message.trim_start_matches("invoice expired. ")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
anyhow::anyhow!("Payment failed: {message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn payment_failure_reason(reason: &str) -> &'static str {
|
||||||
|
match reason {
|
||||||
|
"FAILURE_REASON_NO_ROUTE" => "No route to the recipient",
|
||||||
|
"FAILURE_REASON_INSUFFICIENT_BALANCE" => "Insufficient channel balance",
|
||||||
|
"FAILURE_REASON_TIMEOUT" => "Payment timed out in the network",
|
||||||
|
"FAILURE_REASON_INCORRECT_PAYMENT_DETAILS" => {
|
||||||
|
"Recipient rejected the payment (wrong details or expired invoice)"
|
||||||
|
}
|
||||||
|
_ => "Payment failed",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_i64(value: &serde_json::Value, key: &str) -> Option<i64> {
|
||||||
|
value.get(key).and_then(|v| {
|
||||||
|
v.as_str()
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.or_else(|| v.as_i64())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
impl RpcHandler {
|
impl RpcHandler {
|
||||||
/// Pay a Lightning invoice.
|
/// Pay a Lightning invoice.
|
||||||
pub(in crate::api::rpc) async fn handle_lnd_payinvoice(
|
pub(in crate::api::rpc) async fn handle_lnd_payinvoice(
|
||||||
@@ -65,23 +103,22 @@ impl RpcHandler {
|
|||||||
|
|
||||||
let mut pay_body = serde_json::json!({
|
let mut pay_body = serde_json::json!({
|
||||||
"payment_request": payment_request,
|
"payment_request": payment_request,
|
||||||
|
// Suppress intermediate stream records: one terminal Payment is
|
||||||
|
// enough, and it makes grpc-gateway's response a single JSON value.
|
||||||
|
"no_inflight_updates": true,
|
||||||
|
"timeout_seconds": 120,
|
||||||
});
|
});
|
||||||
if let Some(amt) = amount_sats {
|
if let Some(amt) = amount_sats {
|
||||||
pay_body["amt"] = serde_json::json!(amt.to_string());
|
pay_body["amt"] = serde_json::json!(amt.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// `/v1/channels/transactions` is SYNCHRONOUS: it blocks until the
|
// LND 0.21 removed the deprecated Lightning.SendPaymentSync REST route
|
||||||
// payment settles or definitively fails, and multi-hop routing with
|
// (`/v1/channels/transactions`). Router.SendPaymentV2 is its supported
|
||||||
// retries routinely takes longer than the shared client's 15s budget.
|
// replacement. The old route now returns literal 404 "Not Found" on
|
||||||
// That 15s abort used to surface as "Payment failed" while LND kept
|
// every payment — the fleet failure seen immediately after the 0.21.2
|
||||||
// paying in the background — only LND may declare a payment failed,
|
// update. Keep the short browser-facing wait: after LND accepts a slow
|
||||||
// so a post-connect timeout is IN FLIGHT (status: pending), never
|
// payment we return pending and the UI follows it through
|
||||||
// failure. The window is deliberately SHORT: most payments settle in
|
// lnd.paymentstatus instead of declaring a transport timeout a failure.
|
||||||
// a couple of seconds and still get their answer in one round trip,
|
|
||||||
// while a slow multi-hop route flips the UI into its "settling…"
|
|
||||||
// polling state (lnd.paymentstatus every 3s) after ~8s instead of
|
|
||||||
// freezing the modal for two minutes with no feedback (a test node
|
|
||||||
// user report, 2026-07-29).
|
|
||||||
let pay_client = reqwest::Client::builder()
|
let pay_client = reqwest::Client::builder()
|
||||||
.no_proxy()
|
.no_proxy()
|
||||||
.connect_timeout(std::time::Duration::from_secs(10))
|
.connect_timeout(std::time::Duration::from_secs(10))
|
||||||
@@ -91,7 +128,7 @@ impl RpcHandler {
|
|||||||
.context("Failed to create HTTP client")?;
|
.context("Failed to create HTTP client")?;
|
||||||
|
|
||||||
let resp = match pay_client
|
let resp = match pay_client
|
||||||
.post(format!("{LND_REST_BASE_URL}/v1/channels/transactions"))
|
.post(format!("{LND_REST_BASE_URL}/v2/router/send"))
|
||||||
.header("Grpc-Metadata-macaroon", &macaroon_hex)
|
.header("Grpc-Metadata-macaroon", &macaroon_hex)
|
||||||
.json(&pay_body)
|
.json(&pay_body)
|
||||||
.send()
|
.send()
|
||||||
@@ -119,49 +156,42 @@ impl RpcHandler {
|
|||||||
let body: serde_json::Value = resp
|
let body: serde_json::Value = resp
|
||||||
.json()
|
.json()
|
||||||
.await
|
.await
|
||||||
.context("Failed to parse payment response")?;
|
.context("Failed to parse Router.SendPaymentV2 response")?;
|
||||||
|
|
||||||
|
// grpc-gateway wraps server-streaming records as {"result": ...} and
|
||||||
|
// transport/RPC failures as {"error": {"message": ...}}. Do not look
|
||||||
|
// only for the old endpoint's top-level `message`: that turns useful
|
||||||
|
// LND errors into "Unknown error".
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
let msg = body
|
let msg = router_error_message(&body).unwrap_or("Unknown error");
|
||||||
.get("message")
|
return Err(payment_error(msg));
|
||||||
.and_then(|v| v.as_str())
|
}
|
||||||
.unwrap_or("Unknown error");
|
let payment = body.get("result").unwrap_or(&body);
|
||||||
// Invoices are short-lived; retrying the same one can never
|
match payment.get("status").and_then(|v| v.as_str()).unwrap_or("") {
|
||||||
// succeed, so tell the user the way out instead of just the fact.
|
"SUCCEEDED" => {}
|
||||||
if msg.contains("invoice expired") {
|
"FAILED" => {
|
||||||
return Err(anyhow::anyhow!(
|
let reason = payment
|
||||||
"Payment failed: this invoice has expired ({}). Ask the recipient for a fresh invoice and try again.",
|
.get("failure_reason")
|
||||||
msg.trim_start_matches("invoice expired. ")
|
.and_then(|v| v.as_str())
|
||||||
));
|
.map(payment_failure_reason)
|
||||||
|
.unwrap_or("Payment failed");
|
||||||
|
return Err(anyhow::anyhow!("Payment failed: {reason}"));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Ok(serde_json::json!({
|
||||||
|
"status": "pending",
|
||||||
|
"payment_hash": decoded_hash,
|
||||||
|
"amount_sats": decoded_amt,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
return Err(anyhow::anyhow!("Payment failed: {}", msg));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let payment_error = body
|
let amount_sat = json_i64(payment, "value_sat").unwrap_or(decoded_amt);
|
||||||
.get("payment_error")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
if !payment_error.is_empty() {
|
|
||||||
return Err(anyhow::anyhow!("Payment failed: {}", payment_error));
|
|
||||||
}
|
|
||||||
|
|
||||||
let amount_sat = body
|
|
||||||
.get("payment_route")
|
|
||||||
.and_then(|r| r.get("total_amt"))
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.and_then(|s| s.parse::<i64>().ok())
|
|
||||||
.unwrap_or(decoded_amt);
|
|
||||||
|
|
||||||
let payment_hash = body
|
|
||||||
.get("payment_hash")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.unwrap_or(decoded_hash);
|
|
||||||
|
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"status": "succeeded",
|
"status": "succeeded",
|
||||||
"payment_hash": payment_hash,
|
// The decode endpoint returns the canonical hex hash used by our
|
||||||
|
// polling/list APIs. Router's bytes field is base64 in REST JSON.
|
||||||
|
"payment_hash": decoded_hash,
|
||||||
"amount_sats": amount_sat,
|
"amount_sats": amount_sat,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -482,3 +512,42 @@ impl RpcHandler {
|
|||||||
Ok(serde_json::json!({ "transactions": transactions }))
|
Ok(serde_json::json!({ "transactions": transactions }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unwraps_grpc_gateway_router_success() {
|
||||||
|
let body = serde_json::json!({
|
||||||
|
"result": { "status": "SUCCEEDED", "value_sat": "1000" }
|
||||||
|
});
|
||||||
|
let payment = body.get("result").unwrap_or(&body);
|
||||||
|
assert_eq!(
|
||||||
|
payment.get("status").and_then(|v| v.as_str()),
|
||||||
|
Some("SUCCEEDED")
|
||||||
|
);
|
||||||
|
assert_eq!(json_i64(payment, "value_sat"), Some(1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reads_nested_router_error() {
|
||||||
|
let body = serde_json::json!({
|
||||||
|
"error": { "code": 2, "message": "invoice expired. valid until yesterday" }
|
||||||
|
});
|
||||||
|
let msg = router_error_message(&body).unwrap();
|
||||||
|
assert!(payment_error(msg).to_string().contains("fresh invoice"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn router_failure_reasons_are_actionable() {
|
||||||
|
assert_eq!(
|
||||||
|
payment_failure_reason("FAILURE_REASON_NO_ROUTE"),
|
||||||
|
"No route to the recipient"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
payment_failure_reason("FAILURE_REASON_INSUFFICIENT_BALANCE"),
|
||||||
|
"Insufficient channel balance"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2040,10 +2040,59 @@ autopilot.active=false\n",
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Portainer ≥2.21 no longer lets whoever loads the page first claim the
|
||||||
|
// admin account: on a fresh install it mints a one-time setup token and
|
||||||
|
// prints it to the SERVER LOGS, expecting the operator to go digging.
|
||||||
|
// On an appliance that is hostile UX — "check the Portainer server
|
||||||
|
// logs" is exactly the dead end users cannot follow. The token is the
|
||||||
|
// only thing standing between the user and their own app, so surface
|
||||||
|
// it in the same launch interstitial as the login credentials: extract
|
||||||
|
// it from the container logs and hand it over with a copy button.
|
||||||
|
// Once setup completes Portainer invalidates the token, and a container
|
||||||
|
// recreate (any update) drops the log line entirely — so absence of the
|
||||||
|
// line naturally makes the card disappear and no stale token lingers.
|
||||||
|
if app_id == "portainer" {
|
||||||
|
if let Some(token) = portainer_setup_token(self).await {
|
||||||
|
return Ok(serde_json::json!({
|
||||||
|
"title": "Portainer first-run token",
|
||||||
|
"description": "New Portainer versions protect the first launch with a one-time setup token instead of letting anyone on the network claim the admin account. Paste this token into Portainer's setup screen to create your administrator login. It is only valid until setup finishes — if you already created your admin account, ignore this.",
|
||||||
|
"credentials": [
|
||||||
|
{ "label": "Setup token", "value": token, "sensitive": true }
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(serde_json::json!({ "credentials": [] }))
|
Ok(serde_json::json!({ "credentials": [] }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract Portainer's first-run `setup_token=…` from the live container's
|
||||||
|
/// recent logs. `None` when the line is absent (setup already done, or an
|
||||||
|
/// older Portainer without the token flow).
|
||||||
|
async fn portainer_setup_token(rpc: &RpcHandler) -> Option<String> {
|
||||||
|
let logs = rpc.get_container_logs_value("portainer", 300).await.ok()?;
|
||||||
|
let lines = logs.as_array()?;
|
||||||
|
let lines: Vec<&str> = lines.iter().filter_map(|l| l.as_str()).collect();
|
||||||
|
parse_setup_token(&lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pure log-line scan: the token is 64 hex chars after `setup_token=`.
|
||||||
|
/// Sear newest-first so the most recent mint wins.
|
||||||
|
fn parse_setup_token(lines: &[&str]) -> Option<String> {
|
||||||
|
for line in lines.iter().rev() {
|
||||||
|
let Some(idx) = line.find("setup_token=") else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let tail = &line[idx + "setup_token=".len()..];
|
||||||
|
let token: String = tail.chars().take_while(|c| c.is_ascii_hexdigit()).collect();
|
||||||
|
if token.len() == 64 {
|
||||||
|
return Some(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
async fn cleanup_stale_package_ports(package_id: &str) {
|
async fn cleanup_stale_package_ports(package_id: &str) {
|
||||||
match package_id {
|
match package_id {
|
||||||
"grafana" => cleanup_stale_pasta_port("3000").await,
|
"grafana" => cleanup_stale_pasta_port("3000").await,
|
||||||
@@ -2751,7 +2800,7 @@ fn is_unknown_app_id_error(err: &anyhow::Error) -> bool {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
orchestrator_install_app_id, should_try_orchestrator_install,
|
orchestrator_install_app_id, parse_setup_token, should_try_orchestrator_install,
|
||||||
uses_orchestrator_install_flow,
|
uses_orchestrator_install_flow,
|
||||||
};
|
};
|
||||||
use crate::api::rpc::package::runtime::orchestrator_uninstall_app_ids;
|
use crate::api::rpc::package::runtime::orchestrator_uninstall_app_ids;
|
||||||
@@ -2861,4 +2910,41 @@ mod tests {
|
|||||||
"Error: no container with name or ID \"bitcoin-knots\" found"
|
"Error: no container with name or ID \"bitcoin-knots\" found"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn portainer_setup_token_is_extracted_from_log_lines() {
|
||||||
|
// Shape captured live from portainer:2.45.0 on 2026-09-01 — the
|
||||||
|
// token line is plain text inside the bordered s6 log block.
|
||||||
|
let logs = [
|
||||||
|
"2026/09/01 12:38PM INF github.com/portainer/portainer/api/database/boltdb/db.go:163 > loading PortainerDB | filename=portainer.db",
|
||||||
|
"==========================",
|
||||||
|
"setup_token=27637c02b6323972dff76bcad4caa456f957b521d3cfe3bc7fb95d2488dfd23a",
|
||||||
|
"Paste it into the setup screen, or send it in the X-Setup-Token header.",
|
||||||
|
"==========================",
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
parse_setup_token(&logs).as_deref(),
|
||||||
|
Some("27637c02b6323972dff76bcad4caa456f957b521d3cfe3bc7fb95d2488dfd23a")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn portainer_setup_token_absent_when_setup_already_done() {
|
||||||
|
// An instance with an existing admin account never prints the line —
|
||||||
|
// the credentials card must not render a stale or empty token.
|
||||||
|
let logs = [
|
||||||
|
"2026/09/01 11:37AM INF api/datastore/migrator/migrate_ce.go:76 > db migrated to 2.45.0 |",
|
||||||
|
"2026/09/01 11:37:38 server: Listening on http://0.0.0.0:8000",
|
||||||
|
];
|
||||||
|
assert_eq!(parse_setup_token(&logs), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn portainer_setup_token_rejects_short_or_non_hex_values() {
|
||||||
|
assert_eq!(parse_setup_token(&["setup_token=abc123"]), None);
|
||||||
|
assert_eq!(
|
||||||
|
parse_setup_token(&["setup_token=".to_string().as_str()]),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
//! no listener, so allowing them is inert.
|
//! no listener, so allowing them is inert.
|
||||||
|
|
||||||
pub const APP_LAUNCH_PORTS: &[u16] = &[
|
pub const APP_LAUNCH_PORTS: &[u16] = &[
|
||||||
2283, 2342, 3000, 3001, 3002, 4080, 5180, 7778, 8080, 8081, 8082, 8083, 8084, 8085, 8087, 8088,
|
2283, 2342, 3000, 3001, 3002, 3030, 4080, 5180, 7778, 8080, 8081, 8082, 8083, 8084, 8085, 8087,
|
||||||
8089, 8090, 8096, 8123, 8175, 8176, 8187, 8240, 8334, 8336, 8888, 8999, 9000, 9100, 10380,
|
8090, 8096, 8123, 8175, 8176, 8187, 8240, 8334, 8336, 8888, 8999, 9000, 9100, 10380, 11434,
|
||||||
11434, 18081, 18083, 23000, 32838, 50002,
|
18081, 18083, 23000, 32838, 50002,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Incident + follow-up tracker — 2026-09-01 (post-HTTPS-work, post-LND-0.21.2 breakage)
|
||||||
|
|
||||||
|
Live incident spanning framework-pt and shorty-s after the HTTPS/launcher
|
||||||
|
work and the LND 0.18.4→0.21.2 pin bump. Root causes found on real nodes;
|
||||||
|
status updated as work lands. Each fix ships with a regression test so the
|
||||||
|
same class cannot silently return.
|
||||||
|
|
||||||
|
## A. Root causes (all verified live)
|
||||||
|
|
||||||
|
| # | Symptom | Root cause |
|
||||||
|
|---|---------|-----------|
|
||||||
|
| A1 | LND sends fail "Payment failed: Not Found" | LND 0.21 **removed** the deprecated `/v1/channels/transactions` REST route; backend still called it. Receive was fine; the "Failed to fetch" on framework-pt was A3 masking it. |
|
||||||
|
| A2 | Shorty NPM restart-loops (counter 3176) | Manifest conversion (fc68c5b6) dropped (a) the `/etc/letsencrypt` mount NPM's s6 boot demands, and (b) `NET_BIND_SERVICE` — its internal nginx binds 80/443/81 and the orchestrator runs `--cap-drop=ALL`. |
|
||||||
|
| A3 | framework-pt: every `/rpc/v1` fetch CORS-blocked, "Failed to fetch", dashboard "not responding", mempool/indeehub frames broken | nginx sent `Strict-Transport-Security: max-age=31536000; includeSubDomains` on **HTTPS**; browsers cached it, then silently upgraded the still-open **http** dashboard's fetches/frames to https → scheme change = cross-origin → CORS block. HTTP is a supported mode on purpose (self-signed cert, /ca.crt flow). |
|
||||||
|
| A4 | Mempool/IndeeHub/bitcoin-UI frames stay `http://` on HTTPS pages (mixed content, "does not connect") | `portAuth()` looked the launch port up under the launch alias (`mempool-web`, `lnd`, `bitcoin-knots`…); the signed catalog declares those ports under the manifest id that owns them (`archy-mempool-web`, `lnd-ui`, `bitcoin-ui`) → miss → launcher fell back to http. Cache also only warmed in Store/Discover views. |
|
||||||
|
| A5 | IndeeHub nostr sign-in dead over HTTPS | NIP-07 bridge compared `event.origin` for strict equality with the stored (http) app URL and replied to the **stored** URL as postMessage targetOrigin — both break when the frame was scheme-upgraded. |
|
||||||
|
| A6 | Portainer "disappeared" after restart/update, then demands a setup token "see server logs" | Update to 2.45.0 recreated the container; on a fresh DB Portainer ≥2.21 mints a one-time setup token printed ONLY in container logs — hostile appliance UX. The "disappearance" was the recreate + this unknown-token first screen. |
|
||||||
|
|
||||||
|
## B. Fixes (code)
|
||||||
|
|
||||||
|
| Fix | Files | Status |
|
||||||
|
|-----|-------|--------|
|
||||||
|
| B1 LND pay via `Router.SendPaymentV2` (`/v2/router/send`), pending-status + actionable failure reasons preserved | `core/archipelago/src/api/rpc/lnd/payments.rs` (+ unit tests) | ✅ code |
|
||||||
|
| B2 Portainer setup token surfaced in the existing credentials interstitial (`package.credentials` → AppSidebar card with copy) | `core/archipelago/src/api/rpc/package/install.rs` (+ unit tests) | ✅ code |
|
||||||
|
| B3 HSTS: none on :80, `max-age=0` on :443 (actively clears cached policy) | `image-recipe/configs/nginx-archipelago.conf` | ✅ code |
|
||||||
|
| B4 NPM manifest: `/etc/letsencrypt` mount + `NET_BIND_SERVICE` | `apps/nginx-proxy-manager/manifest.yml` | ✅ code |
|
||||||
|
| B5 `portAuth` alias resolution + unanimous port-wide fallback | `neode-ui/src/views/discover/curatedApps.ts` | ✅ code |
|
||||||
|
| B6 Catalog cache warmed at dashboard bootstrap | `neode-ui/src/App.vue` | ✅ |
|
||||||
|
| B7 NIP-07 bridge: host/port equality + reply to `event.origin` | `neode-ui/src/stores/appLauncher.ts` ✅ · `neode-ui/src/views/appSession/useNostrBridge.ts` ✅ | ✅ |
|
||||||
|
| B8 Stale LND 0.18.4 refs in test expectations | `tests/lifecycle/remote-lifecycle.sh` | ✅ |
|
||||||
|
|
||||||
|
## C. Regression tests ("never again")
|
||||||
|
|
||||||
|
| Test | Guards | Status |
|
||||||
|
|------|-------|--------|
|
||||||
|
| C1 Rust: router v2 response shape, nested errors, failure reasons | B1 | ✅ |
|
||||||
|
| C2 Rust: setup-token log extraction (live-captured 2.45.0 line shape) | B2 | ✅ |
|
||||||
|
| C3 bats: `lnd-api-compat` — POST `/v2/router/send` on the running LND must answer (never 404) | B1 vs image skew at gate time | ✅ (route probe verified live on shorty: HTTP 500 ≠ 404) |
|
||||||
|
| C4 bats: nginx must NOT send HSTS on :80; :443 must send `max-age=0` | B3 | ✅ |
|
||||||
|
| C5 neode-ui unit: portAuth alias + unanimous-scan (incl. bitcoin-knots→8334 https) | B5/B4-mixed-content | ✅ (6 tests) |
|
||||||
|
| C6 neode-ui unit: bridge origin equality ignores scheme | B7 | ✅ (2 tests) |
|
||||||
|
|
||||||
|
Backend suites: 34 targeted Rust tests green (payments v2 shape, setup-token
|
||||||
|
extraction, lnd wallet/info regressions); middleware/dispatcher suite green;
|
||||||
|
full neode-ui suite green (62 tests in the touched areas); production bundle
|
||||||
|
built and verified to embed the alias fix. `cargo fmt` applied.
|
||||||
|
|
||||||
|
## D. Deploy & live verification
|
||||||
|
|
||||||
|
| Step | Status |
|
||||||
|
|------|--------|
|
||||||
|
| D1 shorty NPM crash-loop stopped cleanly (user-stopped marker; public hosts keep serving via host nginx mirror) | ✅ 12:52Z |
|
||||||
|
| D2 shorty live nginx HSTS patch + reload | ✅ verified: :80 and :443 both answer `max-age=0` |
|
||||||
|
| D3 Regenerate catalog (releases/app-catalog.json + store copies) | ✅ semantic diff = exactly the two NPM fixes |
|
||||||
|
| D4 **User runs `scripts/sign-catalog.sh`** (signer built at /tmp/archy-sign-bin) | ⬜ waiting on mnemonic |
|
||||||
|
| D5 Commit + push (origin + gitea-vps2 OTA mirror) | ✅ 6 commits pushed (signed catalog commits after D4) |
|
||||||
|
| D6 Release v1.8.9-alpha: `scripts/create-release.sh 1.8.9-alpha` (mnemonic) → `scripts/publish-release-assets.sh 1.8.9-alpha gitea-vps2` | ⬜ waiting on mnemonic |
|
||||||
|
| D7 OTA on shorty-s + framework-pt (Update button; framework-pt has no SSH from here) | ⬜ |
|
||||||
|
| D8 shorty: clear the NPM user-stopped marker + Start (or it starts via the fixed catalog) | ⬜ |
|
||||||
|
| D9 framework-pt: Start Mempool — its containers are confirmed stopped (port 4080 refuses; gate answers on 7778/8334/50002/18083 so those apps will embed over https immediately) | ⬜ |
|
||||||
|
| D10 Post-deploy live checks: LND send+receive; mempool/IndeeHub/bitcoin-UI frames over https; NPM healthy + admin :8081; portainer token card on fresh DB; zero CORS errors | ⬜ |
|
||||||
|
|
||||||
|
framework-pt immediate workaround until its OTA lands: open the dashboard by
|
||||||
|
IP (`http://192.168.x.x`) instead of `framework-pt.local`, and/or clear the
|
||||||
|
cached policy once via `chrome://net-internals/#hsts` → Delete domain security
|
||||||
|
policies → `framework-pt.local`.
|
||||||
@@ -34,7 +34,14 @@ server {
|
|||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
|
||||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
# NO HSTS on this node, by design (see the HTTPS block below for the
|
||||||
|
# active clear). The dashboard is deliberately reachable over plain
|
||||||
|
# HTTP on LANs/mDNS names where users have not installed the node CA —
|
||||||
|
# setup-node-ca.sh keeps port 80 serving for exactly that reason. A
|
||||||
|
# long-cache HSTS policy upgrades an already-open HTTP page's fetches to
|
||||||
|
# HTTPS; that scheme change is cross-origin, so every /rpc/v1 call died
|
||||||
|
# with "No Access-Control-Allow-Origin header" while the node was
|
||||||
|
# perfectly healthy (framework-pt, 2026-09-01: "Failed to fetch" storm).
|
||||||
add_header X-DNS-Prefetch-Control "off" always;
|
add_header X-DNS-Prefetch-Control "off" always;
|
||||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://*.basemaps.cartocdn.com https://tile.openstreetmap.org; font-src 'self' data:; connect-src 'self' ws: wss: http://$host:* https:; frame-src 'self' http://$host:* https:; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://*.basemaps.cartocdn.com https://tile.openstreetmap.org; font-src 'self' data:; connect-src 'self' ws: wss: http://$host:* https:; frame-src 'self' http://$host:* https:; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
|
||||||
|
|
||||||
@@ -1009,7 +1016,14 @@ server {
|
|||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
|
||||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
# HSTS actively CLEARED (max-age=0), not set: this origin's certificate is
|
||||||
|
# optional/self-signed and plain-HTTP access is a supported mode. Earlier
|
||||||
|
# builds sent max-age=31536000 includeSubDomains, and browsers that had
|
||||||
|
# visited HTTPS once kept silently upgrading the HTTP dashboard's
|
||||||
|
# subresources afterwards — every fetch became cross-origin by scheme and
|
||||||
|
# was CORS-blocked. max-age=0 over HTTPS deletes that cached policy;
|
||||||
|
# never raise it on this origin unless HTTP access is retired first.
|
||||||
|
add_header Strict-Transport-Security "max-age=0" always;
|
||||||
add_header X-DNS-Prefetch-Control "off" always;
|
add_header X-DNS-Prefetch-Control "off" always;
|
||||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://*.basemaps.cartocdn.com https://tile.openstreetmap.org; font-src 'self' data:; connect-src 'self' ws: wss: http://$host:* https:; frame-src 'self' http://$host:* https:; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://*.basemaps.cartocdn.com https://tile.openstreetmap.org; font-src 'self' data:; connect-src 'self' ws: wss: http://$host:* https:; frame-src 'self' http://$host:* https:; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.8.8-alpha",
|
"version": "1.8.9-alpha",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.8.8-alpha",
|
"version": "1.8.9-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.8-alpha",
|
"version": "1.8.9-alpha",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "./start-dev.sh",
|
"start": "./start-dev.sh",
|
||||||
|
|||||||
@@ -13,20 +13,20 @@
|
|||||||
{
|
{
|
||||||
"id": "adguardhome",
|
"id": "adguardhome",
|
||||||
"title": "AdGuard Home",
|
"title": "AdGuard Home",
|
||||||
"version": "v0.107.55",
|
"version": "v0.107.79",
|
||||||
"description": "Network-wide ad and tracker blocking: a DNS server that filters every device on your LAN, with a web console for rules and client management.",
|
"description": "Network-wide ad and tracker blocking: a DNS server that filters every device on your LAN, with a web console for rules and client management.",
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"author": "AdGuard",
|
"author": "AdGuard",
|
||||||
"category": "networking",
|
"category": "networking",
|
||||||
"tier": "optional",
|
"tier": "optional",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.55",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.79",
|
||||||
"repoUrl": "https://github.com/AdguardTeam/AdGuardHome"
|
"repoUrl": "https://github.com/AdguardTeam/AdGuardHome"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "alby-hub",
|
"id": "alby-hub",
|
||||||
"title": "Alby Hub",
|
"title": "Alby Hub",
|
||||||
"version": "1.23.0",
|
"version": "1.23.0",
|
||||||
"description": "Self-custodial Lightning wallet hub. Runs its own Lightning node on your Archipelago and connects your apps to it over Nostr Wallet Connect \u2014 one hub, every app pays through it.",
|
"description": "Self-custodial Lightning wallet hub. Runs its own Lightning node on your Archipelago and connects your apps to it over Nostr Wallet Connect — one hub, every app pays through it.",
|
||||||
"icon": "/assets/img/app-icons/alby-hub.svg",
|
"icon": "/assets/img/app-icons/alby-hub.svg",
|
||||||
"author": "Alby",
|
"author": "Alby",
|
||||||
"category": "money",
|
"category": "money",
|
||||||
@@ -191,13 +191,13 @@
|
|||||||
{
|
{
|
||||||
"id": "filebrowser",
|
"id": "filebrowser",
|
||||||
"title": "File Browser",
|
"title": "File Browser",
|
||||||
"version": "2.27.0",
|
"version": "2.63.23",
|
||||||
"description": "Baseline Archipelago file manager service.",
|
"description": "Baseline Archipelago file manager service.",
|
||||||
"icon": "/assets/img/app-icons/file-browser.webp",
|
"icon": "/assets/img/app-icons/file-browser.webp",
|
||||||
"author": "File Browser",
|
"author": "File Browser",
|
||||||
"category": "data",
|
"category": "data",
|
||||||
"tier": "core",
|
"tier": "core",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/filebrowser:v2.27.0",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/filebrowser:v2.63.23",
|
||||||
"repoUrl": "https://github.com/filebrowser/filebrowser",
|
"repoUrl": "https://github.com/filebrowser/filebrowser",
|
||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"ports": [
|
"ports": [
|
||||||
@@ -218,12 +218,12 @@
|
|||||||
{
|
{
|
||||||
"id": "gitea",
|
"id": "gitea",
|
||||||
"title": "Gitea",
|
"title": "Gitea",
|
||||||
"version": "1.23",
|
"version": "1.27.3",
|
||||||
"description": "Self-hosted Git service with built-in container registry, CI/CD, and package hosting.",
|
"description": "Self-hosted Git service with built-in container registry, CI/CD, and package hosting.",
|
||||||
"icon": "/assets/img/app-icons/gitea.svg",
|
"icon": "/assets/img/app-icons/gitea.svg",
|
||||||
"author": "Gitea",
|
"author": "Gitea",
|
||||||
"category": "development",
|
"category": "development",
|
||||||
"dockerImage": "docker.io/gitea/gitea:1.23",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/gitea:1.27.3",
|
||||||
"repoUrl": "https://gitea.com",
|
"repoUrl": "https://gitea.com",
|
||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"ports": [
|
"ports": [
|
||||||
@@ -274,12 +274,12 @@
|
|||||||
{
|
{
|
||||||
"id": "homeassistant",
|
"id": "homeassistant",
|
||||||
"title": "Home Assistant",
|
"title": "Home Assistant",
|
||||||
"version": "2026.7.3",
|
"version": "2026.8.3",
|
||||||
"description": "Open source home automation platform. Control and monitor your smart home devices.",
|
"description": "Open source home automation platform. Control and monitor your smart home devices.",
|
||||||
"icon": "/assets/img/app-icons/homeassistant.png",
|
"icon": "/assets/img/app-icons/homeassistant.png",
|
||||||
"author": "Home Assistant",
|
"author": "Home Assistant",
|
||||||
"category": "home",
|
"category": "home",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/home-assistant:2026.8.2",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/home-assistant:2026.8.3",
|
||||||
"repoUrl": "https://github.com/home-assistant/core",
|
"repoUrl": "https://github.com/home-assistant/core",
|
||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"ports": [
|
"ports": [
|
||||||
@@ -338,13 +338,13 @@
|
|||||||
{
|
{
|
||||||
"id": "lnd",
|
"id": "lnd",
|
||||||
"title": "LND",
|
"title": "LND",
|
||||||
"version": "0.18.4",
|
"version": "0.21.2",
|
||||||
"description": "Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.",
|
"description": "Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.",
|
||||||
"icon": "/assets/img/app-icons/lnd.png",
|
"icon": "/assets/img/app-icons/lnd.png",
|
||||||
"author": "Lightning Labs",
|
"author": "Lightning Labs",
|
||||||
"category": "money",
|
"category": "money",
|
||||||
"tier": "core",
|
"tier": "core",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/lnd:v0.21.2-beta",
|
||||||
"repoUrl": "https://github.com/lightningnetwork/lnd",
|
"repoUrl": "https://github.com/lightningnetwork/lnd",
|
||||||
"requires": [
|
"requires": [
|
||||||
"bitcoin-knots"
|
"bitcoin-knots"
|
||||||
@@ -370,7 +370,7 @@
|
|||||||
"id": "netbird",
|
"id": "netbird",
|
||||||
"title": "NetBird",
|
"title": "NetBird",
|
||||||
"version": "2.38.0",
|
"version": "2.38.0",
|
||||||
"description": "Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN. The user-facing entry point \u2014 a TLS proxy in front of the dashboard + server.",
|
"description": "Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN. The user-facing entry point — a TLS proxy in front of the dashboard + server.",
|
||||||
"icon": "/assets/img/app-icons/netbird.svg",
|
"icon": "/assets/img/app-icons/netbird.svg",
|
||||||
"author": "NetBird",
|
"author": "NetBird",
|
||||||
"category": "networking",
|
"category": "networking",
|
||||||
@@ -412,7 +412,7 @@
|
|||||||
"id": "nginx-proxy-manager",
|
"id": "nginx-proxy-manager",
|
||||||
"title": "Nginx Proxy Manager",
|
"title": "Nginx Proxy Manager",
|
||||||
"version": "2.12.1",
|
"version": "2.12.1",
|
||||||
"description": "Reverse proxy with SSL. Beautiful web interface for managing proxies. On a node, this manages its admin UI and upstream configuration \u2014 the proxy's own :80/:443 listeners are not published (the node's web server owns those ports).",
|
"description": "Reverse proxy with SSL. Beautiful web interface for managing proxies. On a node, this manages its admin UI and upstream configuration — the proxy's own :80/:443 listeners are not published (the node's web server owns those ports).",
|
||||||
"icon": "/assets/img/app-icons/nginx.svg",
|
"icon": "/assets/img/app-icons/nginx.svg",
|
||||||
"author": "Nginx Proxy Manager",
|
"author": "Nginx Proxy Manager",
|
||||||
"category": "networking",
|
"category": "networking",
|
||||||
@@ -448,7 +448,7 @@
|
|||||||
"id": "ollama",
|
"id": "ollama",
|
||||||
"title": "Ollama",
|
"title": "Ollama",
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
"description": "Run large language models locally. Download and run AI models like Llama, Mistral on your own hardware \u2014 served on the node's loopback for the AI assistant (Settings \u2192 Claude Auth \u2192 model backend), never exposed to the network.",
|
"description": "Run large language models locally. Download and run AI models like Llama, Mistral on your own hardware — served on the node's loopback for the AI assistant (Settings → Claude Auth → model backend), never exposed to the network.",
|
||||||
"icon": "/assets/img/app-icons/ollama.png",
|
"icon": "/assets/img/app-icons/ollama.png",
|
||||||
"author": "Ollama",
|
"author": "Ollama",
|
||||||
"category": "community",
|
"category": "community",
|
||||||
@@ -460,7 +460,7 @@
|
|||||||
"id": "phoenixd",
|
"id": "phoenixd",
|
||||||
"title": "phoenixd",
|
"title": "phoenixd",
|
||||||
"version": "0.9.0",
|
"version": "0.9.0",
|
||||||
"description": "Headless Lightning daemon by ACINQ (the Phoenix wallet team). No screen of its own \u2014 it exposes a small local API that other apps and tools use to send and receive Lightning payments. Channel liquidity is managed automatically for a fee.",
|
"description": "Headless Lightning daemon by ACINQ (the Phoenix wallet team). No screen of its own — it exposes a small local API that other apps and tools use to send and receive Lightning payments. Channel liquidity is managed automatically for a fee.",
|
||||||
"icon": "/assets/img/app-icons/phoenixd.svg",
|
"icon": "/assets/img/app-icons/phoenixd.svg",
|
||||||
"author": "ACINQ",
|
"author": "ACINQ",
|
||||||
"category": "money",
|
"category": "money",
|
||||||
@@ -495,7 +495,7 @@
|
|||||||
"id": "pine",
|
"id": "pine",
|
||||||
"title": "Pine",
|
"title": "Pine",
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"description": "A private voice assistant for your home. Pine runs speech-to-text (Whisper), text-to-speech (Piper) and wake-word detection (openWakeWord) on your own node and pairs with a PineVoice satellite speaker, so Home Assistant Assist works locally with nothing sent to the cloud. Ask it about your node \u2014 block height, sync, peers, Lightning balance \u2014 and, when a Claude API key is set, anything else.",
|
"description": "A private voice assistant for your home. Pine runs speech-to-text (Whisper), text-to-speech (Piper) and wake-word detection (openWakeWord) on your own node and pairs with a PineVoice satellite speaker, so Home Assistant Assist works locally with nothing sent to the cloud. Ask it about your node — block height, sync, peers, Lightning balance — and, when a Claude API key is set, anything else.",
|
||||||
"icon": "/assets/img/app-icons/pine.svg",
|
"icon": "/assets/img/app-icons/pine.svg",
|
||||||
"author": "Archipelago",
|
"author": "Archipelago",
|
||||||
"category": "home",
|
"category": "home",
|
||||||
@@ -505,13 +505,13 @@
|
|||||||
{
|
{
|
||||||
"id": "portainer",
|
"id": "portainer",
|
||||||
"title": "Portainer",
|
"title": "Portainer",
|
||||||
"version": "2.19.4",
|
"version": "2.45.0",
|
||||||
"description": "Container management web UI for the local Podman socket.",
|
"description": "Container management web UI for the local Podman socket.",
|
||||||
"icon": "/assets/img/app-icons/portainer.webp",
|
"icon": "/assets/img/app-icons/portainer.webp",
|
||||||
"author": "Portainer",
|
"author": "Portainer",
|
||||||
"category": "development",
|
"category": "development",
|
||||||
"tier": "optional",
|
"tier": "optional",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/portainer:2.39.6",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/portainer:2.45.0",
|
||||||
"repoUrl": "https://github.com/portainer/portainer",
|
"repoUrl": "https://github.com/portainer/portainer",
|
||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"ports": [
|
"ports": [
|
||||||
@@ -603,13 +603,13 @@
|
|||||||
{
|
{
|
||||||
"id": "vaultwarden",
|
"id": "vaultwarden",
|
||||||
"title": "Vaultwarden",
|
"title": "Vaultwarden",
|
||||||
"version": "1.30.0",
|
"version": "1.37.2",
|
||||||
"description": "Self-hosted password vault with zero-knowledge encryption.",
|
"description": "Self-hosted password vault with zero-knowledge encryption.",
|
||||||
"icon": "/assets/img/app-icons/vaultwarden.webp",
|
"icon": "/assets/img/app-icons/vaultwarden.webp",
|
||||||
"author": "Vaultwarden",
|
"author": "Vaultwarden",
|
||||||
"category": "data",
|
"category": "data",
|
||||||
"tier": "recommended",
|
"tier": "recommended",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/vaultwarden:1.37.1-alpine",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/vaultwarden:1.37.2-alpine",
|
||||||
"repoUrl": "https://github.com/dani-garcia/vaultwarden",
|
"repoUrl": "https://github.com/dani-garcia/vaultwarden",
|
||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"ports": [
|
"ports": [
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ import { useSpotlightStore } from '@/stores/spotlight'
|
|||||||
import { useCLIStore } from '@/stores/cli'
|
import { useCLIStore } from '@/stores/cli'
|
||||||
import { useMessageToast } from '@/composables/useMessageToast'
|
import { useMessageToast } from '@/composables/useMessageToast'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { fetchAppCatalog } from './views/discover/curatedApps'
|
||||||
import { useScreensaverStore } from '@/stores/screensaver'
|
import { useScreensaverStore } from '@/stores/screensaver'
|
||||||
import { useUIModeStore } from '@/stores/uiMode'
|
import { useUIModeStore } from '@/stores/uiMode'
|
||||||
import { startRemoteRelay, stopRemoteRelay } from '@/api/remote-relay'
|
import { startRemoteRelay, stopRemoteRelay } from '@/api/remote-relay'
|
||||||
@@ -396,6 +397,13 @@ function onVisibilityChange() {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
syncKioskSafeArea()
|
syncKioskSafeArea()
|
||||||
|
// Warm the signed-catalog cache before any app launch needs it: port auth
|
||||||
|
// (gate-fronted ⇒ TLS on the app port) decides whether an app frame opens
|
||||||
|
// over https on an HTTPS dashboard. The cache used to be filled only by
|
||||||
|
// the Store/Discover views, so a user who went straight to My Apps got an
|
||||||
|
// http:// frame URL — blocked as mixed content (mempool/indeehub "did not
|
||||||
|
// connect", 2026-09-01). fetchAppCatalog() memoizes with a 1h TTL.
|
||||||
|
void fetchAppCatalog()
|
||||||
// Light app-wide mesh poll so a freshly plugged-in radio surfaces the
|
// Light app-wide mesh poll so a freshly plugged-in radio surfaces the
|
||||||
// setup modal on any page (the Mesh view's own poll takes over there).
|
// setup modal on any page (the Mesh view's own poll takes over there).
|
||||||
useMeshStore().startGlobalDetection()
|
useMeshStore().startGlobalDetection()
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ vi.mock('@/router', () => ({
|
|||||||
|
|
||||||
vi.stubGlobal('open', mockWindowOpen)
|
vi.stubGlobal('open', mockWindowOpen)
|
||||||
|
|
||||||
import { useAppLauncherStore } from '../appLauncher'
|
import { useAppLauncherStore, senderMatchesApp } from '../appLauncher'
|
||||||
|
|
||||||
describe('useAppLauncherStore', () => {
|
describe('useAppLauncherStore', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -448,4 +448,21 @@ describe('useAppLauncherStore', () => {
|
|||||||
vi.runAllTimers()
|
vi.runAllTimers()
|
||||||
vi.useRealTimers()
|
vi.useRealTimers()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('NIP-07 sender origin matching', () => {
|
||||||
|
it('accepts a scheme-upgraded frame (HSTS) as the opened app', () => {
|
||||||
|
// Regression (2026-09-01): the stored app URL was http:// but the
|
||||||
|
// browser loaded the frame as https:// — strict origin equality
|
||||||
|
// dropped every nostr sign-in from the upgraded frame.
|
||||||
|
expect(senderMatchesApp('http://framework-pt.local:7778', 'https://framework-pt.local:7778')).toBe(true)
|
||||||
|
expect(senderMatchesApp('https://framework-pt.local:7778', 'http://framework-pt.local:7778')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('still rejects a different host or port', () => {
|
||||||
|
expect(senderMatchesApp('http://framework-pt.local:7778', 'https://evil.example:7778')).toBe(false)
|
||||||
|
expect(senderMatchesApp('http://framework-pt.local:7778', 'https://framework-pt.local:7777')).toBe(false)
|
||||||
|
expect(senderMatchesApp('http://framework-pt.local:7778', 'null')).toBe(false)
|
||||||
|
expect(senderMatchesApp('', 'https://framework-pt.local:7778')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -29,6 +29,24 @@ function openExternal(launchUrl: string) {
|
|||||||
window.open(launchUrl, '_blank', 'noopener,noreferrer')
|
window.open(launchUrl, '_blank', 'noopener,noreferrer')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether a postMessage sender's origin belongs to the app the launcher
|
||||||
|
* actually opened. Same hostname and port are REQUIRED; the SCHEME is
|
||||||
|
* deliberately not compared: a browser with cached HSTS (or any scheme
|
||||||
|
* upgrade) loads a stored http:// app URL as https://, and strict equality
|
||||||
|
* silently dropped every nostr request from the upgraded frame — nostr
|
||||||
|
* sign-in on IndeeHub died exactly there over HTTPS (2026-09-01). */
|
||||||
|
export function senderMatchesApp(appUrl: string, senderOrigin: string): boolean {
|
||||||
|
let expected: URL
|
||||||
|
let sender: URL
|
||||||
|
try {
|
||||||
|
expected = new URL(appUrl, 'http://localhost/')
|
||||||
|
sender = new URL(senderOrigin)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return sender.hostname === expected.hostname && sender.port === expected.port
|
||||||
|
}
|
||||||
|
|
||||||
/** Ports of apps that set X-Frame-Options (can't iframe, must open in new tab) */
|
/** Ports of apps that set X-Frame-Options (can't iframe, must open in new tab) */
|
||||||
const NEW_TAB_PORTS = new Set([
|
const NEW_TAB_PORTS = new Set([
|
||||||
'23000', // BTCPay — X-Frame-Options: DENY
|
'23000', // BTCPay — X-Frame-Options: DENY
|
||||||
@@ -393,19 +411,11 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
|||||||
const source = event.source as Window | null
|
const source = event.source as Window | null
|
||||||
if (!source) return
|
if (!source) return
|
||||||
|
|
||||||
// Only the app we actually opened may drive this bridge. The sender's
|
// Only the app we actually opened may drive this bridge — see
|
||||||
// real origin must match the open app's URL origin — without this, any
|
// senderMatchesApp for why the scheme is deliberately not compared.
|
||||||
// co-resident iframe could deanonymize the nostr identity or use the
|
if (!senderMatchesApp(url.value, event.origin)) return
|
||||||
// node as a decryption oracle while an app happened to be open.
|
|
||||||
let expectedOrigin: string
|
|
||||||
try {
|
|
||||||
expectedOrigin = new URL(url.value, window.location.href).origin
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (event.origin !== expectedOrigin) return
|
|
||||||
|
|
||||||
const origin = url.value || 'unknown'
|
const origin = event.origin
|
||||||
|
|
||||||
// Check if app has a per-app identity stored (from identity picker)
|
// Check if app has a per-app identity stored (from identity picker)
|
||||||
const IDENTITY_KEY = 'archipelago_app_identity_'
|
const IDENTITY_KEY = 'archipelago_app_identity_'
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ function closeRouteSession() {
|
|||||||
const iframeRef = computed(() => frameRef.value?.iframeRef ?? null)
|
const iframeRef = computed(() => frameRef.value?.iframeRef ?? null)
|
||||||
|
|
||||||
const identity = useAppIdentity(appId, iframeRef, showIdentityPicker)
|
const identity = useAppIdentity(appId, iframeRef, showIdentityPicker)
|
||||||
const nostrBridge = useNostrBridge(identity.getStoredIdentity, () => appUrl.value)
|
const nostrBridge = useNostrBridge(identity.getStoredIdentity)
|
||||||
|
|
||||||
// --- Display mode ---
|
// --- Display mode ---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/** Generated by scripts/generate-app-catalog.py. Do not edit manually. */
|
/** Generated by scripts/generate-app-catalog.py. Do not edit manually. */
|
||||||
|
|
||||||
export const GENERATED_APP_PORTS: Record<string, number> = {
|
export const GENERATED_APP_PORTS: Record<string, number> = {
|
||||||
|
"adguardhome": 3030,
|
||||||
"aiui": 5180,
|
"aiui": 5180,
|
||||||
"alby-hub": 8187,
|
"alby-hub": 8187,
|
||||||
"archy-mempool-web": 4080,
|
"archy-mempool-web": 4080,
|
||||||
@@ -8,7 +9,6 @@ export const GENERATED_APP_PORTS: Record<string, number> = {
|
|||||||
"bitcoin-ui": 8334,
|
"bitcoin-ui": 8334,
|
||||||
"botfights": 9100,
|
"botfights": 9100,
|
||||||
"btcpay-server": 23000,
|
"btcpay-server": 23000,
|
||||||
"did-wallet": 8088,
|
|
||||||
"electrs-ui": 50002,
|
"electrs-ui": 50002,
|
||||||
"electrumx": 50002,
|
"electrumx": 50002,
|
||||||
"fedimint": 8175,
|
"fedimint": 8175,
|
||||||
@@ -23,9 +23,9 @@ export const GENERATED_APP_PORTS: Record<string, number> = {
|
|||||||
"lnd-ui": 18083,
|
"lnd-ui": 18083,
|
||||||
"mempool": 4080,
|
"mempool": 4080,
|
||||||
"mempool-api": 8999,
|
"mempool-api": 8999,
|
||||||
"morphos-server": 8089,
|
|
||||||
"netbird": 8087,
|
"netbird": 8087,
|
||||||
"nextcloud": 8085,
|
"nextcloud": 8085,
|
||||||
|
"nginx-proxy-manager": 8081,
|
||||||
"nostr-rs-relay": 18081,
|
"nostr-rs-relay": 18081,
|
||||||
"photoprism": 2342,
|
"photoprism": 2342,
|
||||||
"pine": 10380,
|
"pine": 10380,
|
||||||
@@ -33,11 +33,13 @@ export const GENERATED_APP_PORTS: Record<string, number> = {
|
|||||||
"router": 8084,
|
"router": 8084,
|
||||||
"searxng": 8888,
|
"searxng": 8888,
|
||||||
"strfry": 8090,
|
"strfry": 8090,
|
||||||
|
"tailscale": 8240,
|
||||||
"uptime-kuma": 3002,
|
"uptime-kuma": 3002,
|
||||||
"vaultwarden": 8082,
|
"vaultwarden": 8082,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GENERATED_APP_TITLES: Record<string, string> = {
|
export const GENERATED_APP_TITLES: Record<string, string> = {
|
||||||
|
"adguardhome": "AdGuard Home",
|
||||||
"aiui": "AI Assistant",
|
"aiui": "AI Assistant",
|
||||||
"alby-hub": "Alby Hub",
|
"alby-hub": "Alby Hub",
|
||||||
"archy-btcpay-db": "BTCPay Postgres",
|
"archy-btcpay-db": "BTCPay Postgres",
|
||||||
@@ -52,7 +54,6 @@ export const GENERATED_APP_TITLES: Record<string, string> = {
|
|||||||
"btcpay-server": "BTCPay Server",
|
"btcpay-server": "BTCPay Server",
|
||||||
"core-lightning": "Core Lightning (CLN)",
|
"core-lightning": "Core Lightning (CLN)",
|
||||||
"cuprate": "Cuprate",
|
"cuprate": "Cuprate",
|
||||||
"did-wallet": "Web5 DID Wallet",
|
|
||||||
"electrs-ui": "Electrs UI",
|
"electrs-ui": "Electrs UI",
|
||||||
"electrumx": "ElectrumX",
|
"electrumx": "ElectrumX",
|
||||||
"fedimint": "Fedimint Guardian",
|
"fedimint": "Fedimint Guardian",
|
||||||
@@ -74,17 +75,17 @@ export const GENERATED_APP_TITLES: Record<string, string> = {
|
|||||||
"indeedhub-redis": "IndeedHub Redis",
|
"indeedhub-redis": "IndeedHub Redis",
|
||||||
"indeedhub-relay": "IndeedHub Nostr Relay",
|
"indeedhub-relay": "IndeedHub Nostr Relay",
|
||||||
"jellyfin": "Jellyfin",
|
"jellyfin": "Jellyfin",
|
||||||
"lightning-stack": "Lightning Stack",
|
|
||||||
"lnd": "LND",
|
"lnd": "LND",
|
||||||
"lnd-ui": "LND UI",
|
"lnd-ui": "LND UI",
|
||||||
"mempool": "Mempool Explorer",
|
"mempool": "Mempool Explorer",
|
||||||
"mempool-api": "Mempool API",
|
"mempool-api": "Mempool API",
|
||||||
"morphos-server": "MorphOS Server",
|
|
||||||
"netbird": "NetBird",
|
"netbird": "NetBird",
|
||||||
"netbird-dashboard": "NetBird Dashboard",
|
"netbird-dashboard": "NetBird Dashboard",
|
||||||
"netbird-server": "NetBird Server",
|
"netbird-server": "NetBird Server",
|
||||||
"nextcloud": "Nextcloud",
|
"nextcloud": "Nextcloud",
|
||||||
|
"nginx-proxy-manager": "Nginx Proxy Manager",
|
||||||
"nostr-rs-relay": "Nostr Relay (Rust)",
|
"nostr-rs-relay": "Nostr Relay (Rust)",
|
||||||
|
"ollama": "Ollama",
|
||||||
"phoenixd": "phoenixd",
|
"phoenixd": "phoenixd",
|
||||||
"photoprism": "PhotoPrism",
|
"photoprism": "PhotoPrism",
|
||||||
"pine": "Pine",
|
"pine": "Pine",
|
||||||
@@ -95,6 +96,7 @@ export const GENERATED_APP_TITLES: Record<string, string> = {
|
|||||||
"router": "Mesh Router",
|
"router": "Mesh Router",
|
||||||
"searxng": "SearXNG",
|
"searxng": "SearXNG",
|
||||||
"strfry": "Strfry Nostr Relay",
|
"strfry": "Strfry Nostr Relay",
|
||||||
|
"tailscale": "Tailscale",
|
||||||
"uptime-kuma": "Uptime Kuma",
|
"uptime-kuma": "Uptime Kuma",
|
||||||
"vaultwarden": "Vaultwarden",
|
"vaultwarden": "Vaultwarden",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
/** Composable for NIP-07 Nostr signing bridge between parent and iframe */
|
/** Composable for NIP-07 Nostr signing between parent and iframe apps.
|
||||||
|
*
|
||||||
|
* Replies always target event.origin — the frame's REAL origin. The app's
|
||||||
|
* recorded URL can carry a stale scheme (HSTS-upgraded http app on an HTTPS
|
||||||
|
* dashboard); targeting it makes postMessage throw and the app never sees
|
||||||
|
* its response. */
|
||||||
|
|
||||||
import { rpcClient } from '@/api/rpc-client'
|
import { rpcClient } from '@/api/rpc-client'
|
||||||
import type { SelectedIdentity } from './useAppIdentity'
|
import type { SelectedIdentity } from './useAppIdentity'
|
||||||
|
|
||||||
export function useNostrBridge(
|
export function useNostrBridge(
|
||||||
getStoredIdentity: () => SelectedIdentity | null,
|
getStoredIdentity: () => SelectedIdentity | null,
|
||||||
getAppUrl: () => string,
|
|
||||||
) {
|
) {
|
||||||
async function handleNostrRequest(event: MessageEvent) {
|
async function handleNostrRequest(event: MessageEvent) {
|
||||||
const { id, method, params } = event.data
|
const { id, method, params } = event.data
|
||||||
@@ -43,14 +47,15 @@ export function useNostrBridge(
|
|||||||
else if (method === 'nip44.encrypt') { result = (await rpcClient.call<{ ciphertext: string }>({ method: 'identity.nostr-encrypt-nip44', params: { id: identityId || undefined, pubkey: params.pubkey, plaintext: params.plaintext } })).ciphertext }
|
else if (method === 'nip44.encrypt') { result = (await rpcClient.call<{ ciphertext: string }>({ method: 'identity.nostr-encrypt-nip44', params: { id: identityId || undefined, pubkey: params.pubkey, plaintext: params.plaintext } })).ciphertext }
|
||||||
else if (method === 'nip44.decrypt') { result = (await rpcClient.call<{ plaintext: string }>({ method: 'identity.nostr-decrypt-nip44', params: { id: identityId || undefined, pubkey: params.pubkey, ciphertext: params.ciphertext } })).plaintext }
|
else if (method === 'nip44.decrypt') { result = (await rpcClient.call<{ plaintext: string }>({ method: 'identity.nostr-decrypt-nip44', params: { id: identityId || undefined, pubkey: params.pubkey, ciphertext: params.ciphertext } })).plaintext }
|
||||||
else { throw new Error(`Unsupported NIP-07 method: ${method}`) }
|
else { throw new Error(`Unsupported NIP-07 method: ${method}`) }
|
||||||
const url = getAppUrl()
|
// Reply to the sender's REAL origin, never to the stored app URL:
|
||||||
const targetOrigin = url ? new URL(url).origin : '*'
|
// a scheme-upgraded frame (HSTS, or any future upgrade) makes the
|
||||||
source.postMessage({ type: 'nostr-response', id, result }, targetOrigin)
|
// stored http:// URL a stale targetOrigin — postMessage then throws
|
||||||
|
// and the app never receives its response. nostr sign-in on IndeeHub
|
||||||
|
// over HTTPS died exactly there (2026-09-01).
|
||||||
|
source.postMessage({ type: 'nostr-response', id, result }, event.origin || '*')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (import.meta.env.DEV) console.error(`[NIP-07] ${method} FAILED:`, err instanceof Error ? err.message : err)
|
if (import.meta.env.DEV) console.error(`[NIP-07] ${method} FAILED:`, err instanceof Error ? err.message : err)
|
||||||
const url = getAppUrl()
|
source.postMessage({ type: 'nostr-response', id, error: err instanceof Error ? err.message : 'Unknown error' }, event.origin || '*')
|
||||||
const targetOrigin = url ? new URL(url).origin : '*'
|
|
||||||
source.postMessage({ type: 'nostr-response', id, error: err instanceof Error ? err.message : 'Unknown error' }, targetOrigin)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,10 +44,8 @@
|
|||||||
class="tier-badge"
|
class="tier-badge"
|
||||||
:class="tier === 'core' ? 'tier-badge-core' : 'tier-badge-recommended'"
|
:class="tier === 'core' ? 'tier-badge-core' : 'tier-badge-recommended'"
|
||||||
>{{ tier }}</span>
|
>{{ tier }}</span>
|
||||||
<span
|
<!-- Update state is the bottom-left button only: a second pill up
|
||||||
v-if="pkg['available-update']"
|
here duplicated the signal and crowded the title. -->
|
||||||
class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold bg-orange-500/20 text-orange-300 border border-orange-500/30"
|
|
||||||
>Update</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-white/50">{{ version ? $ver(version) : '' }}</p>
|
<p class="text-sm text-white/50">{{ version ? $ver(version) : '' }}</p>
|
||||||
<p v-if="author" class="text-xs text-white/40 mt-0.5">{{ author }}</p>
|
<p v-if="author" class="text-xs text-white/40 mt-0.5">{{ author }}</p>
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
|
import { __setSignedCatalogForTests, portAuth, portIsGateFronted, type SignedAppCatalog } from '../curatedApps'
|
||||||
|
|
||||||
|
/** Catalog fragments mirroring the live signed catalog's port declarations
|
||||||
|
* (releases/app-catalog.json, 2026-09-01). */
|
||||||
|
const catalog = (apps: SignedAppCatalog['apps']): SignedAppCatalog => ({ apps })
|
||||||
|
|
||||||
|
const FULL = catalog({
|
||||||
|
'archy-mempool-web': {
|
||||||
|
version: '3.0.1',
|
||||||
|
manifest: { app: { id: 'archy-mempool-web', ports: [{ host: 4080, container: 8080, auth: 'gated' }] } },
|
||||||
|
},
|
||||||
|
'mempool': {
|
||||||
|
version: '3.0.0',
|
||||||
|
manifest: { app: { id: 'mempool', ports: [{ host: 4080, container: 8080, auth: 'gated' }] } },
|
||||||
|
},
|
||||||
|
'lnd-ui': {
|
||||||
|
version: '1.0.0',
|
||||||
|
manifest: { app: { id: 'lnd-ui', ports: [{ host: 18083, container: 18083, auth: 'gated' }] } },
|
||||||
|
},
|
||||||
|
'bitcoin-ui': {
|
||||||
|
version: '1.0.0',
|
||||||
|
manifest: { app: { id: 'bitcoin-ui', ports: [{ host: 8334, container: 8334, auth: 'gated' }] } },
|
||||||
|
},
|
||||||
|
'bitcoin-knots': {
|
||||||
|
version: '29.3',
|
||||||
|
manifest: { app: { id: 'bitcoin-knots', ports: [{ host: 8332, container: 8332, auth: 'none' }] } },
|
||||||
|
},
|
||||||
|
'electrs-ui': {
|
||||||
|
version: '1.0.0',
|
||||||
|
manifest: { app: { id: 'electrs-ui', ports: [{ host: 50002, container: 50002, auth: 'gated' }] } },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => __setSignedCatalogForTests(null))
|
||||||
|
|
||||||
|
describe('portAuth', () => {
|
||||||
|
it('resolves the UI port through the launch alias, not just the app id', () => {
|
||||||
|
__setSignedCatalogForTests(FULL)
|
||||||
|
// 'mempool-web' has no catalog entry of its own; archy-mempool-web owns 4080.
|
||||||
|
expect(portIsGateFronted('mempool-web', 4080)).toBe(true)
|
||||||
|
// 'bitcoin-knots' declares 8332 (auth none) but its UI port 8334 is owned
|
||||||
|
// by bitcoin-ui — the alias must find it, or the new-tab button hands
|
||||||
|
// out an http:// URL on an HTTPS dashboard (2026-09-01 report).
|
||||||
|
expect(portIsGateFronted('bitcoin-knots', 8334)).toBe(true)
|
||||||
|
expect(portIsGateFronted('lnd', 18083)).toBe(true)
|
||||||
|
expect(portIsGateFronted('electrs', 50002)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps a port the app itself publishes as plain HTTP off the gate', () => {
|
||||||
|
__setSignedCatalogForTests(FULL)
|
||||||
|
expect(portAuth('bitcoin-knots', 8332)).toBe('none')
|
||||||
|
expect(portIsGateFronted('bitcoin-knots', 8332)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('answers null for unknown apps and ports (never assume TLS)', () => {
|
||||||
|
__setSignedCatalogForTests(FULL)
|
||||||
|
expect(portAuth('never-installed-app', 1234)).toBeNull()
|
||||||
|
expect(portIsGateFronted('bitcoin-ui', 9999)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to a unanimous port-wide scan for unknown ids', () => {
|
||||||
|
__setSignedCatalogForTests(FULL)
|
||||||
|
// No alias for this id, but every declarer of 4080 says gated.
|
||||||
|
expect(portIsGateFronted('some-future-alias', 4080)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('refuses the port-wide scan when declarers disagree (no TLS guess)', () => {
|
||||||
|
__setSignedCatalogForTests(catalog({
|
||||||
|
'app-a': { version: '1', manifest: { app: { ports: [{ host: 7000, auth: 'gated' }] } } },
|
||||||
|
'app-b': { version: '1', manifest: { app: { ports: [{ host: 7000, auth: 'none' }] } } },
|
||||||
|
}))
|
||||||
|
expect(portAuth('unknown-app', 7000)).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns null without a warmed catalog (cache miss, not a guess)', () => {
|
||||||
|
expect(portAuth('mempool-web', 4080)).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -79,15 +79,60 @@ export function signedCatalogToApps(catalog: SignedAppCatalog): MarketplaceApp[]
|
|||||||
* after fetchAppCatalog() has run. Test-hookable. */
|
* after fetchAppCatalog() has run. Test-hookable. */
|
||||||
let signedCatalogCache: SignedAppCatalog | null = null
|
let signedCatalogCache: SignedAppCatalog | null = null
|
||||||
|
|
||||||
|
/** Launch aliases → the catalog app id that OWNS the UI port.
|
||||||
|
*
|
||||||
|
* The launcher knows apps by several historical names (`mempool-web`, `lnd`,
|
||||||
|
* `electrs`…); the signed catalog knows them by manifest id. Without this
|
||||||
|
* map the port-auth lookup below misses, `portIsGateFronted` answers false,
|
||||||
|
* and an HTTPS dashboard hands the app session an http:// frame URL — which
|
||||||
|
* the browser then blocks outright as mixed content. That is exactly how
|
||||||
|
* Mempool and IndeeHub “did not connect” over HTTPS while working fine over
|
||||||
|
* HTTP (2026-09-01). */
|
||||||
|
const CATALOG_APP_ID_ALIASES: Record<string, string> = {
|
||||||
|
'mempool-web': 'archy-mempool-web',
|
||||||
|
'mempool-electrs': 'electrs-ui',
|
||||||
|
'electrs': 'electrs-ui',
|
||||||
|
'archy-electrs-ui': 'electrs-ui',
|
||||||
|
'lnd': 'lnd-ui',
|
||||||
|
'archy-lnd-ui': 'lnd-ui',
|
||||||
|
'bitcoin-knots': 'bitcoin-ui',
|
||||||
|
'bitcoin-core': 'bitcoin-ui',
|
||||||
|
'fedimintd': 'fedimint',
|
||||||
|
'immich_server': 'immich',
|
||||||
|
}
|
||||||
|
|
||||||
/** Port auth for an app's host port, from the signed catalog's embedded
|
/** Port auth for an app's host port, from the signed catalog's embedded
|
||||||
* manifest. `gated`/`open` = the node's app gate owns the port and serves
|
* manifest. `gated`/`open` = the node's app gate owns the port and serves
|
||||||
* TLS on it; `none`/`local` = container-published plain HTTP; null = app
|
* TLS on it; `none`/`local` = container-published plain HTTP; null = app
|
||||||
* unknown to the signed catalog (legacy curated installs). */
|
* unknown to the signed catalog (legacy curated installs).
|
||||||
|
*
|
||||||
|
* Resolution order: the app's own manifest, then its alias (the manifest
|
||||||
|
* that actually owns the UI port), then — only for ports no known id
|
||||||
|
* declares — a port-wide scan of the catalog. The scan must be UNANIMOUS:
|
||||||
|
* a host port that any app publishes as plain HTTP (`none`) must never be
|
||||||
|
* answered `gated`, or an https frame URL would point at a port that never
|
||||||
|
* serves TLS. */
|
||||||
export function portAuth(appId: string, hostPort: number | string): string | null {
|
export function portAuth(appId: string, hostPort: number | string): string | null {
|
||||||
const ports = signedCatalogCache?.apps?.[appId]?.manifest?.app?.ports
|
const apps = signedCatalogCache?.apps
|
||||||
if (!Array.isArray(ports)) return null
|
if (!apps) return null
|
||||||
const hit = ports.find(p => String(p.host) === String(hostPort))
|
const alias: string | undefined = CATALOG_APP_ID_ALIASES[appId]
|
||||||
return hit?.auth ?? null
|
const ids: string[] = alias === undefined || alias === appId ? [appId] : [appId, alias]
|
||||||
|
for (const id of ids) {
|
||||||
|
const ports = apps[id]?.manifest?.app?.ports
|
||||||
|
if (!Array.isArray(ports)) continue
|
||||||
|
const hit = ports.find(p => String(p.host) === String(hostPort))
|
||||||
|
if (hit?.auth) return hit.auth
|
||||||
|
}
|
||||||
|
let found: string | null = null
|
||||||
|
for (const entry of Object.values(apps)) {
|
||||||
|
const ports = entry?.manifest?.app?.ports
|
||||||
|
if (!Array.isArray(ports)) continue
|
||||||
|
const hit = ports.find(p => String(p.host) === String(hostPort))
|
||||||
|
if (!hit?.auth) continue
|
||||||
|
if (found === null) found = hit.auth
|
||||||
|
else if (found !== hit.auth) return null
|
||||||
|
}
|
||||||
|
return found
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether an app's host port is fronted by the node's app gate (and so
|
/** Whether an app's host port is fronted by the node's app gate (and so
|
||||||
|
|||||||
@@ -362,6 +362,20 @@ 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.9-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.9-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">September 1, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p><strong>Lightning sends work again after the LND 0.21.2 update.</strong> LND 0.21 removed the payment route the node's backend used — every send answered "Not Found". Payments now go through LND's supported v2 router route, slow multi-hop payments are still tracked to completion (never falsely declared failed), failures explain themselves in plain language, and a new test speaks the payment route directly at release-gate time so an image/backend mismatch like this can never ship silently again.</p>
|
||||||
|
<p><strong>HTTP and HTTPS both work, and no longer break each other.</strong> The HTTPS listener used to pin a year-long browser policy (HSTS); once your browser had visited HTTPS, it silently rewrote the HTTP dashboard's calls to HTTPS — cross-origin, so everything showed "Failed to fetch"/CORS errors while the node was healthy. The pin is gone, the HTTPS listener now actively clears the stale policy browsers already cached (visit HTTPS once after this update to clear yours), and plain-HTTP access — which is deliberate on nodes whose self-signed certificate you haven't installed — keeps working exactly as before.</p>
|
||||||
|
<p><strong>Apps open over HTTPS again, including Mempool, Bitcoin and IndeeHub.</strong> The launcher looked each app's port policy up in the signed catalog under the name you click, but the catalog lists that port under the app that owns it — so Mempool "did not connect", Bitcoin opened a plain-http tab, and Nostr sign-in on IndeeHub silently did nothing over HTTPS. Launches now follow the alias to the owning manifest, the catalog is loaded before the first app you open (not just in the App Store), and the Nostr bridge replies to the app frame's real origin instead of a stale recorded address.</p>
|
||||||
|
<p><strong>Nginx Proxy Manager starts again.</strong> Its manifest was missing two things its image requires — the LetsEncrypt folder mount and the permission to bind low ports — leaving it in an endless restart loop on nodes that had it installed. Both are declared now; your existing certificates are untouched, and the fix arrives via the signed catalog without waiting for this release.</p>
|
||||||
|
<p><strong>Portainer's first-run token is on the app page, not buried in "server logs".</strong> New Portainer versions hand the first admin a one-time setup token that was only printed in the container logs — on this box, that token now appears with your app's other credentials, with a copy button, and disappears once setup is done.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- v1.8.8-alpha -->
|
<!-- v1.8.8-alpha -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
|||||||
+20
-20
@@ -1,32 +1,32 @@
|
|||||||
{
|
{
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"**What's New really does stop at v1.8.0 now.** The first correction removed old generated release blocks but missed six much older hand-written v1.2 sections at the bottom of the modal. Those sections are gone, and the release check now recognizes and rejects that legacy format too, so the history floor cannot falsely pass again.",
|
"**Lightning sends work again after the LND 0.21.2 update.** LND 0.21 removed the old synchronous payment route the node's backend paid through (`/v1/channels/transactions`) — every Lightning send answered the literal \"Not Found\" and the wallet showed \"Payment failed: Not Found\". The backend now pays through the supported Router.SendPaymentV2 route, keeps the same settle-then-report behaviour (a slow multi-hop payment is still tracked to completion, never falsely declared failed), and translates LND's failure reasons into plain advice. A new gate test speaks the payment route directly against the running LND, so an image/backend skew like this can never ship silently again.",
|
||||||
"**The installer carries the same corrected release and Companion 0.5.28.** Its artifact gate now checks the companion APK version and the v1.8.0 What's New floor inside the finished ISO, so a stale frontend or phone app cannot be published under the current release label.",
|
"**The node no longer pins HSTS — HTTP access is a supported mode, and it stays working.** The HTTPS listener used to send `Strict-Transport-Security: max-age=31536000; includeSubDomains`; browsers that visited HTTPS once cached that and then silently upgraded the still-open HTTP dashboard's calls to HTTPS, which is a scheme change — cross-origin — so every request died as \"CORS blocked / Failed to fetch\" while the node was perfectly healthy. The HTTPS listener now actively clears the cached policy (`max-age=0`) and port 80 sends no HSTS at all, which is deliberate: the node's certificate is optional and self-signed, and devices that haven't installed the CA must keep plain-HTTP access (that's what Settings → Node certificate is for). If your browser already cached the old policy, visiting the dashboard over HTTPS once after this update clears it; a gate test now refuses any config that reintroduces the pin.",
|
||||||
"**Crash dumps work on fresh installs as well as upgraded nodes.** The installer gate checks every kdump package inside the finished ISO, and `makedumpfile` is installed explicitly rather than accidentally relying on a recommended dependency that the minimal image deliberately omits.",
|
"**App frames open over HTTPS again — including the ones that \"did not connect.\"** The launcher asked the signed catalog for each app's port policy under the name you click (\"Mempool Web\", \"Bitcoin Knots\"), but the catalog declares those ports under the manifest that owns them (the Mempool web container, Bitcoin UI). The lookup missed, the launcher handed the iframe an `http://` address, and the browser blocked it as mixed content — the app tile went blank or spun forever. Port resolution now follows launch aliases (mempool-web, bitcoin-knots/bitcoin-core, lnd, electrs and friends), falls back to a port-wide catalog scan when the id is unknown, and the catalog is warmed as soon as the dashboard loads rather than only in the App Store, so the very first app you open already knows which ports serve TLS.",
|
||||||
"**Apps open over HTTPS when your node does.** Connect to your node over HTTPS and the apps you open — Vaultwarden in its own tab, BTCPay, Grafana, and the rest, on a remote browser or in the phone's in-app browser — now open on the same secure connection instead of silently dropping to plain HTTP. The node's app gate already served TLS on every app port; the dashboard was handing out `http://` addresses regardless of how you reached it. Ports the gate does not front (plain-HTTP publishes, and the API ports like Cuprate's RPC) deliberately stay on `http` — `https` there would simply fail to connect. Plain-HTTP access (the kiosk, LAN browsing) is unchanged.",
|
"**Signing in to IndeeHub with Nostr works over HTTPS.** The NIP-07 bridge compared the app frame's origin for exact equality with the recorded `http://` app URL — a frame the browser upgraded to HTTPS (or any scheme change) was silently ignored, and replies addressed to the stale origin were refused outright, so Nostr sign-in quietly did nothing. The bridge now matches host and port (scheme intentionally ignored) and always replies to the frame's real origin.",
|
||||||
"**Every app in the store is now a first-class platform app.** The last stragglers — Nginx Proxy Manager, Tailscale, Ollama, CryptPad, and AdGuard Home — now carry full manifests: the node's app gate fronts their web ports (TLS on the same port, the node login where appropriate, embedding fixes, Tor), installs go through the orchestrator like every other app, and their pins live in the signed catalog. Ollama stays loopback-only — it is the assistant's local model backend, not a web app. The four apps retired earlier (FIPS, Nostr VPN, Routstr, Penpot) are finally dropped from the catalog, and Cuprate's manifest — which carried a duplicated metadata block that strict parsers reject — is fixed.",
|
"**Nginx Proxy Manager starts again.** Converting it to a platform manifest dropped two things its image needs: the `/etc/letsencrypt` mount its boot script hard-requires, and the `NET_BIND_SERVICE` capability its internal nginx needs to bind ports 80/443/81 under the orchestrator's `--cap-drop=ALL`. The result was an endless start/die loop (a node watched it restart 3,176 times). Both are declared in its manifest now, its certs live on unchanged under the same persistent app directory, and the signed catalog carries the fix so installed nodes heal on the next update.",
|
||||||
"**Newly signed apps appear in the App Store immediately.** The App Store now serves the release-signed catalog the node has already fetched and verified — so publishing a signed app (like Cuprate) makes it appear for every updated node without waiting for a dashboard release. The unsigned community catalog remains only as a fallback for nodes that can't reach the registry. The same signed catalog now also decides which ports serve TLS, so nothing is upgraded to `https` that can't answer it."
|
"**Portainer's first-run token is in the app page, not buried in \"server logs.\"** New Portainer versions mint a one-time setup token on a fresh install and print it only to the container logs — on an appliance that meant telling the user to go read a server log to get into their own app. The token now appears in the same launch interstitial as app login credentials (with a copy button), only while first-run setup is actually pending; once the admin account exists the card disappears on its own."
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"current_version": "1.8.7-alpha",
|
"current_version": "1.8.9-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.7-alpha/archipelago",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.9-alpha/archipelago",
|
||||||
"name": "archipelago",
|
"name": "archipelago",
|
||||||
"new_version": "1.8.7-alpha",
|
"new_version": "1.8.9-alpha",
|
||||||
"sha256": "572accec81e73fbcd5218ddc41f5ec719deda30f104c838398fdfa85a7a276f0",
|
"sha256": "39795958963680f56763e3c05e3fe0cd589c30edd9a09416ad325bab4c862123",
|
||||||
"size_bytes": 63990960
|
"size_bytes": 64139152
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"current_version": "1.8.7-alpha",
|
"current_version": "1.8.9-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.7-alpha/archipelago-frontend-1.8.7-alpha.tar.gz",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.9-alpha/archipelago-frontend-1.8.9-alpha.tar.gz",
|
||||||
"name": "archipelago-frontend-1.8.7-alpha.tar.gz",
|
"name": "archipelago-frontend-1.8.9-alpha.tar.gz",
|
||||||
"new_version": "1.8.7-alpha",
|
"new_version": "1.8.9-alpha",
|
||||||
"sha256": "26b8c12b1b3e6b5c93841f3713037f65d0ea6a52401bd28888eb8d2f5dc483b5",
|
"sha256": "624dd10dfea09809be1fdddc7eac804e1fde66ff3d552cb90d56d9ac550ed944",
|
||||||
"size_bytes": 97779181
|
"size_bytes": 97734650
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"release_date": "2026-08-31",
|
"release_date": "2026-09-01",
|
||||||
"signature": "79332435d436e7bae1a5b0c12158b02083b681b6818ac90219021f1e39eb61a3e1039ed38ee8089006a0dec862fd92997e44a6c9ccf0be17e91a95cba5de6402",
|
"signature": "d7d724b910e827651240bd9520102d66932b57a8a8d674ef645c45eb77f78c123fb45d294ec07f8bbfc3713ed9bd9f98096f59ff18cd6098df51aa473e771908",
|
||||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||||
"version": "1.8.7-alpha"
|
"version": "1.8.9-alpha"
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-33
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"apps": {
|
"apps": {
|
||||||
"adguardhome": {
|
"adguardhome": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.55",
|
"image": "source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.79",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"container": {
|
"container": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.55",
|
"image": "source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.79",
|
||||||
"network": "pasta",
|
"network": "pasta",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
"kind": "github",
|
"kind": "github",
|
||||||
"repo": "AdguardTeam/AdGuardHome"
|
"repo": "AdguardTeam/AdGuardHome"
|
||||||
},
|
},
|
||||||
"version": "v0.107.55",
|
"version": "v0.107.79",
|
||||||
"volumes": [
|
"volumes": [
|
||||||
{
|
{
|
||||||
"options": [
|
"options": [
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version": "v0.107.55"
|
"version": "v0.107.79"
|
||||||
},
|
},
|
||||||
"aiui": {
|
"aiui": {
|
||||||
"manifest": {
|
"manifest": {
|
||||||
@@ -1319,7 +1319,7 @@
|
|||||||
"description": "Alternative Monero node implementation in Rust. Independently validates Monero consensus rules, providing a layer of security and redundancy for the network.",
|
"description": "Alternative Monero node implementation in Rust. Independently validates Monero consensus rules, providing a layer of security and redundancy for the network.",
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"content": "network = \"Mainnet\"\ntarget_max_memory = 3000000000\n\n[rpc.restricted]\nenable = true\n",
|
"content": "network = \"Mainnet\"\ntarget_max_memory = 3000000000\n\n[rpc.restricted]\nenable = true\n\n[tracing.stdout]\nlevel = \"info\"\n\n[tracing.file]\nlevel = \"info\"\nmax_log_files = 14\n",
|
||||||
"overwrite": false,
|
"overwrite": false,
|
||||||
"path": "/var/lib/archipelago/cuprate/Cuprated.toml"
|
"path": "/var/lib/archipelago/cuprate/Cuprated.toml"
|
||||||
}
|
}
|
||||||
@@ -1880,7 +1880,7 @@
|
|||||||
"version": "v0.10.1"
|
"version": "v0.10.1"
|
||||||
},
|
},
|
||||||
"filebrowser": {
|
"filebrowser": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/filebrowser:v2.27.0",
|
"image": "source.archipelago-foundation.org/lfg2025/filebrowser:v2.63.23",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"bitcoin_integration": {
|
"bitcoin_integration": {
|
||||||
@@ -1893,7 +1893,7 @@
|
|||||||
"/data/.filebrowser.json"
|
"/data/.filebrowser.json"
|
||||||
],
|
],
|
||||||
"data_uid": "100000:100000",
|
"data_uid": "100000:100000",
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/filebrowser:v2.27.0",
|
"image": "source.archipelago-foundation.org/lfg2025/filebrowser:v2.63.23",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
@@ -1943,7 +1943,7 @@
|
|||||||
"kind": "github",
|
"kind": "github",
|
||||||
"repo": "filebrowser/filebrowser"
|
"repo": "filebrowser/filebrowser"
|
||||||
},
|
},
|
||||||
"version": "2.27.0",
|
"version": "2.63.23",
|
||||||
"volumes": [
|
"volumes": [
|
||||||
{
|
{
|
||||||
"options": [
|
"options": [
|
||||||
@@ -1964,7 +1964,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version": "v2.27.0"
|
"version": "v2.63.23"
|
||||||
},
|
},
|
||||||
"fips-ui": {
|
"fips-ui": {
|
||||||
"manifest": {
|
"manifest": {
|
||||||
@@ -2019,7 +2019,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"category": "development",
|
"category": "development",
|
||||||
"container": {
|
"container": {
|
||||||
"image": "docker.io/gitea/gitea:1.23",
|
"image": "source.archipelago-foundation.org/lfg2025/gitea:1.27.3",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -2118,7 +2118,7 @@
|
|||||||
"kind": "github",
|
"kind": "github",
|
||||||
"repo": "go-gitea/gitea"
|
"repo": "go-gitea/gitea"
|
||||||
},
|
},
|
||||||
"version": "1.23",
|
"version": "1.27.3",
|
||||||
"volumes": [
|
"volumes": [
|
||||||
{
|
{
|
||||||
"options": [
|
"options": [
|
||||||
@@ -2139,7 +2139,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version": "1.23"
|
"version": "1.27.3"
|
||||||
},
|
},
|
||||||
"grafana": {
|
"grafana": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/grafana:10.2.0",
|
"image": "source.archipelago-foundation.org/lfg2025/grafana:10.2.0",
|
||||||
@@ -2221,11 +2221,11 @@
|
|||||||
"version": "10.2.0"
|
"version": "10.2.0"
|
||||||
},
|
},
|
||||||
"homeassistant": {
|
"homeassistant": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/home-assistant:2026.8.2",
|
"image": "source.archipelago-foundation.org/lfg2025/home-assistant:2026.8.3",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"container": {
|
"container": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/home-assistant:2026.8.2",
|
"image": "source.archipelago-foundation.org/lfg2025/home-assistant:2026.8.3",
|
||||||
"network": "pasta",
|
"network": "pasta",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
@@ -2302,7 +2302,7 @@
|
|||||||
"kind": "github",
|
"kind": "github",
|
||||||
"repo": "home-assistant/core"
|
"repo": "home-assistant/core"
|
||||||
},
|
},
|
||||||
"version": "2026.7.3",
|
"version": "2026.8.3",
|
||||||
"volumes": [
|
"volumes": [
|
||||||
{
|
{
|
||||||
"options": [
|
"options": [
|
||||||
@@ -2315,7 +2315,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version": "2026.8.2"
|
"version": "2026.8.3"
|
||||||
},
|
},
|
||||||
"immich": {
|
"immich": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/immich-server:release",
|
"image": "source.archipelago-foundation.org/lfg2025/immich-server:release",
|
||||||
@@ -3208,7 +3208,7 @@
|
|||||||
"version": "10.11.11"
|
"version": "10.11.11"
|
||||||
},
|
},
|
||||||
"lnd": {
|
"lnd": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta",
|
"image": "source.archipelago-foundation.org/lfg2025/lnd:v0.21.2-beta",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"bitcoin_integration": {
|
"bitcoin_integration": {
|
||||||
@@ -3223,7 +3223,7 @@
|
|||||||
"template": "{{BITCOIN_HOST}}"
|
"template": "{{BITCOIN_HOST}}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta",
|
"image": "source.archipelago-foundation.org/lfg2025/lnd:v0.21.2-beta",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present",
|
"pull_policy": "if-not-present",
|
||||||
"secret_env": [
|
"secret_env": [
|
||||||
@@ -3301,7 +3301,7 @@
|
|||||||
"kind": "github",
|
"kind": "github",
|
||||||
"repo": "lightningnetwork/lnd"
|
"repo": "lightningnetwork/lnd"
|
||||||
},
|
},
|
||||||
"version": "0.18.4",
|
"version": "0.21.2",
|
||||||
"volumes": [
|
"volumes": [
|
||||||
{
|
{
|
||||||
"options": [
|
"options": [
|
||||||
@@ -3314,7 +3314,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version": "v0.18.4-beta"
|
"version": "v0.21.2-beta"
|
||||||
},
|
},
|
||||||
"lnd-ui": {
|
"lnd-ui": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/lnd-ui:1.7.123-alpha",
|
"image": "source.archipelago-foundation.org/lfg2025/lnd-ui:1.7.123-alpha",
|
||||||
@@ -4046,7 +4046,8 @@
|
|||||||
"CHOWN",
|
"CHOWN",
|
||||||
"SETUID",
|
"SETUID",
|
||||||
"SETGID",
|
"SETGID",
|
||||||
"DAC_OVERRIDE"
|
"DAC_OVERRIDE",
|
||||||
|
"NET_BIND_SERVICE"
|
||||||
],
|
],
|
||||||
"network_policy": "isolated",
|
"network_policy": "isolated",
|
||||||
"no_new_privileges": true,
|
"no_new_privileges": true,
|
||||||
@@ -4065,6 +4066,14 @@
|
|||||||
"source": "/var/lib/archipelago/nginx-proxy-manager",
|
"source": "/var/lib/archipelago/nginx-proxy-manager",
|
||||||
"target": "/data",
|
"target": "/data",
|
||||||
"type": "bind"
|
"type": "bind"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"options": [
|
||||||
|
"rw"
|
||||||
|
],
|
||||||
|
"source": "/var/lib/archipelago/nginx-proxy-manager/letsencrypt",
|
||||||
|
"target": "/etc/letsencrypt",
|
||||||
|
"type": "bind"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -4725,7 +4734,7 @@
|
|||||||
"--beam-size",
|
"--beam-size",
|
||||||
"1"
|
"1"
|
||||||
],
|
],
|
||||||
"image": "docker.io/rhasspy/wyoming-whisper:3.4.1",
|
"image": "docker.io/rhasspy/wyoming-whisper:3.6.0",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"network_aliases": [
|
"network_aliases": [
|
||||||
"pine-whisper"
|
"pine-whisper"
|
||||||
@@ -4785,7 +4794,7 @@
|
|||||||
"kind": "dockerhub",
|
"kind": "dockerhub",
|
||||||
"repo": "rhasspy/wyoming-whisper"
|
"repo": "rhasspy/wyoming-whisper"
|
||||||
},
|
},
|
||||||
"version": "3.4.2",
|
"version": "3.6.0",
|
||||||
"volumes": [
|
"volumes": [
|
||||||
{
|
{
|
||||||
"options": [
|
"options": [
|
||||||
@@ -4798,16 +4807,16 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version": "3.4.2"
|
"version": "3.6.0"
|
||||||
},
|
},
|
||||||
"portainer": {
|
"portainer": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/portainer:2.39.6",
|
"image": "source.archipelago-foundation.org/lfg2025/portainer:2.45.0",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"category": "development",
|
"category": "development",
|
||||||
"container": {
|
"container": {
|
||||||
"data_uid": "1000:1000",
|
"data_uid": "1000:1000",
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/portainer:2.39.6",
|
"image": "source.archipelago-foundation.org/lfg2025/portainer:2.45.0",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -4869,7 +4878,7 @@
|
|||||||
"kind": "github",
|
"kind": "github",
|
||||||
"repo": "portainer/portainer"
|
"repo": "portainer/portainer"
|
||||||
},
|
},
|
||||||
"version": "2.19.4",
|
"version": "2.45.0",
|
||||||
"volumes": [
|
"volumes": [
|
||||||
{
|
{
|
||||||
"options": [
|
"options": [
|
||||||
@@ -4898,7 +4907,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version": "2.39.6"
|
"version": "2.45.0"
|
||||||
},
|
},
|
||||||
"router": {
|
"router": {
|
||||||
"manifest": {
|
"manifest": {
|
||||||
@@ -5332,11 +5341,11 @@
|
|||||||
"version": "1"
|
"version": "1"
|
||||||
},
|
},
|
||||||
"vaultwarden": {
|
"vaultwarden": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/vaultwarden:1.37.1-alpine",
|
"image": "source.archipelago-foundation.org/lfg2025/vaultwarden:1.37.2-alpine",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"container": {
|
"container": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/vaultwarden:1.37.1-alpine",
|
"image": "source.archipelago-foundation.org/lfg2025/vaultwarden:1.37.2-alpine",
|
||||||
"network": "pasta",
|
"network": "pasta",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
@@ -5403,7 +5412,7 @@
|
|||||||
"kind": "github",
|
"kind": "github",
|
||||||
"repo": "dani-garcia/vaultwarden"
|
"repo": "dani-garcia/vaultwarden"
|
||||||
},
|
},
|
||||||
"version": "1.30.0",
|
"version": "1.37.2",
|
||||||
"volumes": [
|
"volumes": [
|
||||||
{
|
{
|
||||||
"options": [
|
"options": [
|
||||||
@@ -5416,11 +5425,11 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version": "1.37.1-alpine"
|
"version": "1.37.2-alpine"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schema": 1,
|
"schema": 1,
|
||||||
"signature": "03941cb0dd7ec0ce344e4231da40606f1ef24d34a7f1f872622c42bd982846ec622e5161839a68824be4d386fcc68c4958624daf5249897c620a835dc2d11304",
|
"signature": "f982faeb9823062d9d39f6e4b38a171b4442cad0f35e74792ea161b5d77246ab9128044acbdc390ec23f921363af2d13bbba66c558b188d14d06a3f9a7f42406",
|
||||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||||
"updated": "2026-09-01"
|
"updated": "2026-09-01"
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-20
@@ -1,32 +1,32 @@
|
|||||||
{
|
{
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"**What's New really does stop at v1.8.0 now.** The first correction removed old generated release blocks but missed six much older hand-written v1.2 sections at the bottom of the modal. Those sections are gone, and the release check now recognizes and rejects that legacy format too, so the history floor cannot falsely pass again.",
|
"**Lightning sends work again after the LND 0.21.2 update.** LND 0.21 removed the old synchronous payment route the node's backend paid through (`/v1/channels/transactions`) — every Lightning send answered the literal \"Not Found\" and the wallet showed \"Payment failed: Not Found\". The backend now pays through the supported Router.SendPaymentV2 route, keeps the same settle-then-report behaviour (a slow multi-hop payment is still tracked to completion, never falsely declared failed), and translates LND's failure reasons into plain advice. A new gate test speaks the payment route directly against the running LND, so an image/backend skew like this can never ship silently again.",
|
||||||
"**The installer carries the same corrected release and Companion 0.5.28.** Its artifact gate now checks the companion APK version and the v1.8.0 What's New floor inside the finished ISO, so a stale frontend or phone app cannot be published under the current release label.",
|
"**The node no longer pins HSTS — HTTP access is a supported mode, and it stays working.** The HTTPS listener used to send `Strict-Transport-Security: max-age=31536000; includeSubDomains`; browsers that visited HTTPS once cached that and then silently upgraded the still-open HTTP dashboard's calls to HTTPS, which is a scheme change — cross-origin — so every request died as \"CORS blocked / Failed to fetch\" while the node was perfectly healthy. The HTTPS listener now actively clears the cached policy (`max-age=0`) and port 80 sends no HSTS at all, which is deliberate: the node's certificate is optional and self-signed, and devices that haven't installed the CA must keep plain-HTTP access (that's what Settings → Node certificate is for). If your browser already cached the old policy, visiting the dashboard over HTTPS once after this update clears it; a gate test now refuses any config that reintroduces the pin.",
|
||||||
"**Crash dumps work on fresh installs as well as upgraded nodes.** The installer gate checks every kdump package inside the finished ISO, and `makedumpfile` is installed explicitly rather than accidentally relying on a recommended dependency that the minimal image deliberately omits.",
|
"**App frames open over HTTPS again — including the ones that \"did not connect.\"** The launcher asked the signed catalog for each app's port policy under the name you click (\"Mempool Web\", \"Bitcoin Knots\"), but the catalog declares those ports under the manifest that owns them (the Mempool web container, Bitcoin UI). The lookup missed, the launcher handed the iframe an `http://` address, and the browser blocked it as mixed content — the app tile went blank or spun forever. Port resolution now follows launch aliases (mempool-web, bitcoin-knots/bitcoin-core, lnd, electrs and friends), falls back to a port-wide catalog scan when the id is unknown, and the catalog is warmed as soon as the dashboard loads rather than only in the App Store, so the very first app you open already knows which ports serve TLS.",
|
||||||
"**Apps open over HTTPS when your node does.** Connect to your node over HTTPS and the apps you open — Vaultwarden in its own tab, BTCPay, Grafana, and the rest, on a remote browser or in the phone's in-app browser — now open on the same secure connection instead of silently dropping to plain HTTP. The node's app gate already served TLS on every app port; the dashboard was handing out `http://` addresses regardless of how you reached it. Ports the gate does not front (plain-HTTP publishes, and the API ports like Cuprate's RPC) deliberately stay on `http` — `https` there would simply fail to connect. Plain-HTTP access (the kiosk, LAN browsing) is unchanged.",
|
"**Signing in to IndeeHub with Nostr works over HTTPS.** The NIP-07 bridge compared the app frame's origin for exact equality with the recorded `http://` app URL — a frame the browser upgraded to HTTPS (or any scheme change) was silently ignored, and replies addressed to the stale origin were refused outright, so Nostr sign-in quietly did nothing. The bridge now matches host and port (scheme intentionally ignored) and always replies to the frame's real origin.",
|
||||||
"**Every app in the store is now a first-class platform app.** The last stragglers — Nginx Proxy Manager, Tailscale, Ollama, CryptPad, and AdGuard Home — now carry full manifests: the node's app gate fronts their web ports (TLS on the same port, the node login where appropriate, embedding fixes, Tor), installs go through the orchestrator like every other app, and their pins live in the signed catalog. Ollama stays loopback-only — it is the assistant's local model backend, not a web app. The four apps retired earlier (FIPS, Nostr VPN, Routstr, Penpot) are finally dropped from the catalog, and Cuprate's manifest — which carried a duplicated metadata block that strict parsers reject — is fixed.",
|
"**Nginx Proxy Manager starts again.** Converting it to a platform manifest dropped two things its image needs: the `/etc/letsencrypt` mount its boot script hard-requires, and the `NET_BIND_SERVICE` capability its internal nginx needs to bind ports 80/443/81 under the orchestrator's `--cap-drop=ALL`. The result was an endless start/die loop (a node watched it restart 3,176 times). Both are declared in its manifest now, its certs live on unchanged under the same persistent app directory, and the signed catalog carries the fix so installed nodes heal on the next update.",
|
||||||
"**Newly signed apps appear in the App Store immediately.** The App Store now serves the release-signed catalog the node has already fetched and verified — so publishing a signed app (like Cuprate) makes it appear for every updated node without waiting for a dashboard release. The unsigned community catalog remains only as a fallback for nodes that can't reach the registry. The same signed catalog now also decides which ports serve TLS, so nothing is upgraded to `https` that can't answer it."
|
"**Portainer's first-run token is in the app page, not buried in \"server logs.\"** New Portainer versions mint a one-time setup token on a fresh install and print it only to the container logs — on an appliance that meant telling the user to go read a server log to get into their own app. The token now appears in the same launch interstitial as app login credentials (with a copy button), only while first-run setup is actually pending; once the admin account exists the card disappears on its own."
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"current_version": "1.8.7-alpha",
|
"current_version": "1.8.9-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.7-alpha/archipelago",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.9-alpha/archipelago",
|
||||||
"name": "archipelago",
|
"name": "archipelago",
|
||||||
"new_version": "1.8.7-alpha",
|
"new_version": "1.8.9-alpha",
|
||||||
"sha256": "572accec81e73fbcd5218ddc41f5ec719deda30f104c838398fdfa85a7a276f0",
|
"sha256": "39795958963680f56763e3c05e3fe0cd589c30edd9a09416ad325bab4c862123",
|
||||||
"size_bytes": 63990960
|
"size_bytes": 64139152
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"current_version": "1.8.7-alpha",
|
"current_version": "1.8.9-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.7-alpha/archipelago-frontend-1.8.7-alpha.tar.gz",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.9-alpha/archipelago-frontend-1.8.9-alpha.tar.gz",
|
||||||
"name": "archipelago-frontend-1.8.7-alpha.tar.gz",
|
"name": "archipelago-frontend-1.8.9-alpha.tar.gz",
|
||||||
"new_version": "1.8.7-alpha",
|
"new_version": "1.8.9-alpha",
|
||||||
"sha256": "26b8c12b1b3e6b5c93841f3713037f65d0ea6a52401bd28888eb8d2f5dc483b5",
|
"sha256": "624dd10dfea09809be1fdddc7eac804e1fde66ff3d552cb90d56d9ac550ed944",
|
||||||
"size_bytes": 97779181
|
"size_bytes": 97734650
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"release_date": "2026-08-31",
|
"release_date": "2026-09-01",
|
||||||
"signature": "79332435d436e7bae1a5b0c12158b02083b681b6818ac90219021f1e39eb61a3e1039ed38ee8089006a0dec862fd92997e44a6c9ccf0be17e91a95cba5de6402",
|
"signature": "d7d724b910e827651240bd9520102d66932b57a8a8d674ef645c45eb77f78c123fb45d294ec07f8bbfc3713ed9bd9f98096f59ff18cd6098df51aa473e771908",
|
||||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||||
"version": "1.8.7-alpha"
|
"version": "1.8.9-alpha"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"changelog": [
|
|
||||||
"**SSH over the mesh is now a first-class setting.** Settings gains an \"SSH over mesh\" card: off by default, and when you allow it the node's mesh firewall opens port 22 — either to every mesh peer (behind an explicit \"I understand\" confirmation, because that's a real exposure) or only to the mesh addresses you list. The rule is owned by the node (the `90-ssh.nft` drop-in), so it survives upgrades and daemon reinstalls, and the card tells you up front whether sshd is running, whether it listens on IPv6 (the mesh is IPv6-only — this is what a broken attempt looks like before it happens), and whether password login is on (keys-only is the recommended pairing). From Termux on your phone, `fipssh <user>@<node-npub>` connects once the toggle is on — the npub is the durable address, and the command is shown with a copy button on the card.",
|
|
||||||
"**The App Store now lists apps — not parts of apps.** The signed catalog carries every manifest because the node's update layer needs their pins, and the store briefly listed them all: Mempool API, LND UI, Bitcoin UI, the Pine voice engines, the IndeeHub and Immich backends, the mesh router and friends. Components are hidden from the store listing (they still appear where they belong — the Services tab of My Apps, once installed), and four entries that never earned a tile are gone outright: MorphOS server (old), the Web5 DID wallet, Lightning Stack (an untracked upstream bundle — LND covers the need), and CryptPad (never tested).",
|
|
||||||
"**App icons now persist everywhere, in the proper container style.** Two fixes: installed apps render the icon from their own manifest — Cuprate no longer falls back to the generic A-mark on its Services tile — and the store grids (the Discover page) apply the same icon container treatment (backdrop, border, shadow) as My Apps, the detail pages, and Home. Manifest-declared UI apps also classify correctly again: Alby Hub installs into My Apps with a working tile, not into Services, because a probe miss no longer buries an app the manifest itself says has a frontend.",
|
|
||||||
"**Installing from the store keeps you on the store page.** The install progress lives on the tile itself and the app appears in My Apps when it lands — no more being yanked to My Apps mid-browse."
|
|
||||||
],
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"current_version": "1.8.8-alpha",
|
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.8-alpha/archipelago",
|
|
||||||
"name": "archipelago",
|
|
||||||
"new_version": "1.8.8-alpha",
|
|
||||||
"sha256": "96f39b8db6f08386200e1eab91c8444a7758526e6034100c8a33907ff9263530",
|
|
||||||
"size_bytes": 64175864
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"current_version": "1.8.8-alpha",
|
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.8-alpha/archipelago-frontend-1.8.8-alpha.tar.gz",
|
|
||||||
"name": "archipelago-frontend-1.8.8-alpha.tar.gz",
|
|
||||||
"new_version": "1.8.8-alpha",
|
|
||||||
"sha256": "7829b67edf8dec27997dd821650ed4d61aea721f802d46a8d47014f4b4246db1",
|
|
||||||
"size_bytes": 97730549
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"release_date": "2026-09-01",
|
|
||||||
"signature": "c839cbdcb356a503d87bc17f52b6e5f3a934ae1e72a891f2d21d85366f23debb224a2a40b9124bab50fe95444e01e27711690b1bc50062f40b7ed4f34e078d06",
|
|
||||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
|
||||||
"version": "1.8.8-alpha"
|
|
||||||
}
|
|
||||||
@@ -28,7 +28,7 @@ ARCHY_REGISTRY_FALLBACK=""
|
|||||||
# halts pending the BIP110/RDTS consensus decision, so a moving tag can freeze
|
# halts pending the BIP110/RDTS consensus decision, so a moving tag can freeze
|
||||||
# the fleet's chain sync. Bumping this is a consensus decision.
|
# the fleet's chain sync. Bumping this is a consensus decision.
|
||||||
BITCOIN_KNOTS_IMAGE="$ARCHY_REGISTRY/bitcoin-knots:29.3.knots20260210"
|
BITCOIN_KNOTS_IMAGE="$ARCHY_REGISTRY/bitcoin-knots:29.3.knots20260210"
|
||||||
LND_IMAGE="$ARCHY_REGISTRY/lnd:v0.18.4-beta"
|
LND_IMAGE="$ARCHY_REGISTRY/lnd:v0.21.2-beta"
|
||||||
ELECTRUMX_IMAGE="$ARCHY_REGISTRY/electrumx:v1.18.0"
|
ELECTRUMX_IMAGE="$ARCHY_REGISTRY/electrumx:v1.18.0"
|
||||||
|
|
||||||
# Mempool stack
|
# Mempool stack
|
||||||
@@ -43,18 +43,18 @@ POSTGRES_IMAGE="$ARCHY_REGISTRY/postgres:15.17"
|
|||||||
BTCPAY_POSTGRES_IMAGE="$ARCHY_REGISTRY/postgres:15.17"
|
BTCPAY_POSTGRES_IMAGE="$ARCHY_REGISTRY/postgres:15.17"
|
||||||
|
|
||||||
# Apps
|
# Apps
|
||||||
HOMEASSISTANT_IMAGE="$ARCHY_REGISTRY/home-assistant:2026.8.2"
|
HOMEASSISTANT_IMAGE="$ARCHY_REGISTRY/home-assistant:2026.8.3"
|
||||||
GRAFANA_IMAGE="$ARCHY_REGISTRY/grafana:10.2.0"
|
GRAFANA_IMAGE="$ARCHY_REGISTRY/grafana:10.2.0"
|
||||||
UPTIME_KUMA_IMAGE="$ARCHY_REGISTRY/uptime-kuma:1"
|
UPTIME_KUMA_IMAGE="$ARCHY_REGISTRY/uptime-kuma:1"
|
||||||
JELLYFIN_IMAGE="$ARCHY_REGISTRY/jellyfin:10.11.11"
|
JELLYFIN_IMAGE="$ARCHY_REGISTRY/jellyfin:10.11.11"
|
||||||
PHOTOPRISM_IMAGE="$ARCHY_REGISTRY/photoprism:240915"
|
PHOTOPRISM_IMAGE="$ARCHY_REGISTRY/photoprism:240915"
|
||||||
OLLAMA_IMAGE="$ARCHY_REGISTRY/ollama:latest"
|
OLLAMA_IMAGE="$ARCHY_REGISTRY/ollama:latest"
|
||||||
VAULTWARDEN_IMAGE="$ARCHY_REGISTRY/vaultwarden:1.37.1-alpine"
|
VAULTWARDEN_IMAGE="$ARCHY_REGISTRY/vaultwarden:1.37.2-alpine"
|
||||||
NEXTCLOUD_IMAGE="$ARCHY_REGISTRY/nextcloud:29"
|
NEXTCLOUD_IMAGE="$ARCHY_REGISTRY/nextcloud:29"
|
||||||
SEARXNG_IMAGE="$ARCHY_REGISTRY/searxng:latest"
|
SEARXNG_IMAGE="$ARCHY_REGISTRY/searxng:latest"
|
||||||
# OnlyOffice removed — incompatible with rootless Podman (internal postgres/rabbitmq fail)
|
# OnlyOffice removed — incompatible with rootless Podman (internal postgres/rabbitmq fail)
|
||||||
# Replaced by CryptPad (single Node.js process, e2e encrypted)
|
# Replaced by CryptPad (single Node.js process, e2e encrypted)
|
||||||
FILEBROWSER_IMAGE="$ARCHY_REGISTRY/filebrowser:v2.27.0"
|
FILEBROWSER_IMAGE="$ARCHY_REGISTRY/filebrowser:v2.63.23"
|
||||||
NPM_IMAGE="$ARCHY_REGISTRY/nginx-proxy-manager:latest"
|
NPM_IMAGE="$ARCHY_REGISTRY/nginx-proxy-manager:latest"
|
||||||
# 2.39.1 is what the fleet has actually been running via the moving :latest
|
# 2.39.1 is what the fleet has actually been running via the moving :latest
|
||||||
# tag, and it is the version that wrote their databases. Pinning back to
|
# tag, and it is the version that wrote their databases. Pinning back to
|
||||||
@@ -62,7 +62,7 @@ NPM_IMAGE="$ARCHY_REGISTRY/nginx-proxy-manager:latest"
|
|||||||
# container was recreated: "database schema version does not align with the
|
# container was recreated: "database schema version does not align with the
|
||||||
# server version" — it migrates a DB forward, never backward. Pinned
|
# server version" — it migrates a DB forward, never backward. Pinned
|
||||||
# forward and published as a concrete tag so this is reproducible.
|
# forward and published as a concrete tag so this is reproducible.
|
||||||
PORTAINER_IMAGE="$ARCHY_REGISTRY/portainer:2.39.6"
|
PORTAINER_IMAGE="$ARCHY_REGISTRY/portainer:2.45.0"
|
||||||
|
|
||||||
# Networking
|
# Networking
|
||||||
TAILSCALE_IMAGE="$ARCHY_REGISTRY/tailscale:stable"
|
TAILSCALE_IMAGE="$ARCHY_REGISTRY/tailscale:stable"
|
||||||
@@ -70,7 +70,7 @@ NETBIRD_DASHBOARD_IMAGE="docker.io/netbirdio/dashboard:v2.38.0"
|
|||||||
NETBIRD_SERVER_IMAGE="docker.io/netbirdio/netbird-server:0.71.2"
|
NETBIRD_SERVER_IMAGE="docker.io/netbirdio/netbird-server:0.71.2"
|
||||||
NETBIRD_PROXY_IMAGE="docker.io/library/nginx:1.27-alpine"
|
NETBIRD_PROXY_IMAGE="docker.io/library/nginx:1.27-alpine"
|
||||||
ALPINE_TOR_IMAGE="$ARCHY_REGISTRY/alpine-tor:0.4.8.13"
|
ALPINE_TOR_IMAGE="$ARCHY_REGISTRY/alpine-tor:0.4.8.13"
|
||||||
ADGUARDHOME_IMAGE="$ARCHY_REGISTRY/adguardhome:v0.107.55"
|
ADGUARDHOME_IMAGE="$ARCHY_REGISTRY/adguardhome:v0.107.79"
|
||||||
|
|
||||||
# Fedimint
|
# Fedimint
|
||||||
FEDIMINT_IMAGE="$ARCHY_REGISTRY/fedimintd:v0.10.1"
|
FEDIMINT_IMAGE="$ARCHY_REGISTRY/fedimintd:v0.10.1"
|
||||||
@@ -114,7 +114,7 @@ INDEEDHUB_POSTGRES_IMAGE="$ARCHY_REGISTRY/postgres:16.13-alpine"
|
|||||||
INDEEDHUB_REDIS_IMAGE="$ARCHY_REGISTRY/redis:7.4.8-alpine"
|
INDEEDHUB_REDIS_IMAGE="$ARCHY_REGISTRY/redis:7.4.8-alpine"
|
||||||
|
|
||||||
# Gitea (Git + Container Registry)
|
# Gitea (Git + Container Registry)
|
||||||
GITEA_IMAGE="docker.io/gitea/gitea:1.23"
|
GITEA_IMAGE="source.archipelago-foundation.org/lfg2025/gitea:1.27.3"
|
||||||
|
|
||||||
# DWN (Decentralized Web Node)
|
# DWN (Decentralized Web Node)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
# tests/lifecycle/bats/lnd-api-compat.bats
|
||||||
|
#
|
||||||
|
# Regression guard for the 2026-09-01 fleet breakage: LND 0.21 REMOVED the
|
||||||
|
# deprecated Lightning.SendPaymentSync REST route (`/v1/channels/transactions`)
|
||||||
|
# that the backend paid through — every Lightning send answered the literal
|
||||||
|
# HTTP 404 "Not Found" and the wallet UI showed "Payment failed: Not Found".
|
||||||
|
# The backend now pays via Router.SendPaymentV2 (`/v2/router/send`).
|
||||||
|
#
|
||||||
|
# This test does not send sats. It POSTs a deliberately-invalid invoice to the
|
||||||
|
# v2 route on the RUNNING LND and asserts the route itself answers: a
|
||||||
|
# 400/500 "cannot parse" proves the endpoint exists; a 404 means the pinned
|
||||||
|
# image no longer serves the route the backend calls — the exact image/backend
|
||||||
|
# skew that shipped silently last time because no gate test ever spoke the
|
||||||
|
# payment endpoint.
|
||||||
|
#
|
||||||
|
# Tiers: read-only (invalid payment request; nothing is sent).
|
||||||
|
#
|
||||||
|
# Runs on the archy host (sudo for the macaroon, curl to localhost).
|
||||||
|
|
||||||
|
LND_MAINNET_DIR="/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet"
|
||||||
|
|
||||||
|
_lnd_rest_host_port() {
|
||||||
|
local mf
|
||||||
|
for mf in \
|
||||||
|
"${ARCHIPELAGO_APPS_DIR:-/opt/archipelago/apps}/lnd/manifest.yml" \
|
||||||
|
"${ARCHIPELAGO_APPS_DIR:-/opt/archipelago/apps}/lnd/manifest.yaml" \
|
||||||
|
"$BATS_TEST_DIRNAME/../../../apps/lnd/manifest.yml"; do
|
||||||
|
[[ -r "$mf" ]] || continue
|
||||||
|
awk '
|
||||||
|
/- host:/ { host=$3 }
|
||||||
|
/container:/ { if ($2 == 8080 && host != "") { print host; exit } }
|
||||||
|
' "$mf"
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "running LND serves /v2/router/send (the route the backend pays through)" {
|
||||||
|
if ! podman ps --format '{{.Names}}' 2>/dev/null | grep -qx lnd; then
|
||||||
|
skip "lnd not running"
|
||||||
|
fi
|
||||||
|
local port
|
||||||
|
port=$(_lnd_rest_host_port)
|
||||||
|
[[ -n "$port" ]] || skip "could not resolve LND REST host port from manifest"
|
||||||
|
|
||||||
|
local mac
|
||||||
|
mac=$(sudo cat "$LND_MAINNET_DIR/admin.macaroon" 2>/dev/null | od -An -tx1 -v | tr -d " \n")
|
||||||
|
[[ -n "$mac" ]] || skip "LND admin macaroon not readable (LND installed but wallet not initialized?)"
|
||||||
|
|
||||||
|
local code body
|
||||||
|
body=$(mktemp)
|
||||||
|
code=$(curl -sk -o "$body" -w '%{http_code}' --max-time 10 -X POST \
|
||||||
|
-H "Grpc-Metadata-macaroon: $mac" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
--data '{"payment_request":"lnbc1notarealinvoice","timeout_seconds":5,"no_inflight_updates":true}' \
|
||||||
|
"https://127.0.0.1:${port}/v2/router/send" || echo 000)
|
||||||
|
rm -f "$body"
|
||||||
|
|
||||||
|
# 000 = LND REST unreachable at all — that is port-drift's failure class
|
||||||
|
# (port-drift.bats), but it also breaks payments, so fail loudly here too.
|
||||||
|
if [[ "$code" == "000" ]]; then
|
||||||
|
fail "LND REST not reachable on ${port} — payments cannot be sent at all"
|
||||||
|
fi
|
||||||
|
if [[ "$code" == "404" ]]; then
|
||||||
|
fail "running LND does not serve /v2/router/send (HTTP 404) — the backend's payment route is gone; every Lightning send fails 'Not Found'"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "backend no longer references the removed /v1/channels/transactions route" {
|
||||||
|
# Source-level guard: the removed route must not creep back into the
|
||||||
|
# payment path (the runtime fix is in api/rpc/lnd/payments.rs).
|
||||||
|
local src="$BATS_TEST_DIRNAME/../../../core/archipelago/src/api/rpc/lnd/payments.rs"
|
||||||
|
[[ -r "$src" ]] || skip "source tree not present"
|
||||||
|
if grep -q 'v1/channels/transactions' "$src"; then
|
||||||
|
fail "payments.rs references /v1/channels/transactions — removed in LND 0.21, answers 404"
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
# tests/lifecycle/bats/nginx-hsts.bats
|
||||||
|
#
|
||||||
|
# Regression guard for the 2026-09-01 framework-pt incident: the HTTPS server
|
||||||
|
# block sent `Strict-Transport-Security: max-age=31536000; includeSubDomains`.
|
||||||
|
# Browsers cached that policy, then silently upgraded the still-open
|
||||||
|
# plain-HTTP dashboard's fetches and frames to https. A scheme change makes
|
||||||
|
# the request cross-origin, so every /rpc/v1 call was CORS-blocked — the node
|
||||||
|
# looked "not responding" while being perfectly healthy, and every app frame
|
||||||
|
# died as mixed content.
|
||||||
|
#
|
||||||
|
# Plain HTTP is a SUPPORTED access mode on purpose: the node's certificate is
|
||||||
|
# optional/self-signed (Settings → Node certificate, /ca.crt flow), and
|
||||||
|
# setup-node-ca.sh deliberately keeps port 80 serving for devices that have
|
||||||
|
# not installed the CA. So this node must never pin a live HSTS policy —
|
||||||
|
# the HTTPS listener actively clears it with max-age=0 instead.
|
||||||
|
#
|
||||||
|
# Tiers: read-only (local curl + config inspection). Runs on the archy host.
|
||||||
|
|
||||||
|
@test "nginx :80 never sends a live HSTS policy" {
|
||||||
|
local hdr
|
||||||
|
hdr=$(curl -sD - -o /dev/null --max-time 8 http://127.0.0.1/health 2>/dev/null || true)
|
||||||
|
if grep -qi 'Strict-Transport-Security' <<<"$hdr"; then
|
||||||
|
grep -qi 'max-age=0' <<<"$hdr" \
|
||||||
|
|| fail ":80 answered with a live HSTS policy — an open HTTP dashboard's fetches get force-upgraded and CORS-blocked: $(grep -i 'Strict' <<<"$hdr")"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "nginx :443 actively clears HSTS (max-age=0), never pins it" {
|
||||||
|
# The HTTPS listener binds per-LAN-address (not loopback — tailscaled owns
|
||||||
|
# :443 on tailnet addresses), so probe the node's first global IPv4.
|
||||||
|
local addr hdr
|
||||||
|
addr=$(ip -o -4 addr show scope global 2>/dev/null \
|
||||||
|
| awk '{print $4}' | cut -d/ -f1 | grep -v '^100\.' | head -1)
|
||||||
|
[[ -n "$addr" ]] || skip "no LAN address to probe HTTPS on"
|
||||||
|
hdr=$(curl -skD - -o /dev/null --max-time 8 "https://$addr/health" 2>/dev/null || true)
|
||||||
|
if grep -qi 'Strict-Transport-Security' <<<"$hdr"; then
|
||||||
|
grep -qi 'max-age=0' <<<"$hdr" \
|
||||||
|
|| fail ":443 answered with a live HSTS policy — browsers cache it and then break the HTTP dashboard: $(grep -i 'Strict' <<<"$hdr")"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "deployed nginx config contains no long-lived HSTS pin" {
|
||||||
|
# Config-level guard: catches the pin even when no cert is installed yet
|
||||||
|
# (no TLS listener to probe), and catches it on both server blocks.
|
||||||
|
local conf
|
||||||
|
for conf in /etc/nginx/sites-available/archipelago \
|
||||||
|
/etc/nginx/sites-available/archipelago-http; do
|
||||||
|
[[ -r "$conf" ]] || continue
|
||||||
|
if grep -q 'Strict-Transport-Security.*max-age=31536000' "$conf"; then
|
||||||
|
fail "$conf still pins a year-long HSTS policy (includeSubDomains class)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
true
|
||||||
|
}
|
||||||
@@ -136,7 +136,7 @@ image_for() {
|
|||||||
bitcoin-knots) echo "source.archipelago-foundation.org/lfg2025/bitcoin-knots:latest" ;;
|
bitcoin-knots) echo "source.archipelago-foundation.org/lfg2025/bitcoin-knots:latest" ;;
|
||||||
bitcoin-core) echo "docker.io/bitcoin/bitcoin:28.4" ;;
|
bitcoin-core) echo "docker.io/bitcoin/bitcoin:28.4" ;;
|
||||||
btcpay-server) echo "docker.io/btcpayserver/btcpayserver:2.4.2" ;;
|
btcpay-server) echo "docker.io/btcpayserver/btcpayserver:2.4.2" ;;
|
||||||
lnd) echo "source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta" ;;
|
lnd) echo "source.archipelago-foundation.org/lfg2025/lnd:v0.21.2-beta" ;;
|
||||||
mempool) echo "source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.0.0" ;;
|
mempool) echo "source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.0.0" ;;
|
||||||
homeassistant) echo "source.archipelago-foundation.org/lfg2025/home-assistant:2024.1" ;;
|
homeassistant) echo "source.archipelago-foundation.org/lfg2025/home-assistant:2024.1" ;;
|
||||||
grafana) echo "source.archipelago-foundation.org/lfg2025/grafana:10.2.0" ;;
|
grafana) echo "source.archipelago-foundation.org/lfg2025/grafana:10.2.0" ;;
|
||||||
|
|||||||
Reference in New Issue
Block a user