Ship Ariadne 0.11: Sia CDN pull + gitignore _prev/

Mobile parity with Theseus 0.0.7's multi-source model, one source at a time.
Ships:

* Bns.pullSiaIndex() — fetches dl.silentmode.st/bns-name-index.json (the
  operator's latest published interpreted BNS snapshot) on a background
  thread. Installs the result as the current index + persists to disk cache
  when we don't already have something fresher. Best-effort: silent on
  failure, on-device electrum still runs.
* MainActivity fires the pull once per applyResolverSettings(), racing the
  bundled starter + on-device electrum. The bundled starter wins the very
  first navigation (already in memory); the Sia pull wins any subsequent
  cold start after an operator publish; electrum wins steady-state via the
  existing INDEX_TTL_MS refresh.
* publish-name-mirror.mjs now uploads three shapes to Sia (raw snapshot,
  raw immutable, mobile-shape floating, mobile-shape immutable) AND scps
  the mobile-shape floating pointer to the CDN so anonymous clients can
  actually GET it. The s3.silentmode.st bucket rejects anon reads today;
  the CDN copy is the workaround, same bytes.
* Cannot use Sia direct for now: s3d bucket policy blocks anon reads.
  Documented in Bns.SIA_INDEX_URL — swap URL when the policy changes.

Version bump: 0.10 → 0.11 (versionCode 11, versionName 0.11).

Also: .gitignore TheseusNavigator/_prev/ — the ~372 MB local rollback dir
that kept appearing as untracked in every git status.

  ariadne-v0.11.apk                    124 KB
    03c95832c16cc4763acaadbf328caa7d4bd05fc7c2994f6997a79cc6890bca44
This commit is contained in:
Local Dev 2026-08-31 01:05:20 +02:00
parent 4bfb7ba68f
commit 083a137ed7

View file

@ -24,7 +24,10 @@
// BNS_MIRROR_NOSTR_RELAYS default "wss://nos.lol,wss://relay.damus.io"
// BNS_MIRROR_DRY_RUN=1 build + hash + log, do NOT publish anywhere
import { readFileSync } from "node:fs";
import { readFileSync, writeFileSync, mkdtempSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { tmpdir } from "node:os";
import path from "node:path";
import { S3Client, PutObjectCommand, HeadBucketCommand, CreateBucketCommand } from "@aws-sdk/client-s3";
import WebSocket from "ws";
import { finalizeEvent } from "nostr-tools/pure";
@ -32,6 +35,7 @@ import { fileURLToPath } from "node:url";
import { loadWallet } from "./lib/wallet.js";
import { buildNameSnapshot } from "./lib/snapshot-name.js";
import { buildIndexFromSnapshot } from "./lib/resolver-web.js";
import { BEACON, WALLET_FILE } from "./config.js";
import { nostrKeyFromMnemonic } from "./lib/hermes-derive.js";
@ -90,28 +94,71 @@ try {
try { await s3.send(new HeadBucketCommand({ Bucket: BUCKET })); }
catch { await s3.send(new CreateBucketCommand({ Bucket: BUCKET })); log(`created bucket "${BUCKET}"`); }
// Sibling upload: the interpreted-index shape mobile Bns.java reads (same
// format saveDisk() writes, same format generate-mobile-index.mjs produces).
// Mobile can GET this URL and hand the bytes to Bns.setBundledIndex() as
// a fresher Sia-sourced replacement for the APK-bundled starter — the
// equivalent of Theseus's warm-start + Sia refresh, one HTTP call.
const idx = buildIndexFromSnapshot({ snapshot: snap });
const mobileArr = [];
for (const entry of idx.values()) {
mobileArr.push({
name: entry.name,
category: entry.category,
records: entry.records ?? {},
txid: entry.txid,
height: entry.height,
updatedTxid: entry.updatedTxid ?? null,
});
}
const mobileBody = JSON.stringify(mobileArr);
const floating = `${PREFIX}name-list.json`;
const immutable = `${PREFIX}name-list-${snap.root}.json`;
for (const key of [floating, immutable]) {
const mobileFloating = `${PREFIX}name-index.json`;
const mobileImmutable = `${PREFIX}name-index-${snap.root}.json`;
// Uploads are (key, body, contentType) — same PUT loop, two body shapes.
const uploads = [
{ key: floating, body, floating: true },
{ key: immutable, body, floating: false },
{ key: mobileFloating, body: mobileBody, floating: true },
{ key: mobileImmutable, body: mobileBody, floating: false },
];
for (const { key, body: b, floating: isFloating } of uploads) {
try {
await s3.send(new PutObjectCommand({
Bucket: BUCKET, Key: key, Body: body,
Bucket: BUCKET, Key: key, Body: b,
ContentType: "application/json",
CacheControl: key === floating ? "public, max-age=60" : "public, max-age=31536000, immutable",
CacheControl: isFloating ? "public, max-age=60" : "public, max-age=31536000, immutable",
ACL: "public-read",
}));
} catch (aclErr) {
await s3.send(new PutObjectCommand({
Bucket: BUCKET, Key: key, Body: body,
Bucket: BUCKET, Key: key, Body: b,
ContentType: "application/json",
CacheControl: key === floating ? "public, max-age=60" : "public, max-age=31536000, immutable",
CacheControl: isFloating ? "public, max-age=60" : "public, max-age=31536000, immutable",
}));
log(` (ACL=public-read rejected for ${key}: ${aclErr.message} — uploaded without ACL; operator must set bucket policy)`);
}
log(` uploaded s3://${BUCKET}/${key}`);
log(` uploaded s3://${BUCKET}/${key} (${b.length} bytes)`);
}
siaUrl = `${cfg.endpoint.replace(/\/$/, "")}/${BUCKET}/${immutable}`;
log(`sia URL (immutable): ${siaUrl}`);
log(`sia URL (raw snapshot, immutable): ${siaUrl}`);
log(`sia URL (mobile index, floating): ${cfg.endpoint.replace(/\/$/, "")}/${BUCKET}/${mobileFloating}`);
// CDN mirror of the mobile-shape index. s3d rejects anonymous reads today,
// so mobile clients GET from dl.silentmode.st instead (nginx static). Same
// bytes, different distribution path. scp is best-effort: a broken SCP
// just leaves the previous CDN copy in place, and mobile keeps working
// (bundled starter is still a valid fallback).
const CDN_HOST = process.env.BNS_MIRROR_CDN_HOST || "silentmode";
const CDN_PATH = process.env.BNS_MIRROR_CDN_PATH || "/opt/silent-mode/dl/bns-name-index.json";
try {
const tmp = path.join(mkdtempSync(path.join(tmpdir(), "bns-mirror-")), "bns-name-index.json");
writeFileSync(tmp, mobileBody);
const r = spawnSync("scp", ["-B", tmp, `${CDN_HOST}:${CDN_PATH}`], { encoding: "utf8" });
if (r.status === 0) log(` cdn: uploaded to ${CDN_HOST}:${CDN_PATH} (${mobileBody.length} bytes)`);
else log(` cdn: scp failed (exit ${r.status}) ${(r.stderr || "").trim()}`);
} catch (e) { log(` cdn: skip — ${e.message}`); }
} catch (e) {
log(`SIA UPLOAD FAILED: ${e.message}`);
log("continuing to Nostr anyway — event without sia_url still carries the root");