Compare commits

...
3 Commits
Author SHA1 Message Date
archipelago 2bc5e98edb chore: publish release v1.8.12-alpha
Demo images / Build & push demo images (push) Successful in 3m49s
2026-09-11 15:17:17 -04:00
archipelago c1e14f7c7a chore: prepare release v1.8.12-alpha 2026-09-11 15:13:40 -04:00
archipelago 564ffe1c47 fix(indeedhub): generate per-node encryption root 2026-09-11 11:25:44 -04:00
14 changed files with 3822 additions and 3311 deletions
+2
View File
@@ -4,6 +4,8 @@
## 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.
+8 -8
View File
@@ -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:
+2 -1
View File
@@ -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
+1 -1
View File
@@ -104,7 +104,7 @@ dependencies = [
[[package]] [[package]]
name = "archipelago" name = "archipelago"
version = "1.8.11-alpha" version = "1.8.12-alpha"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"archipelago-container", "archipelago-container",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "archipelago" name = "archipelago"
version = "1.8.11-alpha" version = "1.8.12-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"
+27 -2
View File
@@ -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,
@@ -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
+130
View File
@@ -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();
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "neode-ui", "name": "neode-ui",
"version": "1.8.11-alpha", "version": "1.8.12-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.12-alpha",
"dependencies": { "dependencies": {
"@scure/bip39": "^2.2.0", "@scure/bip39": "^2.2.0",
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "neode-ui", "name": "neode-ui",
"private": true, "private": true,
"version": "1.8.11-alpha", "version": "1.8.12-alpha",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "./start-dev.sh", "start": "./start-dev.sh",
+39 -17
View File
@@ -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"
} }
+3337 -3184
View File
File diff suppressed because one or more lines are too long
+39 -17
View File
@@ -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"
} }
+66 -9
View File
@@ -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