Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72e84439ee | ||
|
|
5081a4fe7d | ||
|
|
39727dacbc | ||
|
|
1e409007d4 | ||
|
|
8f144c3038 | ||
|
|
8258705df7 | ||
|
|
d13002e022 | ||
|
|
e625b29d9e | ||
|
|
c4ed9fb1fa | ||
|
|
2bc5e98edb | ||
|
|
c1e14f7c7a | ||
|
|
564ffe1c47 |
+9
-1
@@ -2,8 +2,16 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## v1.8.13-alpha (2026-09-12)
|
||||||
|
|
||||||
|
- **GitWorkshop installs reliably on fresh nodes.** The app is classified as a user-facing app while its install placeholder is being created, so it remains visible under My Apps instead of Services.
|
||||||
|
- **Fresh GitWorkshop installs build the correct image.** The production orchestrator handles its bundled build context instead of sending the local image reference through the legacy registry-pull path.
|
||||||
|
- **Curated app classification is regression-tested.** Every user-facing app remains in My Apps during installation, while headless services stay in Services.
|
||||||
|
|
||||||
## v1.8.12-alpha (2026-09-11)
|
## v1.8.12-alpha (2026-09-11)
|
||||||
|
|
||||||
|
- **Fresh IndeedHub installs no longer share a fleet-wide encryption root.** The API now generates a persistent per-node AES master secret and shares it with the media worker through the platform's protected secret environment. Existing nodes migrate the exact legacy value they are already using before any container can be recreated, preserving access to encrypted data; an unreadable or empty existing root fails safely instead of being silently replaced. The manifest path, retired fallback installer, and container repair script follow the same rule.
|
||||||
|
|
||||||
- **The Companion download advertises and re-announces the APK it actually serves.** The Discover banner and its install prompt now share the no-cache APK metadata, visibly report Companion 0.5.32 build 52, and remember dismissal per Android build rather than forever, so an existing browser gets one useful update prompt when the APK changes. The ISO gate reads the expected version from the Android build itself instead of accepting the stale 0.5.28 payload.
|
- **The Companion download advertises and re-announces the APK it actually serves.** The Discover banner and its install prompt now share the no-cache APK metadata, visibly report Companion 0.5.32 build 52, and remember dismissal per Android build rather than forever, so an existing browser gets one useful update prompt when the APK changes. The ISO gate reads the expected version from the Android build itself instead of accepting the stale 0.5.28 payload.
|
||||||
|
|
||||||
- **GitWorkshop's dependency audit is clean.** The pinned upstream client keeps its separately reviewable Archipelago integration patch and now applies a deterministic dependency patch: safe lock refreshes plus targeted `fflate`, React Router, and Vitest upgrades remove all ten production advisories and all eight development advisories. A clean install reports zero vulnerabilities; type-check, all 152 upstream unit tests, and the exact Archipelago subpath build pass.
|
- **GitWorkshop's dependency audit is clean.** The pinned upstream client keeps its separately reviewable Archipelago integration patch and now applies a deterministic dependency patch: safe lock refreshes plus targeted `fflate`, React Router, and Vitest upgrades remove all ten production advisories and all eight development advisories. A clean install reports zero vulnerabilities; type-check, all 152 upstream unit tests, and the exact Archipelago subpath build pass.
|
||||||
@@ -104,7 +112,7 @@
|
|||||||
|
|
||||||
- **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.
|
- **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.
|
- **Every app in the store is now a first-class platform app.** The remaining platform apps 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. Retired apps are 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.
|
- **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.
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ is available via Debian (https://snapshot.debian.org) as referenced in each
|
|||||||
release's notes. Container images offered through the app catalog and mirror
|
release's notes. Container images offered through the app catalog and mirror
|
||||||
registry remain under their upstream licenses (including GPL/AGPL software
|
registry remain under their upstream licenses (including GPL/AGPL software
|
||||||
such as mempool, Nextcloud, Vaultwarden, SearXNG, PhotoPrism, Immich,
|
such as mempool, Nextcloud, Vaultwarden, SearXNG, PhotoPrism, Immich,
|
||||||
Jellyfin, MariaDB, AdGuard Home, and strfry); source links are provided in
|
Jellyfin, MariaDB, and strfry); source links are provided in
|
||||||
the app catalog. The modified mempool-frontend image is built from
|
the app catalog. The modified mempool-frontend image is built from
|
||||||
`docker/mempool-frontend/` in this repository (AGPL-3.0 corresponding source).
|
`docker/mempool-frontend/` in this repository (AGPL-3.0 corresponding source).
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,21 @@ Podman containers managed by the Rust backend.
|
|||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://www.rust-lang.org/)
|
[](https://www.rust-lang.org/)
|
||||||
[](https://vuejs.org/)
|
[](https://vuejs.org/)
|
||||||
[]()
|
[](https://source.archipelago-foundation.org/lfg2025/archy/releases)
|
||||||
|
|
||||||
|
## Current release
|
||||||
|
|
||||||
|
The current pre-release is **v1.8.13-alpha**. Release notes and signed OTA
|
||||||
|
artifacts are published on [Gitea](https://source.archipelago-foundation.org/lfg2025/archy/releases).
|
||||||
|
The same source is mirrored through ngit for Nostr-native cloning and
|
||||||
|
contribution:
|
||||||
|
|
||||||
|
```
|
||||||
|
nostr://npub1w3sqdkrhn0gyuvsex32effzgnfpyde6qrrc4u467flg5e9txh4wsfn5vjg/relay.ngit.dev/archy
|
||||||
|
```
|
||||||
|
|
||||||
|
Clone with ngit, or use the Gitea mirror when you need a conventional Git
|
||||||
|
remote. Contributions should follow [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||||
|
|
||||||
## What is here
|
## What is here
|
||||||
|
|
||||||
|
|||||||
@@ -34,18 +34,6 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"apps": [
|
"apps": [
|
||||||
{
|
|
||||||
"id": "adguardhome",
|
|
||||||
"title": "AdGuard Home",
|
|
||||||
"version": "v0.107.79",
|
|
||||||
"description": "Network-wide ad and tracker blocking: a DNS server that filters every device on your LAN, with a web console for rules and client management.",
|
|
||||||
"icon": "",
|
|
||||||
"author": "AdGuard",
|
|
||||||
"category": "networking",
|
|
||||||
"tier": "optional",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.79",
|
|
||||||
"repoUrl": "https://github.com/AdguardTeam/AdGuardHome"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "alby-hub",
|
"id": "alby-hub",
|
||||||
"title": "Alby Hub",
|
"title": "Alby Hub",
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
app:
|
|
||||||
id: adguardhome
|
|
||||||
name: AdGuard Home
|
|
||||||
version: v0.107.79
|
|
||||||
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.79
|
|
||||||
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: 3030
|
|
||||||
container: 3000
|
|
||||||
protocol: tcp
|
|
||||||
bind: 127.0.0.1
|
|
||||||
# 3030, not AdGuard Home's conventional 3000: Grafana owns :3000 on a
|
|
||||||
# node, and both being installable means the host ports must not
|
|
||||||
# collide (the orchestrator refuses/loads warn on overlap).
|
|
||||||
# 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:3030
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
interfaces:
|
|
||||||
main:
|
|
||||||
name: Admin console
|
|
||||||
description: AdGuard Home web console
|
|
||||||
type: ui
|
|
||||||
port: 3030
|
|
||||||
protocol: http
|
|
||||||
path: /
|
|
||||||
|
|
||||||
metadata:
|
|
||||||
author: AdGuard
|
|
||||||
category: networking
|
|
||||||
repo: https://github.com/AdguardTeam/AdGuardHome
|
|
||||||
tier: optional
|
|
||||||
@@ -19,14 +19,15 @@ app:
|
|||||||
pull_policy: if-not-present
|
pull_policy: if-not-present
|
||||||
network: indeedhub-net
|
network: indeedhub-net
|
||||||
network_aliases: [api]
|
network_aliases: [api]
|
||||||
# The JWT signing secret is owned here (no backend container owns it); the
|
# The JWT signing secret and stable envelope-encryption root are owned here;
|
||||||
# db + minio passwords are owned by indeedhub-postgres / indeedhub-minio and
|
# the db + minio passwords are owned by indeedhub-postgres / indeedhub-minio
|
||||||
# only consumed here. ensure_generated_secrets no-ops when a file already
|
# and only consumed here. Existing nodes migrate the legacy AES value into
|
||||||
# exists, so live values on .228 are preserved (postgres pw is fixed at
|
# the secret file once, while fresh nodes receive a unique per-node value.
|
||||||
# PGDATA init — regenerating would lock the API out).
|
|
||||||
generated_secrets:
|
generated_secrets:
|
||||||
- name: indeedhub-jwt
|
- name: indeedhub-jwt
|
||||||
kind: hex32
|
kind: hex32
|
||||||
|
- name: indeedhub-aes-master
|
||||||
|
kind: hex16
|
||||||
secret_env:
|
secret_env:
|
||||||
- key: DATABASE_PASSWORD
|
- key: DATABASE_PASSWORD
|
||||||
secret_file: indeedhub-db-password
|
secret_file: indeedhub-db-password
|
||||||
@@ -34,6 +35,8 @@ app:
|
|||||||
secret_file: indeedhub-minio-password
|
secret_file: indeedhub-minio-password
|
||||||
- key: NOSTR_JWT_SECRET
|
- key: NOSTR_JWT_SECRET
|
||||||
secret_file: indeedhub-jwt
|
secret_file: indeedhub-jwt
|
||||||
|
- key: AES_MASTER_SECRET
|
||||||
|
secret_file: indeedhub-aes-master
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- app_id: indeedhub-postgres
|
- app_id: indeedhub-postgres
|
||||||
@@ -67,9 +70,6 @@ app:
|
|||||||
- S3_PRIVATE_BUCKET_NAME=indeedhub-private
|
- S3_PRIVATE_BUCKET_NAME=indeedhub-private
|
||||||
- S3_PUBLIC_BUCKET_URL=/storage
|
- S3_PUBLIC_BUCKET_URL=/storage
|
||||||
- NOSTR_JWT_EXPIRES_IN=7d
|
- NOSTR_JWT_EXPIRES_IN=7d
|
||||||
# Fixed across the fleet (envelope-encryption master key baked by the legacy
|
|
||||||
# installer); not node-specific, so a plain env literal, not a secret.
|
|
||||||
- AES_MASTER_SECRET=0123456789abcdef0123456789abcdef
|
|
||||||
- ENVIRONMENT=production
|
- ENVIRONMENT=production
|
||||||
|
|
||||||
health_check:
|
health_check:
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ app:
|
|||||||
secret_file: indeedhub-db-password
|
secret_file: indeedhub-db-password
|
||||||
- key: AWS_SECRET_KEY
|
- key: AWS_SECRET_KEY
|
||||||
secret_file: indeedhub-minio-password
|
secret_file: indeedhub-minio-password
|
||||||
|
- key: AES_MASTER_SECRET
|
||||||
|
secret_file: indeedhub-aes-master
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- app_id: indeedhub-api
|
- app_id: indeedhub-api
|
||||||
@@ -51,4 +53,3 @@ app:
|
|||||||
- S3_PUBLIC_BUCKET_NAME=indeedhub-public
|
- S3_PUBLIC_BUCKET_NAME=indeedhub-public
|
||||||
- S3_PRIVATE_BUCKET_NAME=indeedhub-private
|
- S3_PRIVATE_BUCKET_NAME=indeedhub-private
|
||||||
- ENVIRONMENT=production
|
- ENVIRONMENT=production
|
||||||
- AES_MASTER_SECRET=0123456789abcdef0123456789abcdef
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -104,7 +104,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.8.11-alpha"
|
version = "1.8.13-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"archipelago-container",
|
"archipelago-container",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.8.11-alpha"
|
version = "1.8.13-alpha"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||||
|
|||||||
@@ -2743,6 +2743,11 @@ fn uses_orchestrator_install_flow(package_id: &str) -> bool {
|
|||||||
| "gitea"
|
| "gitea"
|
||||||
| "portainer"
|
| "portainer"
|
||||||
| "meshtastic"
|
| "meshtastic"
|
||||||
|
// Build-backed user-facing app. Route it through the production
|
||||||
|
// orchestrator so a fresh node builds its bundled image instead
|
||||||
|
// of treating localhost/archipelago-source:local as a registry
|
||||||
|
// image in the legacy installer.
|
||||||
|
| "archipelago-source"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2822,6 +2827,7 @@ mod tests {
|
|||||||
"gitea",
|
"gitea",
|
||||||
"portainer",
|
"portainer",
|
||||||
"meshtastic",
|
"meshtastic",
|
||||||
|
"archipelago-source",
|
||||||
] {
|
] {
|
||||||
assert!(uses_orchestrator_install_flow(app));
|
assert!(uses_orchestrator_install_flow(app));
|
||||||
assert!(should_try_orchestrator_install(app, true));
|
assert!(should_try_orchestrator_install(app, true));
|
||||||
|
|||||||
@@ -1559,6 +1559,31 @@ impl RpcHandler {
|
|||||||
self.set_install_progress("indeedhub", n_images, n_images)
|
self.set_install_progress("indeedhub", n_images, n_images)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
// The retired installer injected one fleet-wide AES root directly in
|
||||||
|
// the API/worker environment. Detect those consumers before removing
|
||||||
|
// anything, then persist the legacy value exactly once so an upgrade
|
||||||
|
// cannot orphan encrypted data. A genuinely fresh fallback install
|
||||||
|
// receives a random per-node root instead.
|
||||||
|
let mut had_existing_crypto_consumer = false;
|
||||||
|
for name in [
|
||||||
|
"indeedhub-api",
|
||||||
|
"indeedhub-ffmpeg",
|
||||||
|
"indeedhub-build_api_1",
|
||||||
|
"indeedhub-build_ffmpeg-worker_1",
|
||||||
|
] {
|
||||||
|
let status =
|
||||||
|
podman_stack_status(&["container", "exists", name], PODMAN_STACK_PROBE_TIMEOUT)
|
||||||
|
.await?;
|
||||||
|
had_existing_crypto_consumer |= status.success();
|
||||||
|
}
|
||||||
|
let secrets_dir = self.config.data_dir.join("secrets");
|
||||||
|
crate::container::secrets::ensure_indeedhub_aes_master_secret(
|
||||||
|
&secrets_dir,
|
||||||
|
had_existing_crypto_consumer,
|
||||||
|
)
|
||||||
|
.context("preparing IndeedHub encryption root")?;
|
||||||
|
let aes_master = crate::container::secrets::indeedhub_aes_master_secret(&secrets_dir)?;
|
||||||
|
|
||||||
// Remove any leftover containers from a previous partial install (or
|
// Remove any leftover containers from a previous partial install (or
|
||||||
// from the first-boot frontend stub that used to race the installer).
|
// from the first-boot frontend stub that used to race the installer).
|
||||||
// Without this, `podman run --name indeedhub` fails on name conflict
|
// Without this, `podman run --name indeedhub` fails on name conflict
|
||||||
@@ -1759,7 +1784,7 @@ impl RpcHandler {
|
|||||||
"-e".to_string(),
|
"-e".to_string(),
|
||||||
"NOSTR_JWT_EXPIRES_IN=7d".to_string(),
|
"NOSTR_JWT_EXPIRES_IN=7d".to_string(),
|
||||||
"-e".to_string(),
|
"-e".to_string(),
|
||||||
"AES_MASTER_SECRET=0123456789abcdef0123456789abcdef".to_string(),
|
format!("AES_MASTER_SECRET={aes_master}"),
|
||||||
"-e".to_string(),
|
"-e".to_string(),
|
||||||
"ENVIRONMENT=production".to_string(),
|
"ENVIRONMENT=production".to_string(),
|
||||||
format!("{registry}/indeedhub-api:1.0.0"),
|
format!("{registry}/indeedhub-api:1.0.0"),
|
||||||
@@ -1810,7 +1835,7 @@ impl RpcHandler {
|
|||||||
"-e".to_string(),
|
"-e".to_string(),
|
||||||
"ENVIRONMENT=production".to_string(),
|
"ENVIRONMENT=production".to_string(),
|
||||||
"-e".to_string(),
|
"-e".to_string(),
|
||||||
"AES_MASTER_SECRET=0123456789abcdef0123456789abcdef".to_string(),
|
format!("AES_MASTER_SECRET={aes_master}"),
|
||||||
format!("{registry}/indeedhub-ffmpeg:1.0.0"),
|
format!("{registry}/indeedhub-ffmpeg:1.0.0"),
|
||||||
],
|
],
|
||||||
&tmp_env,
|
&tmp_env,
|
||||||
|
|||||||
@@ -182,7 +182,6 @@ fn image_var_for_app(app_id: &str) -> Option<&'static str> {
|
|||||||
"immich" | "immich_server" => Some("IMMICH_SERVER_IMAGE"),
|
"immich" | "immich_server" => Some("IMMICH_SERVER_IMAGE"),
|
||||||
|
|
||||||
// Networking
|
// Networking
|
||||||
"adguardhome" => Some("ADGUARDHOME_IMAGE"),
|
|
||||||
"tor" | "archy-tor" => Some("ALPINE_TOR_IMAGE"),
|
"tor" | "archy-tor" => Some("ALPINE_TOR_IMAGE"),
|
||||||
|
|
||||||
_ => None,
|
_ => None,
|
||||||
|
|||||||
@@ -3565,6 +3565,54 @@ impl ProdContainerOrchestrator {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Materialise IndeedHub's AES root before the generic generated-secret
|
||||||
|
/// pass. Old installers injected one known value directly into the API and
|
||||||
|
/// worker environments, so an upgrade with either consumer still present
|
||||||
|
/// must persist that value before container drift can recreate them. With
|
||||||
|
/// no existing consumer this is a fresh install and receives random bytes.
|
||||||
|
async fn ensure_indeedhub_aes_master(&self, manifest: &AppManifest) -> Result<()> {
|
||||||
|
if manifest.app.id != "indeedhub-api" {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let secret_path = self
|
||||||
|
.secrets_dir
|
||||||
|
.join(crate::container::secrets::INDEEDHUB_AES_SECRET_NAME);
|
||||||
|
let preserve_legacy = if secret_path.exists() {
|
||||||
|
// The secret helper validates the existing file and, critically,
|
||||||
|
// refuses to replace a damaged encryption root.
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
let consumers = [
|
||||||
|
"indeedhub-api",
|
||||||
|
"indeedhub-ffmpeg",
|
||||||
|
"indeedhub-build_api_1",
|
||||||
|
"indeedhub-build_ffmpeg-worker_1",
|
||||||
|
];
|
||||||
|
self.runtime
|
||||||
|
.list_containers()
|
||||||
|
.await
|
||||||
|
.context("detecting an existing IndeedHub encryption-key consumer")?
|
||||||
|
.iter()
|
||||||
|
.any(|container| {
|
||||||
|
let name = container.name.trim_start_matches('/');
|
||||||
|
consumers.contains(&name)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
if crate::container::secrets::ensure_indeedhub_aes_master_secret(
|
||||||
|
&self.secrets_dir,
|
||||||
|
preserve_legacy,
|
||||||
|
)? {
|
||||||
|
tracing::info!(
|
||||||
|
app = "indeedhub-api",
|
||||||
|
path = %secret_path.display(),
|
||||||
|
"Persisted the legacy IndeedHub encryption root for upgrade compatibility"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn resolve_dynamic_env(&self, manifest: &mut AppManifest) -> Result<()> {
|
async fn resolve_dynamic_env(&self, manifest: &mut AppManifest) -> Result<()> {
|
||||||
// Idempotency guard: partitioning already ran on this instance.
|
// Idempotency guard: partitioning already ran on this instance.
|
||||||
// Re-running would re-taint against an environment that no longer
|
// Re-running would re-taint against an environment that no longer
|
||||||
@@ -3573,6 +3621,11 @@ impl ProdContainerOrchestrator {
|
|||||||
if !manifest.app.container.secret_env_refs.is_empty() {
|
if !manifest.app.container.secret_env_refs.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
// IndeedHub's data-encryption root needs an upgrade-aware first pass:
|
||||||
|
// generic generation alone would replace the fleet-wide legacy value
|
||||||
|
// and make previously encrypted data unreadable.
|
||||||
|
self.ensure_indeedhub_aes_master(manifest).await?;
|
||||||
|
|
||||||
// Materialise any manifest-declared generated secrets before they're
|
// Materialise any manifest-declared generated secrets before they're
|
||||||
// read below. This is the single chokepoint every install/reconcile
|
// read below. This is the single chokepoint every install/reconcile
|
||||||
// path funnels through, so an app's secrets exist by the time its
|
// path funnels through, so an app's secrets exist by the time its
|
||||||
@@ -5627,6 +5680,52 @@ app:
|
|||||||
"app:\n id: fedimint-gateway\n name: Fedimint Gateway\n version: 0.10.0\n container:\n image: x:1\n generated_secrets:\n - name: fedimint-gateway-hash\n kind: bcrypt\n secret_env:\n - key: FEDI_HASH\n secret_file: fedimint-gateway-hash\n"
|
"app:\n id: fedimint-gateway\n name: Fedimint Gateway\n version: 0.10.0\n container:\n image: x:1\n generated_secrets:\n - name: fedimint-gateway-hash\n kind: bcrypt\n secret_env:\n - key: FEDI_HASH\n secret_file: fedimint-gateway-hash\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn indeedhub_api_manifest_yaml() -> &'static str {
|
||||||
|
"app:\n id: indeedhub-api\n name: IndeedHub API\n version: 1.0.0\n container:\n image: x:1\n generated_secrets:\n - name: indeedhub-aes-master\n kind: hex16\n secret_env:\n - key: AES_MASTER_SECRET\n secret_file: indeedhub-aes-master\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn existing_indeedhub_consumer_gets_migration_compatible_root() {
|
||||||
|
let rt = Arc::new(MockRuntime::default());
|
||||||
|
rt.set_state("indeedhub-api", ContainerState::Running);
|
||||||
|
let mut orch = orch_with(rt).await;
|
||||||
|
let tmp = tempfile::TempDir::new().unwrap();
|
||||||
|
orch.set_secrets_dir(tmp.path().to_path_buf());
|
||||||
|
|
||||||
|
let mut manifest = AppManifest::parse(indeedhub_api_manifest_yaml()).unwrap();
|
||||||
|
orch.resolve_dynamic_env(&mut manifest).await.unwrap();
|
||||||
|
let resolved = manifest
|
||||||
|
.app
|
||||||
|
.container
|
||||||
|
.secret_env_refs
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.env_key == "AES_MASTER_SECRET")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resolved.value.len(), 32);
|
||||||
|
assert!(tmp.path().join("indeedhub-aes-master").exists());
|
||||||
|
assert!(
|
||||||
|
crate::container::secrets::ensure_indeedhub_aes_master_secret(tmp.path(), true).is_ok(),
|
||||||
|
"the migrated file remains valid and stable"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn fresh_indeedhub_install_gets_random_root() {
|
||||||
|
let rt = Arc::new(MockRuntime::default());
|
||||||
|
let mut orch = orch_with(rt).await;
|
||||||
|
let tmp = tempfile::TempDir::new().unwrap();
|
||||||
|
orch.set_secrets_dir(tmp.path().to_path_buf());
|
||||||
|
|
||||||
|
let mut manifest = AppManifest::parse(indeedhub_api_manifest_yaml()).unwrap();
|
||||||
|
orch.resolve_dynamic_env(&mut manifest).await.unwrap();
|
||||||
|
let first = crate::container::secrets::indeedhub_aes_master_secret(tmp.path()).unwrap();
|
||||||
|
|
||||||
|
let other = tempfile::TempDir::new().unwrap();
|
||||||
|
crate::container::secrets::ensure_indeedhub_aes_master_secret(other.path(), false).unwrap();
|
||||||
|
let second = crate::container::secrets::indeedhub_aes_master_secret(other.path()).unwrap();
|
||||||
|
assert_ne!(first, second, "fresh installs must receive per-node roots");
|
||||||
|
}
|
||||||
|
|
||||||
/// FED-07. Rotating a compromised credential leaves the RUNNING container
|
/// FED-07. Rotating a compromised credential leaves the RUNNING container
|
||||||
/// holding the old value, so the rotation must flag the app for recreate.
|
/// holding the old value, so the rotation must flag the app for recreate.
|
||||||
/// Without the flag the drift check skips it as restart-sensitive and the
|
/// Without the flag the drift check skips it as restart-sensitive and the
|
||||||
|
|||||||
@@ -140,6 +140,79 @@ fn random_base64(bytes: usize) -> String {
|
|||||||
/// daemon read `fedimint-gateway-hash`).
|
/// daemon read `fedimint-gateway-hash`).
|
||||||
pub const GATEWAY_HASH_SECRET_NAME: &str = "fedimint-gateway-hash";
|
pub const GATEWAY_HASH_SECRET_NAME: &str = "fedimint-gateway-hash";
|
||||||
|
|
||||||
|
/// Canonical filename for IndeedHub's envelope-encryption root. API and media
|
||||||
|
/// worker must receive the same stable value: changing it after data has been
|
||||||
|
/// encrypted can make that data unreadable.
|
||||||
|
pub const INDEEDHUB_AES_SECRET_NAME: &str = "indeedhub-aes-master";
|
||||||
|
|
||||||
|
/// The fleet-wide value used by the legacy IndeedHub installers. It remains
|
||||||
|
/// here only for the one-way migration of an already-installed stack: those
|
||||||
|
/// nodes must persist the value they have been using before the manifest
|
||||||
|
/// starts reading it from a file. Fresh installs must never receive it.
|
||||||
|
const KNOWN_LEGACY_INDEEDHUB_AES_MASTER: &str = "0123456789abcdef0123456789abcdef";
|
||||||
|
|
||||||
|
/// Ensure IndeedHub has a stable encryption root.
|
||||||
|
///
|
||||||
|
/// `preserve_legacy` is true only when an API/worker container already exists,
|
||||||
|
/// proving this is an upgrade from the installer that shipped the known legacy
|
||||||
|
/// value. In that case we persist that value once so recreating the containers
|
||||||
|
/// does not orphan encrypted data. A fresh installation gets 16 random bytes
|
||||||
|
/// encoded as 32 hex characters.
|
||||||
|
///
|
||||||
|
/// Unlike ordinary generated credentials, an existing-but-empty or unreadable
|
||||||
|
/// encryption root is never self-healed by rotation: replacement could destroy
|
||||||
|
/// access to data, so this fails loudly and leaves the file untouched.
|
||||||
|
/// Returns true only when the legacy migration value was written.
|
||||||
|
pub fn ensure_indeedhub_aes_master_secret(
|
||||||
|
secrets_dir: &Path,
|
||||||
|
preserve_legacy: bool,
|
||||||
|
) -> Result<bool> {
|
||||||
|
fs::create_dir_all(secrets_dir)
|
||||||
|
.with_context(|| format!("creating secrets dir {}", secrets_dir.display()))?;
|
||||||
|
let path = secrets_dir.join(INDEEDHUB_AES_SECRET_NAME);
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
let value = fs::read_to_string(&path).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"reading IndeedHub encryption root {} (refusing to replace it)",
|
||||||
|
path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if value.trim().is_empty() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"IndeedHub encryption root {} is empty; refusing to replace a potentially \
|
||||||
|
data-bearing key",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if preserve_legacy {
|
||||||
|
write_secret(&path, KNOWN_LEGACY_INDEEDHUB_AES_MASTER)?;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let spec = GeneratedSecret {
|
||||||
|
name: INDEEDHUB_AES_SECRET_NAME.to_string(),
|
||||||
|
kind: SecretGenKind::Hex16,
|
||||||
|
};
|
||||||
|
ensure_one(secrets_dir, &spec)?;
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the stable IndeedHub encryption root after it has been materialised.
|
||||||
|
pub fn indeedhub_aes_master_secret(secrets_dir: &Path) -> Result<String> {
|
||||||
|
let path = secrets_dir.join(INDEEDHUB_AES_SECRET_NAME);
|
||||||
|
let value = fs::read_to_string(&path)
|
||||||
|
.with_context(|| format!("reading IndeedHub encryption root {}", path.display()))?;
|
||||||
|
let value = value.trim();
|
||||||
|
if value.is_empty() {
|
||||||
|
anyhow::bail!("IndeedHub encryption root {} is empty", path.display());
|
||||||
|
}
|
||||||
|
Ok(value.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Detection-only denylist of bcrypt hashes that shipped as hardcoded
|
/// Detection-only denylist of bcrypt hashes that shipped as hardcoded
|
||||||
/// fallback credentials in this repository before FED-07. `t9YjjxkiktrlYvjajB
|
/// fallback credentials in this repository before FED-07. `t9YjjxkiktrlYvjajB
|
||||||
/// /zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC` was substituted for the Fedimint
|
/// /zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC` was substituted for the Fedimint
|
||||||
@@ -356,6 +429,63 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn indeedhub_fresh_installs_get_distinct_per_node_encryption_roots() {
|
||||||
|
let dir_a = tempfile::tempdir().unwrap();
|
||||||
|
let dir_b = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
assert!(!ensure_indeedhub_aes_master_secret(dir_a.path(), false).unwrap());
|
||||||
|
assert!(!ensure_indeedhub_aes_master_secret(dir_b.path(), false).unwrap());
|
||||||
|
let value_a = indeedhub_aes_master_secret(dir_a.path()).unwrap();
|
||||||
|
let value_b = indeedhub_aes_master_secret(dir_b.path()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(value_a.len(), 32);
|
||||||
|
assert!(value_a.chars().all(|c| c.is_ascii_hexdigit()));
|
||||||
|
assert_ne!(value_a, KNOWN_LEGACY_INDEEDHUB_AES_MASTER);
|
||||||
|
assert_ne!(value_a, value_b, "fresh nodes must not share an AES root");
|
||||||
|
let mode = std::fs::metadata(dir_a.path().join(INDEEDHUB_AES_SECRET_NAME))
|
||||||
|
.unwrap()
|
||||||
|
.permissions()
|
||||||
|
.mode()
|
||||||
|
& 0o777;
|
||||||
|
assert_eq!(mode, 0o600);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn indeedhub_existing_install_persists_legacy_root_once() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
assert!(ensure_indeedhub_aes_master_secret(dir.path(), true).unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
indeedhub_aes_master_secret(dir.path()).unwrap(),
|
||||||
|
KNOWN_LEGACY_INDEEDHUB_AES_MASTER
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!ensure_indeedhub_aes_master_secret(dir.path(), true).unwrap(),
|
||||||
|
"a second migration pass must be a no-op"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn indeedhub_existing_unique_root_is_never_rotated() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
ensure_indeedhub_aes_master_secret(dir.path(), false).unwrap();
|
||||||
|
let before = indeedhub_aes_master_secret(dir.path()).unwrap();
|
||||||
|
|
||||||
|
assert!(!ensure_indeedhub_aes_master_secret(dir.path(), true).unwrap());
|
||||||
|
assert_eq!(before, indeedhub_aes_master_secret(dir.path()).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn indeedhub_empty_root_fails_without_overwriting() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join(INDEEDHUB_AES_SECRET_NAME);
|
||||||
|
std::fs::write(&path, "").unwrap();
|
||||||
|
|
||||||
|
let err = ensure_indeedhub_aes_master_secret(dir.path(), true).unwrap_err();
|
||||||
|
assert!(err.to_string().contains("refusing to replace"));
|
||||||
|
assert_eq!(std::fs::read(&path).unwrap(), b"");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn gateway_credential_fresh_generation_verifies_and_is_0600() {
|
fn gateway_credential_fresh_generation_verifies_and_is_0600() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
//! no listener, so allowing them is inert.
|
//! no listener, so allowing them is inert.
|
||||||
|
|
||||||
pub const APP_LAUNCH_PORTS: &[u16] = &[
|
pub const APP_LAUNCH_PORTS: &[u16] = &[
|
||||||
2283, 2342, 3000, 3001, 3002, 3030, 4080, 5180, 7778, 8080, 8081, 8082, 8083, 8084, 8085, 8087,
|
2283, 2342, 3000, 3001, 3002, 4080, 5180, 7778, 8080, 8081, 8082, 8083, 8084, 8085, 8087, 8090,
|
||||||
8090, 8096, 8123, 8175, 8176, 8187, 8240, 8334, 8336, 8337, 8888, 8999, 9000, 9100, 10380,
|
8096, 8123, 8175, 8176, 8187, 8240, 8334, 8336, 8337, 8888, 8999, 9000, 9100, 10380, 11434,
|
||||||
11434, 18081, 18083, 23000, 32838, 50002,
|
18081, 18083, 23000, 32838, 50002,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1746,11 +1746,6 @@ 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,
|
||||||
@@ -1776,7 +1771,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(),
|
||||||
30,
|
28,
|
||||||
"unauthenticated port set changed — review before updating this count: {exempt:?}"
|
"unauthenticated port set changed — review before updating this count: {exempt:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1811,13 +1806,11 @@ app:
|
|||||||
// by anonymous payers), and — since the v1.8.7 platform round — the
|
// by anonymous payers), and — since the v1.8.7 platform round — the
|
||||||
// three own-login consoles brought onto the manifest platform:
|
// three own-login consoles brought onto the manifest platform:
|
||||||
// nginx-proxy-manager 8081 (NPM admin accounts), tailscale 8240
|
// nginx-proxy-manager 8081 (NPM admin accounts), tailscale 8240
|
||||||
// (tailnet login on the web console), adguardhome 3000 (AGH admin
|
// (tailnet login on the web console). Both enforce their own login,
|
||||||
// accounts + first-run wizard). All enforce their own login, and an
|
// and an operator can re-gate either from Settings → Access control.
|
||||||
// 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),
|
("nginx-proxy-manager".to_string(), 8081u16),
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ The ISO redistributes a full Debian (trixie) system plus ~29 container image tar
|
|||||||
|
|
||||||
- [ ] **GPL source offer for the ISO** — kernel, GRUB, busybox/live-boot, coreutils, nftables, cryptsetup, wireguard-tools, SYSLINUX `isohdpfx.bin`, etc. Easiest compliance: keep `/usr/share/doc/*/copyright` (the build already does ✓) **and** publish, per release, either a mirror of the exact Debian source packages (`apt-get source` snapshot / snapshot.debian.org pointer) or a written offer in the docs. Add this to the release checklist.
|
- [ ] **GPL source offer for the ISO** — kernel, GRUB, busybox/live-boot, coreutils, nftables, cryptsetup, wireguard-tools, SYSLINUX `isohdpfx.bin`, etc. Easiest compliance: keep `/usr/share/doc/*/copyright` (the build already does ✓) **and** publish, per release, either a mirror of the exact Debian source packages (`apt-get source` snapshot / snapshot.debian.org pointer) or a written offer in the docs. Add this to the release checklist.
|
||||||
- [ ] **AGPLv3 images redistributed** (mempool, Grafana, Vaultwarden, SearXNG, PhotoPrism, Nextcloud, Immich, CryptPad, MinIO): AGPL compliance = make corresponding source available. You ship a **modified** mempool-frontend (`docker/mempool-frontend` entrypoint patch) — the patch is in-repo, so compliance is met once the repo is public; state this in docs. For unmodified images, link upstream sources in the app catalog.
|
- [ ] **AGPLv3 images redistributed** (mempool, Grafana, Vaultwarden, SearXNG, PhotoPrism, Nextcloud, Immich, CryptPad, MinIO): AGPL compliance = make corresponding source available. You ship a **modified** mempool-frontend (`docker/mempool-frontend` entrypoint patch) — the patch is in-repo, so compliance is met once the repo is public; state this in docs. For unmodified images, link upstream sources in the app catalog.
|
||||||
- [ ] **GPLv2/GPLv3 images** (MariaDB, Jellyfin, AdGuard Home, strfry): unmodified redistribution → provide license text + upstream source links (a `license` + `sourceUrl` field per `app-catalog/catalog.json` entry solves this catalog-wide).
|
- [ ] **GPLv2/GPLv3 images** (MariaDB, Jellyfin, strfry): unmodified redistribution → provide license text + upstream source links (a `license` + `sourceUrl` field per `app-catalog/catalog.json` entry solves this catalog-wide).
|
||||||
- [ ] **Non-free firmware** (firmware-realtek/iwlwifi/misc/linux-nonfree, intel/amd microcode): redistributable but proprietary — disclose in docs ("includes non-free firmware for hardware support"), like Debian's own non-free-firmware ISOs do.
|
- [ ] **Non-free firmware** (firmware-realtek/iwlwifi/misc/linux-nonfree, intel/amd microcode): redistributable but proprietary — disclose in docs ("includes non-free firmware for hardware support"), like Debian's own non-free-firmware ISOs do.
|
||||||
- [ ] The ISO build's live-server image capture (`podman save` of whatever matches on the dev server) is a compliance hazard — bundle only from the audited image list.
|
- [ ] The ISO build's live-server image capture (`podman save` of whatever matches on the dev server) is a compliance hazard — bundle only from the audited image list.
|
||||||
- [ ] FIPS daemon (jmcorgan/fips v0.4.1, MIT ✓) and nostr-rs-relay binary (MIT ✓): include their license texts in the notices bundle.
|
- [ ] FIPS daemon (jmcorgan/fips v0.4.1, MIT ✓) and nostr-rs-relay binary (MIT ✓): include their license texts in the notices bundle.
|
||||||
|
|||||||
@@ -1601,7 +1601,7 @@ IMAGES_CAPTURED_FROM_SERVER=0
|
|||||||
if [ -n "$DEV_SERVER" ] && [ "$DEV_SERVER" != "localhost" ] && [ "$DEV_SERVER" != "127.0.0.1" ]; then
|
if [ -n "$DEV_SERVER" ] && [ "$DEV_SERVER" != "localhost" ] && [ "$DEV_SERVER" != "127.0.0.1" ]; then
|
||||||
echo " Capturing container images from live server ($DEV_SERVER)..."
|
echo " Capturing container images from live server ($DEV_SERVER)..."
|
||||||
# Patterns match against `podman images` repository names (not container names)
|
# Patterns match against `podman images` repository names (not container names)
|
||||||
CAPTURE_PATTERNS="bitcoin-ui bitcoinknots lnd lnd-ui electrs-ui filebrowser mempool backend frontend electrs tailscale homeassistant home-assistant btcpayserver nbxplorer postgres alpine-tor nostr-rs-relay strfry fedimintd gatewayd dwn-server vaultwarden searxng mariadb valkey nginx-alpine portainer nginx-proxy-manager adguard"
|
CAPTURE_PATTERNS="bitcoin-ui bitcoinknots lnd lnd-ui electrs-ui filebrowser mempool backend frontend electrs tailscale homeassistant home-assistant btcpayserver nbxplorer postgres alpine-tor nostr-rs-relay strfry fedimintd gatewayd dwn-server vaultwarden searxng mariadb valkey nginx-alpine portainer nginx-proxy-manager"
|
||||||
REMOTE_TMP="/tmp/archipelago-image-capture-$$"
|
REMOTE_TMP="/tmp/archipelago-image-capture-$$"
|
||||||
SAVED_LIST=$(ssh "$DEV_SERVER" "mkdir -p $REMOTE_TMP && for p in $CAPTURE_PATTERNS; do img=\$(podman images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -i \"\$p\" | head -1); [ -n \"\$img\" ] && podman save -o \"$REMOTE_TMP/\$p.tar\" \"\$img\" 2>/dev/null && echo \"\$p\"; done" 2>/dev/null) || true
|
SAVED_LIST=$(ssh "$DEV_SERVER" "mkdir -p $REMOTE_TMP && for p in $CAPTURE_PATTERNS; do img=\$(podman images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -i \"\$p\" | head -1); [ -n \"\$img\" ] && podman save -o \"$REMOTE_TMP/\$p.tar\" \"\$img\" 2>/dev/null && echo \"\$p\"; done" 2>/dev/null) || true
|
||||||
for p in $SAVED_LIST; do
|
for p in $SAVED_LIST; do
|
||||||
@@ -1648,7 +1648,6 @@ ${PHOTOPRISM_IMAGE} photoprism.tar
|
|||||||
${NEXTCLOUD_IMAGE} nextcloud.tar
|
${NEXTCLOUD_IMAGE} nextcloud.tar
|
||||||
${NPM_IMAGE} nginx-proxy-manager.tar
|
${NPM_IMAGE} nginx-proxy-manager.tar
|
||||||
${ONLYOFFICE_IMAGE} onlyoffice.tar
|
${ONLYOFFICE_IMAGE} onlyoffice.tar
|
||||||
${ADGUARDHOME_IMAGE} adguardhome.tar
|
|
||||||
"
|
"
|
||||||
|
|
||||||
# Pull and save each image (force target arch) only if not already present
|
# Pull and save each image (force target arch) only if not already present
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.8.11-alpha",
|
"version": "1.8.13-alpha",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.8.11-alpha",
|
"version": "1.8.13-alpha",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scure/bip39": "^2.2.0",
|
"@scure/bip39": "^2.2.0",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.8.11-alpha",
|
"version": "1.8.13-alpha",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "./start-dev.sh",
|
"start": "./start-dev.sh",
|
||||||
|
|||||||
@@ -34,18 +34,6 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"apps": [
|
"apps": [
|
||||||
{
|
|
||||||
"id": "adguardhome",
|
|
||||||
"title": "AdGuard Home",
|
|
||||||
"version": "v0.107.79",
|
|
||||||
"description": "Network-wide ad and tracker blocking: a DNS server that filters every device on your LAN, with a web console for rules and client management.",
|
|
||||||
"icon": "",
|
|
||||||
"author": "AdGuard",
|
|
||||||
"category": "networking",
|
|
||||||
"tier": "optional",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.79",
|
|
||||||
"repoUrl": "https://github.com/AdguardTeam/AdGuardHome"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "alby-hub",
|
"id": "alby-hub",
|
||||||
"title": "Alby Hub",
|
"title": "Alby Hub",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/** Generated by scripts/generate-app-catalog.py. Do not edit manually. */
|
/** Generated by scripts/generate-app-catalog.py. Do not edit manually. */
|
||||||
|
|
||||||
export const GENERATED_APP_PORTS: Record<string, number> = {
|
export const GENERATED_APP_PORTS: Record<string, number> = {
|
||||||
"adguardhome": 3030,
|
|
||||||
"aiui": 5180,
|
"aiui": 5180,
|
||||||
"alby-hub": 8187,
|
"alby-hub": 8187,
|
||||||
"archipelago-source": 8337,
|
"archipelago-source": 8337,
|
||||||
@@ -40,7 +39,6 @@ export const GENERATED_APP_PORTS: Record<string, number> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const GENERATED_APP_TITLES: Record<string, string> = {
|
export const GENERATED_APP_TITLES: Record<string, string> = {
|
||||||
"adguardhome": "AdGuard Home",
|
|
||||||
"aiui": "AI Assistant",
|
"aiui": "AI Assistant",
|
||||||
"alby-hub": "Alby Hub",
|
"alby-hub": "Alby Hub",
|
||||||
"archipelago-source": "GitWorkshop",
|
"archipelago-source": "GitWorkshop",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { PackageState, type PackageDataEntry } from '@/types/api'
|
import { PackageState, type PackageDataEntry } from '@/types/api'
|
||||||
import { canLaunch, filterEntriesForTab, hasFrontendUi, isServiceContainer, isServicePackage, isWebsitePackage, launchBlockedReason, resolveAppIcon, useCategoriesWithApps, DEFAULT_APP_ICON } from '../appsConfig'
|
import { APP_CATEGORY_MAP, canLaunch, filterEntriesForTab, hasFrontendUi, isServiceContainer, isServicePackage, isWebsitePackage, launchBlockedReason, resolveAppIcon, useCategoriesWithApps, DEFAULT_APP_ICON } from '../appsConfig'
|
||||||
|
|
||||||
function makePkg(id: string, title: string, category: string): PackageDataEntry {
|
function makePkg(id: string, title: string, category: string): PackageDataEntry {
|
||||||
return {
|
return {
|
||||||
@@ -105,6 +105,25 @@ describe('appsConfig service filtering', () => {
|
|||||||
expect(isWebsitePackage('some-ui-app', uiApp)).toBe(false)
|
expect(isWebsitePackage('some-ui-app', uiApp)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps GitWorkshop in My Apps while installing metadata is minimal', () => {
|
||||||
|
const installing = makePkg('archipelago-source', 'GitWorkshop', 'development')
|
||||||
|
installing.state = PackageState.Installing
|
||||||
|
expect(isWebsitePackage('archipelago-source', installing)).toBe(false)
|
||||||
|
expect(filterEntriesForTab([['archipelago-source', installing]], 'apps', 'all'))
|
||||||
|
.toEqual([['archipelago-source', installing]])
|
||||||
|
expect(filterEntriesForTab([['archipelago-source', installing]], 'services', 'all'))
|
||||||
|
.toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps every curated app placeholder out of Services during install', () => {
|
||||||
|
for (const id of Object.keys(APP_CATEGORY_MAP)) {
|
||||||
|
if (isServiceContainer(id)) continue
|
||||||
|
const placeholder = makePkg(id, id, APP_CATEGORY_MAP[id] ?? 'other')
|
||||||
|
placeholder.state = PackageState.Installing
|
||||||
|
expect(isWebsitePackage(id, placeholder), id).toBe(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('never offers Launch for an unknown container with a bare exposed port', () => {
|
it('never offers Launch for an unknown container with a bare exposed port', () => {
|
||||||
// A self-deployed compose stack (e.g. podsteadr) publishes a port, so it
|
// A self-deployed compose stack (e.g. podsteadr) publishes a port, so it
|
||||||
// has a runtime lan-address — but no manifest-declared or probed UI. It
|
// has a runtime lan-address — but no manifest-declared or probed UI. It
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ export const APP_CATEGORY_MAP: Record<string, string> = {
|
|||||||
'tailscale': 'networking', 'netbird': 'networking', 'nginx-proxy-manager': 'networking', 'portainer': 'networking',
|
'tailscale': 'networking', 'netbird': 'networking', 'nginx-proxy-manager': 'networking', 'portainer': 'networking',
|
||||||
'uptime-kuma': 'networking',
|
'uptime-kuma': 'networking',
|
||||||
'botfights': 'community',
|
'botfights': 'community',
|
||||||
|
// User-facing app: keep the install placeholder in My Apps while its
|
||||||
|
// manifest (including interfaces.main.ui) is not available yet.
|
||||||
|
'archipelago-source': 'development',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAppCategory(id: string, pkg: PackageDataEntry): string {
|
export function getAppCategory(id: string, pkg: PackageDataEntry): string {
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export function categorizeCommunityApp(app: MarketplaceApp): string {
|
|||||||
|
|
||||||
if (id.includes('vpn') || id.includes('wireguard') || id.includes('tailscale') || id.includes('netbird') ||
|
if (id.includes('vpn') || id.includes('wireguard') || id.includes('tailscale') || id.includes('netbird') ||
|
||||||
id.includes('proxy') || id.includes('dns') || id.includes('pihole') ||
|
id.includes('proxy') || id.includes('dns') || id.includes('pihole') ||
|
||||||
id.includes('adguard') || id.includes('nginx') || id.includes('tor') ||
|
id.includes('nginx') || id.includes('tor') ||
|
||||||
combined.includes('network') || combined.includes('firewall')) {
|
combined.includes('network') || combined.includes('firewall')) {
|
||||||
return 'networking'
|
return 'networking'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -362,6 +362,16 @@ init()
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
||||||
|
<!-- v1.8.13-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.8.13-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">September 12, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p><strong>GitWorkshop installs reliably on fresh nodes.</strong> The app is classified as a user-facing app while its install placeholder is being created, so it remains visible under My Apps instead of Services. Fresh installs now use the production orchestrator to build the bundled GitWorkshop image rather than sending its local image reference through the legacy registry-pull path. Regression coverage now protects all curated app classifications.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- v1.8.12-alpha -->
|
<!-- v1.8.12-alpha -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
@@ -369,6 +379,7 @@ init()
|
|||||||
<span class="text-xs text-white/40">September 11, 2026</span>
|
<span class="text-xs text-white/40">September 11, 2026</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p><strong>Fresh IndeedHub installs no longer share a fleet-wide encryption root.</strong> The API now generates a persistent per-node AES master secret and shares it with the media worker through the platform's protected secret environment. Existing nodes migrate the exact legacy value they are already using before any container can be recreated, preserving access to encrypted data; an unreadable or empty existing root fails safely instead of being silently replaced. The manifest path, retired fallback installer, and container repair script follow the same rule.</p>
|
||||||
<p><strong>The Companion download advertises and re-announces the APK it actually serves.</strong> The Discover banner and its install prompt now share the no-cache APK metadata, visibly report Companion 0.5.32 build 52, and remember dismissal per Android build rather than forever, so an existing browser gets one useful update prompt when the APK changes. The ISO gate reads the expected version from the Android build itself instead of accepting the stale 0.5.28 payload.</p>
|
<p><strong>The Companion download advertises and re-announces the APK it actually serves.</strong> The Discover banner and its install prompt now share the no-cache APK metadata, visibly report Companion 0.5.32 build 52, and remember dismissal per Android build rather than forever, so an existing browser gets one useful update prompt when the APK changes. The ISO gate reads the expected version from the Android build itself instead of accepting the stale 0.5.28 payload.</p>
|
||||||
<p><strong>GitWorkshop's dependency audit is clean.</strong> The pinned upstream client keeps its separately reviewable Archipelago integration patch and now applies a deterministic dependency patch: safe lock refreshes plus targeted fflate, React Router, and Vitest upgrades remove all ten production advisories and all eight development advisories. A clean install reports zero vulnerabilities; type-check, all 152 upstream unit tests, and the exact Archipelago subpath build pass.</p>
|
<p><strong>GitWorkshop's dependency audit is clean.</strong> The pinned upstream client keeps its separately reviewable Archipelago integration patch and now applies a deterministic dependency patch: safe lock refreshes plus targeted fflate, React Router, and Vitest upgrades remove all ten production advisories and all eight development advisories. A clean install reports zero vulnerabilities; type-check, all 152 upstream unit tests, and the exact Archipelago subpath build pass.</p>
|
||||||
<p><strong>Every completed payment now gets the full Lightning-style receipt screen.</strong> Cashu and Fedimint sends no longer leave the payment form open behind a token; wallet, QR-scan, Web5, and app-requested sends all replace their forms with the animated success state. Payment hashes, transaction IDs, ecash tokens/notes, mint details, and other useful references remain copyable in the receipt, and receive completions open the same distinct payment-success modal. Minibits claims retain a short-lived durable receipt so the visible modal still reports success when another dashboard or Companion context wins the claim-poll race, while concurrent watchers now share one bounded relay fetch instead of queueing several long polls.</p>
|
<p><strong>Every completed payment now gets the full Lightning-style receipt screen.</strong> Cashu and Fedimint sends no longer leave the payment form open behind a token; wallet, QR-scan, Web5, and app-requested sends all replace their forms with the animated success state. Payment hashes, transaction IDs, ecash tokens/notes, mint details, and other useful references remain copyable in the receipt, and receive completions open the same distinct payment-success modal. Minibits claims retain a short-lived durable receipt so the visible modal still reports success when another dashboard or Companion context wins the claim-poll race, while concurrent watchers now share one bounded relay fetch instead of queueing several long polls.</p>
|
||||||
@@ -459,7 +470,7 @@ init()
|
|||||||
<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 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>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>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>Every app in the store is now a first-class platform app.</strong> The remaining platform apps 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. Retired apps are 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>
|
<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>
|
||||||
|
|||||||
+39
-17
@@ -1,29 +1,51 @@
|
|||||||
{
|
{
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"**Cuprate now syncs without burning a core for days.** The app's shipped config now enables Cuprate's checkpoint-backed `fast_sync` path, raises the database cache to 8 GiB, and gives the container a 10 GiB memory limit so the cache has real headroom. A live comparison that motivated the change saw the affected node sit around 45% CPU while the corrected config held near low single digits at the same chain height and block rate. The restricted RPC remains fronted through the safe app gate/Tor path.",
|
"**Fresh IndeedHub installs no longer share a fleet-wide encryption root.** The API now generates a persistent per-node AES master secret and shares it with the media worker through the platform's protected secret environment. Existing nodes migrate the exact legacy value they are already using before any container can be recreated, preserving access to encrypted data; an unreadable or empty existing root fails safely instead of being silently replaced. The manifest path, retired fallback installer, and container repair script follow the same rule.",
|
||||||
"**OpenWrt Gateway setup is documented from a real install, and two setup bugs are fixed.** The new guide walks a node operator through flashing a GL.iNet AX3000 to stock OpenWrt, pairing it with Archipelago, and installing TollGate pay-as-you-go WiFi. The installer now finds `opkg`/`apk` through the router's actual `PATH` instead of assuming `/usr/bin`, the UI no longer sends an empty password over a saved router connection, and the pinned TollGate package moves to `v0.5.0` with a native `.apk` install path where upstream provides one.",
|
"**The Companion download advertises and re-announces the APK it actually serves.** The Discover banner and its install prompt now share the no-cache APK metadata, visibly report Companion 0.5.32 build 52, and remember dismissal per Android build rather than forever, so an existing browser gets one useful update prompt when the APK changes. The ISO gate reads the expected version from the Android build itself instead of accepting the stale 0.5.28 payload.",
|
||||||
"**Release publishing now checks the public Gitea download links before a manifest goes live.** The publisher already fetched every artifact back and verified its size and SHA-256; this release adds a second guard for the release page itself, so a bad Gitea `ROOT_URL` or proxy setting cannot publish working files behind broken public HTTPS download links."
|
"**GitWorkshop's dependency audit is clean.** The pinned upstream client keeps its separately reviewable Archipelago integration patch and now applies a deterministic dependency patch: safe lock refreshes plus targeted `fflate`, React Router, and Vitest upgrades remove all ten production advisories and all eight development advisories. A clean install reports zero vulnerabilities; type-check, all 152 upstream unit tests, and the exact Archipelago subpath build pass.",
|
||||||
|
"**Every completed payment now gets the full Lightning-style receipt screen.** Cashu and Fedimint sends no longer leave the payment form open behind a token; wallet, QR-scan, Web5, and app-requested sends all replace their forms with the animated success state. Payment hashes, transaction IDs, ecash tokens/notes, mint details, and other useful references remain copyable in the receipt, and receive completions open the same distinct payment-success modal. Minibits claims retain a short-lived durable receipt so the visible modal still reports success when another dashboard or Companion context wins the claim-poll race, while concurrent watchers now share one bounded relay fetch instead of queueing several long polls.",
|
||||||
|
"**TollGate provisioning closes the free-access path without taking over an admin network.** Confirmed upstream `TollGate-*` access points are moved from LAN onto the paid network, mint URLs are normalized consistently, and operators can set a validated Lightning payout address without replacing merchant keys or other revenue-share identities. Malformed existing identity data now stops provisioning safely instead of being overwritten.",
|
||||||
|
"**Cashu receive gains a human-readable Minibits Lightning address.** The node derives the profile from the existing ecash recovery phrase, collects payments from the Minibits Nostr delivery relays, and redeems them into the Cashu wallet. Claim polling is single-flight, state and already-consumed tokens are written atomically with private permissions, same-second events are deduplicated without being skipped, restored seeds cannot reuse another wallet's profile, and pending claims retain the service key that encrypted them across key rotations. The UI identifies Minibits as a third-party beta service and recommends small balances.",
|
||||||
|
"**Nostr sign-in returns directly to the app instead of a black or grey frame.** The top-level signer broker now stays loaded as a 1px non-interactive surface parked physically off-screen; removing or display-hiding its full-screen cross-origin iframe could leave stale compositor pixels above IndeeHub or GitWorkshop in Android WebView and mobile Chromium until refresh. One retained broker also keeps identity selection and its immediately following signing request in a continuous UI, while Companion no longer adds a separate 180ms cover that made GitWorkshop visibly flicker.",
|
||||||
|
"**Gitea is sized for source and release hosting, not an empty demo.** Its manifest storage allowance is now 50GiB, release attachments accept individual files up to 10GiB, container-package owner storage remains unlimited, and HTTP/HTTPS proxy uploads share a streamed 10GiB ceiling. Existing repository, package, LFS and release data is unchanged.",
|
||||||
|
"**Companion browser-tab signing now accepts the app gate's complete session.** A fresh external browser no longer needs a prior dashboard login/localStorage marker before the dashboard-origin signer can load. The app gate now issues both the shared HttpOnly node session and its matching readable CSRF token, so identity discovery and signing RPCs work after that one login instead of rendering a misleading “No identities found” state. Normal dashboard logout/session checks keep their existing behavior.",
|
||||||
|
"**Fast Nostr identity choices now survive app startup and Companion tabs.** The tab/WebView broker waits for the application load event before opening its first-run picker, queues every NIP-07 call until the signer is initialized, and hands the just-selected public key directly to the immediate login request. GitWorkshop now turns that first-run choice into its normal extension account automatically, eliminating the startup race that surfaced as IndeedHub's “Could not get public key from extension.”",
|
||||||
|
"**GitWorkshop makes network projects and Archipelago login explicit.** Its signed-in dashboard now includes recent repositories from the Nostr git index, the NIP-07 action reads “Extension / Archipelago,” and explicit Archipelago logins reopen the node identity chooser instead of silently reusing the first identity. Direct, user-triggered NIP-07 logins receive the same account-switch behavior for upstream apps such as IndeedHub.",
|
||||||
|
"**IndeedHub tab signing now tracks the dashboard signer.** The injected provider supports the contained signer broker in direct tabs, is cache-busted, and is reconciled after dashboard-only updates as well as app installs and starts.",
|
||||||
|
"**App launches now honor credentials everywhere.** Home, Spotlight, Discover, My Apps, and app-detail launches all pass through one platform-owned credential handoff, so Portainer's first-run token and the File Browser/PhotoPrism login details can no longer be skipped by launching from the Home grid.",
|
||||||
|
"**Manage Updates returns to Download immediately after cancellation.** Canceling a stalled OTA now clears both the local staged state and progress state instead of leaving an incorrect Install button visible until the page is refreshed.",
|
||||||
|
"**GitWorkshop no longer probes a desktop-only localhost relay or unauthenticated manifest.** The packaged upstream client disables its default `localhost:4869` nostrdb probe, uses credentialed manifest loading, drops dead lookup relays, and permits the dashboard's contained signer broker in its frame policy.",
|
||||||
|
"**Rootless app ports self-heal when `pasta` drops a listener.** The five-minute container doctor compares every running container's declared Podman port bindings with actual host listeners and restarts only a container whose listener vanished. TCP and UDP are checked separately, avoiding false restarts of services such as NetBird's UDP port 3478. This covers the intermittent Nginx Proxy Manager port 8081 rebind failure without requiring a node reboot.",
|
||||||
|
"**Nostr identity actions now use one contained, companion-safe signing experience.** The old full-screen signer has been replaced by the same in-app consent surface used by embedded apps, with the animated identity circle as a brief signing indicator and an explicit completion state. Editing an identity now ends on a dedicated success screen that reports relay coverage and the event ID instead of disappearing back into the form. The app developer guide defines this platform-owned NIP-07 flow and its browser/Companion test matrix so apps do not add a second signer UI.",
|
||||||
|
"**Discovery merchandising is now owned by the signed app registry.** The catalog declares the Popular Apps set and contribution promotion; Discover renders two desktop rows of popular apps, then the “Your node. Your source.” banner, then the remaining apps. GitWorkshop uses a cache-busted copy of its current upstream mark, and its catalog entry identifies the canonical Archipelago maintainer npub.",
|
||||||
|
"**Companion opens Source in its native WebView and installs the node certificate.** GitWorkshop is a top-level page in the Companion in-app browser—not a dashboard iframe—and its injected provider uses the contained, consent-gated signer broker. The generic native launcher turns relative app paths into complete URLs before handing them to Android. The Node certificate button uses Android's system credential installer in the companion instead of an unsupported WebView download.",
|
||||||
|
"**Node certificate guidance now covers installation and the failures people actually see.** Settings includes the complete macOS, iOS/iPadOS, Windows, Android, Linux, Firefox, and Arch/Manjaro steps; reminds users to restart browsers that cache trust decisions; separates certificate trust from DNS; and maps common browser symptoms to their likely cause.",
|
||||||
|
"**Tab and Companion Nostr sign-in no longer loses the broker or an early identity choice.** The signer route validates the shared app-gate session with the implemented, authenticated `system.get-hostname` RPC instead of the nonexistent `system.get-version`. The provider also exposes a sticky identity subscription so a GitWorkshop React listener that mounts just after selection still completes the normal NIP-07 login. The dashboard service worker no longer precaches the signer route or provider, preventing an old bridge from surviving an update. This repairs GitWorkshop automatic login and IndeeHub's external mobile-browser flow.",
|
||||||
|
"**The App Store now makes Archipelago's source an invitation to contribute.** GitWorkshop has its real upstream icon and source-focused description, plus a dedicated “Your node. Your source.” banner explaining that users can browse the code, clone with ngit, and send issues, patches, and reviews over Nostr.",
|
||||||
|
"**Source now packages GitWorkshop instead of maintaining a separate Nostr Git interface.** The pinned upstream client runs read-only behind the authenticated app gate, launches at the dashboard's same origin under `/app/archipelago-source/`, and uses the node's consent-gated NIP-07 bridge. The upstream revision declares no license; Archipelago's owner accepted that redistribution risk without representing the client as licensed. Production publication still requires a tested canonical Archipelago NIP-34/GRASP announcement.",
|
||||||
|
"**Changing the node password now reports a wrong current password directly.** The backend was already rejecting the request before changing either the web or SSH password, but its error sanitizer replaced that safe, actionable explanation with “check server logs.” The real validation error now reaches the password dialog.",
|
||||||
|
"**The periodic container doctor runs from the same canonical path used by OTA updates.** Its systemd unit and embedded bootstrap still pointed at the retired source-checkout path while release updates installed the script under `/opt/archipelago/scripts`, leaving the doctor failed on nodes without that checkout. ISO, OTA bootstrap, and the deployment smoke test now agree on the `/opt` path."
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"current_version": "1.8.11-alpha",
|
"current_version": "1.8.12-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.11-alpha/archipelago",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.12-alpha/archipelago",
|
||||||
"name": "archipelago",
|
"name": "archipelago",
|
||||||
"new_version": "1.8.11-alpha",
|
"new_version": "1.8.12-alpha",
|
||||||
"sha256": "ae569054edd6b2491beb101815f6809bc00c95a7dbe86bd084bcb9a7c36e1853",
|
"sha256": "c06a45eab3a6b377ba6baf385ccc92f712437f1ab4d52ca9265d6876976ec506",
|
||||||
"size_bytes": 64179264
|
"size_bytes": 64589048
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"current_version": "1.8.11-alpha",
|
"current_version": "1.8.12-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.11-alpha/archipelago-frontend-1.8.11-alpha.tar.gz",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.12-alpha/archipelago-frontend-1.8.12-alpha.tar.gz",
|
||||||
"name": "archipelago-frontend-1.8.11-alpha.tar.gz",
|
"name": "archipelago-frontend-1.8.12-alpha.tar.gz",
|
||||||
"new_version": "1.8.11-alpha",
|
"new_version": "1.8.12-alpha",
|
||||||
"sha256": "192fd0470b6ccf66e78c80b4a4c3af5468882b85d81959362a3bd11f88b9d71d",
|
"sha256": "8349c4e6b5024d910c3417db1eccdb175590a80504308b99b61475b069b46994",
|
||||||
"size_bytes": 97741740
|
"size_bytes": 97917145
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"release_date": "2026-09-07",
|
"release_date": "2026-09-11",
|
||||||
"signature": "6449ce6ef35a4ef4fa6d0923bb58a2bff52ea5430d5532496e8f0af9ed52eaec293f19d7bec272dc9bc1af5fb2cdfa0e46068c827a9a4dd9cbef92d1c5845301",
|
"signature": "febcc318f2bb26606cdd19725f0ae546b57e1c1c13736a807267d9e07b2f815df8fe1bf264084d2db1e01bb8b38200959cd9ce8b20f8d4134beae5839540ce06",
|
||||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||||
"version": "1.8.11-alpha"
|
"version": "1.8.12-alpha"
|
||||||
}
|
}
|
||||||
|
|||||||
+3275
-3218
File diff suppressed because one or more lines are too long
+39
-17
@@ -1,29 +1,51 @@
|
|||||||
{
|
{
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"**Cuprate now syncs without burning a core for days.** The app's shipped config now enables Cuprate's checkpoint-backed `fast_sync` path, raises the database cache to 8 GiB, and gives the container a 10 GiB memory limit so the cache has real headroom. A live comparison that motivated the change saw the affected node sit around 45% CPU while the corrected config held near low single digits at the same chain height and block rate. The restricted RPC remains fronted through the safe app gate/Tor path.",
|
"**Fresh IndeedHub installs no longer share a fleet-wide encryption root.** The API now generates a persistent per-node AES master secret and shares it with the media worker through the platform's protected secret environment. Existing nodes migrate the exact legacy value they are already using before any container can be recreated, preserving access to encrypted data; an unreadable or empty existing root fails safely instead of being silently replaced. The manifest path, retired fallback installer, and container repair script follow the same rule.",
|
||||||
"**OpenWrt Gateway setup is documented from a real install, and two setup bugs are fixed.** The new guide walks a node operator through flashing a GL.iNet AX3000 to stock OpenWrt, pairing it with Archipelago, and installing TollGate pay-as-you-go WiFi. The installer now finds `opkg`/`apk` through the router's actual `PATH` instead of assuming `/usr/bin`, the UI no longer sends an empty password over a saved router connection, and the pinned TollGate package moves to `v0.5.0` with a native `.apk` install path where upstream provides one.",
|
"**The Companion download advertises and re-announces the APK it actually serves.** The Discover banner and its install prompt now share the no-cache APK metadata, visibly report Companion 0.5.32 build 52, and remember dismissal per Android build rather than forever, so an existing browser gets one useful update prompt when the APK changes. The ISO gate reads the expected version from the Android build itself instead of accepting the stale 0.5.28 payload.",
|
||||||
"**Release publishing now checks the public Gitea download links before a manifest goes live.** The publisher already fetched every artifact back and verified its size and SHA-256; this release adds a second guard for the release page itself, so a bad Gitea `ROOT_URL` or proxy setting cannot publish working files behind broken public HTTPS download links."
|
"**GitWorkshop's dependency audit is clean.** The pinned upstream client keeps its separately reviewable Archipelago integration patch and now applies a deterministic dependency patch: safe lock refreshes plus targeted `fflate`, React Router, and Vitest upgrades remove all ten production advisories and all eight development advisories. A clean install reports zero vulnerabilities; type-check, all 152 upstream unit tests, and the exact Archipelago subpath build pass.",
|
||||||
|
"**Every completed payment now gets the full Lightning-style receipt screen.** Cashu and Fedimint sends no longer leave the payment form open behind a token; wallet, QR-scan, Web5, and app-requested sends all replace their forms with the animated success state. Payment hashes, transaction IDs, ecash tokens/notes, mint details, and other useful references remain copyable in the receipt, and receive completions open the same distinct payment-success modal. Minibits claims retain a short-lived durable receipt so the visible modal still reports success when another dashboard or Companion context wins the claim-poll race, while concurrent watchers now share one bounded relay fetch instead of queueing several long polls.",
|
||||||
|
"**TollGate provisioning closes the free-access path without taking over an admin network.** Confirmed upstream `TollGate-*` access points are moved from LAN onto the paid network, mint URLs are normalized consistently, and operators can set a validated Lightning payout address without replacing merchant keys or other revenue-share identities. Malformed existing identity data now stops provisioning safely instead of being overwritten.",
|
||||||
|
"**Cashu receive gains a human-readable Minibits Lightning address.** The node derives the profile from the existing ecash recovery phrase, collects payments from the Minibits Nostr delivery relays, and redeems them into the Cashu wallet. Claim polling is single-flight, state and already-consumed tokens are written atomically with private permissions, same-second events are deduplicated without being skipped, restored seeds cannot reuse another wallet's profile, and pending claims retain the service key that encrypted them across key rotations. The UI identifies Minibits as a third-party beta service and recommends small balances.",
|
||||||
|
"**Nostr sign-in returns directly to the app instead of a black or grey frame.** The top-level signer broker now stays loaded as a 1px non-interactive surface parked physically off-screen; removing or display-hiding its full-screen cross-origin iframe could leave stale compositor pixels above IndeeHub or GitWorkshop in Android WebView and mobile Chromium until refresh. One retained broker also keeps identity selection and its immediately following signing request in a continuous UI, while Companion no longer adds a separate 180ms cover that made GitWorkshop visibly flicker.",
|
||||||
|
"**Gitea is sized for source and release hosting, not an empty demo.** Its manifest storage allowance is now 50GiB, release attachments accept individual files up to 10GiB, container-package owner storage remains unlimited, and HTTP/HTTPS proxy uploads share a streamed 10GiB ceiling. Existing repository, package, LFS and release data is unchanged.",
|
||||||
|
"**Companion browser-tab signing now accepts the app gate's complete session.** A fresh external browser no longer needs a prior dashboard login/localStorage marker before the dashboard-origin signer can load. The app gate now issues both the shared HttpOnly node session and its matching readable CSRF token, so identity discovery and signing RPCs work after that one login instead of rendering a misleading “No identities found” state. Normal dashboard logout/session checks keep their existing behavior.",
|
||||||
|
"**Fast Nostr identity choices now survive app startup and Companion tabs.** The tab/WebView broker waits for the application load event before opening its first-run picker, queues every NIP-07 call until the signer is initialized, and hands the just-selected public key directly to the immediate login request. GitWorkshop now turns that first-run choice into its normal extension account automatically, eliminating the startup race that surfaced as IndeedHub's “Could not get public key from extension.”",
|
||||||
|
"**GitWorkshop makes network projects and Archipelago login explicit.** Its signed-in dashboard now includes recent repositories from the Nostr git index, the NIP-07 action reads “Extension / Archipelago,” and explicit Archipelago logins reopen the node identity chooser instead of silently reusing the first identity. Direct, user-triggered NIP-07 logins receive the same account-switch behavior for upstream apps such as IndeedHub.",
|
||||||
|
"**IndeedHub tab signing now tracks the dashboard signer.** The injected provider supports the contained signer broker in direct tabs, is cache-busted, and is reconciled after dashboard-only updates as well as app installs and starts.",
|
||||||
|
"**App launches now honor credentials everywhere.** Home, Spotlight, Discover, My Apps, and app-detail launches all pass through one platform-owned credential handoff, so Portainer's first-run token and the File Browser/PhotoPrism login details can no longer be skipped by launching from the Home grid.",
|
||||||
|
"**Manage Updates returns to Download immediately after cancellation.** Canceling a stalled OTA now clears both the local staged state and progress state instead of leaving an incorrect Install button visible until the page is refreshed.",
|
||||||
|
"**GitWorkshop no longer probes a desktop-only localhost relay or unauthenticated manifest.** The packaged upstream client disables its default `localhost:4869` nostrdb probe, uses credentialed manifest loading, drops dead lookup relays, and permits the dashboard's contained signer broker in its frame policy.",
|
||||||
|
"**Rootless app ports self-heal when `pasta` drops a listener.** The five-minute container doctor compares every running container's declared Podman port bindings with actual host listeners and restarts only a container whose listener vanished. TCP and UDP are checked separately, avoiding false restarts of services such as NetBird's UDP port 3478. This covers the intermittent Nginx Proxy Manager port 8081 rebind failure without requiring a node reboot.",
|
||||||
|
"**Nostr identity actions now use one contained, companion-safe signing experience.** The old full-screen signer has been replaced by the same in-app consent surface used by embedded apps, with the animated identity circle as a brief signing indicator and an explicit completion state. Editing an identity now ends on a dedicated success screen that reports relay coverage and the event ID instead of disappearing back into the form. The app developer guide defines this platform-owned NIP-07 flow and its browser/Companion test matrix so apps do not add a second signer UI.",
|
||||||
|
"**Discovery merchandising is now owned by the signed app registry.** The catalog declares the Popular Apps set and contribution promotion; Discover renders two desktop rows of popular apps, then the “Your node. Your source.” banner, then the remaining apps. GitWorkshop uses a cache-busted copy of its current upstream mark, and its catalog entry identifies the canonical Archipelago maintainer npub.",
|
||||||
|
"**Companion opens Source in its native WebView and installs the node certificate.** GitWorkshop is a top-level page in the Companion in-app browser—not a dashboard iframe—and its injected provider uses the contained, consent-gated signer broker. The generic native launcher turns relative app paths into complete URLs before handing them to Android. The Node certificate button uses Android's system credential installer in the companion instead of an unsupported WebView download.",
|
||||||
|
"**Node certificate guidance now covers installation and the failures people actually see.** Settings includes the complete macOS, iOS/iPadOS, Windows, Android, Linux, Firefox, and Arch/Manjaro steps; reminds users to restart browsers that cache trust decisions; separates certificate trust from DNS; and maps common browser symptoms to their likely cause.",
|
||||||
|
"**Tab and Companion Nostr sign-in no longer loses the broker or an early identity choice.** The signer route validates the shared app-gate session with the implemented, authenticated `system.get-hostname` RPC instead of the nonexistent `system.get-version`. The provider also exposes a sticky identity subscription so a GitWorkshop React listener that mounts just after selection still completes the normal NIP-07 login. The dashboard service worker no longer precaches the signer route or provider, preventing an old bridge from surviving an update. This repairs GitWorkshop automatic login and IndeeHub's external mobile-browser flow.",
|
||||||
|
"**The App Store now makes Archipelago's source an invitation to contribute.** GitWorkshop has its real upstream icon and source-focused description, plus a dedicated “Your node. Your source.” banner explaining that users can browse the code, clone with ngit, and send issues, patches, and reviews over Nostr.",
|
||||||
|
"**Source now packages GitWorkshop instead of maintaining a separate Nostr Git interface.** The pinned upstream client runs read-only behind the authenticated app gate, launches at the dashboard's same origin under `/app/archipelago-source/`, and uses the node's consent-gated NIP-07 bridge. The upstream revision declares no license; Archipelago's owner accepted that redistribution risk without representing the client as licensed. Production publication still requires a tested canonical Archipelago NIP-34/GRASP announcement.",
|
||||||
|
"**Changing the node password now reports a wrong current password directly.** The backend was already rejecting the request before changing either the web or SSH password, but its error sanitizer replaced that safe, actionable explanation with “check server logs.” The real validation error now reaches the password dialog.",
|
||||||
|
"**The periodic container doctor runs from the same canonical path used by OTA updates.** Its systemd unit and embedded bootstrap still pointed at the retired source-checkout path while release updates installed the script under `/opt/archipelago/scripts`, leaving the doctor failed on nodes without that checkout. ISO, OTA bootstrap, and the deployment smoke test now agree on the `/opt` path."
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"current_version": "1.8.11-alpha",
|
"current_version": "1.8.12-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.11-alpha/archipelago",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.12-alpha/archipelago",
|
||||||
"name": "archipelago",
|
"name": "archipelago",
|
||||||
"new_version": "1.8.11-alpha",
|
"new_version": "1.8.12-alpha",
|
||||||
"sha256": "ae569054edd6b2491beb101815f6809bc00c95a7dbe86bd084bcb9a7c36e1853",
|
"sha256": "c06a45eab3a6b377ba6baf385ccc92f712437f1ab4d52ca9265d6876976ec506",
|
||||||
"size_bytes": 64179264
|
"size_bytes": 64589048
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"current_version": "1.8.11-alpha",
|
"current_version": "1.8.12-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.11-alpha/archipelago-frontend-1.8.11-alpha.tar.gz",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.12-alpha/archipelago-frontend-1.8.12-alpha.tar.gz",
|
||||||
"name": "archipelago-frontend-1.8.11-alpha.tar.gz",
|
"name": "archipelago-frontend-1.8.12-alpha.tar.gz",
|
||||||
"new_version": "1.8.11-alpha",
|
"new_version": "1.8.12-alpha",
|
||||||
"sha256": "192fd0470b6ccf66e78c80b4a4c3af5468882b85d81959362a3bd11f88b9d71d",
|
"sha256": "8349c4e6b5024d910c3417db1eccdb175590a80504308b99b61475b069b46994",
|
||||||
"size_bytes": 97741740
|
"size_bytes": 97917145
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"release_date": "2026-09-07",
|
"release_date": "2026-09-11",
|
||||||
"signature": "6449ce6ef35a4ef4fa6d0923bb58a2bff52ea5430d5532496e8f0af9ed52eaec293f19d7bec272dc9bc1af5fb2cdfa0e46068c827a9a4dd9cbef92d1c5845301",
|
"signature": "febcc318f2bb26606cdd19725f0ae546b57e1c1c13736a807267d9e07b2f815df8fe1bf264084d2db1e01bb8b38200959cd9ce8b20f8d4134beae5839540ce06",
|
||||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||||
"version": "1.8.11-alpha"
|
"version": "1.8.12-alpha"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"changelog": [
|
||||||
|
"**GitWorkshop installs reliably on fresh nodes.** The app is classified as a user-facing app while its install placeholder is being created, so it remains visible under My Apps instead of Services.",
|
||||||
|
"**Fresh GitWorkshop installs build the correct image.** The production orchestrator handles its bundled build context instead of sending the local image reference through the legacy registry-pull path.",
|
||||||
|
"**Curated app classification is regression-tested.** Every user-facing app remains in My Apps during installation, while headless services stay in Services."
|
||||||
|
],
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"current_version": "1.8.13-alpha",
|
||||||
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.13-alpha/archipelago",
|
||||||
|
"name": "archipelago",
|
||||||
|
"new_version": "1.8.13-alpha",
|
||||||
|
"sha256": "832c7e75b395f94f919a21d1ad7d32d367e1ef84a0e0995b4e8fe87268aa21a4",
|
||||||
|
"size_bytes": 64585424
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current_version": "1.8.13-alpha",
|
||||||
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.13-alpha/archipelago-frontend-1.8.13-alpha.tar.gz",
|
||||||
|
"name": "archipelago-frontend-1.8.13-alpha.tar.gz",
|
||||||
|
"new_version": "1.8.13-alpha",
|
||||||
|
"sha256": "d0159b61f84eb30013634a97177801474376dddb1189024b94f16236ce7ff538",
|
||||||
|
"size_bytes": 97915796
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"release_date": "2026-09-12",
|
||||||
|
"signature": "45a322e793a2fc7565cc148f14efbbcf03e0c934183590375368091368ffe49ed2a7e9962692caf54b3432c71976a6c1b0f06a48cf8c4634c7b2e573e5782a0f",
|
||||||
|
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||||
|
"version": "1.8.13-alpha"
|
||||||
|
}
|
||||||
@@ -21,6 +21,63 @@ echo "Node IP: $NODE_IP"
|
|||||||
|
|
||||||
NETWORK="indeedhub-build_indeedhub-network"
|
NETWORK="indeedhub-build_indeedhub-network"
|
||||||
|
|
||||||
|
# Preserve every credential before any existing stack member is removed. Old
|
||||||
|
# installs carried some only in container environments; fresh repairs get
|
||||||
|
# random per-node values. Never print a value into repair logs.
|
||||||
|
SECRETS_DIR="/var/lib/archipelago/secrets"
|
||||||
|
ensure_secret_from_container_env() {
|
||||||
|
secret_name="$1"
|
||||||
|
random_bytes="$2"
|
||||||
|
shift 2
|
||||||
|
secret_path="$SECRETS_DIR/$secret_name"
|
||||||
|
if [ -e "$secret_path" ] && { [ ! -r "$secret_path" ] || [ ! -s "$secret_path" ]; }; then
|
||||||
|
echo "ERROR: $secret_name exists but is unreadable or empty; refusing to replace it."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ ! -e "$secret_path" ]; then
|
||||||
|
recovered=""
|
||||||
|
for source in "$@"; do
|
||||||
|
c="${source%%:*}"
|
||||||
|
env_key="${source#*:}"
|
||||||
|
if podman container exists "$c" 2>/dev/null; then
|
||||||
|
candidate=$(podman inspect "$c" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | sed -n "s/^${env_key}=//p" | head -1)
|
||||||
|
if [ -n "$candidate" ]; then
|
||||||
|
recovered="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -z "$recovered" ]; then
|
||||||
|
recovered=$(openssl rand -hex "$random_bytes")
|
||||||
|
fi
|
||||||
|
mkdir -p "$SECRETS_DIR"
|
||||||
|
umask 077
|
||||||
|
secret_tmp=$(mktemp "$SECRETS_DIR/.${secret_name}.XXXXXX")
|
||||||
|
printf '%s' "$recovered" > "$secret_tmp"
|
||||||
|
chmod 600 "$secret_tmp"
|
||||||
|
mv "$secret_tmp" "$secret_path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_secret_from_container_env indeedhub-aes-master 16 \
|
||||||
|
indeedhub-api:AES_MASTER_SECRET indeedhub-ffmpeg:AES_MASTER_SECRET \
|
||||||
|
indeedhub-build_api_1:AES_MASTER_SECRET indeedhub-build_ffmpeg-worker_1:AES_MASTER_SECRET
|
||||||
|
ensure_secret_from_container_env indeedhub-db-password 24 \
|
||||||
|
indeedhub-api:DATABASE_PASSWORD indeedhub-ffmpeg:DATABASE_PASSWORD \
|
||||||
|
indeedhub-build_api_1:DATABASE_PASSWORD indeedhub-build_ffmpeg-worker_1:DATABASE_PASSWORD \
|
||||||
|
indeedhub-postgres:POSTGRES_PASSWORD
|
||||||
|
ensure_secret_from_container_env indeedhub-minio-password 24 \
|
||||||
|
indeedhub-api:AWS_SECRET_KEY indeedhub-ffmpeg:AWS_SECRET_KEY \
|
||||||
|
indeedhub-build_api_1:AWS_SECRET_KEY indeedhub-build_ffmpeg-worker_1:AWS_SECRET_KEY \
|
||||||
|
indeedhub-minio:MINIO_ROOT_PASSWORD
|
||||||
|
ensure_secret_from_container_env indeedhub-jwt 32 \
|
||||||
|
indeedhub-api:NOSTR_JWT_SECRET indeedhub-build_api_1:NOSTR_JWT_SECRET
|
||||||
|
|
||||||
|
AES_MASTER_SECRET=$(tr -d '\r\n' < "$SECRETS_DIR/indeedhub-aes-master")
|
||||||
|
DATABASE_PASSWORD=$(tr -d '\r\n' < "$SECRETS_DIR/indeedhub-db-password")
|
||||||
|
MINIO_ROOT_PASSWORD=$(tr -d '\r\n' < "$SECRETS_DIR/indeedhub-minio-password")
|
||||||
|
NOSTR_JWT_SECRET=$(tr -d '\r\n' < "$SECRETS_DIR/indeedhub-jwt")
|
||||||
|
|
||||||
# Load custom images if tar exists
|
# Load custom images if tar exists
|
||||||
if [ -f /tmp/indeedhub-images.tar ]; then
|
if [ -f /tmp/indeedhub-images.tar ]; then
|
||||||
echo "Loading custom images from tar..."
|
echo "Loading custom images from tar..."
|
||||||
@@ -62,7 +119,7 @@ podman run -d --name indeedhub-postgres \
|
|||||||
--network "$NETWORK" --network-alias postgres \
|
--network "$NETWORK" --network-alias postgres \
|
||||||
-v indeedhub-postgres-data:/var/lib/postgresql/data \
|
-v indeedhub-postgres-data:/var/lib/postgresql/data \
|
||||||
-e POSTGRES_USER=indeedhub \
|
-e POSTGRES_USER=indeedhub \
|
||||||
-e POSTGRES_PASSWORD=indeehhub-archy-2026 \
|
-e POSTGRES_PASSWORD="$DATABASE_PASSWORD" \
|
||||||
-e POSTGRES_DB=indeedhub \
|
-e POSTGRES_DB=indeedhub \
|
||||||
"$INDEEDHUB_POSTGRES_IMAGE"
|
"$INDEEDHUB_POSTGRES_IMAGE"
|
||||||
|
|
||||||
@@ -92,7 +149,7 @@ podman run -d --name indeedhub-minio \
|
|||||||
--network "$NETWORK" --network-alias minio \
|
--network "$NETWORK" --network-alias minio \
|
||||||
-v indeedhub-minio-data:/data \
|
-v indeedhub-minio-data:/data \
|
||||||
-e MINIO_ROOT_USER=indeeadmin \
|
-e MINIO_ROOT_USER=indeeadmin \
|
||||||
-e MINIO_ROOT_PASSWORD=indeeadmin2026 \
|
-e MINIO_ROOT_PASSWORD="$MINIO_ROOT_PASSWORD" \
|
||||||
"${MINIO_IMAGE}" \
|
"${MINIO_IMAGE}" \
|
||||||
server /data --console-address ":9001"
|
server /data --console-address ":9001"
|
||||||
|
|
||||||
@@ -116,7 +173,7 @@ podman run -d --name indeedhub-build_api_1 \
|
|||||||
-e DATABASE_HOST=postgres \
|
-e DATABASE_HOST=postgres \
|
||||||
-e DATABASE_PORT=5432 \
|
-e DATABASE_PORT=5432 \
|
||||||
-e DATABASE_USER=indeedhub \
|
-e DATABASE_USER=indeedhub \
|
||||||
-e DATABASE_PASSWORD=indeehhub-archy-2026 \
|
-e DATABASE_PASSWORD="$DATABASE_PASSWORD" \
|
||||||
-e DATABASE_NAME=indeedhub \
|
-e DATABASE_NAME=indeedhub \
|
||||||
-e QUEUE_HOST=redis \
|
-e QUEUE_HOST=redis \
|
||||||
-e QUEUE_PORT=6379 \
|
-e QUEUE_PORT=6379 \
|
||||||
@@ -124,7 +181,7 @@ podman run -d --name indeedhub-build_api_1 \
|
|||||||
-e S3_ENDPOINT=http://minio:9000 \
|
-e S3_ENDPOINT=http://minio:9000 \
|
||||||
-e AWS_REGION=us-east-1 \
|
-e AWS_REGION=us-east-1 \
|
||||||
-e AWS_ACCESS_KEY=indeeadmin \
|
-e AWS_ACCESS_KEY=indeeadmin \
|
||||||
-e AWS_SECRET_KEY=indeeadmin2026 \
|
-e AWS_SECRET_KEY="$MINIO_ROOT_PASSWORD" \
|
||||||
-e S3_PRIVATE_BUCKET_NAME=indeedhub-private \
|
-e S3_PRIVATE_BUCKET_NAME=indeedhub-private \
|
||||||
-e S3_PUBLIC_BUCKET_NAME=indeedhub-public \
|
-e S3_PUBLIC_BUCKET_NAME=indeedhub-public \
|
||||||
-e S3_PUBLIC_BUCKET_URL=/storage \
|
-e S3_PUBLIC_BUCKET_URL=/storage \
|
||||||
@@ -132,9 +189,9 @@ podman run -d --name indeedhub-build_api_1 \
|
|||||||
-e "BTCPAY_API_KEY=" \
|
-e "BTCPAY_API_KEY=" \
|
||||||
-e "BTCPAY_STORE_ID=" \
|
-e "BTCPAY_STORE_ID=" \
|
||||||
-e "BTCPAY_WEBHOOK_SECRET=" \
|
-e "BTCPAY_WEBHOOK_SECRET=" \
|
||||||
-e NOSTR_JWT_SECRET=archipelago-indeehhub-jwt-secret-2026 \
|
-e NOSTR_JWT_SECRET="$NOSTR_JWT_SECRET" \
|
||||||
-e NOSTR_JWT_EXPIRES_IN=7d \
|
-e NOSTR_JWT_EXPIRES_IN=7d \
|
||||||
-e AES_MASTER_SECRET=0123456789abcdef0123456789abcdef \
|
-e AES_MASTER_SECRET="$AES_MASTER_SECRET" \
|
||||||
-e "ADMIN_API_KEY=" \
|
-e "ADMIN_API_KEY=" \
|
||||||
-e NODE_OPTIONS=--max-old-space-size=1024 \
|
-e NODE_OPTIONS=--max-old-space-size=1024 \
|
||||||
--health-cmd "wget --no-verbose --tries=1 --spider http://localhost:4000/nostr-auth/health || exit 1" \
|
--health-cmd "wget --no-verbose --tries=1 --spider http://localhost:4000/nostr-auth/health || exit 1" \
|
||||||
@@ -154,7 +211,7 @@ podman run -d --name indeedhub-build_ffmpeg-worker_1 \
|
|||||||
-e DATABASE_HOST=postgres \
|
-e DATABASE_HOST=postgres \
|
||||||
-e DATABASE_PORT=5432 \
|
-e DATABASE_PORT=5432 \
|
||||||
-e DATABASE_USER=indeedhub \
|
-e DATABASE_USER=indeedhub \
|
||||||
-e DATABASE_PASSWORD=indeehhub-archy-2026 \
|
-e DATABASE_PASSWORD="$DATABASE_PASSWORD" \
|
||||||
-e DATABASE_NAME=indeedhub \
|
-e DATABASE_NAME=indeedhub \
|
||||||
-e QUEUE_HOST=redis \
|
-e QUEUE_HOST=redis \
|
||||||
-e QUEUE_PORT=6379 \
|
-e QUEUE_PORT=6379 \
|
||||||
@@ -162,11 +219,11 @@ podman run -d --name indeedhub-build_ffmpeg-worker_1 \
|
|||||||
-e S3_ENDPOINT=http://minio:9000 \
|
-e S3_ENDPOINT=http://minio:9000 \
|
||||||
-e AWS_REGION=us-east-1 \
|
-e AWS_REGION=us-east-1 \
|
||||||
-e AWS_ACCESS_KEY=indeeadmin \
|
-e AWS_ACCESS_KEY=indeeadmin \
|
||||||
-e AWS_SECRET_KEY=indeeadmin2026 \
|
-e AWS_SECRET_KEY="$MINIO_ROOT_PASSWORD" \
|
||||||
-e S3_PRIVATE_BUCKET_NAME=indeedhub-private \
|
-e S3_PRIVATE_BUCKET_NAME=indeedhub-private \
|
||||||
-e S3_PUBLIC_BUCKET_NAME=indeedhub-public \
|
-e S3_PUBLIC_BUCKET_NAME=indeedhub-public \
|
||||||
-e S3_PUBLIC_BUCKET_URL=/storage \
|
-e S3_PUBLIC_BUCKET_URL=/storage \
|
||||||
-e AES_MASTER_SECRET=0123456789abcdef0123456789abcdef \
|
-e AES_MASTER_SECRET="$AES_MASTER_SECRET" \
|
||||||
localhost/indeedhub-build_ffmpeg-worker:local
|
localhost/indeedhub-build_ffmpeg-worker:local
|
||||||
|
|
||||||
# 7. IndeedHub Frontend
|
# 7. IndeedHub Frontend
|
||||||
|
|||||||
@@ -81,7 +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",
|
||||||
"adguardhome": "ADGUARDHOME_IMAGE",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Stack apps: app_id -> {container_name: image variable}. The FIRST entry is the
|
# Stack apps: app_id -> {container_name: image variable}. The FIRST entry is the
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ NETBIRD_DASHBOARD_IMAGE="docker.io/netbirdio/dashboard:v2.38.0"
|
|||||||
NETBIRD_SERVER_IMAGE="docker.io/netbirdio/netbird-server:0.71.2"
|
NETBIRD_SERVER_IMAGE="docker.io/netbirdio/netbird-server:0.71.2"
|
||||||
NETBIRD_PROXY_IMAGE="docker.io/library/nginx:1.27-alpine"
|
NETBIRD_PROXY_IMAGE="docker.io/library/nginx:1.27-alpine"
|
||||||
ALPINE_TOR_IMAGE="$ARCHY_REGISTRY/alpine-tor:0.4.8.13"
|
ALPINE_TOR_IMAGE="$ARCHY_REGISTRY/alpine-tor:0.4.8.13"
|
||||||
ADGUARDHOME_IMAGE="$ARCHY_REGISTRY/adguardhome:v0.107.79"
|
|
||||||
|
|
||||||
# Fedimint
|
# Fedimint
|
||||||
FEDIMINT_IMAGE="$ARCHY_REGISTRY/fedimintd:v0.10.1"
|
FEDIMINT_IMAGE="$ARCHY_REGISTRY/fedimintd:v0.10.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user