Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2174128c5 | ||
|
|
2ad0171e5f | ||
|
|
46cb0bfd37 | ||
|
|
b8593c9090 | ||
|
|
fc68c5b680 | ||
|
|
3ed75c328d |
+4
-2
@@ -8,9 +8,11 @@
|
|||||||
|
|
||||||
- **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.
|
- **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.
|
||||||
|
|
||||||
- **Apps open over HTTPS when your node does.** Connect to your node over HTTPS and every app 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 opens 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. Plain-HTTP access (the kiosk, LAN browsing) is unchanged.
|
- **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.
|
||||||
|
|
||||||
- **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. Cuprate was invisible on updated nodes for exactly this reason; it is now in both.
|
- **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.
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
|
||||||
## v1.8.6-alpha (2026-08-31)
|
## v1.8.6-alpha (2026-08-31)
|
||||||
|
|
||||||
|
|||||||
+402
-354
@@ -11,16 +11,47 @@
|
|||||||
},
|
},
|
||||||
"apps": [
|
"apps": [
|
||||||
{
|
{
|
||||||
"id": "bitcoin-knots",
|
"id": "adguardhome",
|
||||||
"title": "Bitcoin Knots",
|
"title": "AdGuard Home",
|
||||||
"version": "28.1.0",
|
"version": "v0.107.55",
|
||||||
"description": "Full Bitcoin Knots node with dynamic prune/full-mode startup based on host disk.",
|
"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": "/assets/img/app-icons/bitcoin-knots.webp",
|
"icon": "",
|
||||||
"author": "Bitcoin Knots",
|
"author": "AdGuard",
|
||||||
|
"category": "networking",
|
||||||
|
"tier": "optional",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.55",
|
||||||
|
"repoUrl": "https://github.com/AdguardTeam/AdGuardHome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "alby-hub",
|
||||||
|
"title": "Alby Hub",
|
||||||
|
"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.",
|
||||||
|
"icon": "/assets/img/app-icons/alby-hub.svg",
|
||||||
|
"author": "Alby",
|
||||||
"category": "money",
|
"category": "money",
|
||||||
"tier": "core",
|
"tier": "optional",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260210",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/alby-hub:v1.24.0",
|
||||||
"repoUrl": "https://github.com/bitcoinknots/bitcoin"
|
"repoUrl": "https://github.com/getAlby/hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "barkd",
|
||||||
|
"title": "Ark Wallet",
|
||||||
|
"version": "0.3.0",
|
||||||
|
"description": "Ark protocol wallet daemon (barkd). Lets the node hold self-custodial off-chain bitcoin via an Ark server; the wallet talks to it over a local REST API. Signet by default while Ark matures.",
|
||||||
|
"icon": "/assets/img/app-icons/bark.png",
|
||||||
|
"author": "Second",
|
||||||
|
"category": "money",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/barkd:0.3.0",
|
||||||
|
"repoUrl": "https://gitlab.com/ark-bitcoin/bark",
|
||||||
|
"containerConfig": {
|
||||||
|
"ports": [
|
||||||
|
"3535:3535"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/archipelago/barkd:/data"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "bitcoin-core",
|
"id": "bitcoin-core",
|
||||||
@@ -35,76 +66,16 @@
|
|||||||
"repoUrl": "https://github.com/bitcoin/bitcoin"
|
"repoUrl": "https://github.com/bitcoin/bitcoin"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "lnd",
|
"id": "bitcoin-knots",
|
||||||
"title": "LND",
|
"title": "Bitcoin Knots",
|
||||||
"version": "0.18.4",
|
"version": "28.1.0",
|
||||||
"description": "Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.",
|
"description": "Full Bitcoin Knots node with dynamic prune/full-mode startup based on host disk.",
|
||||||
"icon": "/assets/img/app-icons/lnd.png",
|
"icon": "/assets/img/app-icons/bitcoin-knots.webp",
|
||||||
"author": "Lightning Labs",
|
"author": "Bitcoin Knots",
|
||||||
"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/bitcoin-knots:29.3.knots20260210",
|
||||||
"repoUrl": "https://github.com/lightningnetwork/lnd",
|
"repoUrl": "https://github.com/bitcoinknots/bitcoin"
|
||||||
"requires": [
|
|
||||||
"bitcoin-knots"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "btcpay-server",
|
|
||||||
"title": "BTCPay Server",
|
|
||||||
"version": "2.4.3",
|
|
||||||
"description": "Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries.",
|
|
||||||
"icon": "/assets/img/app-icons/btcpay-server.png",
|
|
||||||
"author": "BTCPay Server Foundation",
|
|
||||||
"category": "commerce",
|
|
||||||
"tier": "core",
|
|
||||||
"dockerImage": "docker.io/btcpayserver/btcpayserver:2.4.3",
|
|
||||||
"repoUrl": "https://github.com/btcpayserver/btcpayserver",
|
|
||||||
"requires": [
|
|
||||||
"bitcoin-knots"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "mempool",
|
|
||||||
"title": "Mempool Explorer",
|
|
||||||
"version": "3.0.0",
|
|
||||||
"description": "Bitcoin mempool and blockchain explorer. Real-time transaction and block visualization.",
|
|
||||||
"icon": "/assets/img/app-icons/mempool.webp",
|
|
||||||
"author": "Mempool",
|
|
||||||
"category": "money",
|
|
||||||
"tier": "core",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.3.1",
|
|
||||||
"repoUrl": "https://github.com/mempool/mempool",
|
|
||||||
"requires": [
|
|
||||||
"bitcoin-knots",
|
|
||||||
"electrumx"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "electrumx",
|
|
||||||
"title": "ElectrumX",
|
|
||||||
"version": "1.18.0",
|
|
||||||
"description": "Electrum server indexing Bitcoin chain data for lightweight wallet queries.",
|
|
||||||
"icon": "/assets/img/app-icons/electrumx.png",
|
|
||||||
"author": "Luke Childs",
|
|
||||||
"category": "money",
|
|
||||||
"tier": "core",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/electrumx:v1.18.0",
|
|
||||||
"repoUrl": "https://github.com/spesmilo/electrumx",
|
|
||||||
"requires": [
|
|
||||||
"bitcoin-knots"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "indeedhub",
|
|
||||||
"title": "IndeeHub",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Bitcoin documentary streaming platform featuring God Bless Bitcoin and other educational content about Bitcoin, sovereignty, and decentralized technology. Sign in with your Nostr identity.",
|
|
||||||
"icon": "/assets/img/app-icons/indeedhub.png",
|
|
||||||
"author": "IndeeHub",
|
|
||||||
"category": "community",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/indeedhub:1.0.0",
|
|
||||||
"repoUrl": "https://github.com/indeedhub/indeedhub"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "botfights",
|
"id": "botfights",
|
||||||
@@ -132,127 +103,58 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gitea",
|
"id": "btcpay-server",
|
||||||
"title": "Gitea",
|
"title": "BTCPay Server",
|
||||||
"version": "1.23",
|
"version": "2.4.3",
|
||||||
"description": "Self-hosted Git service with built-in container registry, CI/CD, and package hosting.",
|
"description": "Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries.",
|
||||||
"icon": "/assets/img/app-icons/gitea.svg",
|
"icon": "/assets/img/app-icons/btcpay-server.png",
|
||||||
"author": "Gitea",
|
"author": "BTCPay Server Foundation",
|
||||||
"category": "development",
|
"category": "commerce",
|
||||||
"dockerImage": "docker.io/gitea/gitea:1.23",
|
|
||||||
"repoUrl": "https://gitea.com",
|
|
||||||
"containerConfig": {
|
|
||||||
"ports": [
|
|
||||||
"3001:3000",
|
|
||||||
"2222:22"
|
|
||||||
],
|
|
||||||
"volumes": [
|
|
||||||
"/var/lib/archipelago/gitea/data:/data",
|
|
||||||
"/var/lib/archipelago/gitea/config:/etc/gitea"
|
|
||||||
],
|
|
||||||
"env": [
|
|
||||||
"GITEA__database__DB_TYPE=sqlite3",
|
|
||||||
"GITEA__server__SSH_PORT=2222",
|
|
||||||
"GITEA__server__SSH_LISTEN_PORT=22",
|
|
||||||
"GITEA__server__LFS_START_SERVER=true",
|
|
||||||
"GITEA__packages__ENABLED=true",
|
|
||||||
"GITEA__repository__ENABLE_PUSH_CREATE_USER=true",
|
|
||||||
"GITEA__repository__ENABLE_PUSH_CREATE_ORG=true",
|
|
||||||
"GITEA__security__X_FRAME_OPTIONS="
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"tier": "optional"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "filebrowser",
|
|
||||||
"title": "File Browser",
|
|
||||||
"version": "2.27.0",
|
|
||||||
"description": "Baseline Archipelago file manager service.",
|
|
||||||
"icon": "/assets/img/app-icons/file-browser.webp",
|
|
||||||
"author": "File Browser",
|
|
||||||
"category": "data",
|
|
||||||
"tier": "core",
|
"tier": "core",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/filebrowser:v2.27.0",
|
"dockerImage": "docker.io/btcpayserver/btcpayserver:2.4.3",
|
||||||
"repoUrl": "https://github.com/filebrowser/filebrowser",
|
"repoUrl": "https://github.com/btcpayserver/btcpayserver",
|
||||||
"containerConfig": {
|
"requires": [
|
||||||
"ports": [
|
"bitcoin-knots"
|
||||||
"8083:80"
|
]
|
||||||
],
|
|
||||||
"volumes": [
|
|
||||||
"/var/lib/archipelago/filebrowser:/srv",
|
|
||||||
"/var/lib/archipelago/filebrowser-data:/data"
|
|
||||||
],
|
|
||||||
"args": [
|
|
||||||
"--database=/data/database.db",
|
|
||||||
"--root=/srv",
|
|
||||||
"--address=0.0.0.0",
|
|
||||||
"--port=80"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nostr-rs-relay",
|
"id": "cryptpad",
|
||||||
"title": "Nostr Relay (Rust)",
|
"title": "CryptPad",
|
||||||
"version": "0.10.0",
|
"version": "2024.12.0",
|
||||||
"description": "High-performance Nostr relay written in Rust. Host your own decentralized social media relay and earn networking profits.",
|
"description": "End-to-end encrypted documents, spreadsheets, and presentations. Zero-knowledge collaboration.",
|
||||||
"icon": "/assets/img/app-icons/nostrudel.svg",
|
"icon": "/assets/icon/favico-black-v2.svg",
|
||||||
"author": "Nostr RS Relay",
|
"author": "XWiki SAS",
|
||||||
"category": "community",
|
|
||||||
"tier": "recommended",
|
|
||||||
"dockerImage": "scsibug/nostr-rs-relay:0.10.0",
|
|
||||||
"repoUrl": "https://github.com/scsibug/nostr-rs-relay",
|
|
||||||
"containerConfig": {
|
|
||||||
"ports": [
|
|
||||||
"8081:8080"
|
|
||||||
],
|
|
||||||
"volumes": [
|
|
||||||
"/var/lib/archipelago/nostr-relay:/usr/src/app/db"
|
|
||||||
],
|
|
||||||
"env": [
|
|
||||||
"RELAY_NAME=Archipelago Nostr Relay",
|
|
||||||
"RELAY_DESCRIPTION=Self-hosted Nostr relay on Archipelago"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "vaultwarden",
|
|
||||||
"title": "Vaultwarden",
|
|
||||||
"version": "1.30.0",
|
|
||||||
"description": "Self-hosted password vault with zero-knowledge encryption.",
|
|
||||||
"icon": "/assets/img/app-icons/vaultwarden.webp",
|
|
||||||
"author": "Vaultwarden",
|
|
||||||
"category": "data",
|
"category": "data",
|
||||||
"tier": "recommended",
|
"tier": "optional",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/vaultwarden:1.37.1-alpine",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/cryptpad:2024.12.0",
|
||||||
"repoUrl": "https://github.com/dani-garcia/vaultwarden",
|
"repoUrl": "https://github.com/cryptpad/cryptpad"
|
||||||
"containerConfig": {
|
|
||||||
"ports": [
|
|
||||||
"8082:80"
|
|
||||||
],
|
|
||||||
"volumes": [
|
|
||||||
"/var/lib/archipelago/vaultwarden:/data"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "searxng",
|
"id": "cuprate",
|
||||||
"title": "SearXNG",
|
"title": "Cuprate",
|
||||||
"version": "1.0.0",
|
"version": "0.1.0-preview",
|
||||||
"description": "Privacy-respecting metasearch engine. Search the web without tracking.",
|
"description": "Alternative Monero node implementation in Rust. Independently validates Monero consensus rules, providing a layer of security and redundancy for the network.",
|
||||||
"icon": "/assets/img/app-icons/searxng.png",
|
"icon": "/assets/img/app-icons/cuprate.svg",
|
||||||
"author": "SearXNG",
|
"author": "Cuprate contributors",
|
||||||
"category": "data",
|
"category": "money",
|
||||||
"tier": "recommended",
|
"tier": "optional",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/searxng:latest",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/cuprate:0.1.0-preview-18-g618ff14",
|
||||||
"repoUrl": "https://github.com/searxng/searxng",
|
"repoUrl": "https://github.com/Cuprate/cuprate"
|
||||||
"containerConfig": {
|
},
|
||||||
"ports": [
|
{
|
||||||
"8888:8080"
|
"id": "electrumx",
|
||||||
],
|
"title": "ElectrumX",
|
||||||
"volumes": [
|
"version": "1.18.0",
|
||||||
"/var/lib/archipelago/searxng:/etc/searxng"
|
"description": "Electrum server indexing Bitcoin chain data for lightweight wallet queries.",
|
||||||
]
|
"icon": "/assets/img/app-icons/electrumx.png",
|
||||||
}
|
"author": "Luke Childs",
|
||||||
|
"category": "money",
|
||||||
|
"tier": "core",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/electrumx:v1.18.0",
|
||||||
|
"repoUrl": "https://github.com/spesmilo/electrumx",
|
||||||
|
"requires": [
|
||||||
|
"bitcoin-knots"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "fedimint",
|
"id": "fedimint",
|
||||||
@@ -299,54 +201,87 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "barkd",
|
"id": "filebrowser",
|
||||||
"title": "Ark Wallet",
|
"title": "File Browser",
|
||||||
"version": "0.3.0",
|
"version": "2.27.0",
|
||||||
"description": "Ark protocol wallet daemon (barkd). Lets the node hold self-custodial off-chain bitcoin via an Ark server; the wallet talks to it over a local REST API. Signet by default while Ark matures.",
|
"description": "Baseline Archipelago file manager service.",
|
||||||
"icon": "/assets/img/app-icons/bark.png",
|
"icon": "/assets/img/app-icons/file-browser.webp",
|
||||||
"author": "Second",
|
"author": "File Browser",
|
||||||
"category": "money",
|
"category": "data",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/barkd:0.3.0",
|
"tier": "core",
|
||||||
"repoUrl": "https://gitlab.com/ark-bitcoin/bark",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/filebrowser:v2.27.0",
|
||||||
|
"repoUrl": "https://github.com/filebrowser/filebrowser",
|
||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"ports": [
|
"ports": [
|
||||||
"3535:3535"
|
"8083:80"
|
||||||
],
|
],
|
||||||
"volumes": [
|
"volumes": [
|
||||||
"/var/lib/archipelago/barkd:/data"
|
"/var/lib/archipelago/filebrowser:/srv",
|
||||||
|
"/var/lib/archipelago/filebrowser-data:/data"
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
"--database=/data/database.db",
|
||||||
|
"--root=/srv",
|
||||||
|
"--address=0.0.0.0",
|
||||||
|
"--port=80"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "jellyfin",
|
"id": "gitea",
|
||||||
"title": "Jellyfin",
|
"title": "Gitea",
|
||||||
"version": "10.8.13",
|
"version": "1.23",
|
||||||
"description": "Free media server. Stream movies, music, and photos.",
|
"description": "Self-hosted Git service with built-in container registry, CI/CD, and package hosting.",
|
||||||
"icon": "/assets/img/app-icons/jellyfin.webp",
|
"icon": "/assets/img/app-icons/gitea.svg",
|
||||||
"author": "Jellyfin",
|
"author": "Gitea",
|
||||||
"category": "data",
|
"category": "development",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/jellyfin:10.11.11",
|
"dockerImage": "docker.io/gitea/gitea:1.23",
|
||||||
"repoUrl": "https://github.com/jellyfin/jellyfin",
|
"repoUrl": "https://gitea.com",
|
||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"ports": [
|
"ports": [
|
||||||
"8096:8096"
|
"3001:3000",
|
||||||
|
"2222:22"
|
||||||
],
|
],
|
||||||
"volumes": [
|
"volumes": [
|
||||||
"/var/lib/archipelago/jellyfin/config:/config",
|
"/var/lib/archipelago/gitea/data:/data",
|
||||||
"/var/lib/archipelago/jellyfin/cache:/cache"
|
"/var/lib/archipelago/gitea/config:/etc/gitea"
|
||||||
|
],
|
||||||
|
"env": [
|
||||||
|
"GITEA__database__DB_TYPE=sqlite3",
|
||||||
|
"GITEA__server__SSH_PORT=2222",
|
||||||
|
"GITEA__server__SSH_LISTEN_PORT=22",
|
||||||
|
"GITEA__server__LFS_START_SERVER=true",
|
||||||
|
"GITEA__packages__ENABLED=true",
|
||||||
|
"GITEA__repository__ENABLE_PUSH_CREATE_USER=true",
|
||||||
|
"GITEA__repository__ENABLE_PUSH_CREATE_ORG=true",
|
||||||
|
"GITEA__security__X_FRAME_OPTIONS="
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"tier": "optional"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "immich",
|
"id": "grafana",
|
||||||
"title": "Immich",
|
"title": "Grafana",
|
||||||
"version": "2.7.4",
|
"version": "10.2.0",
|
||||||
"description": "Self-hosted photo and video backup with mobile apps and search.",
|
"description": "Analytics and monitoring platform. Visualize metrics and create dashboards.",
|
||||||
"icon": "/assets/img/app-icons/immich.png",
|
"icon": "/assets/img/app-icons/grafana.png",
|
||||||
"author": "Immich",
|
"author": "Grafana Labs",
|
||||||
"category": "data",
|
"category": "data",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/immich-server:release",
|
"tier": "recommended",
|
||||||
"repoUrl": "https://github.com/immich-app/immich"
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/grafana:10.2.0",
|
||||||
|
"repoUrl": "https://github.com/grafana/grafana",
|
||||||
|
"containerConfig": {
|
||||||
|
"ports": [
|
||||||
|
"3000:3000"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/archipelago/grafana:/var/lib/grafana"
|
||||||
|
],
|
||||||
|
"env": [
|
||||||
|
"GF_PATHS_DATA=/var/lib/grafana",
|
||||||
|
"GF_USERS_ALLOW_SIGN_UP=false"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "homeassistant",
|
"id": "homeassistant",
|
||||||
@@ -370,11 +305,209 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "immich",
|
||||||
|
"title": "Immich",
|
||||||
|
"version": "2.7.4",
|
||||||
|
"description": "Self-hosted photo and video backup with mobile apps and search.",
|
||||||
|
"icon": "/assets/img/app-icons/immich.png",
|
||||||
|
"author": "Immich",
|
||||||
|
"category": "data",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/immich-server:release",
|
||||||
|
"repoUrl": "https://github.com/immich-app/immich"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "indeedhub",
|
||||||
|
"title": "IndeeHub",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Bitcoin documentary streaming platform featuring God Bless Bitcoin and other educational content about Bitcoin, sovereignty, and decentralized technology. Sign in with your Nostr identity.",
|
||||||
|
"icon": "/assets/img/app-icons/indeedhub.png",
|
||||||
|
"author": "IndeeHub",
|
||||||
|
"category": "community",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/indeedhub:1.0.0",
|
||||||
|
"repoUrl": "https://github.com/indeedhub/indeedhub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "jellyfin",
|
||||||
|
"title": "Jellyfin",
|
||||||
|
"version": "10.8.13",
|
||||||
|
"description": "Free media server. Stream movies, music, and photos.",
|
||||||
|
"icon": "/assets/img/app-icons/jellyfin.webp",
|
||||||
|
"author": "Jellyfin",
|
||||||
|
"category": "data",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/jellyfin:10.11.11",
|
||||||
|
"repoUrl": "https://github.com/jellyfin/jellyfin",
|
||||||
|
"containerConfig": {
|
||||||
|
"ports": [
|
||||||
|
"8096:8096"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/archipelago/jellyfin/config:/config",
|
||||||
|
"/var/lib/archipelago/jellyfin/cache:/cache"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "lnd",
|
||||||
|
"title": "LND",
|
||||||
|
"version": "0.18.4",
|
||||||
|
"description": "Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.",
|
||||||
|
"icon": "/assets/img/app-icons/lnd.png",
|
||||||
|
"author": "Lightning Labs",
|
||||||
|
"category": "money",
|
||||||
|
"tier": "core",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta",
|
||||||
|
"repoUrl": "https://github.com/lightningnetwork/lnd",
|
||||||
|
"requires": [
|
||||||
|
"bitcoin-knots"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mempool",
|
||||||
|
"title": "Mempool Explorer",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"description": "Bitcoin mempool and blockchain explorer. Real-time transaction and block visualization.",
|
||||||
|
"icon": "/assets/img/app-icons/mempool.webp",
|
||||||
|
"author": "Mempool",
|
||||||
|
"category": "money",
|
||||||
|
"tier": "core",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.3.1",
|
||||||
|
"repoUrl": "https://github.com/mempool/mempool",
|
||||||
|
"requires": [
|
||||||
|
"bitcoin-knots",
|
||||||
|
"electrumx"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "netbird",
|
||||||
|
"title": "NetBird",
|
||||||
|
"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.",
|
||||||
|
"icon": "/assets/img/app-icons/netbird.svg",
|
||||||
|
"author": "NetBird",
|
||||||
|
"category": "networking",
|
||||||
|
"tier": "recommended",
|
||||||
|
"dockerImage": "docker.io/library/nginx:1.31.4-alpine",
|
||||||
|
"repoUrl": "https://github.com/netbirdio/netbird",
|
||||||
|
"containerConfig": {
|
||||||
|
"ports": [
|
||||||
|
"8087:80",
|
||||||
|
"8086:80",
|
||||||
|
"3478:3478/udp"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/archipelago/netbird:/var/lib/netbird"
|
||||||
|
],
|
||||||
|
"notes": "Installed as a two-container stack: netbird dashboard on 8087 and netbird-server control plane on 8086 plus UDP 3478. For production clients, publish a DNS name over HTTPS with gRPC/WebSocket routing."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nextcloud",
|
||||||
|
"title": "Nextcloud",
|
||||||
|
"version": "29",
|
||||||
|
"description": "Your own private cloud. File sync, calendars, contacts.",
|
||||||
|
"icon": "/assets/img/app-icons/nextcloud.webp",
|
||||||
|
"author": "Nextcloud",
|
||||||
|
"category": "data",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/nextcloud:29",
|
||||||
|
"repoUrl": "https://github.com/nextcloud/server",
|
||||||
|
"containerConfig": {
|
||||||
|
"ports": [
|
||||||
|
"8085:80"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/archipelago/nextcloud:/var/www/html"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nginx-proxy-manager",
|
||||||
|
"title": "Nginx Proxy Manager",
|
||||||
|
"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).",
|
||||||
|
"icon": "/assets/img/app-icons/nginx.svg",
|
||||||
|
"author": "Nginx Proxy Manager",
|
||||||
|
"category": "networking",
|
||||||
|
"tier": "optional",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/nginx-proxy-manager:latest",
|
||||||
|
"repoUrl": "https://github.com/NginxProxyManager/nginx-proxy-manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nostr-rs-relay",
|
||||||
|
"title": "Nostr Relay (Rust)",
|
||||||
|
"version": "0.10.0",
|
||||||
|
"description": "High-performance Nostr relay written in Rust. Host your own decentralized social media relay and earn networking profits.",
|
||||||
|
"icon": "/assets/img/app-icons/nostrudel.svg",
|
||||||
|
"author": "Nostr RS Relay",
|
||||||
|
"category": "community",
|
||||||
|
"tier": "recommended",
|
||||||
|
"dockerImage": "scsibug/nostr-rs-relay:0.10.0",
|
||||||
|
"repoUrl": "https://github.com/scsibug/nostr-rs-relay",
|
||||||
|
"containerConfig": {
|
||||||
|
"ports": [
|
||||||
|
"8081:8080"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/archipelago/nostr-relay:/usr/src/app/db"
|
||||||
|
],
|
||||||
|
"env": [
|
||||||
|
"RELAY_NAME=Archipelago Nostr Relay",
|
||||||
|
"RELAY_DESCRIPTION=Self-hosted Nostr relay on Archipelago"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ollama",
|
||||||
|
"title": "Ollama",
|
||||||
|
"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.",
|
||||||
|
"icon": "/assets/img/app-icons/ollama.png",
|
||||||
|
"author": "Ollama",
|
||||||
|
"category": "community",
|
||||||
|
"tier": "optional",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/ollama:latest",
|
||||||
|
"repoUrl": "https://github.com/ollama/ollama"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "phoenixd",
|
||||||
|
"title": "phoenixd",
|
||||||
|
"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.",
|
||||||
|
"icon": "/assets/img/app-icons/phoenixd.svg",
|
||||||
|
"author": "ACINQ",
|
||||||
|
"category": "money",
|
||||||
|
"tier": "optional",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/phoenixd:0.9.0",
|
||||||
|
"repoUrl": "https://github.com/ACINQ/phoenixd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "photoprism",
|
||||||
|
"title": "PhotoPrism",
|
||||||
|
"version": "240915",
|
||||||
|
"description": "AI-powered photo management with facial recognition.",
|
||||||
|
"icon": "/assets/img/app-icons/photoprism.svg",
|
||||||
|
"author": "PhotoPrism",
|
||||||
|
"category": "data",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/photoprism:240915",
|
||||||
|
"repoUrl": "https://github.com/photoprism/photoprism",
|
||||||
|
"containerConfig": {
|
||||||
|
"ports": [
|
||||||
|
"2342:2342"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/archipelago/photoprism:/photoprism/storage"
|
||||||
|
],
|
||||||
|
"env": [
|
||||||
|
"PHOTOPRISM_ADMIN_PASSWORD=archipelago",
|
||||||
|
"PHOTOPRISM_DEFAULT_LOCALE=en"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"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 — block height, sync, peers, Lightning balance — 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 \u2014 block height, sync, peers, Lightning balance \u2014 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",
|
||||||
@@ -382,26 +515,44 @@
|
|||||||
"repoUrl": "https://github.com/rhasspy/wyoming"
|
"repoUrl": "https://github.com/rhasspy/wyoming"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "grafana",
|
"id": "portainer",
|
||||||
"title": "Grafana",
|
"title": "Portainer",
|
||||||
"version": "10.2.0",
|
"version": "2.19.4",
|
||||||
"description": "Analytics and monitoring platform. Visualize metrics and create dashboards.",
|
"description": "Container management web UI for the local Podman socket.",
|
||||||
"icon": "/assets/img/app-icons/grafana.png",
|
"icon": "/assets/img/app-icons/portainer.webp",
|
||||||
"author": "Grafana Labs",
|
"author": "Portainer",
|
||||||
"category": "data",
|
"category": "development",
|
||||||
"tier": "recommended",
|
"tier": "optional",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/grafana:10.2.0",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/portainer:2.39.6",
|
||||||
"repoUrl": "https://github.com/grafana/grafana",
|
"repoUrl": "https://github.com/portainer/portainer",
|
||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"ports": [
|
"ports": [
|
||||||
"3000:3000"
|
"9000:9000"
|
||||||
],
|
],
|
||||||
"volumes": [
|
"volumes": [
|
||||||
"/var/lib/archipelago/grafana:/var/lib/grafana"
|
"/var/lib/archipelago/portainer:/data",
|
||||||
|
"/run/user/1000/podman/podman.sock:/var/run/docker.sock"
|
||||||
],
|
],
|
||||||
"env": [
|
"notes": "Uses the manifest-owned Podman socket bind mount preparation path."
|
||||||
"GF_PATHS_DATA=/var/lib/grafana",
|
}
|
||||||
"GF_USERS_ALLOW_SIGN_UP=false"
|
},
|
||||||
|
{
|
||||||
|
"id": "searxng",
|
||||||
|
"title": "SearXNG",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Privacy-respecting metasearch engine. Search the web without tracking.",
|
||||||
|
"icon": "/assets/img/app-icons/searxng.png",
|
||||||
|
"author": "SearXNG",
|
||||||
|
"category": "data",
|
||||||
|
"tier": "recommended",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/searxng:latest",
|
||||||
|
"repoUrl": "https://github.com/searxng/searxng",
|
||||||
|
"containerConfig": {
|
||||||
|
"ports": [
|
||||||
|
"8888:8080"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/archipelago/searxng:/etc/searxng"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -433,51 +584,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "portainer",
|
|
||||||
"title": "Portainer",
|
|
||||||
"version": "2.19.4",
|
|
||||||
"description": "Container management web UI for the local Podman socket.",
|
|
||||||
"icon": "/assets/img/app-icons/portainer.webp",
|
|
||||||
"author": "Portainer",
|
|
||||||
"category": "development",
|
|
||||||
"tier": "optional",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/portainer:2.39.6",
|
|
||||||
"repoUrl": "https://github.com/portainer/portainer",
|
|
||||||
"containerConfig": {
|
|
||||||
"ports": [
|
|
||||||
"9000:9000"
|
|
||||||
],
|
|
||||||
"volumes": [
|
|
||||||
"/var/lib/archipelago/portainer:/data",
|
|
||||||
"/run/user/1000/podman/podman.sock:/var/run/docker.sock"
|
|
||||||
],
|
|
||||||
"notes": "Uses the manifest-owned Podman socket bind mount preparation path."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "netbird",
|
|
||||||
"title": "NetBird",
|
|
||||||
"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 — a TLS proxy in front of the dashboard + server.",
|
|
||||||
"icon": "/assets/img/app-icons/netbird.svg",
|
|
||||||
"author": "NetBird",
|
|
||||||
"category": "networking",
|
|
||||||
"tier": "recommended",
|
|
||||||
"dockerImage": "docker.io/library/nginx:1.31.4-alpine",
|
|
||||||
"repoUrl": "https://github.com/netbirdio/netbird",
|
|
||||||
"containerConfig": {
|
|
||||||
"ports": [
|
|
||||||
"8087:80",
|
|
||||||
"8086:80",
|
|
||||||
"3478:3478/udp"
|
|
||||||
],
|
|
||||||
"volumes": [
|
|
||||||
"/var/lib/archipelago/netbird:/var/lib/netbird"
|
|
||||||
],
|
|
||||||
"notes": "Installed as a two-container stack: netbird dashboard on 8087 and netbird-server control plane on 8086 plus UDP 3478. For production clients, publish a DNS name over HTTPS with gRPC/WebSocket routing."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "uptime-kuma",
|
"id": "uptime-kuma",
|
||||||
"title": "Uptime Kuma",
|
"title": "Uptime Kuma",
|
||||||
@@ -507,82 +613,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "photoprism",
|
"id": "vaultwarden",
|
||||||
"title": "PhotoPrism",
|
"title": "Vaultwarden",
|
||||||
"version": "240915",
|
"version": "1.30.0",
|
||||||
"description": "AI-powered photo management with facial recognition.",
|
"description": "Self-hosted password vault with zero-knowledge encryption.",
|
||||||
"icon": "/assets/img/app-icons/photoprism.svg",
|
"icon": "/assets/img/app-icons/vaultwarden.webp",
|
||||||
"author": "PhotoPrism",
|
"author": "Vaultwarden",
|
||||||
"category": "data",
|
"category": "data",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/photoprism:240915",
|
"tier": "recommended",
|
||||||
"repoUrl": "https://github.com/photoprism/photoprism",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/vaultwarden:1.37.1-alpine",
|
||||||
|
"repoUrl": "https://github.com/dani-garcia/vaultwarden",
|
||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"ports": [
|
"ports": [
|
||||||
"2342:2342"
|
"8082:80"
|
||||||
],
|
],
|
||||||
"volumes": [
|
"volumes": [
|
||||||
"/var/lib/archipelago/photoprism:/photoprism/storage"
|
"/var/lib/archipelago/vaultwarden:/data"
|
||||||
],
|
|
||||||
"env": [
|
|
||||||
"PHOTOPRISM_ADMIN_PASSWORD=archipelago",
|
|
||||||
"PHOTOPRISM_DEFAULT_LOCALE=en"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "nextcloud",
|
|
||||||
"title": "Nextcloud",
|
|
||||||
"version": "29",
|
|
||||||
"description": "Your own private cloud. File sync, calendars, contacts.",
|
|
||||||
"icon": "/assets/img/app-icons/nextcloud.webp",
|
|
||||||
"author": "Nextcloud",
|
|
||||||
"category": "data",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/nextcloud:29",
|
|
||||||
"repoUrl": "https://github.com/nextcloud/server",
|
|
||||||
"containerConfig": {
|
|
||||||
"ports": [
|
|
||||||
"8085:80"
|
|
||||||
],
|
|
||||||
"volumes": [
|
|
||||||
"/var/lib/archipelago/nextcloud:/var/www/html"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "alby-hub",
|
|
||||||
"title": "Alby Hub",
|
|
||||||
"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 — one hub, every app pays through it.",
|
|
||||||
"icon": "/assets/img/app-icons/alby-hub.svg",
|
|
||||||
"author": "Alby",
|
|
||||||
"category": "money",
|
|
||||||
"tier": "optional",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/alby-hub:v1.24.0",
|
|
||||||
"repoUrl": "https://github.com/getAlby/hub"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "phoenixd",
|
|
||||||
"title": "phoenixd",
|
|
||||||
"version": "0.9.0",
|
|
||||||
"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",
|
|
||||||
"author": "ACINQ",
|
|
||||||
"category": "money",
|
|
||||||
"tier": "optional",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/phoenixd:0.9.0",
|
|
||||||
"repoUrl": "https://github.com/ACINQ/phoenixd"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "cuprate",
|
|
||||||
"title": "Cuprate",
|
|
||||||
"version": "0.1.0-preview",
|
|
||||||
"description": "Alternative Monero node implementation in Rust. Independently validates Monero consensus rules, providing a layer of security and redundancy for the network.",
|
|
||||||
"icon": "/assets/img/app-icons/cuprate.svg",
|
|
||||||
"author": "Cuprate contributors",
|
|
||||||
"category": "money",
|
|
||||||
"tier": "optional",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/cuprate:0.1.0-preview-18-g618ff14",
|
|
||||||
"repoUrl": "https://github.com/Cuprate/cuprate"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
app:
|
||||||
|
id: adguardhome
|
||||||
|
name: AdGuard Home
|
||||||
|
version: v0.107.55
|
||||||
|
upstream:
|
||||||
|
kind: github
|
||||||
|
repo: AdguardTeam/AdGuardHome
|
||||||
|
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.
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.55
|
||||||
|
pull_policy: if-not-present
|
||||||
|
network: pasta
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
- storage: 1Gi
|
||||||
|
|
||||||
|
resources:
|
||||||
|
memory_limit: 512Mi
|
||||||
|
disk_limit: 1Gi
|
||||||
|
|
||||||
|
security:
|
||||||
|
capabilities: [NET_BIND_SERVICE]
|
||||||
|
readonly_root: false
|
||||||
|
no_new_privileges: true
|
||||||
|
network_policy: isolated
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- host: 3000
|
||||||
|
container: 3000
|
||||||
|
protocol: tcp
|
||||||
|
bind: 127.0.0.1
|
||||||
|
# open: the setup wizard and admin console carry AdGuard Home's own
|
||||||
|
# login; the gate fronts the port (TLS, header fixes) without a
|
||||||
|
# second cookie challenge.
|
||||||
|
auth: open
|
||||||
|
auth_rationale: >-
|
||||||
|
AdGuard Home enforces its own admin login on the console, and the
|
||||||
|
first-run wizard must answer before any account exists.
|
||||||
|
- host: 53
|
||||||
|
container: 53
|
||||||
|
protocol: udp
|
||||||
|
# none: plain DNS must answer every unauthenticated query from LAN
|
||||||
|
# devices — a login page in front of :53 breaks every client on the
|
||||||
|
# network by design.
|
||||||
|
auth: none
|
||||||
|
auth_rationale: >-
|
||||||
|
Plain DNS answers unauthenticated by protocol: resolvers and clients
|
||||||
|
send queries directly; a login challenge would make DNS unreachable.
|
||||||
|
- host: 53
|
||||||
|
container: 53
|
||||||
|
protocol: tcp
|
||||||
|
auth: none
|
||||||
|
auth_rationale: >-
|
||||||
|
DNS-over-TCP fallback (truncated responses, zone transfers); same
|
||||||
|
protocol-level requirement as the UDP port.
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: /var/lib/archipelago/adguardhome
|
||||||
|
target: /opt/adguardhome
|
||||||
|
options: [rw]
|
||||||
|
|
||||||
|
environment: []
|
||||||
|
|
||||||
|
health_check:
|
||||||
|
type: tcp
|
||||||
|
endpoint: localhost:3000
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
interfaces:
|
||||||
|
main:
|
||||||
|
name: Admin console
|
||||||
|
description: AdGuard Home web console
|
||||||
|
type: ui
|
||||||
|
port: 3000
|
||||||
|
protocol: http
|
||||||
|
path: /
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
author: AdGuard
|
||||||
|
category: networking
|
||||||
|
repo: https://github.com/AdguardTeam/AdGuardHome
|
||||||
|
tier: optional
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
app:
|
||||||
|
id: cryptpad
|
||||||
|
name: CryptPad
|
||||||
|
version: 2024.12.0
|
||||||
|
upstream:
|
||||||
|
kind: github
|
||||||
|
repo: cryptpad/cryptpad
|
||||||
|
description: End-to-end encrypted documents, spreadsheets, and presentations. Zero-knowledge collaboration.
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: source.archipelago-foundation.org/lfg2025/cryptpad:2024.12.0
|
||||||
|
pull_policy: if-not-present
|
||||||
|
network: pasta
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
- storage: 5Gi
|
||||||
|
|
||||||
|
resources:
|
||||||
|
memory_limit: 1Gi
|
||||||
|
disk_limit: 5Gi
|
||||||
|
|
||||||
|
security:
|
||||||
|
capabilities: []
|
||||||
|
readonly_root: false
|
||||||
|
no_new_privileges: true
|
||||||
|
network_policy: isolated
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- host: 3000
|
||||||
|
container: 3000
|
||||||
|
protocol: tcp
|
||||||
|
bind: 127.0.0.1
|
||||||
|
# gated: CryptPad is browser-only (its own per-user accounts sit on top
|
||||||
|
# of the node login, exactly like Vaultwarden), so the gate's session
|
||||||
|
# challenge costs nothing and keeps the pads behind the node login.
|
||||||
|
auth: gated
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: /var/lib/archipelago/cryptpad
|
||||||
|
target: /cryptpad/data
|
||||||
|
options: [rw]
|
||||||
|
|
||||||
|
environment: []
|
||||||
|
|
||||||
|
health_check:
|
||||||
|
type: tcp
|
||||||
|
endpoint: localhost:3000
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
interfaces:
|
||||||
|
main:
|
||||||
|
name: CryptPad
|
||||||
|
description: Encrypted collaboration suite
|
||||||
|
type: ui
|
||||||
|
port: 3000
|
||||||
|
protocol: http
|
||||||
|
path: /
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
author: XWiki SAS
|
||||||
|
category: data
|
||||||
|
icon: /assets/icon/favico-black-v2.svg
|
||||||
|
repo: https://github.com/cryptpad/cryptpad
|
||||||
|
tier: optional
|
||||||
@@ -15,11 +15,6 @@ app:
|
|||||||
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.
|
||||||
category: money
|
category: money
|
||||||
|
|
||||||
metadata:
|
|
||||||
icon: /assets/img/app-icons/cuprate.svg
|
|
||||||
repo: https://github.com/Cuprate/cuprate
|
|
||||||
tier: optional
|
|
||||||
|
|
||||||
container:
|
container:
|
||||||
# Built from the upstream Dockerfile at the tip of main, 18 commits past
|
# Built from the upstream Dockerfile at the tip of main, 18 commits past
|
||||||
# the cuprated-0.1.0-preview tag (commit 618ff14, 2026-08-19) — there is
|
# the cuprated-0.1.0-preview tag (commit 618ff14, 2026-08-19) — there is
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
app:
|
||||||
|
id: nginx-proxy-manager
|
||||||
|
name: Nginx Proxy Manager
|
||||||
|
version: 2.12.1
|
||||||
|
upstream:
|
||||||
|
kind: github
|
||||||
|
repo: NginxProxyManager/nginx-proxy-manager
|
||||||
|
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).
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: source.archipelago-foundation.org/lfg2025/nginx-proxy-manager:latest
|
||||||
|
pull_policy: if-not-present
|
||||||
|
network: pasta
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
- storage: 1Gi
|
||||||
|
|
||||||
|
resources:
|
||||||
|
memory_limit: 512Mi
|
||||||
|
disk_limit: 1Gi
|
||||||
|
|
||||||
|
security:
|
||||||
|
capabilities: [CHOWN, SETUID, SETGID, DAC_OVERRIDE]
|
||||||
|
readonly_root: false
|
||||||
|
no_new_privileges: true
|
||||||
|
network_policy: isolated
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- host: 8081
|
||||||
|
container: 81
|
||||||
|
protocol: tcp
|
||||||
|
bind: 127.0.0.1
|
||||||
|
# open, not gated: NPM carries a complete admin login of its own. The
|
||||||
|
# gate still fronts the port (TLS on the same port, header fixes, retry
|
||||||
|
# page, Tor) without putting a cookie challenge in front of it.
|
||||||
|
auth: open
|
||||||
|
auth_rationale: >-
|
||||||
|
Nginx Proxy Manager enforces its own admin account on every page;
|
||||||
|
the initial setup wizard also has to answer before any account exists.
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: /var/lib/archipelago/nginx-proxy-manager
|
||||||
|
target: /data
|
||||||
|
options: [rw]
|
||||||
|
|
||||||
|
environment: []
|
||||||
|
|
||||||
|
health_check:
|
||||||
|
type: tcp
|
||||||
|
endpoint: localhost:81
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
interfaces:
|
||||||
|
main:
|
||||||
|
name: Admin UI
|
||||||
|
description: Nginx Proxy Manager admin interface
|
||||||
|
type: ui
|
||||||
|
port: 8081
|
||||||
|
protocol: http
|
||||||
|
path: /
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
author: Nginx Proxy Manager
|
||||||
|
category: networking
|
||||||
|
icon: /assets/img/app-icons/nginx.svg
|
||||||
|
repo: https://github.com/NginxProxyManager/nginx-proxy-manager
|
||||||
|
tier: optional
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
app:
|
||||||
|
id: ollama
|
||||||
|
name: Ollama
|
||||||
|
version: 0.5.4
|
||||||
|
upstream:
|
||||||
|
kind: github
|
||||||
|
repo: ollama/ollama
|
||||||
|
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.
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: source.archipelago-foundation.org/lfg2025/ollama:latest
|
||||||
|
pull_policy: if-not-present
|
||||||
|
network: pasta
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
- storage: 50Gi
|
||||||
|
|
||||||
|
resources:
|
||||||
|
memory_limit: 0
|
||||||
|
disk_limit: 50Gi
|
||||||
|
|
||||||
|
security:
|
||||||
|
capabilities: []
|
||||||
|
readonly_root: false
|
||||||
|
no_new_privileges: true
|
||||||
|
network_policy: isolated
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- host: 11434
|
||||||
|
container: 11434
|
||||||
|
protocol: tcp
|
||||||
|
# local: Ollama's REST API is consumed by the node's own assistant over
|
||||||
|
# loopback — never externally reachable, so no gate, no TLS, and no
|
||||||
|
# login surface exist at all.
|
||||||
|
bind: 127.0.0.1
|
||||||
|
auth: local
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: /var/lib/archipelago/ollama
|
||||||
|
target: /root/.ollama
|
||||||
|
options: [rw]
|
||||||
|
|
||||||
|
environment: []
|
||||||
|
|
||||||
|
health_check:
|
||||||
|
type: tcp
|
||||||
|
endpoint: localhost:11434
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
author: Ollama
|
||||||
|
category: community
|
||||||
|
icon: /assets/img/app-icons/ollama.png
|
||||||
|
repo: https://github.com/ollama/ollama
|
||||||
|
tier: optional
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
app:
|
||||||
|
id: tailscale
|
||||||
|
name: Tailscale
|
||||||
|
version: 1.78.0
|
||||||
|
upstream:
|
||||||
|
kind: github
|
||||||
|
repo: tailscale/tailscale
|
||||||
|
description: Zero-config VPN with WireGuard mesh networking.
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: source.archipelago-foundation.org/lfg2025/tailscale:stable
|
||||||
|
pull_policy: if-not-present
|
||||||
|
network: pasta
|
||||||
|
# Mirrors the legacy curated install exactly: tailscaled in userspace
|
||||||
|
# networking (no host TUN device needed — the rootless container cannot
|
||||||
|
# have one anyway), then `tailscale web` serving the console on :8240 as
|
||||||
|
# plain HTTP the app gate can front (TLS on the same port via the node
|
||||||
|
# certificate, framing-header fixes, retry page, Tor).
|
||||||
|
entrypoint: ["sh", "-c", "tailscaled --tun=userspace-networking & for i in $(seq 1 30); do [ -S /var/run/tailscale/tailscaled.sock ] && break; sleep 1; done; tailscale web --listen 0.0.0.0:8240 & wait"]
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
- storage: 1Gi
|
||||||
|
|
||||||
|
resources:
|
||||||
|
memory_limit: 512Mi
|
||||||
|
disk_limit: 1Gi
|
||||||
|
|
||||||
|
security:
|
||||||
|
capabilities: []
|
||||||
|
readonly_root: false
|
||||||
|
no_new_privileges: true
|
||||||
|
network_policy: isolated
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- host: 8240
|
||||||
|
container: 8240
|
||||||
|
protocol: tcp
|
||||||
|
bind: 127.0.0.1
|
||||||
|
# open, not gated: the web console requires the tailnet's own login for
|
||||||
|
# every administrative action — the gate fronts the port without adding
|
||||||
|
# a second login in front of it.
|
||||||
|
auth: open
|
||||||
|
auth_rationale: >-
|
||||||
|
Tailscale's web console authenticates against the tailnet account for
|
||||||
|
all administrative actions; the node's cookie challenge would be a
|
||||||
|
second, redundant login.
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: /var/lib/archipelago/tailscale
|
||||||
|
target: /var/lib/tailscale
|
||||||
|
options: [rw]
|
||||||
|
|
||||||
|
environment:
|
||||||
|
- TS_STATE_DIR=/var/lib/tailscale
|
||||||
|
|
||||||
|
health_check:
|
||||||
|
type: tcp
|
||||||
|
endpoint: localhost:8240
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
interfaces:
|
||||||
|
main:
|
||||||
|
name: Web console
|
||||||
|
description: Tailscale web console
|
||||||
|
type: ui
|
||||||
|
port: 8240
|
||||||
|
protocol: http
|
||||||
|
path: /
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
author: Tailscale
|
||||||
|
category: networking
|
||||||
|
icon: /assets/img/app-icons/tailscale.webp
|
||||||
|
repo: https://github.com/tailscale/tailscale
|
||||||
|
tier: recommended
|
||||||
@@ -150,7 +150,9 @@ impl ApiHandler {
|
|||||||
// appears immediately, without a frontend release. The old external UI
|
// appears immediately, without a frontend release. The old external UI
|
||||||
// catalog below is emergency compatibility only; it must never override
|
// catalog below is emergency compatibility only; it must never override
|
||||||
// a healthy signed catalog (Cuprate was invisible for exactly that reason).
|
// a healthy signed catalog (Cuprate was invisible for exactly that reason).
|
||||||
if let Ok(body) = crate::container::app_catalog::verified_catalog_body(&self.config.data_dir).await {
|
if let Ok(body) =
|
||||||
|
crate::container::app_catalog::verified_catalog_body(&self.config.data_dir).await
|
||||||
|
{
|
||||||
return Ok(Response::builder()
|
return Ok(Response::builder()
|
||||||
.status(hyper::StatusCode::OK)
|
.status(hyper::StatusCode::OK)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
|
|||||||
@@ -207,7 +207,9 @@ pub async fn verified_catalog_body(data_dir: &Path) -> anyhow::Result<String> {
|
|||||||
let raw: serde_json::Value = serde_json::from_str(&body)?;
|
let raw: serde_json::Value = serde_json::from_str(&body)?;
|
||||||
match crate::trust::verify_detached(&raw)? {
|
match crate::trust::verify_detached(&raw)? {
|
||||||
crate::trust::SignatureStatus::Verified { anchored: true, .. } => Ok(body),
|
crate::trust::SignatureStatus::Verified { anchored: true, .. } => Ok(body),
|
||||||
crate::trust::SignatureStatus::Verified { anchored: false, .. } => {
|
crate::trust::SignatureStatus::Verified {
|
||||||
|
anchored: false, ..
|
||||||
|
} => {
|
||||||
anyhow::bail!("app catalog signer is not anchored to the release root")
|
anyhow::bail!("app catalog signer is not anchored to the release root")
|
||||||
}
|
}
|
||||||
crate::trust::SignatureStatus::Unsigned => anyhow::bail!("app catalog is unsigned"),
|
crate::trust::SignatureStatus::Unsigned => anyhow::bail!("app catalog is unsigned"),
|
||||||
@@ -670,7 +672,11 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn verified_catalog_body_rejects_unsigned_cache() {
|
async fn verified_catalog_body_rejects_unsigned_cache() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
write_cache(dir.path(), r#"{"schema":1,"apps":{"demo":{"version":"1"}}}"#).unwrap();
|
write_cache(
|
||||||
|
dir.path(),
|
||||||
|
r#"{"schema":1,"apps":{"demo":{"version":"1"}}}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let err = verified_catalog_body(dir.path()).await.unwrap_err();
|
let err = verified_catalog_body(dir.path()).await.unwrap_err();
|
||||||
assert!(err.to_string().contains("unsigned"));
|
assert!(err.to_string().contains("unsigned"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,18 +178,10 @@ fn image_var_for_app(app_id: &str) -> Option<&'static str> {
|
|||||||
|
|
||||||
// Nostr / VPN
|
// Nostr / VPN
|
||||||
"nostr-rs-relay" => Some("NOSTR_RS_RELAY_IMAGE"),
|
"nostr-rs-relay" => Some("NOSTR_RS_RELAY_IMAGE"),
|
||||||
"nostr-vpn" => Some("NOSTR_VPN_IMAGE"),
|
|
||||||
"fips" => Some("FIPS_IMAGE"),
|
|
||||||
|
|
||||||
// Immich (primary = server)
|
// Immich (primary = server)
|
||||||
"immich" | "immich_server" => Some("IMMICH_SERVER_IMAGE"),
|
"immich" | "immich_server" => Some("IMMICH_SERVER_IMAGE"),
|
||||||
|
|
||||||
// Penpot (primary = frontend)
|
|
||||||
"penpot" | "penpot-frontend" => Some("PENPOT_FRONTEND_IMAGE"),
|
|
||||||
|
|
||||||
// AI
|
|
||||||
"routstr" => Some("ROUTSTR_IMAGE"),
|
|
||||||
|
|
||||||
// Networking
|
// Networking
|
||||||
"adguardhome" => Some("ADGUARDHOME_IMAGE"),
|
"adguardhome" => Some("ADGUARDHOME_IMAGE"),
|
||||||
"tor" | "archy-tor" => Some("ALPINE_TOR_IMAGE"),
|
"tor" | "archy-tor" => Some("ALPINE_TOR_IMAGE"),
|
||||||
@@ -341,13 +333,6 @@ pub fn containers_for_stack(app_id: &str) -> Vec<(&'static str, &'static str)> {
|
|||||||
("immich_redis", "REDIS_IMAGE"),
|
("immich_redis", "REDIS_IMAGE"),
|
||||||
("immich_server", "IMMICH_SERVER_IMAGE"),
|
("immich_server", "IMMICH_SERVER_IMAGE"),
|
||||||
],
|
],
|
||||||
"penpot" | "penpot-frontend" => vec![
|
|
||||||
("penpot-postgres", "PENPOT_POSTGRES_IMAGE"),
|
|
||||||
("penpot-valkey", "PENPOT_VALKEY_IMAGE"),
|
|
||||||
("penpot-backend", "PENPOT_BACKEND_IMAGE"),
|
|
||||||
("penpot-exporter", "PENPOT_EXPORTER_IMAGE"),
|
|
||||||
("penpot-frontend", "PENPOT_FRONTEND_IMAGE"),
|
|
||||||
],
|
|
||||||
"netbird" => vec![
|
"netbird" => vec![
|
||||||
("netbird", "NETBIRD_PROXY_IMAGE"),
|
("netbird", "NETBIRD_PROXY_IMAGE"),
|
||||||
("netbird-dashboard", "NETBIRD_DASHBOARD_IMAGE"),
|
("netbird-dashboard", "NETBIRD_DASHBOARD_IMAGE"),
|
||||||
|
|||||||
@@ -1746,6 +1746,11 @@ app:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
exempt.sort();
|
exempt.sort();
|
||||||
|
// 30 as of 2026-08-31: the 28 below plus adguardhome's two DNS ports
|
||||||
|
// (53 udp + tcp) — plain DNS answers unauthenticated by protocol, the
|
||||||
|
// same reason router's mDNS/SSDP and every p2p port is exempt; each
|
||||||
|
// carries its auth_rationale in the manifest.
|
||||||
|
//
|
||||||
// 28 as of 2026-08-23: the 26 below plus cuprate's two exemptions —
|
// 28 as of 2026-08-23: the 26 below plus cuprate's two exemptions —
|
||||||
// 18183 (Monero p2p gossip, same reasoning as bitcoin's 8333) and
|
// 18183 (Monero p2p gossip, same reasoning as bitcoin's 8333) and
|
||||||
// 18090 (host mapping for Monero's canonical 18089 restricted RPC,
|
// 18090 (host mapping for Monero's canonical 18089 restricted RPC,
|
||||||
@@ -1771,7 +1776,7 @@ app:
|
|||||||
// stage timed out that cycle, so the count here lagged at 17.
|
// stage timed out that cycle, so the count here lagged at 17.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
exempt.len(),
|
exempt.len(),
|
||||||
28,
|
30,
|
||||||
"unauthenticated port set changed — review before updating this count: {exempt:?}"
|
"unauthenticated port set changed — review before updating this count: {exempt:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1801,15 +1806,22 @@ app:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
open.sort();
|
open.sort();
|
||||||
// Gitea 3001 (git clients speak basic-auth, not browser cookies) and
|
// Gitea 3001 (git clients speak basic-auth, not browser cookies),
|
||||||
// BTCPay 23000 (checkout/invoice/webhook endpoints must be reachable
|
// BTCPay 23000 (checkout/invoice/webhook endpoints must be reachable
|
||||||
// by anonymous payers). Both enforce their own account login, and an
|
// by anonymous payers), and — since the v1.8.7 platform round — the
|
||||||
// operator can re-gate either from Settings → Access control.
|
// three own-login consoles brought onto the manifest platform:
|
||||||
|
// nginx-proxy-manager 8081 (NPM admin accounts), tailscale 8240
|
||||||
|
// (tailnet login on the web console), adguardhome 3000 (AGH admin
|
||||||
|
// accounts + first-run wizard). All enforce their own login, and an
|
||||||
|
// operator can re-gate any of them from Settings → Access control.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
open,
|
open,
|
||||||
vec![
|
vec![
|
||||||
|
("adguardhome".to_string(), 3000u16),
|
||||||
("btcpay-server".to_string(), 23000u16),
|
("btcpay-server".to_string(), 23000u16),
|
||||||
("gitea".to_string(), 3001u16)
|
("gitea".to_string(), 3001u16),
|
||||||
|
("nginx-proxy-manager".to_string(), 8081u16),
|
||||||
|
("tailscale".to_string(), 8240u16),
|
||||||
],
|
],
|
||||||
"gate-open port set changed — every entry must be an app with its own login"
|
"gate-open port set changed — every entry must be an app with its own login"
|
||||||
);
|
);
|
||||||
|
|||||||
+402
-354
@@ -11,16 +11,47 @@
|
|||||||
},
|
},
|
||||||
"apps": [
|
"apps": [
|
||||||
{
|
{
|
||||||
"id": "bitcoin-knots",
|
"id": "adguardhome",
|
||||||
"title": "Bitcoin Knots",
|
"title": "AdGuard Home",
|
||||||
"version": "28.1.0",
|
"version": "v0.107.55",
|
||||||
"description": "Full Bitcoin Knots node with dynamic prune/full-mode startup based on host disk.",
|
"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": "/assets/img/app-icons/bitcoin-knots.webp",
|
"icon": "",
|
||||||
"author": "Bitcoin Knots",
|
"author": "AdGuard",
|
||||||
|
"category": "networking",
|
||||||
|
"tier": "optional",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.55",
|
||||||
|
"repoUrl": "https://github.com/AdguardTeam/AdGuardHome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "alby-hub",
|
||||||
|
"title": "Alby Hub",
|
||||||
|
"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.",
|
||||||
|
"icon": "/assets/img/app-icons/alby-hub.svg",
|
||||||
|
"author": "Alby",
|
||||||
"category": "money",
|
"category": "money",
|
||||||
"tier": "core",
|
"tier": "optional",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260210",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/alby-hub:v1.24.0",
|
||||||
"repoUrl": "https://github.com/bitcoinknots/bitcoin"
|
"repoUrl": "https://github.com/getAlby/hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "barkd",
|
||||||
|
"title": "Ark Wallet",
|
||||||
|
"version": "0.3.0",
|
||||||
|
"description": "Ark protocol wallet daemon (barkd). Lets the node hold self-custodial off-chain bitcoin via an Ark server; the wallet talks to it over a local REST API. Signet by default while Ark matures.",
|
||||||
|
"icon": "/assets/img/app-icons/bark.png",
|
||||||
|
"author": "Second",
|
||||||
|
"category": "money",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/barkd:0.3.0",
|
||||||
|
"repoUrl": "https://gitlab.com/ark-bitcoin/bark",
|
||||||
|
"containerConfig": {
|
||||||
|
"ports": [
|
||||||
|
"3535:3535"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/archipelago/barkd:/data"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "bitcoin-core",
|
"id": "bitcoin-core",
|
||||||
@@ -35,76 +66,16 @@
|
|||||||
"repoUrl": "https://github.com/bitcoin/bitcoin"
|
"repoUrl": "https://github.com/bitcoin/bitcoin"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "lnd",
|
"id": "bitcoin-knots",
|
||||||
"title": "LND",
|
"title": "Bitcoin Knots",
|
||||||
"version": "0.18.4",
|
"version": "28.1.0",
|
||||||
"description": "Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.",
|
"description": "Full Bitcoin Knots node with dynamic prune/full-mode startup based on host disk.",
|
||||||
"icon": "/assets/img/app-icons/lnd.png",
|
"icon": "/assets/img/app-icons/bitcoin-knots.webp",
|
||||||
"author": "Lightning Labs",
|
"author": "Bitcoin Knots",
|
||||||
"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/bitcoin-knots:29.3.knots20260210",
|
||||||
"repoUrl": "https://github.com/lightningnetwork/lnd",
|
"repoUrl": "https://github.com/bitcoinknots/bitcoin"
|
||||||
"requires": [
|
|
||||||
"bitcoin-knots"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "btcpay-server",
|
|
||||||
"title": "BTCPay Server",
|
|
||||||
"version": "2.4.3",
|
|
||||||
"description": "Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries.",
|
|
||||||
"icon": "/assets/img/app-icons/btcpay-server.png",
|
|
||||||
"author": "BTCPay Server Foundation",
|
|
||||||
"category": "commerce",
|
|
||||||
"tier": "core",
|
|
||||||
"dockerImage": "docker.io/btcpayserver/btcpayserver:2.4.3",
|
|
||||||
"repoUrl": "https://github.com/btcpayserver/btcpayserver",
|
|
||||||
"requires": [
|
|
||||||
"bitcoin-knots"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "mempool",
|
|
||||||
"title": "Mempool Explorer",
|
|
||||||
"version": "3.0.0",
|
|
||||||
"description": "Bitcoin mempool and blockchain explorer. Real-time transaction and block visualization.",
|
|
||||||
"icon": "/assets/img/app-icons/mempool.webp",
|
|
||||||
"author": "Mempool",
|
|
||||||
"category": "money",
|
|
||||||
"tier": "core",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.3.1",
|
|
||||||
"repoUrl": "https://github.com/mempool/mempool",
|
|
||||||
"requires": [
|
|
||||||
"bitcoin-knots",
|
|
||||||
"electrumx"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "electrumx",
|
|
||||||
"title": "ElectrumX",
|
|
||||||
"version": "1.18.0",
|
|
||||||
"description": "Electrum server indexing Bitcoin chain data for lightweight wallet queries.",
|
|
||||||
"icon": "/assets/img/app-icons/electrumx.png",
|
|
||||||
"author": "Luke Childs",
|
|
||||||
"category": "money",
|
|
||||||
"tier": "core",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/electrumx:v1.18.0",
|
|
||||||
"repoUrl": "https://github.com/spesmilo/electrumx",
|
|
||||||
"requires": [
|
|
||||||
"bitcoin-knots"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "indeedhub",
|
|
||||||
"title": "IndeeHub",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Bitcoin documentary streaming platform featuring God Bless Bitcoin and other educational content about Bitcoin, sovereignty, and decentralized technology. Sign in with your Nostr identity.",
|
|
||||||
"icon": "/assets/img/app-icons/indeedhub.png",
|
|
||||||
"author": "IndeeHub",
|
|
||||||
"category": "community",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/indeedhub:1.0.0",
|
|
||||||
"repoUrl": "https://github.com/indeedhub/indeedhub"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "botfights",
|
"id": "botfights",
|
||||||
@@ -132,127 +103,58 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gitea",
|
"id": "btcpay-server",
|
||||||
"title": "Gitea",
|
"title": "BTCPay Server",
|
||||||
"version": "1.23",
|
"version": "2.4.3",
|
||||||
"description": "Self-hosted Git service with built-in container registry, CI/CD, and package hosting.",
|
"description": "Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries.",
|
||||||
"icon": "/assets/img/app-icons/gitea.svg",
|
"icon": "/assets/img/app-icons/btcpay-server.png",
|
||||||
"author": "Gitea",
|
"author": "BTCPay Server Foundation",
|
||||||
"category": "development",
|
"category": "commerce",
|
||||||
"dockerImage": "docker.io/gitea/gitea:1.23",
|
|
||||||
"repoUrl": "https://gitea.com",
|
|
||||||
"containerConfig": {
|
|
||||||
"ports": [
|
|
||||||
"3001:3000",
|
|
||||||
"2222:22"
|
|
||||||
],
|
|
||||||
"volumes": [
|
|
||||||
"/var/lib/archipelago/gitea/data:/data",
|
|
||||||
"/var/lib/archipelago/gitea/config:/etc/gitea"
|
|
||||||
],
|
|
||||||
"env": [
|
|
||||||
"GITEA__database__DB_TYPE=sqlite3",
|
|
||||||
"GITEA__server__SSH_PORT=2222",
|
|
||||||
"GITEA__server__SSH_LISTEN_PORT=22",
|
|
||||||
"GITEA__server__LFS_START_SERVER=true",
|
|
||||||
"GITEA__packages__ENABLED=true",
|
|
||||||
"GITEA__repository__ENABLE_PUSH_CREATE_USER=true",
|
|
||||||
"GITEA__repository__ENABLE_PUSH_CREATE_ORG=true",
|
|
||||||
"GITEA__security__X_FRAME_OPTIONS="
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"tier": "optional"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "filebrowser",
|
|
||||||
"title": "File Browser",
|
|
||||||
"version": "2.27.0",
|
|
||||||
"description": "Baseline Archipelago file manager service.",
|
|
||||||
"icon": "/assets/img/app-icons/file-browser.webp",
|
|
||||||
"author": "File Browser",
|
|
||||||
"category": "data",
|
|
||||||
"tier": "core",
|
"tier": "core",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/filebrowser:v2.27.0",
|
"dockerImage": "docker.io/btcpayserver/btcpayserver:2.4.3",
|
||||||
"repoUrl": "https://github.com/filebrowser/filebrowser",
|
"repoUrl": "https://github.com/btcpayserver/btcpayserver",
|
||||||
"containerConfig": {
|
"requires": [
|
||||||
"ports": [
|
"bitcoin-knots"
|
||||||
"8083:80"
|
]
|
||||||
],
|
|
||||||
"volumes": [
|
|
||||||
"/var/lib/archipelago/filebrowser:/srv",
|
|
||||||
"/var/lib/archipelago/filebrowser-data:/data"
|
|
||||||
],
|
|
||||||
"args": [
|
|
||||||
"--database=/data/database.db",
|
|
||||||
"--root=/srv",
|
|
||||||
"--address=0.0.0.0",
|
|
||||||
"--port=80"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nostr-rs-relay",
|
"id": "cryptpad",
|
||||||
"title": "Nostr Relay (Rust)",
|
"title": "CryptPad",
|
||||||
"version": "0.10.0",
|
"version": "2024.12.0",
|
||||||
"description": "High-performance Nostr relay written in Rust. Host your own decentralized social media relay and earn networking profits.",
|
"description": "End-to-end encrypted documents, spreadsheets, and presentations. Zero-knowledge collaboration.",
|
||||||
"icon": "/assets/img/app-icons/nostrudel.svg",
|
"icon": "/assets/icon/favico-black-v2.svg",
|
||||||
"author": "Nostr RS Relay",
|
"author": "XWiki SAS",
|
||||||
"category": "community",
|
|
||||||
"tier": "recommended",
|
|
||||||
"dockerImage": "scsibug/nostr-rs-relay:0.10.0",
|
|
||||||
"repoUrl": "https://github.com/scsibug/nostr-rs-relay",
|
|
||||||
"containerConfig": {
|
|
||||||
"ports": [
|
|
||||||
"8081:8080"
|
|
||||||
],
|
|
||||||
"volumes": [
|
|
||||||
"/var/lib/archipelago/nostr-relay:/usr/src/app/db"
|
|
||||||
],
|
|
||||||
"env": [
|
|
||||||
"RELAY_NAME=Archipelago Nostr Relay",
|
|
||||||
"RELAY_DESCRIPTION=Self-hosted Nostr relay on Archipelago"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "vaultwarden",
|
|
||||||
"title": "Vaultwarden",
|
|
||||||
"version": "1.30.0",
|
|
||||||
"description": "Self-hosted password vault with zero-knowledge encryption.",
|
|
||||||
"icon": "/assets/img/app-icons/vaultwarden.webp",
|
|
||||||
"author": "Vaultwarden",
|
|
||||||
"category": "data",
|
"category": "data",
|
||||||
"tier": "recommended",
|
"tier": "optional",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/vaultwarden:1.37.1-alpine",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/cryptpad:2024.12.0",
|
||||||
"repoUrl": "https://github.com/dani-garcia/vaultwarden",
|
"repoUrl": "https://github.com/cryptpad/cryptpad"
|
||||||
"containerConfig": {
|
|
||||||
"ports": [
|
|
||||||
"8082:80"
|
|
||||||
],
|
|
||||||
"volumes": [
|
|
||||||
"/var/lib/archipelago/vaultwarden:/data"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "searxng",
|
"id": "cuprate",
|
||||||
"title": "SearXNG",
|
"title": "Cuprate",
|
||||||
"version": "1.0.0",
|
"version": "0.1.0-preview",
|
||||||
"description": "Privacy-respecting metasearch engine. Search the web without tracking.",
|
"description": "Alternative Monero node implementation in Rust. Independently validates Monero consensus rules, providing a layer of security and redundancy for the network.",
|
||||||
"icon": "/assets/img/app-icons/searxng.png",
|
"icon": "/assets/img/app-icons/cuprate.svg",
|
||||||
"author": "SearXNG",
|
"author": "Cuprate contributors",
|
||||||
"category": "data",
|
"category": "money",
|
||||||
"tier": "recommended",
|
"tier": "optional",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/searxng:latest",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/cuprate:0.1.0-preview-18-g618ff14",
|
||||||
"repoUrl": "https://github.com/searxng/searxng",
|
"repoUrl": "https://github.com/Cuprate/cuprate"
|
||||||
"containerConfig": {
|
},
|
||||||
"ports": [
|
{
|
||||||
"8888:8080"
|
"id": "electrumx",
|
||||||
],
|
"title": "ElectrumX",
|
||||||
"volumes": [
|
"version": "1.18.0",
|
||||||
"/var/lib/archipelago/searxng:/etc/searxng"
|
"description": "Electrum server indexing Bitcoin chain data for lightweight wallet queries.",
|
||||||
]
|
"icon": "/assets/img/app-icons/electrumx.png",
|
||||||
}
|
"author": "Luke Childs",
|
||||||
|
"category": "money",
|
||||||
|
"tier": "core",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/electrumx:v1.18.0",
|
||||||
|
"repoUrl": "https://github.com/spesmilo/electrumx",
|
||||||
|
"requires": [
|
||||||
|
"bitcoin-knots"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "fedimint",
|
"id": "fedimint",
|
||||||
@@ -299,54 +201,87 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "barkd",
|
"id": "filebrowser",
|
||||||
"title": "Ark Wallet",
|
"title": "File Browser",
|
||||||
"version": "0.3.0",
|
"version": "2.27.0",
|
||||||
"description": "Ark protocol wallet daemon (barkd). Lets the node hold self-custodial off-chain bitcoin via an Ark server; the wallet talks to it over a local REST API. Signet by default while Ark matures.",
|
"description": "Baseline Archipelago file manager service.",
|
||||||
"icon": "/assets/img/app-icons/bark.png",
|
"icon": "/assets/img/app-icons/file-browser.webp",
|
||||||
"author": "Second",
|
"author": "File Browser",
|
||||||
"category": "money",
|
"category": "data",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/barkd:0.3.0",
|
"tier": "core",
|
||||||
"repoUrl": "https://gitlab.com/ark-bitcoin/bark",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/filebrowser:v2.27.0",
|
||||||
|
"repoUrl": "https://github.com/filebrowser/filebrowser",
|
||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"ports": [
|
"ports": [
|
||||||
"3535:3535"
|
"8083:80"
|
||||||
],
|
],
|
||||||
"volumes": [
|
"volumes": [
|
||||||
"/var/lib/archipelago/barkd:/data"
|
"/var/lib/archipelago/filebrowser:/srv",
|
||||||
|
"/var/lib/archipelago/filebrowser-data:/data"
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
"--database=/data/database.db",
|
||||||
|
"--root=/srv",
|
||||||
|
"--address=0.0.0.0",
|
||||||
|
"--port=80"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "jellyfin",
|
"id": "gitea",
|
||||||
"title": "Jellyfin",
|
"title": "Gitea",
|
||||||
"version": "10.8.13",
|
"version": "1.23",
|
||||||
"description": "Free media server. Stream movies, music, and photos.",
|
"description": "Self-hosted Git service with built-in container registry, CI/CD, and package hosting.",
|
||||||
"icon": "/assets/img/app-icons/jellyfin.webp",
|
"icon": "/assets/img/app-icons/gitea.svg",
|
||||||
"author": "Jellyfin",
|
"author": "Gitea",
|
||||||
"category": "data",
|
"category": "development",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/jellyfin:10.11.11",
|
"dockerImage": "docker.io/gitea/gitea:1.23",
|
||||||
"repoUrl": "https://github.com/jellyfin/jellyfin",
|
"repoUrl": "https://gitea.com",
|
||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"ports": [
|
"ports": [
|
||||||
"8096:8096"
|
"3001:3000",
|
||||||
|
"2222:22"
|
||||||
],
|
],
|
||||||
"volumes": [
|
"volumes": [
|
||||||
"/var/lib/archipelago/jellyfin/config:/config",
|
"/var/lib/archipelago/gitea/data:/data",
|
||||||
"/var/lib/archipelago/jellyfin/cache:/cache"
|
"/var/lib/archipelago/gitea/config:/etc/gitea"
|
||||||
|
],
|
||||||
|
"env": [
|
||||||
|
"GITEA__database__DB_TYPE=sqlite3",
|
||||||
|
"GITEA__server__SSH_PORT=2222",
|
||||||
|
"GITEA__server__SSH_LISTEN_PORT=22",
|
||||||
|
"GITEA__server__LFS_START_SERVER=true",
|
||||||
|
"GITEA__packages__ENABLED=true",
|
||||||
|
"GITEA__repository__ENABLE_PUSH_CREATE_USER=true",
|
||||||
|
"GITEA__repository__ENABLE_PUSH_CREATE_ORG=true",
|
||||||
|
"GITEA__security__X_FRAME_OPTIONS="
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"tier": "optional"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "immich",
|
"id": "grafana",
|
||||||
"title": "Immich",
|
"title": "Grafana",
|
||||||
"version": "2.7.4",
|
"version": "10.2.0",
|
||||||
"description": "Self-hosted photo and video backup with mobile apps and search.",
|
"description": "Analytics and monitoring platform. Visualize metrics and create dashboards.",
|
||||||
"icon": "/assets/img/app-icons/immich.png",
|
"icon": "/assets/img/app-icons/grafana.png",
|
||||||
"author": "Immich",
|
"author": "Grafana Labs",
|
||||||
"category": "data",
|
"category": "data",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/immich-server:release",
|
"tier": "recommended",
|
||||||
"repoUrl": "https://github.com/immich-app/immich"
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/grafana:10.2.0",
|
||||||
|
"repoUrl": "https://github.com/grafana/grafana",
|
||||||
|
"containerConfig": {
|
||||||
|
"ports": [
|
||||||
|
"3000:3000"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/archipelago/grafana:/var/lib/grafana"
|
||||||
|
],
|
||||||
|
"env": [
|
||||||
|
"GF_PATHS_DATA=/var/lib/grafana",
|
||||||
|
"GF_USERS_ALLOW_SIGN_UP=false"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "homeassistant",
|
"id": "homeassistant",
|
||||||
@@ -370,11 +305,209 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "immich",
|
||||||
|
"title": "Immich",
|
||||||
|
"version": "2.7.4",
|
||||||
|
"description": "Self-hosted photo and video backup with mobile apps and search.",
|
||||||
|
"icon": "/assets/img/app-icons/immich.png",
|
||||||
|
"author": "Immich",
|
||||||
|
"category": "data",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/immich-server:release",
|
||||||
|
"repoUrl": "https://github.com/immich-app/immich"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "indeedhub",
|
||||||
|
"title": "IndeeHub",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Bitcoin documentary streaming platform featuring God Bless Bitcoin and other educational content about Bitcoin, sovereignty, and decentralized technology. Sign in with your Nostr identity.",
|
||||||
|
"icon": "/assets/img/app-icons/indeedhub.png",
|
||||||
|
"author": "IndeeHub",
|
||||||
|
"category": "community",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/indeedhub:1.0.0",
|
||||||
|
"repoUrl": "https://github.com/indeedhub/indeedhub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "jellyfin",
|
||||||
|
"title": "Jellyfin",
|
||||||
|
"version": "10.8.13",
|
||||||
|
"description": "Free media server. Stream movies, music, and photos.",
|
||||||
|
"icon": "/assets/img/app-icons/jellyfin.webp",
|
||||||
|
"author": "Jellyfin",
|
||||||
|
"category": "data",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/jellyfin:10.11.11",
|
||||||
|
"repoUrl": "https://github.com/jellyfin/jellyfin",
|
||||||
|
"containerConfig": {
|
||||||
|
"ports": [
|
||||||
|
"8096:8096"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/archipelago/jellyfin/config:/config",
|
||||||
|
"/var/lib/archipelago/jellyfin/cache:/cache"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "lnd",
|
||||||
|
"title": "LND",
|
||||||
|
"version": "0.18.4",
|
||||||
|
"description": "Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.",
|
||||||
|
"icon": "/assets/img/app-icons/lnd.png",
|
||||||
|
"author": "Lightning Labs",
|
||||||
|
"category": "money",
|
||||||
|
"tier": "core",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta",
|
||||||
|
"repoUrl": "https://github.com/lightningnetwork/lnd",
|
||||||
|
"requires": [
|
||||||
|
"bitcoin-knots"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mempool",
|
||||||
|
"title": "Mempool Explorer",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"description": "Bitcoin mempool and blockchain explorer. Real-time transaction and block visualization.",
|
||||||
|
"icon": "/assets/img/app-icons/mempool.webp",
|
||||||
|
"author": "Mempool",
|
||||||
|
"category": "money",
|
||||||
|
"tier": "core",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.3.1",
|
||||||
|
"repoUrl": "https://github.com/mempool/mempool",
|
||||||
|
"requires": [
|
||||||
|
"bitcoin-knots",
|
||||||
|
"electrumx"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "netbird",
|
||||||
|
"title": "NetBird",
|
||||||
|
"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.",
|
||||||
|
"icon": "/assets/img/app-icons/netbird.svg",
|
||||||
|
"author": "NetBird",
|
||||||
|
"category": "networking",
|
||||||
|
"tier": "recommended",
|
||||||
|
"dockerImage": "docker.io/library/nginx:1.31.4-alpine",
|
||||||
|
"repoUrl": "https://github.com/netbirdio/netbird",
|
||||||
|
"containerConfig": {
|
||||||
|
"ports": [
|
||||||
|
"8087:80",
|
||||||
|
"8086:80",
|
||||||
|
"3478:3478/udp"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/archipelago/netbird:/var/lib/netbird"
|
||||||
|
],
|
||||||
|
"notes": "Installed as a two-container stack: netbird dashboard on 8087 and netbird-server control plane on 8086 plus UDP 3478. For production clients, publish a DNS name over HTTPS with gRPC/WebSocket routing."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nextcloud",
|
||||||
|
"title": "Nextcloud",
|
||||||
|
"version": "29",
|
||||||
|
"description": "Your own private cloud. File sync, calendars, contacts.",
|
||||||
|
"icon": "/assets/img/app-icons/nextcloud.webp",
|
||||||
|
"author": "Nextcloud",
|
||||||
|
"category": "data",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/nextcloud:29",
|
||||||
|
"repoUrl": "https://github.com/nextcloud/server",
|
||||||
|
"containerConfig": {
|
||||||
|
"ports": [
|
||||||
|
"8085:80"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/archipelago/nextcloud:/var/www/html"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nginx-proxy-manager",
|
||||||
|
"title": "Nginx Proxy Manager",
|
||||||
|
"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).",
|
||||||
|
"icon": "/assets/img/app-icons/nginx.svg",
|
||||||
|
"author": "Nginx Proxy Manager",
|
||||||
|
"category": "networking",
|
||||||
|
"tier": "optional",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/nginx-proxy-manager:latest",
|
||||||
|
"repoUrl": "https://github.com/NginxProxyManager/nginx-proxy-manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nostr-rs-relay",
|
||||||
|
"title": "Nostr Relay (Rust)",
|
||||||
|
"version": "0.10.0",
|
||||||
|
"description": "High-performance Nostr relay written in Rust. Host your own decentralized social media relay and earn networking profits.",
|
||||||
|
"icon": "/assets/img/app-icons/nostrudel.svg",
|
||||||
|
"author": "Nostr RS Relay",
|
||||||
|
"category": "community",
|
||||||
|
"tier": "recommended",
|
||||||
|
"dockerImage": "scsibug/nostr-rs-relay:0.10.0",
|
||||||
|
"repoUrl": "https://github.com/scsibug/nostr-rs-relay",
|
||||||
|
"containerConfig": {
|
||||||
|
"ports": [
|
||||||
|
"8081:8080"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/archipelago/nostr-relay:/usr/src/app/db"
|
||||||
|
],
|
||||||
|
"env": [
|
||||||
|
"RELAY_NAME=Archipelago Nostr Relay",
|
||||||
|
"RELAY_DESCRIPTION=Self-hosted Nostr relay on Archipelago"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ollama",
|
||||||
|
"title": "Ollama",
|
||||||
|
"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.",
|
||||||
|
"icon": "/assets/img/app-icons/ollama.png",
|
||||||
|
"author": "Ollama",
|
||||||
|
"category": "community",
|
||||||
|
"tier": "optional",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/ollama:latest",
|
||||||
|
"repoUrl": "https://github.com/ollama/ollama"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "phoenixd",
|
||||||
|
"title": "phoenixd",
|
||||||
|
"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.",
|
||||||
|
"icon": "/assets/img/app-icons/phoenixd.svg",
|
||||||
|
"author": "ACINQ",
|
||||||
|
"category": "money",
|
||||||
|
"tier": "optional",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/phoenixd:0.9.0",
|
||||||
|
"repoUrl": "https://github.com/ACINQ/phoenixd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "photoprism",
|
||||||
|
"title": "PhotoPrism",
|
||||||
|
"version": "240915",
|
||||||
|
"description": "AI-powered photo management with facial recognition.",
|
||||||
|
"icon": "/assets/img/app-icons/photoprism.svg",
|
||||||
|
"author": "PhotoPrism",
|
||||||
|
"category": "data",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/photoprism:240915",
|
||||||
|
"repoUrl": "https://github.com/photoprism/photoprism",
|
||||||
|
"containerConfig": {
|
||||||
|
"ports": [
|
||||||
|
"2342:2342"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/archipelago/photoprism:/photoprism/storage"
|
||||||
|
],
|
||||||
|
"env": [
|
||||||
|
"PHOTOPRISM_ADMIN_PASSWORD=archipelago",
|
||||||
|
"PHOTOPRISM_DEFAULT_LOCALE=en"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"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 — block height, sync, peers, Lightning balance — 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 \u2014 block height, sync, peers, Lightning balance \u2014 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",
|
||||||
@@ -382,26 +515,44 @@
|
|||||||
"repoUrl": "https://github.com/rhasspy/wyoming"
|
"repoUrl": "https://github.com/rhasspy/wyoming"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "grafana",
|
"id": "portainer",
|
||||||
"title": "Grafana",
|
"title": "Portainer",
|
||||||
"version": "10.2.0",
|
"version": "2.19.4",
|
||||||
"description": "Analytics and monitoring platform. Visualize metrics and create dashboards.",
|
"description": "Container management web UI for the local Podman socket.",
|
||||||
"icon": "/assets/img/app-icons/grafana.png",
|
"icon": "/assets/img/app-icons/portainer.webp",
|
||||||
"author": "Grafana Labs",
|
"author": "Portainer",
|
||||||
"category": "data",
|
"category": "development",
|
||||||
"tier": "recommended",
|
"tier": "optional",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/grafana:10.2.0",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/portainer:2.39.6",
|
||||||
"repoUrl": "https://github.com/grafana/grafana",
|
"repoUrl": "https://github.com/portainer/portainer",
|
||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"ports": [
|
"ports": [
|
||||||
"3000:3000"
|
"9000:9000"
|
||||||
],
|
],
|
||||||
"volumes": [
|
"volumes": [
|
||||||
"/var/lib/archipelago/grafana:/var/lib/grafana"
|
"/var/lib/archipelago/portainer:/data",
|
||||||
|
"/run/user/1000/podman/podman.sock:/var/run/docker.sock"
|
||||||
],
|
],
|
||||||
"env": [
|
"notes": "Uses the manifest-owned Podman socket bind mount preparation path."
|
||||||
"GF_PATHS_DATA=/var/lib/grafana",
|
}
|
||||||
"GF_USERS_ALLOW_SIGN_UP=false"
|
},
|
||||||
|
{
|
||||||
|
"id": "searxng",
|
||||||
|
"title": "SearXNG",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Privacy-respecting metasearch engine. Search the web without tracking.",
|
||||||
|
"icon": "/assets/img/app-icons/searxng.png",
|
||||||
|
"author": "SearXNG",
|
||||||
|
"category": "data",
|
||||||
|
"tier": "recommended",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/searxng:latest",
|
||||||
|
"repoUrl": "https://github.com/searxng/searxng",
|
||||||
|
"containerConfig": {
|
||||||
|
"ports": [
|
||||||
|
"8888:8080"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/var/lib/archipelago/searxng:/etc/searxng"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -433,51 +584,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "portainer",
|
|
||||||
"title": "Portainer",
|
|
||||||
"version": "2.19.4",
|
|
||||||
"description": "Container management web UI for the local Podman socket.",
|
|
||||||
"icon": "/assets/img/app-icons/portainer.webp",
|
|
||||||
"author": "Portainer",
|
|
||||||
"category": "development",
|
|
||||||
"tier": "optional",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/portainer:2.39.6",
|
|
||||||
"repoUrl": "https://github.com/portainer/portainer",
|
|
||||||
"containerConfig": {
|
|
||||||
"ports": [
|
|
||||||
"9000:9000"
|
|
||||||
],
|
|
||||||
"volumes": [
|
|
||||||
"/var/lib/archipelago/portainer:/data",
|
|
||||||
"/run/user/1000/podman/podman.sock:/var/run/docker.sock"
|
|
||||||
],
|
|
||||||
"notes": "Uses the manifest-owned Podman socket bind mount preparation path."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "netbird",
|
|
||||||
"title": "NetBird",
|
|
||||||
"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 — a TLS proxy in front of the dashboard + server.",
|
|
||||||
"icon": "/assets/img/app-icons/netbird.svg",
|
|
||||||
"author": "NetBird",
|
|
||||||
"category": "networking",
|
|
||||||
"tier": "recommended",
|
|
||||||
"dockerImage": "docker.io/library/nginx:1.31.4-alpine",
|
|
||||||
"repoUrl": "https://github.com/netbirdio/netbird",
|
|
||||||
"containerConfig": {
|
|
||||||
"ports": [
|
|
||||||
"8087:80",
|
|
||||||
"8086:80",
|
|
||||||
"3478:3478/udp"
|
|
||||||
],
|
|
||||||
"volumes": [
|
|
||||||
"/var/lib/archipelago/netbird:/var/lib/netbird"
|
|
||||||
],
|
|
||||||
"notes": "Installed as a two-container stack: netbird dashboard on 8087 and netbird-server control plane on 8086 plus UDP 3478. For production clients, publish a DNS name over HTTPS with gRPC/WebSocket routing."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "uptime-kuma",
|
"id": "uptime-kuma",
|
||||||
"title": "Uptime Kuma",
|
"title": "Uptime Kuma",
|
||||||
@@ -507,82 +613,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "photoprism",
|
"id": "vaultwarden",
|
||||||
"title": "PhotoPrism",
|
"title": "Vaultwarden",
|
||||||
"version": "240915",
|
"version": "1.30.0",
|
||||||
"description": "AI-powered photo management with facial recognition.",
|
"description": "Self-hosted password vault with zero-knowledge encryption.",
|
||||||
"icon": "/assets/img/app-icons/photoprism.svg",
|
"icon": "/assets/img/app-icons/vaultwarden.webp",
|
||||||
"author": "PhotoPrism",
|
"author": "Vaultwarden",
|
||||||
"category": "data",
|
"category": "data",
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/photoprism:240915",
|
"tier": "recommended",
|
||||||
"repoUrl": "https://github.com/photoprism/photoprism",
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/vaultwarden:1.37.1-alpine",
|
||||||
|
"repoUrl": "https://github.com/dani-garcia/vaultwarden",
|
||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"ports": [
|
"ports": [
|
||||||
"2342:2342"
|
"8082:80"
|
||||||
],
|
],
|
||||||
"volumes": [
|
"volumes": [
|
||||||
"/var/lib/archipelago/photoprism:/photoprism/storage"
|
"/var/lib/archipelago/vaultwarden:/data"
|
||||||
],
|
|
||||||
"env": [
|
|
||||||
"PHOTOPRISM_ADMIN_PASSWORD=archipelago",
|
|
||||||
"PHOTOPRISM_DEFAULT_LOCALE=en"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "nextcloud",
|
|
||||||
"title": "Nextcloud",
|
|
||||||
"version": "29",
|
|
||||||
"description": "Your own private cloud. File sync, calendars, contacts.",
|
|
||||||
"icon": "/assets/img/app-icons/nextcloud.webp",
|
|
||||||
"author": "Nextcloud",
|
|
||||||
"category": "data",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/nextcloud:29",
|
|
||||||
"repoUrl": "https://github.com/nextcloud/server",
|
|
||||||
"containerConfig": {
|
|
||||||
"ports": [
|
|
||||||
"8085:80"
|
|
||||||
],
|
|
||||||
"volumes": [
|
|
||||||
"/var/lib/archipelago/nextcloud:/var/www/html"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "alby-hub",
|
|
||||||
"title": "Alby Hub",
|
|
||||||
"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 — one hub, every app pays through it.",
|
|
||||||
"icon": "/assets/img/app-icons/alby-hub.svg",
|
|
||||||
"author": "Alby",
|
|
||||||
"category": "money",
|
|
||||||
"tier": "optional",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/alby-hub:v1.24.0",
|
|
||||||
"repoUrl": "https://github.com/getAlby/hub"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "phoenixd",
|
|
||||||
"title": "phoenixd",
|
|
||||||
"version": "0.9.0",
|
|
||||||
"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",
|
|
||||||
"author": "ACINQ",
|
|
||||||
"category": "money",
|
|
||||||
"tier": "optional",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/phoenixd:0.9.0",
|
|
||||||
"repoUrl": "https://github.com/ACINQ/phoenixd"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "cuprate",
|
|
||||||
"title": "Cuprate",
|
|
||||||
"version": "0.1.0-preview",
|
|
||||||
"description": "Alternative Monero node implementation in Rust. Independently validates Monero consensus rules, providing a layer of security and redundancy for the network.",
|
|
||||||
"icon": "/assets/img/app-icons/cuprate.svg",
|
|
||||||
"author": "Cuprate contributors",
|
|
||||||
"category": "money",
|
|
||||||
"tier": "optional",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/cuprate:0.1.0-preview-18-g618ff14",
|
|
||||||
"repoUrl": "https://github.com/Cuprate/cuprate"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
import { setActivePinia, createPinia } from 'pinia'
|
import { setActivePinia, createPinia } from 'pinia'
|
||||||
|
import { __setSignedCatalogForTests } from '@/views/discover/curatedApps'
|
||||||
|
|
||||||
|
// The signed catalog's embedded manifests decide which ports the app gate
|
||||||
|
// fronts (TLS on the same port) — prime the same shape the live catalog
|
||||||
|
// carries for the apps these tests launch.
|
||||||
|
const SIGNED = {
|
||||||
|
apps: {
|
||||||
|
vaultwarden: { version: '1.37.1', manifest: { app: { ports: [{ host: 8082, auth: 'gated' }] } } },
|
||||||
|
gitea: { version: '1.23', manifest: { app: { ports: [{ host: 3001, auth: 'open' }] } } },
|
||||||
|
'nginx-proxy-manager': { version: 'latest' }, // legacy: no manifest → http
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// vi.hoisted runs before vi.mock hoisting
|
// vi.hoisted runs before vi.mock hoisting
|
||||||
const { mockPush, mockWindowOpen } = vi.hoisted(() => ({
|
const { mockPush, mockWindowOpen } = vi.hoisted(() => ({
|
||||||
@@ -23,6 +35,7 @@ describe('useAppLauncherStore', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setActivePinia(createPinia())
|
setActivePinia(createPinia())
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
__setSignedCatalogForTests(SIGNED as never)
|
||||||
// Default to HTTP to avoid proxy rewriting
|
// Default to HTTP to avoid proxy rewriting
|
||||||
Object.defineProperty(window, 'location', {
|
Object.defineProperty(window, 'location', {
|
||||||
value: { origin: 'http://192.0.2.10', protocol: 'http:', hostname: '192.0.2.10' },
|
value: { origin: 'http://192.0.2.10', protocol: 'http:', hostname: '192.0.2.10' },
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { recordAppLaunch } from '@/utils/appUsage'
|
|||||||
import { requestExternalOpen } from '@/api/remote-relay'
|
import { requestExternalOpen } from '@/api/remote-relay'
|
||||||
import { openInAppOrNewTab, isCompanionApp, type InAppLaunchMeta } from '@/utils/openExternal'
|
import { openInAppOrNewTab, isCompanionApp, type InAppLaunchMeta } from '@/utils/openExternal'
|
||||||
import { directAppUrl, HTTPS_APP_IDS, resolveAppUrl } from '@/views/appSession/appSessionConfig'
|
import { directAppUrl, HTTPS_APP_IDS, resolveAppUrl } from '@/views/appSession/appSessionConfig'
|
||||||
|
import { portIsGateFronted } from '@/views/discover/curatedApps'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { resolveAppIcon } from '@/views/apps/appsConfig'
|
import { resolveAppIcon } from '@/views/apps/appsConfig'
|
||||||
import { IS_DEMO, isDemoApp, isDemoExternal, demoAppUrl } from '@/composables/useDemoIntro'
|
import { IS_DEMO, isDemoApp, isDemoExternal, demoAppUrl } from '@/composables/useDemoIntro'
|
||||||
@@ -256,20 +257,20 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
|||||||
let launchUrl = normalizeLaunchUrl(payload.url, titleHintId)
|
let launchUrl = normalizeLaunchUrl(payload.url, titleHintId)
|
||||||
const resolvedId = resolveAppIdFromUrl(launchUrl) || titleHintId
|
const resolvedId = resolveAppIdFromUrl(launchUrl) || titleHintId
|
||||||
|
|
||||||
// Scheme discipline for everything launched on this host. App ports are
|
// Scheme discipline for everything launched on this host. Ports fronted
|
||||||
// owned by the app gate, which serves TLS on the same port whenever the
|
// by the node's app gate (manifest auth gated/open) serve TLS on the same
|
||||||
// node has a certificate — so on an HTTPS connection every same-host
|
// port — on an HTTPS connection those must open over https. Ports that
|
||||||
// app URL must be https: plain http is a silent downgrade at best and
|
// are NOT gate-fronted (legacy curated installs like Nginx Proxy Manager,
|
||||||
// mixed-content-blocked at worst (remote browsers, the companion
|
// Tailscale; `auth: none` publishes) are plain HTTP and https would fail
|
||||||
// webview). Apps that are ALWAYS https (netbird's secure-context OIDC
|
// to connect outright, so they keep http. External hosts keep their own
|
||||||
// dashboard) upgrade regardless of the page, and external hosts keep
|
// scheme.
|
||||||
// their own scheme.
|
|
||||||
try {
|
try {
|
||||||
const u = new URL(launchUrl, window.location.origin)
|
const u = new URL(launchUrl, window.location.origin)
|
||||||
const sameHost = u.hostname === window.location.hostname
|
const sameHost = u.hostname === window.location.hostname
|
||||||
const alwaysHttps = !!resolvedId && HTTPS_APP_IDS.has(resolvedId)
|
const alwaysHttps = !!resolvedId && HTTPS_APP_IDS.has(resolvedId)
|
||||||
const httpsPage = window.location.protocol === 'https:'
|
const httpsPage = window.location.protocol === 'https:'
|
||||||
if (u.protocol === 'http:' && (alwaysHttps || (httpsPage && sameHost && (resolvedId || mustOpenInNewTab(launchUrl))))) {
|
const gateFronted = !!resolvedId && portIsGateFronted(resolvedId, u.port)
|
||||||
|
if (u.protocol === 'http:' && sameHost && (alwaysHttps || (httpsPage && gateFronted))) {
|
||||||
// Pure prefix swap — never re-serialize the URL (URL.href would add
|
// Pure prefix swap — never re-serialize the URL (URL.href would add
|
||||||
// a trailing slash and change the string the caller handed over).
|
// a trailing slash and change the string the caller handed over).
|
||||||
launchUrl = launchUrl.replace(/^http:\/\//i, 'https://')
|
launchUrl = launchUrl.replace(/^http:\/\//i, 'https://')
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ import {
|
|||||||
categorizeCommunityApp,
|
categorizeCommunityApp,
|
||||||
getCuratedAppList,
|
getCuratedAppList,
|
||||||
} from './marketplace/marketplaceData'
|
} from './marketplace/marketplaceData'
|
||||||
|
import { fetchAppCatalog } from './discover/curatedApps'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -238,10 +239,17 @@ watch(() => route.query.category, (category) => {
|
|||||||
// Community marketplace state — cached (D-09/D-06: near-static catalog, long
|
// Community marketplace state — cached (D-09/D-06: near-static catalog, long
|
||||||
// TTL) behind a shared key so Discover.vue's identical loader picks up the
|
// TTL) behind a shared key so Discover.vue's identical loader picks up the
|
||||||
// same cache entry without its own conversion (plan 02-04). Non-sensitive
|
// same cache entry without its own conversion (plan 02-04). Non-sensitive
|
||||||
// and small, so it persists across reloads.
|
// and small, so it persists across reloads. Dynamic-catalog-first: the
|
||||||
|
// daemon-verified signed catalog is what makes a newly published app appear
|
||||||
|
// without a dashboard release — the static list below is only the offline
|
||||||
|
// fallback (same fetcher contract as Discover.vue for this shared key).
|
||||||
const catalogResource = useCachedResource<MarketplaceApp[]>({
|
const catalogResource = useCachedResource<MarketplaceApp[]>({
|
||||||
key: 'app-catalog',
|
key: 'app-catalog',
|
||||||
fetcher: async () => getCuratedAppList(),
|
fetcher: async () => {
|
||||||
|
const catalog = await fetchAppCatalog()
|
||||||
|
if (catalog && catalog.apps.length) return catalog.apps
|
||||||
|
return getCuratedAppList()
|
||||||
|
},
|
||||||
ttlMs: 300_000,
|
ttlMs: 300_000,
|
||||||
persist: true,
|
persist: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,39 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it, beforeEach } from 'vitest'
|
||||||
import { NEW_TAB_APPS, directAppUrl, resolveAppUrl } from '../appSessionConfig'
|
import { NEW_TAB_APPS, directAppUrl, resolveAppUrl } from '../appSessionConfig'
|
||||||
import { GENERATED_NEW_TAB_APPS } from '../generatedAppSessionConfig'
|
import { GENERATED_NEW_TAB_APPS } from '../generatedAppSessionConfig'
|
||||||
|
import { __setSignedCatalogForTests } from '../../discover/curatedApps'
|
||||||
|
|
||||||
|
// Mirror of the live signed catalog's embedded manifests (the ports[] auth
|
||||||
|
// that decides TLS eligibility). Kept minimal — only what the scheme logic
|
||||||
|
// consults.
|
||||||
|
const SIGNED = {
|
||||||
|
apps: {
|
||||||
|
vaultwarden: { version: '1.37.1', manifest: { app: { ports: [{ host: 8082, auth: 'gated' }] } } },
|
||||||
|
gitea: { version: '1.23', manifest: { app: { ports: [{ host: 3001, auth: 'open' }, { host: 2222, auth: 'none' }] } } },
|
||||||
|
'btcpay-server': { version: '2.4.3', manifest: { app: { ports: [{ host: 23000, auth: 'open' }] } } },
|
||||||
|
mempool: { version: '3.3.1', manifest: { app: { ports: [{ host: 4080, auth: 'gated' }] } } },
|
||||||
|
filebrowser: { version: '2.27.0', manifest: { app: { ports: [{ host: 8083, auth: 'gated' }] } } },
|
||||||
|
// Legacy curated installs — in the community list, NOT in the signed
|
||||||
|
// catalog's manifests. Their ports publish plain HTTP: https fails.
|
||||||
|
'nginx-proxy-manager': { version: 'latest' },
|
||||||
|
tailscale: { version: 'stable' },
|
||||||
|
// auth:none ports are container-published too — https would fail.
|
||||||
|
cuprate: { version: '0.1.0-preview', manifest: { app: { ports: [{ host: 18090, auth: 'none' }] } } },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function stubLocation(value: { hostname: string; protocol: string }) {
|
||||||
|
Object.defineProperty(window, 'location', {
|
||||||
|
value,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
describe('appSessionConfig', () => {
|
describe('appSessionConfig', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
__setSignedCatalogForTests(SIGNED as never)
|
||||||
|
})
|
||||||
it('keeps manifest-owned new-tab apps marked on every viewport', () => {
|
it('keeps manifest-owned new-tab apps marked on every viewport', () => {
|
||||||
expect(NEW_TAB_APPS.has('btcpay-server')).toBe(true)
|
expect(NEW_TAB_APPS.has('btcpay-server')).toBe(true)
|
||||||
expect(NEW_TAB_APPS.has('photoprism')).toBe(true)
|
expect(NEW_TAB_APPS.has('photoprism')).toBe(true)
|
||||||
@@ -56,7 +87,9 @@ describe('appSessionConfig', () => {
|
|||||||
configurable: true,
|
configurable: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(resolveAppUrl('netbird', undefined, 'http://localhost:8086')).toBe('http://192.0.2.10:8087')
|
// NetBird's dashboard needs a secure context (OIDC PKCE), so it is
|
||||||
|
// ALWAYS launched over https — on either page scheme.
|
||||||
|
expect(resolveAppUrl('netbird', undefined, 'http://localhost:8086')).toBe('https://192.0.2.10:8087')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses backend runtime URLs for apps with dynamic launch surfaces', () => {
|
it('uses backend runtime URLs for apps with dynamic launch surfaces', () => {
|
||||||
@@ -66,53 +99,52 @@ describe('appSessionConfig', () => {
|
|||||||
configurable: true,
|
configurable: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(resolveAppUrl('filebrowser', undefined, 'http://localhost:18083')).toBe('http://192.0.2.10:18083')
|
expect(resolveAppUrl('filebrowser', undefined, 'http://localhost:8083')).toBe('http://192.0.2.10:8083')
|
||||||
})
|
})
|
||||||
|
|
||||||
// The direct-port launch path (new-tab apps on desktop, the companion's
|
// The direct-port launch path (new-tab apps on desktop, the companion's
|
||||||
// native WebView on phones) used to hardcode http:// — so a node reached
|
// native WebView on phones) used to hardcode http:// — so a node reached
|
||||||
// over HTTPS opened Vaultwarden and friends in cleartext. These pin the
|
// over HTTPS opened Vaultwarden and friends in cleartext. It must follow
|
||||||
// scheme-following contract on both page schemes.
|
// the page scheme ONLY for ports the app gate fronts (TLS on the same
|
||||||
it('builds direct app URLs on the page scheme — https page, https app', () => {
|
// port); legacy installs without manifests (Nginx Proxy Manager, Tailscale)
|
||||||
Object.defineProperty(window, 'location', {
|
// and auth:none ports stay on http or https would fail to connect.
|
||||||
value: { hostname: '192.0.2.10', protocol: 'https:' },
|
it('builds direct app URLs on the page scheme — https page, gate-fronted app', () => {
|
||||||
writable: true,
|
stubLocation({ hostname: '192.0.2.10', protocol: 'https:' })
|
||||||
configurable: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(directAppUrl('vaultwarden')).toBe('https://192.0.2.10:8082')
|
expect(directAppUrl('vaultwarden')).toBe('https://192.0.2.10:8082')
|
||||||
expect(directAppUrl('gitea')).toBe('https://192.0.2.10:3001')
|
expect(directAppUrl('gitea')).toBe('https://192.0.2.10:3001')
|
||||||
expect(directAppUrl('btcpay-server')).toBe('https://192.0.2.10:23000')
|
expect(directAppUrl('btcpay-server')).toBe('https://192.0.2.10:23000')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps legacy manifest-less apps on http even on an https page', () => {
|
||||||
|
stubLocation({ hostname: '192.0.2.10', protocol: 'https:' })
|
||||||
|
|
||||||
|
expect(directAppUrl('nginx-proxy-manager')).toBe('http://192.0.2.10:8081')
|
||||||
|
expect(directAppUrl('tailscale')).toBe('http://192.0.2.10:8240')
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps plain-http direct app URLs on a plain-http page', () => {
|
it('keeps plain-http direct app URLs on a plain-http page', () => {
|
||||||
Object.defineProperty(window, 'location', {
|
stubLocation({ hostname: '192.0.2.10', protocol: 'http:' })
|
||||||
value: { hostname: '192.0.2.10', protocol: 'http:' },
|
|
||||||
writable: true,
|
|
||||||
configurable: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(directAppUrl('vaultwarden')).toBe('http://192.0.2.10:8082')
|
expect(directAppUrl('vaultwarden')).toBe('http://192.0.2.10:8082')
|
||||||
|
expect(directAppUrl('nginx-proxy-manager')).toBe('http://192.0.2.10:8081')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('always launches secure-context apps over https, on either page scheme', () => {
|
it('always launches secure-context apps over https, on either page scheme', () => {
|
||||||
Object.defineProperty(window, 'location', {
|
stubLocation({ hostname: '192.0.2.10', protocol: 'http:' })
|
||||||
value: { hostname: '192.0.2.10', protocol: 'http:' },
|
|
||||||
writable: true,
|
|
||||||
configurable: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(directAppUrl('netbird')).toBe('https://192.0.2.10:8087')
|
expect(directAppUrl('netbird')).toBe('https://192.0.2.10:8087')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('resolves session app URLs on the page scheme too (https page)', () => {
|
it('resolves session app URLs on the page scheme for gate-fronted ports only (https page)', () => {
|
||||||
Object.defineProperty(window, 'location', {
|
stubLocation({ hostname: '192.0.2.10', protocol: 'https:' })
|
||||||
value: { hostname: '192.0.2.10', protocol: 'https:' },
|
|
||||||
writable: true,
|
|
||||||
configurable: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(resolveAppUrl('mempool')).toBe('https://192.0.2.10:4080')
|
expect(resolveAppUrl('mempool')).toBe('https://192.0.2.10:4080')
|
||||||
expect(resolveAppUrl('filebrowser', undefined, 'http://localhost:18083')).toBe('https://192.0.2.10:18083')
|
expect(resolveAppUrl('filebrowser', undefined, 'http://localhost:8083')).toBe('https://192.0.2.10:8083')
|
||||||
|
// A runtime port the gate does NOT front keeps plain http (https would
|
||||||
|
// fail to connect outright).
|
||||||
|
expect(resolveAppUrl('filebrowser', undefined, 'http://localhost:18083')).toBe('http://192.0.2.10:18083')
|
||||||
|
// Cuprate's UI port is auth:none — plain HTTP stays plain.
|
||||||
|
expect(resolveAppUrl('cuprate', undefined, 'http://localhost:18090')).toBe('http://192.0.2.10:18090')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/** Static configuration maps for app session routing and display */
|
/** Static configuration maps for app session routing and display */
|
||||||
|
|
||||||
|
import { portIsGateFronted } from '../discover/curatedApps'
|
||||||
import { GENERATED_APP_PORTS, GENERATED_APP_TITLES, GENERATED_NEW_TAB_APPS } from './generatedAppSessionConfig'
|
import { GENERATED_APP_PORTS, GENERATED_APP_TITLES, GENERATED_NEW_TAB_APPS } from './generatedAppSessionConfig'
|
||||||
import { IS_DEMO, demoAppUrl } from '@/composables/useDemoIntro'
|
import { IS_DEMO, demoAppUrl } from '@/composables/useDemoIntro'
|
||||||
|
|
||||||
@@ -107,15 +108,20 @@ export function resolveAppUrl(id: string, routeQueryPath?: string, runtimeUrl?:
|
|||||||
// shell when proxied under a path prefix on some nodes.
|
// shell when proxied under a path prefix on some nodes.
|
||||||
if (id === 'bitcoin-knots' || id === 'bitcoin-core' || id === 'bitcoin-ui') {
|
if (id === 'bitcoin-knots' || id === 'bitcoin-core' || id === 'bitcoin-ui') {
|
||||||
if (import.meta.env.DEV) return '/app/bitcoin-ui/'
|
if (import.meta.env.DEV) return '/app/bitcoin-ui/'
|
||||||
return appOrigin(8334)
|
return appOrigin(8334, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (runtimeUrl && id !== 'netbird') {
|
if (runtimeUrl && id !== 'netbird') {
|
||||||
let base = runtimeUrl.replace(/localhost/i, window.location.hostname)
|
let base = runtimeUrl.replace(/localhost/i, window.location.hostname)
|
||||||
// The backend reports runtime URLs as http:// because that is how the app
|
// The backend reports runtime URLs as http:// because that is how the app
|
||||||
// binds locally. Sent to a browser on an HTTPS dashboard that is mixed
|
// binds locally. On an HTTPS dashboard that is mixed content and the
|
||||||
// content and the frame is blocked outright, so follow the page instead.
|
// frame is blocked outright — but ONLY upgrade when the gate fronts the
|
||||||
base = matchPageScheme(base)
|
// port (it serves TLS there); a container-published plain-HTTP port
|
||||||
|
// would fail to connect over https at all.
|
||||||
|
try {
|
||||||
|
const port = new URL(base).port
|
||||||
|
if (portIsGateFronted(id, port)) base = matchPageScheme(base)
|
||||||
|
} catch { /* keep as-is */ }
|
||||||
if (routeQueryPath) base += routeQueryPath
|
if (routeQueryPath) base += routeQueryPath
|
||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
@@ -124,13 +130,14 @@ export function resolveAppUrl(id: string, routeQueryPath?: string, runtimeUrl?:
|
|||||||
const port = APP_PORTS[id]
|
const port = APP_PORTS[id]
|
||||||
if (!port) return ''
|
if (!port) return ''
|
||||||
|
|
||||||
let base = appOrigin(port)
|
let base = appOrigin(port, id)
|
||||||
if (routeQueryPath) base += routeQueryPath
|
if (routeQueryPath) base += routeQueryPath
|
||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An app's origin on this host, on the SAME scheme as the page.
|
* An app's origin on this host, on the SAME scheme as the page when the
|
||||||
|
* app gate fronts the port (TLS on the same port), plain http otherwise.
|
||||||
*
|
*
|
||||||
* An HTTPS dashboard cannot embed an HTTP frame at all — browsers block it as
|
* An HTTPS dashboard cannot embed an HTTP frame at all — browsers block it as
|
||||||
* mixed content before any cookie question arises — and it is also what makes
|
* mixed content before any cookie question arises — and it is also what makes
|
||||||
@@ -143,8 +150,11 @@ export function resolveAppUrl(id: string, routeQueryPath?: string, runtimeUrl?:
|
|||||||
* Node certificate. A certificate warning cannot be accepted inside an iframe,
|
* Node certificate. A certificate warning cannot be accepted inside an iframe,
|
||||||
* so an untrusted app port renders nothing rather than prompting.
|
* so an untrusted app port renders nothing rather than prompting.
|
||||||
*/
|
*/
|
||||||
export function appOrigin(port: number): string {
|
export function appOrigin(port: number, appId?: string): string {
|
||||||
return `${pageScheme()}//${window.location.hostname}:${port}`
|
const https = appId
|
||||||
|
? HTTPS_APP_IDS.has(appId) || (portIsGateFronted(appId, port) && pageScheme() === 'https:')
|
||||||
|
: pageScheme() === 'https:'
|
||||||
|
return `${https ? 'https' : 'http'}://${window.location.hostname}:${port}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Rewrite a URL's scheme to the page's, leaving everything else alone. */
|
/** Rewrite a URL's scheme to the page's, leaving everything else alone. */
|
||||||
@@ -205,7 +215,10 @@ export const DIRECT_APP_PORTS: Record<string, string> = {
|
|||||||
export function directAppUrl(appId: string): string | null {
|
export function directAppUrl(appId: string): string | null {
|
||||||
const port = DIRECT_APP_PORTS[appId]
|
const port = DIRECT_APP_PORTS[appId]
|
||||||
if (!port || typeof window === 'undefined') return null
|
if (!port || typeof window === 'undefined') return null
|
||||||
const scheme = HTTPS_APP_IDS.has(appId) || pageScheme() === 'https:' ? 'https' : 'http'
|
const scheme = HTTPS_APP_IDS.has(appId)
|
||||||
|
|| (portIsGateFronted(appId, port) && pageScheme() === 'https:')
|
||||||
|
? 'https'
|
||||||
|
: 'http'
|
||||||
return `${scheme}://${window.location.hostname}:${port}`
|
return `${scheme}://${window.location.hostname}:${port}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { Ref } from 'vue'
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { PackageState, type PackageDataEntry } from '@/types/api'
|
import { PackageState, type PackageDataEntry } from '@/types/api'
|
||||||
import { matchPageScheme, resolveAppUrl } from '../appSession/appSessionConfig'
|
import { matchPageScheme, resolveAppUrl } from '../appSession/appSessionConfig'
|
||||||
|
import { portIsGateFronted } from '../discover/curatedApps'
|
||||||
import { isAutoTabApp } from '@/utils/autoTabApps'
|
import { isAutoTabApp } from '@/utils/autoTabApps'
|
||||||
|
|
||||||
export type AppsTab = 'apps' | 'websites' | 'services'
|
export type AppsTab = 'apps' | 'websites' | 'services'
|
||||||
@@ -301,10 +302,13 @@ export function resolveRuntimeLaunchUrl(pkg: PackageDataEntry): string {
|
|||||||
if (!addr || typeof window === 'undefined') return addr
|
if (!addr || typeof window === 'undefined') return addr
|
||||||
const local = addr.replace(/^http:\/\/(localhost|127\.0\.0\.1)(?=[:/]|$)/, `http://${window.location.hostname}`)
|
const local = addr.replace(/^http:\/\/(localhost|127\.0\.0\.1)(?=[:/]|$)/, `http://${window.location.hostname}`)
|
||||||
// The backend reports runtime URLs as http:// because that is how the app
|
// The backend reports runtime URLs as http:// because that is how the app
|
||||||
// binds locally — on an HTTPS connection that is a cleartext downgrade
|
// binds locally — on an HTTPS connection that is a cleartext downgrade.
|
||||||
// (and mixed-content-blocked when opened from the dashboard). The gate
|
// Upgrade only when the app gate fronts the port (it serves TLS there);
|
||||||
// serves TLS on every app port, so follow the page's scheme, exactly like
|
// a container-published plain-HTTP port would fail over https outright.
|
||||||
// resolveAppUrl() does for the same runtime URLs.
|
try {
|
||||||
|
const port = new URL(local).port
|
||||||
|
if (!portIsGateFronted(pkg.manifest.id, port)) return local
|
||||||
|
} catch { /* keep as-is */ }
|
||||||
return matchPageScheme(local)
|
return matchPageScheme(local)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,17 +18,94 @@ export interface AppCatalog {
|
|||||||
apps: MarketplaceApp[]
|
apps: MarketplaceApp[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Shape of the release-signed catalog (`releases/app-catalog.json`) served
|
||||||
|
* by the daemon at /api/app-catalog after release-root verification. `apps`
|
||||||
|
* is keyed by app id and each entry embeds the app's full manifest — the
|
||||||
|
* ports[] there (auth: gated/open/none) are what decides whether a port is
|
||||||
|
* fronted by the node's app gate (and therefore serves TLS on the same
|
||||||
|
* port) or published by the container as plain HTTP. */
|
||||||
|
export interface SignedAppCatalog {
|
||||||
|
schema?: number
|
||||||
|
updated?: string
|
||||||
|
apps: Record<string, SignedAppEntry>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SignedAppEntry {
|
||||||
|
version: string
|
||||||
|
image?: string
|
||||||
|
manifest?: {
|
||||||
|
app?: {
|
||||||
|
id?: string
|
||||||
|
name?: string
|
||||||
|
version?: string
|
||||||
|
description?: string
|
||||||
|
category?: string
|
||||||
|
container?: { image?: string }
|
||||||
|
metadata?: { icon?: string; author?: string; repo?: string }
|
||||||
|
ports?: { host?: number | string; container?: number | string; auth?: string }[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convert the signed catalog's keyed entries into store-listing apps.
|
||||||
|
* Pure — unit-tested against the live catalog's shape (Cuprate). */
|
||||||
|
export function signedCatalogToApps(catalog: SignedAppCatalog): MarketplaceApp[] {
|
||||||
|
const out: MarketplaceApp[] = []
|
||||||
|
for (const [id, entry] of Object.entries(catalog.apps || {})) {
|
||||||
|
const app = entry.manifest?.app
|
||||||
|
out.push({
|
||||||
|
id,
|
||||||
|
title: app?.name || id,
|
||||||
|
version: entry.version || app?.version || '',
|
||||||
|
description: app?.description || '',
|
||||||
|
icon: app?.metadata?.icon || '/assets/icon/favico-black-v2.svg',
|
||||||
|
author: app?.metadata?.author,
|
||||||
|
dockerImage: entry.image || app?.container?.image || '',
|
||||||
|
repoUrl: app?.metadata?.repo,
|
||||||
|
category: app?.category,
|
||||||
|
source: 'signed-catalog',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The daemon-verified signed catalog, kept for synchronous port-auth lookups
|
||||||
|
* after fetchAppCatalog() has run. Test-hookable. */
|
||||||
|
let signedCatalogCache: SignedAppCatalog | null = null
|
||||||
|
|
||||||
|
/** 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
|
||||||
|
* TLS on it; `none`/`local` = container-published plain HTTP; null = app
|
||||||
|
* unknown to the signed catalog (legacy curated installs). */
|
||||||
|
export function portAuth(appId: string, hostPort: number | string): string | null {
|
||||||
|
const ports = signedCatalogCache?.apps?.[appId]?.manifest?.app?.ports
|
||||||
|
if (!Array.isArray(ports)) return null
|
||||||
|
const hit = ports.find(p => String(p.host) === String(hostPort))
|
||||||
|
return hit?.auth ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether an app's host port is fronted by the node's app gate (and so
|
||||||
|
* serves TLS alongside HTTP on the same port). Unknown apps are NOT —
|
||||||
|
* assuming TLS for a container-published port breaks it outright. */
|
||||||
|
export function portIsGateFronted(appId: string, hostPort: number | string): boolean {
|
||||||
|
const auth = portAuth(appId, hostPort)
|
||||||
|
return auth === 'gated' || auth === 'open'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function __setSignedCatalogForTests(catalog: SignedAppCatalog | null) {
|
||||||
|
signedCatalogCache = catalog
|
||||||
|
}
|
||||||
|
|
||||||
let cachedCatalog: AppCatalog | null = null
|
let cachedCatalog: AppCatalog | null = null
|
||||||
let catalogFetchedAt = 0
|
let catalogFetchedAt = 0
|
||||||
const CATALOG_TTL = 60 * 60 * 1000 // 1 hour cache
|
const CATALOG_TTL = 60 * 60 * 1000 // 1 hour cache
|
||||||
|
|
||||||
/** Catalog URLs tried in order. First success wins.
|
/** Catalog URLs for the community listing. The signed catalog is served by
|
||||||
* Primary is the backend proxy (`/api/app-catalog`) — server-side fetch
|
* the backend proxy (`/api/app-catalog`) — server-side fetch bypasses CORS
|
||||||
* bypasses CORS on the upstream Gitea and CSP restrictions on the IP-port
|
* on the upstream Gitea and verifies the release-root signature. If the
|
||||||
* fallback. If the backend is offline (mid-restart etc.) we fall back
|
* backend is offline (mid-restart etc.) the static community copy baked
|
||||||
* to the static copy baked into the frontend build. */
|
* into the frontend build still renders the store. */
|
||||||
const CATALOG_URLS = [
|
const CATALOG_URLS = [
|
||||||
'/api/app-catalog',
|
|
||||||
'/catalog.json',
|
'/catalog.json',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -38,29 +115,61 @@ export async function fetchAppCatalog(): Promise<AppCatalog | null> {
|
|||||||
// Return cache if fresh
|
// Return cache if fresh
|
||||||
if (cachedCatalog && Date.now() - catalogFetchedAt < CATALOG_TTL) return cachedCatalog
|
if (cachedCatalog && Date.now() - catalogFetchedAt < CATALOG_TTL) return cachedCatalog
|
||||||
|
|
||||||
|
// The daemon-verified signed catalog first (release-root signature checked
|
||||||
|
// server-side): it is what makes a newly published app appear without a
|
||||||
|
// dashboard release. The community catalog supplies the featured banner
|
||||||
|
// and curated copy for shared ids; signed-only ids join the listing as-is.
|
||||||
|
let signedApps: MarketplaceApp[] = []
|
||||||
|
let signedOk = false
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/app-catalog', { credentials: 'include', signal: AbortSignal.timeout(20000) })
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json() as SignedAppCatalog
|
||||||
|
if (data.apps && !Array.isArray(data.apps)) {
|
||||||
|
signedCatalogCache = data
|
||||||
|
signedApps = signedCatalogToApps(data)
|
||||||
|
signedOk = signedApps.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* fall through to the community catalog */ }
|
||||||
|
|
||||||
|
let community: AppCatalog | null = null
|
||||||
for (const url of CATALOG_URLS) {
|
for (const url of CATALOG_URLS) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, { credentials: 'include', signal: AbortSignal.timeout(20000) })
|
const res = await fetch(url, { credentials: 'include', signal: AbortSignal.timeout(20000) })
|
||||||
if (!res.ok) continue
|
if (!res.ok) continue
|
||||||
const data = await res.json() as AppCatalog
|
const data = await res.json() as AppCatalog
|
||||||
if (!data.apps?.length) continue
|
if (!data.apps?.length) continue
|
||||||
|
|
||||||
// Expand short docker image refs to full registry paths
|
|
||||||
const registry = data.registry || R
|
const registry = data.registry || R
|
||||||
for (const app of data.apps) {
|
for (const app of data.apps) {
|
||||||
if (app.dockerImage && !app.dockerImage.includes('/')) {
|
if (app.dockerImage && !app.dockerImage.includes('/')) {
|
||||||
app.dockerImage = `${registry}/${app.dockerImage}`
|
app.dockerImage = `${registry}/${app.dockerImage}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cachedCatalog = data
|
community = data
|
||||||
catalogFetchedAt = Date.now()
|
break
|
||||||
// Cache in localStorage for offline fallback
|
} catch { /* try the next source */ }
|
||||||
try { localStorage.setItem('archy_catalog', JSON.stringify(data)) } catch {}
|
}
|
||||||
return data
|
|
||||||
} catch (e) {
|
if (signedOk || community) {
|
||||||
console.warn(`[catalog] fetch failed for ${url}:`, e)
|
// Community copy wins for shared ids (curated descriptions, webUrl-only
|
||||||
continue
|
// apps); signed entries fill version/image gaps and append brand-new apps.
|
||||||
|
const byId = new Map<string, MarketplaceApp>()
|
||||||
|
for (const app of signedApps) byId.set(app.id, app)
|
||||||
|
for (const app of community?.apps ?? []) {
|
||||||
|
const existing = byId.get(app.id)
|
||||||
|
byId.set(app.id, existing ? { ...app, version: app.version || existing.version, dockerImage: app.dockerImage || existing.dockerImage } : app)
|
||||||
}
|
}
|
||||||
|
const merged: AppCatalog = {
|
||||||
|
version: community?.version ?? 1,
|
||||||
|
registry: community?.registry ?? R,
|
||||||
|
featured: community?.featured ?? { id: 'bitcoin-knots', banner: '', headline: '', description: '', tag: '' },
|
||||||
|
apps: [...byId.values()],
|
||||||
|
}
|
||||||
|
cachedCatalog = merged
|
||||||
|
catalogFetchedAt = Date.now()
|
||||||
|
try { localStorage.setItem('archy_catalog', JSON.stringify(merged)) } catch {}
|
||||||
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try localStorage cache as final fallback
|
// Try localStorage cache as final fallback
|
||||||
|
|||||||
@@ -372,8 +372,9 @@ init()
|
|||||||
<p><strong>What's New really does stop at v1.8.0 now.</strong> 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.</p>
|
<p><strong>What's New really does stop at v1.8.0 now.</strong> 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.</p>
|
||||||
<p><strong>The installer carries the same corrected release and Companion 0.5.28.</strong> 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.</p>
|
<p><strong>The installer carries the same corrected release and Companion 0.5.28.</strong> 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.</p>
|
||||||
<p><strong>Crash dumps work on fresh installs as well as upgraded nodes.</strong> 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.</p>
|
<p><strong>Crash dumps work on fresh installs as well as upgraded nodes.</strong> 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.</p>
|
||||||
<p><strong>Apps open over HTTPS when your node does.</strong> Connect to your node over HTTPS and every app 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 opens 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. Plain-HTTP access (the kiosk, LAN browsing) is unchanged.</p>
|
<p><strong>Apps open over HTTPS when your node does.</strong> 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.</p>
|
||||||
<p><strong>Newly signed apps appear in the App Store immediately.</strong> 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. Cuprate was invisible on updated nodes for exactly this reason; it is now in both.</p>
|
<p><strong>Every app in the store is now a first-class platform app.</strong> 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.</p>
|
||||||
|
<p><strong>Newly signed apps appear in the App Store immediately.</strong> 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.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- v1.8.6-alpha -->
|
<!-- v1.8.6-alpha -->
|
||||||
|
|||||||
+402
-25
@@ -2,6 +2,98 @@
|
|||||||
"apps": {
|
"apps": {
|
||||||
"adguardhome": {
|
"adguardhome": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.55",
|
"image": "source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.55",
|
||||||
|
"manifest": {
|
||||||
|
"app": {
|
||||||
|
"container": {
|
||||||
|
"image": "source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.55",
|
||||||
|
"network": "pasta",
|
||||||
|
"pull_policy": "if-not-present"
|
||||||
|
},
|
||||||
|
"dependencies": [
|
||||||
|
{
|
||||||
|
"storage": "1Gi"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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.",
|
||||||
|
"environment": [],
|
||||||
|
"health_check": {
|
||||||
|
"endpoint": "localhost:3000",
|
||||||
|
"interval": "30s",
|
||||||
|
"retries": 3,
|
||||||
|
"timeout": "5s",
|
||||||
|
"type": "tcp"
|
||||||
|
},
|
||||||
|
"id": "adguardhome",
|
||||||
|
"interfaces": {
|
||||||
|
"main": {
|
||||||
|
"description": "AdGuard Home web console",
|
||||||
|
"name": "Admin console",
|
||||||
|
"path": "/",
|
||||||
|
"port": 3000,
|
||||||
|
"protocol": "http",
|
||||||
|
"type": "ui"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"author": "AdGuard",
|
||||||
|
"category": "networking",
|
||||||
|
"repo": "https://github.com/AdguardTeam/AdGuardHome",
|
||||||
|
"tier": "optional"
|
||||||
|
},
|
||||||
|
"name": "AdGuard Home",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"auth": "open",
|
||||||
|
"auth_rationale": "AdGuard Home enforces its own admin login on the console, and the first-run wizard must answer before any account exists.",
|
||||||
|
"bind": "127.0.0.1",
|
||||||
|
"container": 3000,
|
||||||
|
"host": 3000,
|
||||||
|
"protocol": "tcp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"auth": "none",
|
||||||
|
"auth_rationale": "Plain DNS answers unauthenticated by protocol: resolvers and clients send queries directly; a login challenge would make DNS unreachable.",
|
||||||
|
"container": 53,
|
||||||
|
"host": 53,
|
||||||
|
"protocol": "udp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"auth": "none",
|
||||||
|
"auth_rationale": "DNS-over-TCP fallback (truncated responses, zone transfers); same protocol-level requirement as the UDP port.",
|
||||||
|
"container": 53,
|
||||||
|
"host": 53,
|
||||||
|
"protocol": "tcp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"disk_limit": "1Gi",
|
||||||
|
"memory_limit": "512Mi"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"capabilities": [
|
||||||
|
"NET_BIND_SERVICE"
|
||||||
|
],
|
||||||
|
"network_policy": "isolated",
|
||||||
|
"no_new_privileges": true,
|
||||||
|
"readonly_root": false
|
||||||
|
},
|
||||||
|
"upstream": {
|
||||||
|
"kind": "github",
|
||||||
|
"repo": "AdguardTeam/AdGuardHome"
|
||||||
|
},
|
||||||
|
"version": "v0.107.55",
|
||||||
|
"volumes": [
|
||||||
|
{
|
||||||
|
"options": [
|
||||||
|
"rw"
|
||||||
|
],
|
||||||
|
"source": "/var/lib/archipelago/adguardhome",
|
||||||
|
"target": "/opt/adguardhome",
|
||||||
|
"type": "bind"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"version": "v0.107.55"
|
"version": "v0.107.55"
|
||||||
},
|
},
|
||||||
"aiui": {
|
"aiui": {
|
||||||
@@ -1207,6 +1299,82 @@
|
|||||||
},
|
},
|
||||||
"cryptpad": {
|
"cryptpad": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/cryptpad:2024.12.0",
|
"image": "source.archipelago-foundation.org/lfg2025/cryptpad:2024.12.0",
|
||||||
|
"manifest": {
|
||||||
|
"app": {
|
||||||
|
"container": {
|
||||||
|
"image": "source.archipelago-foundation.org/lfg2025/cryptpad:2024.12.0",
|
||||||
|
"network": "pasta",
|
||||||
|
"pull_policy": "if-not-present"
|
||||||
|
},
|
||||||
|
"dependencies": [
|
||||||
|
{
|
||||||
|
"storage": "5Gi"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "End-to-end encrypted documents, spreadsheets, and presentations. Zero-knowledge collaboration.",
|
||||||
|
"environment": [],
|
||||||
|
"health_check": {
|
||||||
|
"endpoint": "localhost:3000",
|
||||||
|
"interval": "30s",
|
||||||
|
"retries": 3,
|
||||||
|
"timeout": "5s",
|
||||||
|
"type": "tcp"
|
||||||
|
},
|
||||||
|
"id": "cryptpad",
|
||||||
|
"interfaces": {
|
||||||
|
"main": {
|
||||||
|
"description": "Encrypted collaboration suite",
|
||||||
|
"name": "CryptPad",
|
||||||
|
"path": "/",
|
||||||
|
"port": 3000,
|
||||||
|
"protocol": "http",
|
||||||
|
"type": "ui"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"author": "XWiki SAS",
|
||||||
|
"category": "data",
|
||||||
|
"icon": "/assets/icon/favico-black-v2.svg",
|
||||||
|
"repo": "https://github.com/cryptpad/cryptpad",
|
||||||
|
"tier": "optional"
|
||||||
|
},
|
||||||
|
"name": "CryptPad",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"auth": "gated",
|
||||||
|
"bind": "127.0.0.1",
|
||||||
|
"container": 3000,
|
||||||
|
"host": 3000,
|
||||||
|
"protocol": "tcp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"disk_limit": "5Gi",
|
||||||
|
"memory_limit": "1Gi"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"capabilities": [],
|
||||||
|
"network_policy": "isolated",
|
||||||
|
"no_new_privileges": true,
|
||||||
|
"readonly_root": false
|
||||||
|
},
|
||||||
|
"upstream": {
|
||||||
|
"kind": "github",
|
||||||
|
"repo": "cryptpad/cryptpad"
|
||||||
|
},
|
||||||
|
"version": "2024.12.0",
|
||||||
|
"volumes": [
|
||||||
|
{
|
||||||
|
"options": [
|
||||||
|
"rw"
|
||||||
|
],
|
||||||
|
"source": "/var/lib/archipelago/cryptpad",
|
||||||
|
"target": "/cryptpad/data",
|
||||||
|
"type": "bind"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"version": "2024.12.0"
|
"version": "2024.12.0"
|
||||||
},
|
},
|
||||||
"cuprate": {
|
"cuprate": {
|
||||||
@@ -1951,10 +2119,6 @@
|
|||||||
},
|
},
|
||||||
"version": "v2.27.0"
|
"version": "v2.27.0"
|
||||||
},
|
},
|
||||||
"fips": {
|
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/fips:v0.1.0",
|
|
||||||
"version": "v0.1.0"
|
|
||||||
},
|
|
||||||
"fips-ui": {
|
"fips-ui": {
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
@@ -4145,6 +4309,88 @@
|
|||||||
},
|
},
|
||||||
"nginx-proxy-manager": {
|
"nginx-proxy-manager": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/nginx-proxy-manager:latest",
|
"image": "source.archipelago-foundation.org/lfg2025/nginx-proxy-manager:latest",
|
||||||
|
"manifest": {
|
||||||
|
"app": {
|
||||||
|
"container": {
|
||||||
|
"image": "source.archipelago-foundation.org/lfg2025/nginx-proxy-manager:latest",
|
||||||
|
"network": "pasta",
|
||||||
|
"pull_policy": "if-not-present"
|
||||||
|
},
|
||||||
|
"dependencies": [
|
||||||
|
{
|
||||||
|
"storage": "1Gi"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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).",
|
||||||
|
"environment": [],
|
||||||
|
"health_check": {
|
||||||
|
"endpoint": "localhost:81",
|
||||||
|
"interval": "30s",
|
||||||
|
"retries": 3,
|
||||||
|
"timeout": "5s",
|
||||||
|
"type": "tcp"
|
||||||
|
},
|
||||||
|
"id": "nginx-proxy-manager",
|
||||||
|
"interfaces": {
|
||||||
|
"main": {
|
||||||
|
"description": "Nginx Proxy Manager admin interface",
|
||||||
|
"name": "Admin UI",
|
||||||
|
"path": "/",
|
||||||
|
"port": 8081,
|
||||||
|
"protocol": "http",
|
||||||
|
"type": "ui"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"author": "Nginx Proxy Manager",
|
||||||
|
"category": "networking",
|
||||||
|
"icon": "/assets/img/app-icons/nginx.svg",
|
||||||
|
"repo": "https://github.com/NginxProxyManager/nginx-proxy-manager",
|
||||||
|
"tier": "optional"
|
||||||
|
},
|
||||||
|
"name": "Nginx Proxy Manager",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"auth": "open",
|
||||||
|
"auth_rationale": "Nginx Proxy Manager enforces its own admin account on every page; the initial setup wizard also has to answer before any account exists.",
|
||||||
|
"bind": "127.0.0.1",
|
||||||
|
"container": 81,
|
||||||
|
"host": 8081,
|
||||||
|
"protocol": "tcp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"disk_limit": "1Gi",
|
||||||
|
"memory_limit": "512Mi"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"capabilities": [
|
||||||
|
"CHOWN",
|
||||||
|
"SETUID",
|
||||||
|
"SETGID",
|
||||||
|
"DAC_OVERRIDE"
|
||||||
|
],
|
||||||
|
"network_policy": "isolated",
|
||||||
|
"no_new_privileges": true,
|
||||||
|
"readonly_root": false
|
||||||
|
},
|
||||||
|
"upstream": {
|
||||||
|
"kind": "github",
|
||||||
|
"repo": "NginxProxyManager/nginx-proxy-manager"
|
||||||
|
},
|
||||||
|
"version": "2.12.1",
|
||||||
|
"volumes": [
|
||||||
|
{
|
||||||
|
"options": [
|
||||||
|
"rw"
|
||||||
|
],
|
||||||
|
"source": "/var/lib/archipelago/nginx-proxy-manager",
|
||||||
|
"target": "/data",
|
||||||
|
"type": "bind"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"version": "latest"
|
"version": "latest"
|
||||||
},
|
},
|
||||||
"nostr-rs-relay": {
|
"nostr-rs-relay": {
|
||||||
@@ -4226,24 +4472,75 @@
|
|||||||
},
|
},
|
||||||
"version": "0.10.0"
|
"version": "0.10.0"
|
||||||
},
|
},
|
||||||
"nostr-vpn": {
|
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/nostr-vpn:v0.3.7",
|
|
||||||
"version": "v0.3.7"
|
|
||||||
},
|
|
||||||
"ollama": {
|
"ollama": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/ollama:latest",
|
"image": "source.archipelago-foundation.org/lfg2025/ollama:latest",
|
||||||
"version": "latest"
|
"manifest": {
|
||||||
},
|
"app": {
|
||||||
"penpot": {
|
"container": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/penpot-frontend:2.4",
|
"image": "source.archipelago-foundation.org/lfg2025/ollama:latest",
|
||||||
"images": {
|
"network": "pasta",
|
||||||
"penpot-backend": "source.archipelago-foundation.org/lfg2025/penpot-backend:2.4",
|
"pull_policy": "if-not-present"
|
||||||
"penpot-exporter": "source.archipelago-foundation.org/lfg2025/penpot-exporter:2.4",
|
},
|
||||||
"penpot-frontend": "source.archipelago-foundation.org/lfg2025/penpot-frontend:2.4",
|
"dependencies": [
|
||||||
"penpot-postgres": "source.archipelago-foundation.org/lfg2025/postgres:15",
|
{
|
||||||
"penpot-valkey": "source.archipelago-foundation.org/lfg2025/valkey:8.1"
|
"storage": "50Gi"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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.",
|
||||||
|
"environment": [],
|
||||||
|
"health_check": {
|
||||||
|
"endpoint": "localhost:11434",
|
||||||
|
"interval": "30s",
|
||||||
|
"retries": 3,
|
||||||
|
"timeout": "5s",
|
||||||
|
"type": "tcp"
|
||||||
|
},
|
||||||
|
"id": "ollama",
|
||||||
|
"metadata": {
|
||||||
|
"author": "Ollama",
|
||||||
|
"category": "community",
|
||||||
|
"icon": "/assets/img/app-icons/ollama.png",
|
||||||
|
"repo": "https://github.com/ollama/ollama",
|
||||||
|
"tier": "optional"
|
||||||
|
},
|
||||||
|
"name": "Ollama",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"auth": "local",
|
||||||
|
"bind": "127.0.0.1",
|
||||||
|
"container": 11434,
|
||||||
|
"host": 11434,
|
||||||
|
"protocol": "tcp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"disk_limit": "50Gi",
|
||||||
|
"memory_limit": 0
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"capabilities": [],
|
||||||
|
"network_policy": "isolated",
|
||||||
|
"no_new_privileges": true,
|
||||||
|
"readonly_root": false
|
||||||
|
},
|
||||||
|
"upstream": {
|
||||||
|
"kind": "github",
|
||||||
|
"repo": "ollama/ollama"
|
||||||
|
},
|
||||||
|
"version": "0.5.4",
|
||||||
|
"volumes": [
|
||||||
|
{
|
||||||
|
"options": [
|
||||||
|
"rw"
|
||||||
|
],
|
||||||
|
"source": "/var/lib/archipelago/ollama",
|
||||||
|
"target": "/root/.ollama",
|
||||||
|
"type": "bind"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"version": "2.4"
|
"version": "latest"
|
||||||
},
|
},
|
||||||
"phoenixd": {
|
"phoenixd": {
|
||||||
"manifest": {
|
"manifest": {
|
||||||
@@ -5030,10 +5327,6 @@
|
|||||||
},
|
},
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
},
|
},
|
||||||
"routstr": {
|
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/routstr:v0.4.3",
|
|
||||||
"version": "v0.4.3"
|
|
||||||
},
|
|
||||||
"searxng": {
|
"searxng": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/searxng:latest",
|
"image": "source.archipelago-foundation.org/lfg2025/searxng:latest",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
@@ -5190,6 +5483,90 @@
|
|||||||
},
|
},
|
||||||
"tailscale": {
|
"tailscale": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/tailscale:stable",
|
"image": "source.archipelago-foundation.org/lfg2025/tailscale:stable",
|
||||||
|
"manifest": {
|
||||||
|
"app": {
|
||||||
|
"container": {
|
||||||
|
"entrypoint": [
|
||||||
|
"sh",
|
||||||
|
"-c",
|
||||||
|
"tailscaled --tun=userspace-networking & for i in $(seq 1 30); do [ -S /var/run/tailscale/tailscaled.sock ] && break; sleep 1; done; tailscale web --listen 0.0.0.0:8240 & wait"
|
||||||
|
],
|
||||||
|
"image": "source.archipelago-foundation.org/lfg2025/tailscale:stable",
|
||||||
|
"network": "pasta",
|
||||||
|
"pull_policy": "if-not-present"
|
||||||
|
},
|
||||||
|
"dependencies": [
|
||||||
|
{
|
||||||
|
"storage": "1Gi"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Zero-config VPN with WireGuard mesh networking.",
|
||||||
|
"environment": [
|
||||||
|
"TS_STATE_DIR=/var/lib/tailscale"
|
||||||
|
],
|
||||||
|
"health_check": {
|
||||||
|
"endpoint": "localhost:8240",
|
||||||
|
"interval": "30s",
|
||||||
|
"retries": 3,
|
||||||
|
"timeout": "5s",
|
||||||
|
"type": "tcp"
|
||||||
|
},
|
||||||
|
"id": "tailscale",
|
||||||
|
"interfaces": {
|
||||||
|
"main": {
|
||||||
|
"description": "Tailscale web console",
|
||||||
|
"name": "Web console",
|
||||||
|
"path": "/",
|
||||||
|
"port": 8240,
|
||||||
|
"protocol": "http",
|
||||||
|
"type": "ui"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"author": "Tailscale",
|
||||||
|
"category": "networking",
|
||||||
|
"icon": "/assets/img/app-icons/tailscale.webp",
|
||||||
|
"repo": "https://github.com/tailscale/tailscale",
|
||||||
|
"tier": "recommended"
|
||||||
|
},
|
||||||
|
"name": "Tailscale",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"auth": "open",
|
||||||
|
"auth_rationale": "Tailscale's web console authenticates against the tailnet account for all administrative actions; the node's cookie challenge would be a second, redundant login.",
|
||||||
|
"bind": "127.0.0.1",
|
||||||
|
"container": 8240,
|
||||||
|
"host": 8240,
|
||||||
|
"protocol": "tcp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"disk_limit": "1Gi",
|
||||||
|
"memory_limit": "512Mi"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"capabilities": [],
|
||||||
|
"network_policy": "isolated",
|
||||||
|
"no_new_privileges": true,
|
||||||
|
"readonly_root": false
|
||||||
|
},
|
||||||
|
"upstream": {
|
||||||
|
"kind": "github",
|
||||||
|
"repo": "tailscale/tailscale"
|
||||||
|
},
|
||||||
|
"version": "1.78.0",
|
||||||
|
"volumes": [
|
||||||
|
{
|
||||||
|
"options": [
|
||||||
|
"rw"
|
||||||
|
],
|
||||||
|
"source": "/var/lib/archipelago/tailscale",
|
||||||
|
"target": "/var/lib/tailscale",
|
||||||
|
"type": "bind"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"version": "stable"
|
"version": "stable"
|
||||||
},
|
},
|
||||||
"uptime-kuma": {
|
"uptime-kuma": {
|
||||||
@@ -5366,7 +5743,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schema": 1,
|
"schema": 1,
|
||||||
"signature": "da5b6b183ac46c062945c27abdc06affb558e805e1ccf67ac0ee17e5e3dd85cc05a0656dd83bdacb1e1d237445145d00995f55e77209e1cbb2b6d8ce47084e0a",
|
"signature": "3b30d9e1afd59d2de4e9e48e5556c8c4ff54d13ae1f25ac2b5897aead6aa8ae3a503dfe56232329e8b02226ea98d3415ff220b4fcdf40913cfe9300aa99fc807",
|
||||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||||
"updated": "2026-08-30"
|
"updated": "2026-08-31"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"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.",
|
||||||
|
"**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.",
|
||||||
|
"**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.",
|
||||||
|
"**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.",
|
||||||
|
"**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.",
|
||||||
|
"**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."
|
||||||
|
],
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"current_version": "1.8.7-alpha",
|
||||||
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.7-alpha/archipelago",
|
||||||
|
"name": "archipelago",
|
||||||
|
"new_version": "1.8.7-alpha",
|
||||||
|
"sha256": "572accec81e73fbcd5218ddc41f5ec719deda30f104c838398fdfa85a7a276f0",
|
||||||
|
"size_bytes": 63990960
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current_version": "1.8.7-alpha",
|
||||||
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.7-alpha/archipelago-frontend-1.8.7-alpha.tar.gz",
|
||||||
|
"name": "archipelago-frontend-1.8.7-alpha.tar.gz",
|
||||||
|
"new_version": "1.8.7-alpha",
|
||||||
|
"sha256": "26b8c12b1b3e6b5c93841f3713037f65d0ea6a52401bd28888eb8d2f5dc483b5",
|
||||||
|
"size_bytes": 97779181
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"release_date": "2026-08-31",
|
||||||
|
"signature": "79332435d436e7bae1a5b0c12158b02083b681b6818ac90219021f1e39eb61a3e1039ed38ee8089006a0dec862fd92997e44a6c9ccf0be17e91a95cba5de6402",
|
||||||
|
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||||
|
"version": "1.8.7-alpha"
|
||||||
|
}
|
||||||
@@ -81,9 +81,6 @@ SINGLE = {
|
|||||||
"fedimint": "FEDIMINT_IMAGE",
|
"fedimint": "FEDIMINT_IMAGE",
|
||||||
"fedimint-gateway": "FEDIMINT_GATEWAY_IMAGE",
|
"fedimint-gateway": "FEDIMINT_GATEWAY_IMAGE",
|
||||||
"nostr-rs-relay": "NOSTR_RS_RELAY_IMAGE",
|
"nostr-rs-relay": "NOSTR_RS_RELAY_IMAGE",
|
||||||
"nostr-vpn": "NOSTR_VPN_IMAGE",
|
|
||||||
"fips": "FIPS_IMAGE",
|
|
||||||
"routstr": "ROUTSTR_IMAGE",
|
|
||||||
"adguardhome": "ADGUARDHOME_IMAGE",
|
"adguardhome": "ADGUARDHOME_IMAGE",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,13 +97,6 @@ STACK = {
|
|||||||
"immich_postgres": "IMMICH_POSTGRES_IMAGE",
|
"immich_postgres": "IMMICH_POSTGRES_IMAGE",
|
||||||
"immich_redis": "REDIS_IMAGE",
|
"immich_redis": "REDIS_IMAGE",
|
||||||
},
|
},
|
||||||
"penpot": {
|
|
||||||
"penpot-frontend": "PENPOT_FRONTEND_IMAGE",
|
|
||||||
"penpot-backend": "PENPOT_BACKEND_IMAGE",
|
|
||||||
"penpot-exporter": "PENPOT_EXPORTER_IMAGE",
|
|
||||||
"penpot-postgres": "PENPOT_POSTGRES_IMAGE",
|
|
||||||
"penpot-valkey": "PENPOT_VALKEY_IMAGE",
|
|
||||||
},
|
|
||||||
"mempool": {
|
"mempool": {
|
||||||
"archy-mempool-web": "MEMPOOL_WEB_IMAGE",
|
"archy-mempool-web": "MEMPOOL_WEB_IMAGE",
|
||||||
"mempool-api": "MEMPOOL_BACKEND_IMAGE",
|
"mempool-api": "MEMPOOL_BACKEND_IMAGE",
|
||||||
|
|||||||
@@ -99,13 +99,9 @@ VALKEY_IMAGE="$ARCHY_REGISTRY/valkey:8.1.6"
|
|||||||
# Nostr
|
# Nostr
|
||||||
NOSTR_RS_RELAY_IMAGE="$ARCHY_REGISTRY/nostr-rs-relay:0.10.0"
|
NOSTR_RS_RELAY_IMAGE="$ARCHY_REGISTRY/nostr-rs-relay:0.10.0"
|
||||||
STRFRY_IMAGE="$ARCHY_REGISTRY/strfry:1.0.4"
|
STRFRY_IMAGE="$ARCHY_REGISTRY/strfry:1.0.4"
|
||||||
NOSTR_VPN_IMAGE="$ARCHY_REGISTRY/nostr-vpn:v0.3.7"
|
|
||||||
NOSTR_VPN_UI_IMAGE="$ARCHY_REGISTRY/nostr-vpn-ui:latest"
|
|
||||||
FIPS_IMAGE="$ARCHY_REGISTRY/fips:v0.1.0"
|
|
||||||
FIPS_UI_IMAGE="$ARCHY_REGISTRY/fips-ui:1.7.123-alpha"
|
FIPS_UI_IMAGE="$ARCHY_REGISTRY/fips-ui:1.7.123-alpha"
|
||||||
|
|
||||||
# AI / Routing
|
# AI / Routing
|
||||||
ROUTSTR_IMAGE="$ARCHY_REGISTRY/routstr:v0.4.3"
|
|
||||||
|
|
||||||
# Community / Gaming
|
# Community / Gaming
|
||||||
BOTFIGHTS_IMAGE="$ARCHY_REGISTRY/botfights:1.2.11"
|
BOTFIGHTS_IMAGE="$ARCHY_REGISTRY/botfights:1.2.11"
|
||||||
@@ -127,12 +123,6 @@ GITEA_IMAGE="docker.io/gitea/gitea:1.23"
|
|||||||
IMMICH_POSTGRES_IMAGE="$ARCHY_REGISTRY/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0"
|
IMMICH_POSTGRES_IMAGE="$ARCHY_REGISTRY/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0"
|
||||||
IMMICH_SERVER_IMAGE="$ARCHY_REGISTRY/immich-server:release"
|
IMMICH_SERVER_IMAGE="$ARCHY_REGISTRY/immich-server:release"
|
||||||
|
|
||||||
# Penpot stack
|
|
||||||
PENPOT_POSTGRES_IMAGE="$ARCHY_REGISTRY/postgres:15"
|
|
||||||
PENPOT_VALKEY_IMAGE="$ARCHY_REGISTRY/valkey:8.1"
|
|
||||||
PENPOT_BACKEND_IMAGE="$ARCHY_REGISTRY/penpot-backend:2.4"
|
|
||||||
PENPOT_EXPORTER_IMAGE="$ARCHY_REGISTRY/penpot-exporter:2.4"
|
|
||||||
PENPOT_FRONTEND_IMAGE="$ARCHY_REGISTRY/penpot-frontend:2.4"
|
|
||||||
|
|
||||||
# Custom UI containers (built from docker/ dirs, pushed to registry)
|
# Custom UI containers (built from docker/ dirs, pushed to registry)
|
||||||
BITCOIN_UI_IMAGE="$ARCHY_REGISTRY/bitcoin-ui:1.7.123-alpha"
|
BITCOIN_UI_IMAGE="$ARCHY_REGISTRY/bitcoin-ui:1.7.123-alpha"
|
||||||
|
|||||||
Reference in New Issue
Block a user