Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33477f284b | ||
|
|
540639d2c1 | ||
|
|
562871b1ce | ||
|
|
cca3f8bfcd | ||
|
|
89c08be712 | ||
|
|
b4ecf86c13 | ||
|
|
b14fe78306 | ||
|
|
3f0c1038c3 | ||
|
|
1fbefce6df |
@@ -2,6 +2,12 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
## v1.8.19-alpha (2026-09-28)
|
||||
|
||||
- Fixed the embedded AIUI chat page painting a second background and dark scrim over Archy’s dashboard background.
|
||||
- Embedded AIUI now stays transparent so the dashboard background appears once.
|
||||
- AIUI background fixes are now included reliably in OTA updates and fresh installations.
|
||||
|
||||
## v1.8.18-alpha (2026-09-18)
|
||||
|
||||
- Framework startup prioritizes Bitcoin and LND before unrelated containers, and unavailable LND balances remain unavailable instead of appearing as false zeroes.
|
||||
|
||||
@@ -46,13 +46,14 @@ interface RateBucket {
|
||||
|
||||
const rateBuckets = new Map<string, RateBucket>()
|
||||
|
||||
// Clean up stale buckets every 5 minutes
|
||||
// Vite imports this module during builds too; cleanup must not keep the
|
||||
// process alive once compilation has finished.
|
||||
setInterval(() => {
|
||||
const now = Date.now()
|
||||
for (const [key, bucket] of rateBuckets) {
|
||||
if (now > bucket.resetAt) rateBuckets.delete(key)
|
||||
}
|
||||
}, 5 * 60_000)
|
||||
}, 5 * 60_000).unref()
|
||||
|
||||
function getClientIp(req: IncomingMessage): string {
|
||||
return req.socket.remoteAddress ?? 'unknown'
|
||||
|
||||
@@ -33,6 +33,7 @@ const PWA_CACHE_VERSION = '2'
|
||||
// Only embedded when explicitly requested via ?embedded param
|
||||
const _embeddedFlag = new URLSearchParams(window.location.search).has('embedded')
|
||||
;(window as unknown as Record<string, unknown>).__AIUI_EMBEDDED__ = _embeddedFlag
|
||||
document.documentElement.classList.toggle('aiui-embedded', _embeddedFlag)
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
|
||||
@@ -57,12 +57,8 @@ body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
/* Every page paints its own explicit background (bg-[#0a0a0a] / bg-[#faf9f6])
|
||||
EXCEPT the embedded Chat page, which intentionally goes transparent so
|
||||
Archy's own dark chrome can show behind it (Chat.vue's iframe host). With
|
||||
no background-color here, "transparent" fell through to the browser's
|
||||
default white canvas instead. Match the theme's own dark/light default so
|
||||
nothing above this ever needs to guess. */
|
||||
/* Standalone canvas fallback. Embedded mode overrides this below so
|
||||
Archy's wallpaper remains visible through the iframe. */
|
||||
background-color: #0a0a0a;
|
||||
}
|
||||
|
||||
@@ -70,6 +66,19 @@ html.light body {
|
||||
background-color: #faf9f6;
|
||||
}
|
||||
|
||||
/* The host owns the wallpaper when AIUI is embedded. The document canvas
|
||||
must be transparent too, otherwise it hides the host behind ChatPage. */
|
||||
html.aiui-embedded {
|
||||
/* Match Archy's dark canvas scheme. Browsers otherwise give an iframe
|
||||
with a different scheme an opaque canvas despite transparent CSS. */
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
html.aiui-embedded,
|
||||
html.aiui-embedded body {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ===== DARK MODE GLASSMORPHISM — from Archy ===== */
|
||||
|
||||
@layer components {
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
app:
|
||||
id: conduit-market
|
||||
name: Conduit Market
|
||||
version: 1.0.0
|
||||
# Built by this project from a pinned upstream commit (Conduit ships no
|
||||
# Dockerfile of its own) — there is no upstream release feed/tag to watch,
|
||||
# so re-pin CONDUIT_COMMIT in the Dockerfile deliberately, by hand.
|
||||
upstream:
|
||||
kind: github
|
||||
repo: Conduit-BTC/conduit-mono
|
||||
description: |
|
||||
Decentralized Nostr + Lightning marketplace (apps/market from
|
||||
Conduit-BTC/conduit-mono). Pure client-side SPA — listings are NIP-99
|
||||
events, orders are encrypted DMs, payments settle over Lightning via
|
||||
NWC — confirmed by reading packages/core/src/config.ts and
|
||||
apps/market/package.json directly; there is no custom backend to run.
|
||||
|
||||
Public/private is a genuine runtime toggle, not two separate builds:
|
||||
Vite bakes VITE_* relay env vars into the bundle at compile time (see
|
||||
the comment on getViteEnv() in config.ts), so a single image built once
|
||||
could never be reconfigured afterward via ordinary env vars. This
|
||||
package patches config.ts (docker/conduit-market/apply-runtime-override.py)
|
||||
to merge in a window.__CONDUIT_RUNTIME_ENV__ override object, written by
|
||||
docker-entrypoint.sh from CONDUIT_MODE/CONDUIT_PRIVATE_RELAY_URLS at
|
||||
container *start*. Flipping CONDUIT_MODE and restarting the container is
|
||||
enough — no rebuild required. CONDUIT_MODE=private with no relay URL set
|
||||
refuses to start rather than silently falling back to the public network.
|
||||
category: money
|
||||
|
||||
container:
|
||||
build:
|
||||
context: /opt/archipelago/docker/conduit-market
|
||||
dockerfile: Dockerfile
|
||||
tag: localhost/conduit-market:local
|
||||
network: archy-net
|
||||
|
||||
dependencies: []
|
||||
|
||||
resources:
|
||||
memory_limit: 64Mi
|
||||
|
||||
security:
|
||||
capabilities: []
|
||||
# false, not true: docker-entrypoint.sh regenerates runtime-env.js under
|
||||
# /usr/share/nginx/html (part of the image root fs, not a volume) on
|
||||
# every container start — that's the actual public/private switch, so
|
||||
# the root fs must stay writable for the mode flip to take effect.
|
||||
readonly_root: false
|
||||
no_new_privileges: true
|
||||
network_policy: bridge
|
||||
|
||||
ports:
|
||||
- host: 8091
|
||||
container: 8080
|
||||
protocol: tcp
|
||||
bind: 127.0.0.1
|
||||
auth: none
|
||||
auth_rationale: >-
|
||||
Public storefront meant for anonymous shoppers, not an admin
|
||||
surface — Conduit has no login of its own (identity is a Nostr
|
||||
keypair held client-side) and a cookie/session gate in front of it
|
||||
would just block real customers from browsing or checking out.
|
||||
|
||||
volumes: []
|
||||
|
||||
# CONDUIT_MODE (public|private) and CONDUIT_PRIVATE_RELAY_URLS are the
|
||||
# actual public/private switch, read by docker-entrypoint.sh at container
|
||||
# start — see the description above. Defaults here are the safe/inert
|
||||
# choice (public, using Conduit's own canonical relay network); an
|
||||
# operator who wants an isolated single-relay storefront sets both.
|
||||
environment:
|
||||
- CONDUIT_MODE=public
|
||||
- CONDUIT_PRIVATE_RELAY_URLS=
|
||||
- CONDUIT_LIGHTNING_NETWORK=mainnet
|
||||
|
||||
health_check:
|
||||
type: http
|
||||
endpoint: http://127.0.0.1:8091
|
||||
path: /health
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
interfaces:
|
||||
main:
|
||||
name: Storefront
|
||||
description: Conduit Market storefront
|
||||
type: ui
|
||||
port: 8091
|
||||
protocol: http
|
||||
path: /
|
||||
|
||||
metadata:
|
||||
category: money
|
||||
tier: optional
|
||||
author: Conduit-BTC
|
||||
repo: https://github.com/Conduit-BTC/conduit-mono
|
||||
features:
|
||||
- Nostr-native marketplace listings (NIP-99)
|
||||
- Encrypted order flow over Nostr DMs
|
||||
- Lightning checkout via Nostr Wallet Connect
|
||||
- Runtime-configurable public or private relay mode, no rebuild
|
||||
Generated
+1
-1
@@ -104,7 +104,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.8.18-alpha"
|
||||
version = "1.8.19-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.8.18-alpha"
|
||||
version = "1.8.19-alpha"
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
|
||||
@@ -678,28 +678,12 @@ impl RpcHandler {
|
||||
.unwrap_or("download")
|
||||
.to_string();
|
||||
let dir = self.config.data_dir.join("filebrowser").join(folder);
|
||||
if let Err(e) = tokio::fs::create_dir_all(&dir).await {
|
||||
tracing::warn!("paid download: cannot create {}: {e}", dir.display());
|
||||
} else {
|
||||
// Don't clobber an existing file of the same name: "x.jpg"
|
||||
// → "x (2).jpg" etc.
|
||||
let mut target = dir.join(&base);
|
||||
let (stem, ext) = match base.rsplit_once('.') {
|
||||
Some((s, e)) if !s.is_empty() => (s.to_string(), format!(".{e}")),
|
||||
_ => (base.clone(), String::new()),
|
||||
};
|
||||
let mut n = 2;
|
||||
while target.exists() {
|
||||
target = dir.join(format!("{stem} ({n}){ext}"));
|
||||
n += 1;
|
||||
}
|
||||
match tokio::fs::write(&target, &bytes).await {
|
||||
Ok(()) => tracing::info!("paid download: filed into {}", target.display()),
|
||||
Err(e) => tracing::warn!(
|
||||
"paid download: filing into {} failed (non-fatal): {e}",
|
||||
target.display()
|
||||
),
|
||||
}
|
||||
match crate::container::filebrowser::save_new_file(&dir, &base, &bytes).await {
|
||||
Ok(path) => tracing::info!("paid download: filed into {}", path.display()),
|
||||
Err(e) => tracing::warn!(
|
||||
"paid download: filing into {} failed (non-fatal): {e:#}",
|
||||
dir.display()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! starting the container with `--config /data/.filebrowser.json`.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::update::host_sudo;
|
||||
@@ -117,6 +117,123 @@ fn shell_quote(s: &str) -> String {
|
||||
s.replace('\'', "'\\''")
|
||||
}
|
||||
|
||||
/// Save `bytes` into FileBrowser's storage as a new file in `dir`, named
|
||||
/// `name` or, if that's taken, `name (2)`, `name (3)`… Never overwrites.
|
||||
/// Returns the path written.
|
||||
///
|
||||
/// FileBrowser's folders belong to its rootless container range (host uid
|
||||
/// 100000, mode 755), so this service — host uid 1000, outside that range —
|
||||
/// can read them but not write into them, and filing a purchase into Files
|
||||
/// failed with EACCES (2026-09-29). When a direct write is refused, the file
|
||||
/// is written through `podman unshare`, where that range is ours, and given
|
||||
/// the folder's owner so FileBrowser manages it like its own uploads.
|
||||
pub async fn save_new_file(dir: &Path, name: &str, bytes: &[u8]) -> Result<PathBuf> {
|
||||
save_new_file_with(dir, name, bytes, write_via_userns).await
|
||||
}
|
||||
|
||||
async fn save_new_file_with<F, Fut>(
|
||||
dir: &Path,
|
||||
name: &str,
|
||||
bytes: &[u8],
|
||||
fallback: F,
|
||||
) -> Result<PathBuf>
|
||||
where
|
||||
F: FnOnce(PathBuf, Vec<u8>) -> Fut,
|
||||
Fut: std::future::Future<Output = Result<()>>,
|
||||
{
|
||||
let target = unused_name(dir, name);
|
||||
match write_direct(dir, &target, bytes).await {
|
||||
Ok(()) => Ok(target),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
|
||||
fallback(target.clone(), bytes.to_vec())
|
||||
.await
|
||||
.with_context(|| format!("writing {} via podman unshare", target.display()))?;
|
||||
Ok(target)
|
||||
}
|
||||
Err(e) => Err(e).with_context(|| format!("writing {}", target.display())),
|
||||
}
|
||||
}
|
||||
|
||||
/// `dir/name`, or the first free `dir/stem (n).ext` from n = 2.
|
||||
fn unused_name(dir: &Path, name: &str) -> PathBuf {
|
||||
let mut target = dir.join(name);
|
||||
let (stem, ext) = match name.rsplit_once('.') {
|
||||
Some((s, e)) if !s.is_empty() => (s.to_string(), format!(".{e}")),
|
||||
_ => (name.to_string(), String::new()),
|
||||
};
|
||||
let mut n = 2;
|
||||
while target.exists() {
|
||||
target = dir.join(format!("{stem} ({n}){ext}"));
|
||||
n += 1;
|
||||
}
|
||||
target
|
||||
}
|
||||
|
||||
async fn write_direct(dir: &Path, target: &Path, bytes: &[u8]) -> std::io::Result<()> {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
fs::create_dir_all(dir).await?;
|
||||
let mut f = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(target)
|
||||
.await?;
|
||||
let written = async {
|
||||
f.write_all(bytes).await?;
|
||||
f.flush().await
|
||||
}
|
||||
.await;
|
||||
if written.is_err() {
|
||||
let _ = fs::remove_file(target).await;
|
||||
}
|
||||
written
|
||||
}
|
||||
|
||||
/// Write `bytes` (piped on stdin) to `target` from inside the rootless user
|
||||
/// namespace. It goes to a temp file first and is hard-linked into place, so
|
||||
/// FileBrowser never sees a partial file and an existing file is never
|
||||
/// replaced (`ln` refuses an existing name).
|
||||
async fn write_via_userns(target: PathBuf, bytes: Vec<u8>) -> Result<()> {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
const SCRIPT: &str = r#"set -eu
|
||||
dst=$1
|
||||
dir=$(dirname -- "$dst")
|
||||
if [ ! -d "$dir" ]; then
|
||||
mkdir -- "$dir"
|
||||
chown --reference="$(dirname -- "$dir")" -- "$dir"
|
||||
fi
|
||||
tmp="$dir/.archy-saving.$$"
|
||||
trap 'rm -f -- "$tmp"' EXIT
|
||||
cat > "$tmp"
|
||||
chown --reference="$dir" -- "$tmp"
|
||||
chmod 0644 -- "$tmp"
|
||||
ln -- "$tmp" "$dst"
|
||||
"#;
|
||||
let mut child = tokio::process::Command::new("podman")
|
||||
.args(["unshare", "sh", "-c", SCRIPT, "sh"])
|
||||
.arg(&target)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.context("Failed to run podman unshare")?;
|
||||
let mut stdin = child.stdin.take().context("podman unshare stdin")?;
|
||||
let fed = stdin.write_all(&bytes).await;
|
||||
drop(stdin);
|
||||
let out = child
|
||||
.wait_with_output()
|
||||
.await
|
||||
.context("Failed to wait for podman unshare")?;
|
||||
if !out.status.success() {
|
||||
anyhow::bail!(
|
||||
"podman unshare exited with {}: {}",
|
||||
out.status,
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
);
|
||||
}
|
||||
fed.context("Failed to pipe the file to podman unshare")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -151,4 +268,95 @@ mod tests {
|
||||
let second = ensure_config(&paths).await.unwrap();
|
||||
assert_eq!(second, EnsureOutcome::Unchanged);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unused_name_numbers_duplicates_and_keeps_the_extension() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let d = dir.path();
|
||||
assert_eq!(unused_name(d, "song.mp3"), d.join("song.mp3"));
|
||||
std::fs::write(d.join("song.mp3"), b"").unwrap();
|
||||
assert_eq!(unused_name(d, "song.mp3"), d.join("song (2).mp3"));
|
||||
std::fs::write(d.join("song (2).mp3"), b"").unwrap();
|
||||
assert_eq!(unused_name(d, "song.mp3"), d.join("song (3).mp3"));
|
||||
std::fs::write(d.join("README"), b"").unwrap();
|
||||
assert_eq!(unused_name(d, "README"), d.join("README (2)"));
|
||||
std::fs::write(d.join(".hidden"), b"").unwrap();
|
||||
assert_eq!(unused_name(d, ".hidden"), d.join(".hidden (2)"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn save_new_file_writes_directly_into_a_writable_folder() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let music = dir.path().join("Music");
|
||||
let path = save_new_file_with(&music, "a.mp3", b"abc", |_, _| async {
|
||||
anyhow::bail!("fallback must not run")
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(path, music.join("a.mp3"));
|
||||
assert_eq!(std::fs::read(&path).unwrap(), b"abc");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn save_new_file_never_overwrites_an_existing_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(dir.path().join("a.mp3"), b"original").unwrap();
|
||||
let path = save_new_file_with(dir.path(), "a.mp3", b"new", |_, _| async {
|
||||
anyhow::bail!("fallback must not run")
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(path, dir.path().join("a (2).mp3"));
|
||||
assert_eq!(
|
||||
std::fs::read(dir.path().join("a.mp3")).unwrap(),
|
||||
b"original"
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression (2026-09-29): filing a purchase into a FileBrowser folder
|
||||
/// owned by the container's uid range failed with EACCES. A refused
|
||||
/// write must go through the user-namespace fallback, with the same
|
||||
/// target and bytes.
|
||||
#[tokio::test]
|
||||
async fn a_refused_write_goes_through_the_userns_fallback() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let music = dir.path().join("Music");
|
||||
std::fs::create_dir(&music).unwrap();
|
||||
std::fs::set_permissions(&music, std::fs::Permissions::from_mode(0o555)).unwrap();
|
||||
if std::fs::File::create(music.join("probe")).is_ok() {
|
||||
return; // running as root: mode bits don't refuse the write
|
||||
}
|
||||
|
||||
let seen = std::sync::Mutex::new(None);
|
||||
let path = save_new_file_with(&music, "a.mp3", b"abc", |target, bytes| {
|
||||
*seen.lock().unwrap() = Some((target, bytes));
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(path, music.join("a.mp3"));
|
||||
assert_eq!(
|
||||
seen.into_inner().unwrap(),
|
||||
Some((music.join("a.mp3"), b"abc".to_vec()))
|
||||
);
|
||||
std::fs::set_permissions(&music, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_failed_fallback_is_reported() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o555)).unwrap();
|
||||
if std::fs::File::create(dir.path().join("probe")).is_ok() {
|
||||
return;
|
||||
}
|
||||
let err = save_new_file_with(dir.path(), "a.mp3", b"abc", |_, _| async {
|
||||
anyhow::bail!("no podman")
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(format!("{err:#}").contains("no podman"));
|
||||
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
# syntax=docker/dockerfile:1.6
|
||||
#
|
||||
# Packages Conduit Market (github.com/Conduit-BTC/conduit-mono, apps/market)
|
||||
# as a static Archipelago app. Conduit itself is a pure client-side SPA --
|
||||
# products are Nostr events (NIP-99), orders are encrypted DMs, payments go
|
||||
# over Lightning via NWC -- no custom backend, confirmed by reading
|
||||
# packages/core/src/config.ts and apps/market/package.json directly rather
|
||||
# than assumed. Upstream ships no Dockerfile; this one is new.
|
||||
#
|
||||
# Pinned to a specific upstream commit (not a moving branch) for
|
||||
# reproducibility, matching the archy convention already used for
|
||||
# apps/cuprate. Re-pin deliberately, not on every rebuild.
|
||||
#
|
||||
# oven/bun:1.1-slim is pinned to an EOL Debian bullseye base whose
|
||||
# -security pool no longer serves the package versions it references
|
||||
# (404s on ca-certificates/curl et al). oven/bun:1-slim currently resolves
|
||||
# to bun 1.4.x on Debian trixie (current), so use that instead -- still a
|
||||
# floating minor tag, but the alternative (hand-pinning a bullseye/bookworm
|
||||
# digest) just reintroduces the same staleness problem later.
|
||||
FROM oven/bun:1-slim AS build
|
||||
|
||||
ARG CONDUIT_COMMIT=c6606382dda8953763646f4498b2c4a4103da0a3
|
||||
|
||||
WORKDIR /build
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -fsSL "https://github.com/Conduit-BTC/conduit-mono/archive/${CONDUIT_COMMIT}.tar.gz" \
|
||||
-o /tmp/conduit.tar.gz \
|
||||
&& tar xzf /tmp/conduit.tar.gz --strip-components=1 \
|
||||
&& rm /tmp/conduit.tar.gz
|
||||
|
||||
# Full workspace install: apps/market depends on @conduit/core and
|
||||
# @conduit/ui via workspace:*, so bun needs the whole monorepo present to
|
||||
# link them, even though only the market app actually gets built below.
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
COPY apply-runtime-override.py /build/apply-runtime-override.py
|
||||
RUN python3 apply-runtime-override.py
|
||||
|
||||
# Loads window.__CONDUIT_RUNTIME_ENV__ (written by docker-entrypoint.sh at
|
||||
# container start) before the app bundle runs. A plain (non-module) script
|
||||
# tag with an absolute path: Vite copies these through to dist/index.html
|
||||
# verbatim without trying to resolve them as part of the module graph, and
|
||||
# does not require the file to exist in the source tree at build time.
|
||||
RUN sed -i 's#<script type="module" src="/src/main.tsx"></script>#<script src="/runtime-env.js"></script>\n <script type="module" src="/src/main.tsx"></script>#' \
|
||||
apps/market/index.html
|
||||
|
||||
# Leave every VITE_* relay/network var unset here on purpose -- the runtime
|
||||
# override (see above) is the actual public/private switch; baking any of
|
||||
# these in at build time would defeat the point of a live-configurable
|
||||
# single image.
|
||||
RUN bun run --filter '@conduit/market' build
|
||||
|
||||
FROM nginx:1.27-alpine
|
||||
|
||||
COPY --from=build /build/apps/market/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.d/50-conduit-runtime-env.sh
|
||||
RUN chmod +x /docker-entrypoint.d/50-conduit-runtime-env.sh
|
||||
|
||||
EXPOSE 8080
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Patches packages/core/src/config.ts so it can be reconfigured
|
||||
public/private at container-start time instead of only at build time.
|
||||
|
||||
Vite bakes VITE_* env vars into the bundle as literal strings when it
|
||||
compiles (import.meta.env.VITE_FOO is a static replacement, not a runtime
|
||||
lookup) -- confirmed directly in config.ts's own comment on getViteEnv().
|
||||
That means a container image built once can never be flipped between an
|
||||
isolated private relay and the public Conduit network via ordinary env
|
||||
vars after the fact; the values are frozen into the compiled JS.
|
||||
|
||||
This packaging needs exactly that live toggle, so docker-entrypoint.sh
|
||||
writes a small /runtime-env.js at *container start* (from real env vars)
|
||||
setting window.__CONDUIT_RUNTIME_ENV__ before the app bundle runs. This
|
||||
script inserts one small override function right before the single call
|
||||
site that consumes getViteEnv()'s result (`const env = getViteEnv()`,
|
||||
confirmed to be the only call site in the file and to run once at module
|
||||
load), so only that one merge point needs to change. Only non-empty
|
||||
string fields override the Vite-baked default; a build with no runtime
|
||||
override behaves identically to stock upstream Conduit.
|
||||
|
||||
Uses a literal string anchor rather than a line-numbered diff/patch --
|
||||
more robust against upstream reformatting a comment or reflowing
|
||||
unrelated lines than a traditional unified diff would be.
|
||||
"""
|
||||
import sys
|
||||
|
||||
CONFIG_PATH = "packages/core/src/config.ts"
|
||||
ANCHOR = "const env = getViteEnv()"
|
||||
|
||||
OVERRIDE_BLOCK = '''// --- Archipelago runtime relay override -------------------------------------
|
||||
// See docker/conduit-market/apply-runtime-override.py in the archy repo for
|
||||
// why this exists: Vite bakes VITE_* env vars into the bundle at build
|
||||
// time, so a container image built once can never be reconfigured
|
||||
// public/private afterward via ordinary env vars. docker-entrypoint.sh
|
||||
// writes window.__CONDUIT_RUNTIME_ENV__ from real environment variables at
|
||||
// container start, before this bundle runs.
|
||||
function getRuntimeOverrides(): Partial<ReturnType<typeof getViteEnv>> {
|
||||
if (typeof window === "undefined") return {}
|
||||
const raw = (window as unknown as Record<string, unknown>)
|
||||
.__CONDUIT_RUNTIME_ENV__
|
||||
if (!raw || typeof raw !== "object") return {}
|
||||
const out: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
||||
if (typeof value === "string" && value.length > 0) out[key] = value
|
||||
}
|
||||
return out as Partial<ReturnType<typeof getViteEnv>>
|
||||
}
|
||||
|
||||
const env = { ...getViteEnv(), ...getRuntimeOverrides() }'''
|
||||
|
||||
with open(CONFIG_PATH) as f:
|
||||
content = f.read()
|
||||
|
||||
count = content.count(ANCHOR)
|
||||
if count != 1:
|
||||
print(
|
||||
f"FATAL: expected exactly 1 occurrence of anchor line in {CONFIG_PATH}, "
|
||||
f"found {count}. Upstream config.ts has likely changed in a way that "
|
||||
"needs this script re-checked by hand before it can safely patch it.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
content = content.replace(ANCHOR, OVERRIDE_BLOCK, 1)
|
||||
|
||||
with open(CONFIG_PATH, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
print(f"Patched {CONFIG_PATH}: runtime relay override installed.")
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Runs via nginx's own /docker-entrypoint.d/ hook mechanism (official nginx
|
||||
# image sources every executable script there before starting nginx) --
|
||||
# no custom ENTRYPOINT needed. Writes /runtime-env.js, loaded by index.html
|
||||
# before the app bundle (see Dockerfile), so this container's own env vars
|
||||
# can flip Conduit between an isolated private relay and the public Conduit
|
||||
# network without rebuilding the image -- see
|
||||
# packages/core/src/config.ts's getRuntimeOverrides() (installed by
|
||||
# apply-runtime-override.py) for the other half of this.
|
||||
set -eu
|
||||
|
||||
CONDUIT_MODE="${CONDUIT_MODE:-public}"
|
||||
OUT=/usr/share/nginx/html/runtime-env.js
|
||||
|
||||
if [ "$CONDUIT_MODE" = "private" ]; then
|
||||
if [ -z "${CONDUIT_PRIVATE_RELAY_URLS:-}" ]; then
|
||||
echo "conduit-market: CONDUIT_MODE=private requires CONDUIT_PRIVATE_RELAY_URLS (comma-separated wss:// URLs) -- refusing to start with no relay configured" >&2
|
||||
exit 1
|
||||
fi
|
||||
cat > "$OUT" <<EOF
|
||||
window.__CONDUIT_RUNTIME_ENV__ = {
|
||||
relayUrl: "",
|
||||
defaultRelayUrl: "${CONDUIT_PRIVATE_RELAY_URLS}",
|
||||
defaultRelays: "${CONDUIT_PRIVATE_RELAY_URLS}",
|
||||
appWriteRelayUrls: "${CONDUIT_PRIVATE_RELAY_URLS}",
|
||||
publicRelayUrls: "${CONDUIT_PRIVATE_RELAY_URLS}",
|
||||
commerceRelayUrls: "${CONDUIT_PRIVATE_RELAY_URLS}",
|
||||
lightningNetwork: "${CONDUIT_LIGHTNING_NETWORK:-mainnet}"
|
||||
};
|
||||
EOF
|
||||
echo "conduit-market: CONDUIT_MODE=private -- isolated to ${CONDUIT_PRIVATE_RELAY_URLS}"
|
||||
else
|
||||
# public: no relay overrides at all, so Conduit's own built-in canonical
|
||||
# public relay defaults (packages/core/src/config.ts,
|
||||
# CANONICAL_APP_RELAY_DEFINITIONS) apply exactly as they do upstream --
|
||||
# this store fully participates in the wider Conduit network.
|
||||
cat > "$OUT" <<EOF
|
||||
window.__CONDUIT_RUNTIME_ENV__ = {
|
||||
lightningNetwork: "${CONDUIT_LIGHTNING_NETWORK:-mainnet}"
|
||||
};
|
||||
EOF
|
||||
echo "conduit-market: CONDUIT_MODE=public -- using Conduit's default public relay network"
|
||||
fi
|
||||
@@ -1,28 +0,0 @@
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Regenerated by docker-entrypoint.d/50-conduit-runtime-env.sh on every
|
||||
# container start -- must never be cached, or a mode flip (public <->
|
||||
# private) followed by a container restart would silently keep serving
|
||||
# the old relay config to anyone with a warm cache.
|
||||
location = /runtime-env.js {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
expires 0;
|
||||
}
|
||||
|
||||
# Client-side routing (@tanstack/react-router) -- unknown paths fall
|
||||
# through to index.html rather than 404ing.
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location = /health {
|
||||
access_log off;
|
||||
default_type text/plain;
|
||||
return 200 "ok\n";
|
||||
}
|
||||
}
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"version": "1.8.18-alpha",
|
||||
"version": "1.8.19-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "neode-ui",
|
||||
"version": "1.8.18-alpha",
|
||||
"version": "1.8.19-alpha",
|
||||
"dependencies": {
|
||||
"@scure/bip39": "^2.2.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"private": true,
|
||||
"version": "1.8.18-alpha",
|
||||
"version": "1.8.19-alpha",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "./start-dev.sh",
|
||||
|
||||
@@ -362,6 +362,18 @@ init()
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
||||
<!-- v1.8.19-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.19-alpha</span>
|
||||
<span class="text-xs text-white/40">September 28, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Fixed the embedded AIUI chat page painting a second background and dark scrim over Archy’s dashboard background.</p>
|
||||
<p>Embedded AIUI now stays transparent so the dashboard background appears once.</p>
|
||||
<p>AIUI background fixes are now included reliably in OTA updates and fresh installations.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.8.18-alpha -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
@@ -369,9 +381,9 @@ init()
|
||||
<span class="text-xs text-white/40">September 18, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Framework startup now brings Bitcoin and LND up before unrelated containers, and the dashboard keeps balances unavailable instead of showing a false zero when LND is not ready.</p>
|
||||
<p>Cashu wallets can set up a recovery phrase from Receive, with clearer backup and restore guidance.</p>
|
||||
<p>Ecash backup guidance is shorter and arranged in a single column for easier use on small screens.</p>
|
||||
<p>Framework startup prioritizes Bitcoin and LND before unrelated containers, and unavailable LND balances remain unavailable instead of appearing as false zeroes.</p>
|
||||
<p>Cashu Receive guides unseeded wallets through recovery-phrase setup, with shorter backup guidance and a single-column layout.</p>
|
||||
<p>Added live Framework verification for automatic LND unlock, native balance preservation, Cashu address registration, and proof preservation.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.8.17-alpha -->
|
||||
|
||||
+17
-17
@@ -1,29 +1,29 @@
|
||||
{
|
||||
"changelog": [
|
||||
"Framework startup prioritizes Bitcoin and LND before unrelated containers, and unavailable LND balances remain unavailable instead of appearing as false zeroes.",
|
||||
"Cashu Receive guides unseeded wallets through recovery-phrase setup, with shorter backup guidance and a single-column layout.",
|
||||
"Added live Framework verification for automatic LND unlock, native balance preservation, Cashu address registration, and proof preservation."
|
||||
"Fixed the embedded AIUI chat page painting a second background and dark scrim over Archy’s dashboard background.",
|
||||
"Embedded AIUI now stays transparent so the dashboard background appears once.",
|
||||
"AIUI background fixes are now included reliably in OTA updates and fresh installations."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"current_version": "1.8.18-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.18-alpha/archipelago",
|
||||
"current_version": "1.8.19-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.19-alpha/archipelago",
|
||||
"name": "archipelago",
|
||||
"new_version": "1.8.18-alpha",
|
||||
"sha256": "c76415c295dd42159e45e74d93f7c2f6fa39b0351946a8303f8cfba1dd3a6087",
|
||||
"size_bytes": 64497240
|
||||
"new_version": "1.8.19-alpha",
|
||||
"sha256": "bb500d02567ad16179179d43138cc6fdafee680835262026eeaa7d1bc8cdd307",
|
||||
"size_bytes": 64495784
|
||||
},
|
||||
{
|
||||
"current_version": "1.8.18-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.18-alpha/archipelago-frontend-1.8.18-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.8.18-alpha.tar.gz",
|
||||
"new_version": "1.8.18-alpha",
|
||||
"sha256": "57da88b1a8f3d2841867994a95e9e4e728c19616abcde8e3d3f151c6d94ed501",
|
||||
"size_bytes": 98801020
|
||||
"current_version": "1.8.19-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.19-alpha/archipelago-frontend-1.8.19-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.8.19-alpha.tar.gz",
|
||||
"new_version": "1.8.19-alpha",
|
||||
"sha256": "fbbaa237e2ea4e9e576dda9b15fdcf3c59c40bc55bfee4ebbea303b0172fc49b",
|
||||
"size_bytes": 97142031
|
||||
}
|
||||
],
|
||||
"release_date": "2026-09-20",
|
||||
"signature": "84e0e1a20750f0d4ff27ba539c2e9424a5b02c19a5689f73cdf82813212acae19e0af244a62e66ac37a35d97d80deb2833819d809b91a7822f2bfd785d3fe30d",
|
||||
"release_date": "2026-09-28",
|
||||
"signature": "4da9bf766d94c2de6641619a36663106791c7a1d342b729c666662cdde1feabdf11c51994c1cbbe2a0e2b270c316d77763435e976ce22730855b3822bc9d390a",
|
||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||
"version": "1.8.18-alpha"
|
||||
"version": "1.8.19-alpha"
|
||||
}
|
||||
|
||||
+17
-17
@@ -1,29 +1,29 @@
|
||||
{
|
||||
"changelog": [
|
||||
"Framework startup prioritizes Bitcoin and LND before unrelated containers, and unavailable LND balances remain unavailable instead of appearing as false zeroes.",
|
||||
"Cashu Receive guides unseeded wallets through recovery-phrase setup, with shorter backup guidance and a single-column layout.",
|
||||
"Added live Framework verification for automatic LND unlock, native balance preservation, Cashu address registration, and proof preservation."
|
||||
"Fixed the embedded AIUI chat page painting a second background and dark scrim over Archy’s dashboard background.",
|
||||
"Embedded AIUI now stays transparent so the dashboard background appears once.",
|
||||
"AIUI background fixes are now included reliably in OTA updates and fresh installations."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"current_version": "1.8.18-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.18-alpha/archipelago",
|
||||
"current_version": "1.8.19-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.19-alpha/archipelago",
|
||||
"name": "archipelago",
|
||||
"new_version": "1.8.18-alpha",
|
||||
"sha256": "c76415c295dd42159e45e74d93f7c2f6fa39b0351946a8303f8cfba1dd3a6087",
|
||||
"size_bytes": 64497240
|
||||
"new_version": "1.8.19-alpha",
|
||||
"sha256": "bb500d02567ad16179179d43138cc6fdafee680835262026eeaa7d1bc8cdd307",
|
||||
"size_bytes": 64495784
|
||||
},
|
||||
{
|
||||
"current_version": "1.8.18-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.18-alpha/archipelago-frontend-1.8.18-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.8.18-alpha.tar.gz",
|
||||
"new_version": "1.8.18-alpha",
|
||||
"sha256": "57da88b1a8f3d2841867994a95e9e4e728c19616abcde8e3d3f151c6d94ed501",
|
||||
"size_bytes": 98801020
|
||||
"current_version": "1.8.19-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.19-alpha/archipelago-frontend-1.8.19-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.8.19-alpha.tar.gz",
|
||||
"new_version": "1.8.19-alpha",
|
||||
"sha256": "fbbaa237e2ea4e9e576dda9b15fdcf3c59c40bc55bfee4ebbea303b0172fc49b",
|
||||
"size_bytes": 97142031
|
||||
}
|
||||
],
|
||||
"release_date": "2026-09-20",
|
||||
"signature": "84e0e1a20750f0d4ff27ba539c2e9424a5b02c19a5689f73cdf82813212acae19e0af244a62e66ac37a35d97d80deb2833819d809b91a7822f2bfd785d3fe30d",
|
||||
"release_date": "2026-09-28",
|
||||
"signature": "4da9bf766d94c2de6641619a36663106791c7a1d342b729c666662cdde1feabdf11c51994c1cbbe2a0e2b270c316d77763435e976ce22730855b3822bc9d390a",
|
||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||
"version": "1.8.18-alpha"
|
||||
"version": "1.8.19-alpha"
|
||||
}
|
||||
|
||||
@@ -78,15 +78,17 @@ if [ -z "$FRONTEND_ARCHIVE" ]; then
|
||||
STAGING_DIR=$(mktemp -d -t archipelago-frontend.XXXXXX)
|
||||
echo "Staging frontend archive in $STAGING_DIR..."
|
||||
cp -r "$FRONTEND_DIST/." "$STAGING_DIR/"
|
||||
# Bake AIUI in so fresh installs pick it up. OTA already
|
||||
# carries-forward the existing aiui/ if the tarball lacks one
|
||||
# (update.rs:922), but including it here makes the tarball
|
||||
# the single source of truth instead of relying on a side-
|
||||
# effect of the in-place swap.
|
||||
if [ -d "$PROJECT_ROOT/demo/aiui" ] && [ -f "$PROJECT_ROOT/demo/aiui/index.html" ]; then
|
||||
echo " Including AIUI from demo/aiui/"
|
||||
cp -r "$PROJECT_ROOT/demo/aiui" "$STAGING_DIR/aiui"
|
||||
# create-release.sh folds the freshly built AIUI into FRONTEND_DIST.
|
||||
# Never overlay it with the older demo bundle (or nest aiui/aiui/).
|
||||
if [ ! -f "$STAGING_DIR/aiui/index.html" ] || \
|
||||
[ ! -f "$STAGING_DIR/aiui/BUILD-INFO" ]; then
|
||||
echo "Error: fresh AIUI payload missing from frontend dist" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -Fxq "commit=$(git -C "$PROJECT_ROOT" rev-parse HEAD)" "$STAGING_DIR/aiui/BUILD-INFO" || {
|
||||
echo "Error: AIUI payload was not built from the current commit" >&2
|
||||
exit 1
|
||||
}
|
||||
# OTA bridge for nodes running older updaters: they only know how to
|
||||
# apply the backend binary and frontend archive. Carry host runtime
|
||||
# assets inside the frontend tarball; the new backend promotes them
|
||||
|
||||
@@ -169,15 +169,11 @@ else
|
||||
fi
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# npm run build wipes web/dist — fold AIUI straight back in. The OTA tarball
|
||||
# bakes it from demo/aiui independently, but build-iso-release.sh's
|
||||
# verify-artifacts guard checks web/dist/neode-ui/aiui and failed on two
|
||||
# consecutive releases (.127, .129) because this fold-in was manual.
|
||||
if [ -d "$PROJECT_ROOT/demo/aiui" ] && [ -f "$PROJECT_ROOT/demo/aiui/index.html" ]; then
|
||||
rm -rf "$PROJECT_ROOT/web/dist/neode-ui/aiui"
|
||||
cp -r "$PROJECT_ROOT/demo/aiui" "$PROJECT_ROOT/web/dist/neode-ui/aiui"
|
||||
echo " AIUI folded into web/dist from demo/aiui"
|
||||
fi
|
||||
# Build AIUI from the same source as the release. The checked-in demo bundle
|
||||
# can predate source fixes and must never overwrite the production payload.
|
||||
bash "$SCRIPT_DIR/build-aiui.sh"
|
||||
rm -rf "$PROJECT_ROOT/web/dist/neode-ui/aiui"
|
||||
cp -r "$PROJECT_ROOT/aiui/packages/app/dist" "$PROJECT_ROOT/web/dist/neode-ui/aiui"
|
||||
|
||||
# npm run build can silently no-op (vue-tsc EACCES burned us before) — a stale
|
||||
# dist would ship with a perfectly valid sha256. Require the freshly built
|
||||
|
||||
Reference in New Issue
Block a user