diff --git a/src/publish-name-mirror.mjs b/src/publish-name-mirror.mjs index 1aa56f0..a0dceec 100644 --- a/src/publish-name-mirror.mjs +++ b/src/publish-name-mirror.mjs @@ -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");