Compare commits

...
Author SHA1 Message Date
ssmithxandClaude Opus 5.5 e5fc99d66c fix(content): never charge for a file the seller can't serve or replay a spent token
After the keyset-id fix, a Minibits paid download still failed and the
buyer lost the sats. What happened, 2026-09-29, amishparadise:

1. The seller redeemed the token, then failed to read the file. It was a
   FileBrowser upload owned by the container subuid (100999) with mode
   0640. The handler mapped that Err to 404.
2. The buyer's FIPS dial treats 404 as "fall back to Tor" and resent the
   request with the same, now spent, token. The seller answered 402, and
   the buyer showed "seller doesn't accept your Cashu mint".

Fixes:
- serve_content checks the file is readable before the paid gate. If it
  isn't, it grants read with `podman unshare chmod a+r`, which matches
  the other shared files. If that also fails it returns Unavailable (503)
  without taking payment.
- The content handler returns 500 on internal errors and logs them,
  instead of a silent 404.
- New PeerRequest::single_delivery(), used for the paid download: the
  FIPS answer is final, FIPS retries only when it never connected, and
  there's no Tor replay once the request may have been delivered.
- The buyer shows the seller's error text for non-402 failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:42:20 +00:00
ssmithxandClaude Opus 5.5 8b74803290 fix(ecash): repair short v2 keyset ids on every swap, not just receive
Paid cloud downloads paid with Minibits ecash were always rejected. The
buyer sends a cashuB token, which carries NUT-02 v2 keyset ids in their
8-byte short form. Minibits rotated its active keyset to a v2 id, and the
seller's verify_and_receive_payment called MintClient::swap directly,
skipping the short->full id repair that only receive_token applied. The
mint answered 422 ("ID length invalid"). The buyer then showed the
misleading "seller doesn't accept your Cashu mint" hint.

Move the repair into swap() so every caller is covered: payment verify,
streaming gate, send change, and cross-mint swaps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:47:23 +00:00
archipelago 540639d2c1 chore: publish release v1.8.19-alpha 2026-09-28 13:21:51 -04:00
archipelago 562871b1ce chore: prepare signed release v1.8.19-alpha 2026-09-28 13:18:34 -04:00
archipelago cca3f8bfcd docs: include AIUI packaging fix in v1.8.19 release notes
Demo images / Build & push demo images (push) Failing after 44s
2026-09-28 13:13:31 -04:00
archipelago 89c08be712 fix(aiui): match host color scheme for iframe transparency
Demo images / Build & push demo images (push) Failing after 56s
2026-09-28 12:37:40 -04:00
archipelago b4ecf86c13 chore: stage v1.8.19-alpha version bump 2026-09-28 12:33:59 -04:00
archipelago b14fe78306 fix(aiui): expose host wallpaper and package fresh production builds 2026-09-28 12:32:18 -04:00
archipelago 3f0c1038c3 docs: add v1.8.19 release notes to settings 2026-09-28 12:23:57 -04:00
archipelago 1fbefce6df docs: add v1.8.19-alpha release notes 2026-09-28 12:23:05 -04:00
archipelago 63cb68451a fix(aiui): keep embedded chat background transparent 2026-09-22 05:04:59 -04:00
archipelago 17cfebbe26 chore: publish release v1.8.18-alpha 2026-09-20 11:49:39 -04:00
19 changed files with 250 additions and 99 deletions
+6
View File
@@ -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.
+3 -2
View File
@@ -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'
+1
View File
@@ -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),
+5 -5
View File
@@ -2,13 +2,13 @@
<div
class="h-full flex flex-col relative overflow-hidden transition-colors duration-300"
:class="[]"
:style="isDark
? { background: '#000 url(' + bgImageUrl + ') center center / cover no-repeat fixed' }
: isEmbedded
? { background: 'transparent' }
:style="isEmbedded
? { background: 'transparent' }
: isDark
? { background: '#000 url(' + bgImageUrl + ') center center / cover no-repeat fixed' }
: { backgroundColor: '#f5f4f1' }"
>
<div v-if="isDark" class="absolute inset-0 pointer-events-none bg-black/20" />
<div v-if="isDark && !isEmbedded" class="absolute inset-0 pointer-events-none bg-black/20" />
<!-- Desktop layout -->
<div
+15 -6
View File
@@ -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 -1
View File
@@ -104,7 +104,7 @@ dependencies = [
[[package]]
name = "archipelago"
version = "1.8.18-alpha"
version = "1.8.19-alpha"
dependencies = [
"anyhow",
"archipelago-container",
+1 -1
View File
@@ -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"
+18 -1
View File
@@ -162,11 +162,28 @@ impl ApiHandler {
r#"{"error":"This file is shared with the host's federation peers only. Federate with that node (exchange invites) so it recognizes you, then try again."}"#,
),
)),
Ok(content_server::ServeResult::NotFound) | Err(_) => Ok(build_response(
Ok(content_server::ServeResult::Unavailable) => Ok(build_response(
StatusCode::SERVICE_UNAVAILABLE,
"application/json",
hyper::Body::from(
r#"{"error":"The seller's node can't read this file right now. No payment was taken."}"#,
),
)),
Ok(content_server::ServeResult::NotFound) => Ok(build_response(
StatusCode::NOT_FOUND,
"text/plain",
hyper::Body::from("Content not found"),
)),
// Not a 404: a paid request may already have been charged by the
// time this fails, and "not found" hid the real error entirely.
Err(e) => {
tracing::error!("Serving content {content_id} failed: {e:#}");
Ok(build_response(
StatusCode::INTERNAL_SERVER_ERROR,
"text/plain",
hyper::Body::from("Failed to serve content"),
))
}
}
}
+8 -1
View File
@@ -555,6 +555,9 @@ impl RpcHandler {
.service(crate::settings::transport::PeerService::PeerFiles)
.header("X-Federation-DID", local_did)
.header("X-Payment-Token", token_str.clone())
// The token is a bearer instrument the seller redeems on first sight:
// a Tor replay after FIPS delivered it can only arrive spent.
.single_delivery()
.timeout(std::time::Duration::from_secs(900))
.send_get()
.await
@@ -610,8 +613,12 @@ impl RpcHandler {
let body = response.text().await.unwrap_or_default();
tracing::warn!("paid download: seller {onion} returned {status}: {body}");
reclaim_spent_ecash(&self.config.data_dir, &token_str, used_backend).await;
let reason = serde_json::from_str::<serde_json::Value>(&body)
.ok()
.and_then(|v| v.get("error").and_then(|e| e.as_str()).map(str::to_string))
.unwrap_or_else(|| format!("Peer returned an error ({status})."));
return Ok(serde_json::json!({
"error": format!("Peer returned an error ({status}). Your ecash was refunded to your wallet.")
"error": format!("{reason} Your ecash was refunded to your wallet.")
}));
}
+67 -18
View File
@@ -7,7 +7,7 @@ use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tokio::fs;
use tracing::{debug, warn};
use tracing::{debug, info, warn};
const CATALOG_FILE: &str = "content/catalog.json";
const CONTENT_DIR: &str = "content/files";
@@ -238,6 +238,9 @@ pub enum ServeResult {
Forbidden,
/// Content not found.
NotFound,
/// The catalog entry and file exist but this node can't read the file.
/// Returned before any payment is taken.
Unavailable,
}
/// Serve a content item by ID with access control and optional range request.
@@ -296,6 +299,37 @@ pub async fn serve_content(
}
}
let file_path = content_file_path(data_dir, item);
if !file_path.exists() {
// The catalog entry survived (it's a separate JSON file) but its
// backing file is gone — most likely lost in an unrelated data-dir
// reset (a shared filebrowser file, 2026-07-01: two catalog entries
// outlived a filebrowser reinstall that wiped the files themselves).
// Leaving the entry in place would keep advertising it as available
// to every peer forever, each hitting the exact same dead end this
// one just did. Prune it so it stops being offered.
warn!(
content_id = %id,
filename = %item.filename,
"content catalog entry's file is missing on disk — pruning the stale entry"
);
prune_missing_content_entry(data_dir, id).await;
return Ok(ServeResult::NotFound);
}
// Confirm the file is readable BEFORE the paid gate below redeems the
// buyer's token. Reading it only afterwards meant a permission error
// surfaced after the sale: the buyer was charged and got an error
// instead of the file (2026-09-29, a FileBrowser upload left 0640).
if let Err(e) = ensure_readable(&file_path).await {
warn!(
content_id = %id,
path = %file_path.display(),
"shared content file is not readable by this node: {e:#}"
);
return Ok(ServeResult::Unavailable);
}
// Check access control
if !owner_session {
match &item.access {
@@ -336,23 +370,6 @@ pub async fn serve_content(
}
}
let file_path = content_file_path(data_dir, item);
if !file_path.exists() {
// The catalog entry survived (it's a separate JSON file) but its
// backing file is gone — most likely lost in an unrelated data-dir
// reset (a shared filebrowser file, 2026-07-01: two catalog entries
// outlived a filebrowser reinstall that wiped the files themselves).
// Leaving the entry in place would keep advertising it as available
// to every peer forever, each hitting the exact same dead end this
// one just did. Prune it so it stops being offered.
warn!(
content_id = %id,
filename = %item.filename,
"content catalog entry's file is missing on disk — pruning the stale entry"
);
prune_missing_content_entry(data_dir, id).await;
return Ok(ServeResult::NotFound);
}
let metadata = fs::metadata(&file_path)
.await
@@ -572,6 +589,38 @@ pub async fn serve_content_preview(data_dir: &Path, id: &str) -> Result<PreviewR
}
}
/// Make sure this service can open `path`, granting read access if it can't.
///
/// FileBrowser writes uploads as its container user (a rootless subuid such
/// as 100999), and some arrive 0640 — unreadable by this service, although
/// most shared files are already 0644. Inside the rootless user namespace
/// that subuid is ours, so `podman unshare chmod a+r` grants the same read
/// access the other shared files have, without sudo.
async fn ensure_readable(path: &Path) -> Result<()> {
match fs::File::open(path).await {
Ok(_) => return Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {}
Err(e) => return Err(e).context("Failed to open content file"),
}
let out = tokio::process::Command::new("podman")
.args(["unshare", "chmod", "a+r"])
.arg(path)
.output()
.await
.context("Failed to run podman unshare chmod")?;
if !out.status.success() {
anyhow::bail!(
"podman unshare chmod a+r failed: {}",
String::from_utf8_lossy(&out.stderr).trim()
);
}
info!("Granted read access to shared content file {}", path.display());
fs::File::open(path)
.await
.context("Content file still unreadable after chmod")?;
Ok(())
}
/// Verify a payment token covers the required amount.
/// Accepts both cashuA tokens (real Cashu) and legacy cashuSend_ format.
/// Swaps proofs at the mint to verify they're unspent before accepting.
+50 -3
View File
@@ -130,10 +130,18 @@ pub fn client_with_timeout(timeout: Duration) -> reqwest::Client {
/// robust". Only connect/timeout errors are retried (a real HTTP response,
/// including 4xx/5xx, is returned as-is for the caller to interpret).
async fn send_with_retry(rb: reqwest::RequestBuilder) -> Result<reqwest::Response, reqwest::Error> {
send_with_retry_if(rb, |e| e.is_connect() || e.is_timeout()).await
}
/// [`send_with_retry`], retrying only on errors `retryable` accepts.
async fn send_with_retry_if(
rb: reqwest::RequestBuilder,
retryable: impl Fn(&reqwest::Error) -> bool,
) -> Result<reqwest::Response, reqwest::Error> {
let retry = rb.try_clone();
match rb.send().await {
Ok(resp) => Ok(resp),
Err(e) if (e.is_connect() || e.is_timeout()) && retry.is_some() => {
Err(e) if retryable(&e) && retry.is_some() => {
// Brief pause so the hole-punch packets from the first attempt can
// traverse before we re-dial onto the warmed path.
tokio::time::sleep(Duration::from_millis(600)).await;
@@ -350,6 +358,9 @@ pub struct PeerRequest<'a> {
/// the per-peer FIPS/Tor badge reflects reality. Opt-in because not
/// every caller has a data dir in scope.
pub record_data_dir: Option<std::path::PathBuf>,
/// The request carries something that must reach the peer at most once
/// (a bearer ecash token). See [`PeerRequest::single_delivery`].
pub single_delivery: bool,
}
impl<'a> PeerRequest<'a> {
@@ -363,9 +374,25 @@ impl<'a> PeerRequest<'a> {
fips_timeout: None,
service: None,
record_data_dir: None,
single_delivery: false,
}
}
/// Never send this request twice. A paid download carries a bearer ecash
/// token that the seller redeems on first sight; replaying it over Tor
/// after FIPS already delivered it hands the seller a spent token, so the
/// buyer is charged and gets a 402 instead of the file (2026-09-29: FIPS
/// answered 404 after the seller redeemed, the Tor retry got 402).
///
/// With this set, whatever FIPS answers is final, the FIPS retry fires
/// only when the first attempt never connected, and Tor is used only when
/// FIPS could not have delivered the request. An attempt that may have
/// been delivered but timed out is an error, not a fallback.
pub fn single_delivery(mut self) -> Self {
self.single_delivery = true;
self
}
/// Record the transport that serves this request into federation storage
/// (matched by this request's onion host). Best-effort, off the hot path.
pub fn record_transport(mut self, data_dir: impl Into<std::path::PathBuf>) -> Self {
@@ -481,7 +508,10 @@ impl<'a> PeerRequest<'a> {
if matches!(pref, TransportPref::Auto | TransportPref::Fips) {
match self.try_fips_get().await? {
Some(resp) => {
if pref == TransportPref::Fips || !fips_should_fall_back(resp.status()) {
if pref == TransportPref::Fips
|| self.single_delivery
|| !fips_should_fall_back(resp.status())
{
telemetry::record_fips_ok();
self.spawn_record(crate::transport::TransportKind::Fips);
return Ok((resp, crate::transport::TransportKind::Fips));
@@ -617,8 +647,25 @@ impl<'a> PeerRequest<'a> {
for (k, v) in &self.headers {
rb = rb.header(*k, v);
}
match tokio::time::timeout(budget, send_with_retry(rb)).await {
let single = self.single_delivery;
let attempt = send_with_retry_if(rb, |e| {
e.is_connect() || (!single && e.is_timeout())
});
match tokio::time::timeout(budget, attempt).await {
Ok(Ok(r)) => Ok(Some(r)),
// Anything but a failed connect may have reached the peer.
Ok(Err(e)) if single && !e.is_connect() => Err(anyhow::anyhow!(
"FIPS GET {} failed after the request may have been delivered \
(not retrying over Tor): {}",
self.path,
e
)),
Err(_) if single => Err(anyhow::anyhow!(
"FIPS GET {} exceeded its {:?} budget after the request may have \
been delivered (not retrying over Tor)",
self.path,
budget
)),
Ok(Err(e)) => {
telemetry::record_fallback(FallbackReason::ConnectFail);
tracing::info!(
+8 -2
View File
@@ -514,6 +514,13 @@ impl MintClient {
pub async fn swap(&self, inputs: &[Proof], target_amounts: &[u64]) -> Result<SwapResult> {
let keyset = self.get_active_sat_keyset().await?;
// cashuB tokens carry NUT-02 v2 keyset ids in their 8-byte short form,
// which the mint rejects (bare 422). Repair here, not at each caller:
// the paid-download seller path swapped directly and every Minibits
// payment failed once the mint rotated to a v2 keyset.
let repaired = self.resolve_truncated_keyset_ids(inputs).await;
let inputs: &[Proof] = &repaired;
// NUT-02: a mint may charge a per-input fee, and it rejects the swap
// outright unless outputs == inputs - fee (`11005 Transaction inputs
// should equal outputs less fee`). Applied here rather than at each
@@ -813,8 +820,7 @@ impl MintClient {
let total: u64 = entry.proofs.iter().map(|p| p.amount).sum();
let target_amounts = amount_to_denominations(total);
let proofs = self.resolve_truncated_keyset_ids(&entry.proofs).await;
let result = self.swap(&proofs, &target_amounts).await?;
let result = self.swap(&entry.proofs, &target_amounts).await?;
all_new_proofs.extend(result.new_proofs);
}
+2 -2
View File
@@ -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 -1
View File
@@ -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 -18
View File
@@ -1,30 +1,29 @@
{
"changelog": [
"Minibits claims that every mint reports as already spent leave the retry queue, clearing repeated failure notices. Network errors and mixed mint failures remain queued for another attempt.",
"Minibits polls its primary relay first and connects to public fallback relays only when the primary is unreachable, reducing unnecessary connections.",
"Large payment backlogs are fetched from newest to oldest with a saved cursor, so polling can resume after interruptions or page limits. Payments sharing the same timestamp remain reachable.",
"Added regression coverage for spent-claim classification, wrapped and mixed mint errors, same-second payments, and interrupted or multi-poll backlogs."
"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.17-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.17-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.17-alpha",
"sha256": "32a7b009eb58f8c9f256e6597711a77ded11e15d5865a3fe16901603264e1f70",
"size_bytes": 64953344
"new_version": "1.8.19-alpha",
"sha256": "bb500d02567ad16179179d43138cc6fdafee680835262026eeaa7d1bc8cdd307",
"size_bytes": 64495784
},
{
"current_version": "1.8.17-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.17-alpha/archipelago-frontend-1.8.17-alpha.tar.gz",
"name": "archipelago-frontend-1.8.17-alpha.tar.gz",
"new_version": "1.8.17-alpha",
"sha256": "faf692e9a0e16268357bcac2bf86b62950ae49663e3c95982e54a132bb761980",
"size_bytes": 98801608
"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-15",
"signature": "c8196fe278a5747b3c3ba3bf70998874f1e3e6eedbdab33b9e33c3339a3769ab4431f41d99924ec4cdd15a5ffed299a5af786c7ab5e9d084cdc11beabbee9103",
"release_date": "2026-09-28",
"signature": "4da9bf766d94c2de6641619a36663106791c7a1d342b729c666662cdde1feabdf11c51994c1cbbe2a0e2b270c316d77763435e976ce22730855b3822bc9d390a",
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
"version": "1.8.17-alpha"
"version": "1.8.19-alpha"
}
+17 -18
View File
@@ -1,30 +1,29 @@
{
"changelog": [
"Minibits claims that every mint reports as already spent leave the retry queue, clearing repeated failure notices. Network errors and mixed mint failures remain queued for another attempt.",
"Minibits polls its primary relay first and connects to public fallback relays only when the primary is unreachable, reducing unnecessary connections.",
"Large payment backlogs are fetched from newest to oldest with a saved cursor, so polling can resume after interruptions or page limits. Payments sharing the same timestamp remain reachable.",
"Added regression coverage for spent-claim classification, wrapped and mixed mint errors, same-second payments, and interrupted or multi-poll backlogs."
"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.17-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.17-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.17-alpha",
"sha256": "32a7b009eb58f8c9f256e6597711a77ded11e15d5865a3fe16901603264e1f70",
"size_bytes": 64953344
"new_version": "1.8.19-alpha",
"sha256": "bb500d02567ad16179179d43138cc6fdafee680835262026eeaa7d1bc8cdd307",
"size_bytes": 64495784
},
{
"current_version": "1.8.17-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.17-alpha/archipelago-frontend-1.8.17-alpha.tar.gz",
"name": "archipelago-frontend-1.8.17-alpha.tar.gz",
"new_version": "1.8.17-alpha",
"sha256": "faf692e9a0e16268357bcac2bf86b62950ae49663e3c95982e54a132bb761980",
"size_bytes": 98801608
"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-15",
"signature": "c8196fe278a5747b3c3ba3bf70998874f1e3e6eedbdab33b9e33c3339a3769ab4431f41d99924ec4cdd15a5ffed299a5af786c7ab5e9d084cdc11beabbee9103",
"release_date": "2026-09-28",
"signature": "4da9bf766d94c2de6641619a36663106791c7a1d342b729c666662cdde1feabdf11c51994c1cbbe2a0e2b270c316d77763435e976ce22730855b3822bc9d390a",
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
"version": "1.8.17-alpha"
"version": "1.8.19-alpha"
}
+10 -8
View File
@@ -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
+5 -9
View File
@@ -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