Release v0.1.1:2 with certrelay, nacho, and prebuilt subspaces.
Build Service / BuildPackage (push) Has been cancelled

Remove the embedded explorer/indexer stack, switch subspaces to prebuilt images, add certrelay and nacho as always-on services, expose Spaces and Subs APIs, and wire optional HTTP basic auth for subs and subs-prover with the correct SUBS_BASIC_AUTH_PASSWORD env var.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-23 12:06:16 -04:00
co-authored by Cursor
parent 931becd274
commit ad64a47d42
30 changed files with 1364 additions and 831 deletions
+109 -36
View File
@@ -16,42 +16,6 @@ export const BITCOIND_RPC_PORT = 8332
export const SPACED_CHAIN = 'mainnet'
export const POSTGRES_PORT = 5432
export const POSTGRES_USER = 'postgres'
export const POSTGRES_DB = 'spacesprotocol_explorer'
export const pgDataDir = '/data/postgres'
export const INDEXER_REPO = 'spacesprotocol/explorer-indexer'
export const INDEXER_GIT_SHA = '00ae1e548734d93f1a8bb9f48d2290f459e12b35'
// Bump the suffix to force a full re-fetch + rebuild of /data/explorer-indexer.
export const INDEXER_BUILD_ID = `${INDEXER_GIT_SHA}-g3`
export const INDEXER_TARBALL_URL = `https://github.com/${INDEXER_REPO}/archive/${INDEXER_GIT_SHA}.tar.gz`
export const INDEXER_DIR = '/data/explorer-indexer'
export const INDEXER_BIN_DIR = '/data/explorer-indexer/bin'
export const INDEXER_SYNC_BIN = '/data/explorer-indexer/bin/sync'
export const INDEXER_GOOSE_BIN = '/data/explorer-indexer/bin/goose'
export const INDEXER_SCHEMA_DIR = '/data/explorer-indexer/sql/schema'
export const INDEXER_MARKER = '/data/explorer-indexer/.installed-sha'
// Spaces protocol mainnet activation block (per spacesprotocol/explorer-indexer
// env.example). Indexer skips fast-sync below FAST_SYNC and starts indexing
// spaces data at ACTIVATION.
export const INDEXER_ACTIVATION_HEIGHT = '871222'
export const INDEXER_FAST_SYNC_HEIGHT = '864000'
export const INDEXER_UPDATE_INTERVAL = '5'
export const INDEXER_MEMPOOL_CHUNK_SIZE = '200'
export const EXPLORER_REPO = 'randomlogin/explorer'
export const EXPLORER_GIT_SHA = 'c827da1754c3cba5c5507d2c29f21b8fa231344d'
// Bump suffix to force re-fetch + rebuild of /data/explorer-ui.
export const EXPLORER_BUILD_ID = `${EXPLORER_GIT_SHA}-e1`
export const EXPLORER_TARBALL_URL = `https://github.com/${EXPLORER_REPO}/archive/${EXPLORER_GIT_SHA}.tar.gz`
export const EXPLORER_DIR = '/data/explorer-ui'
export const EXPLORER_BUILD_DIR = '/data/explorer-ui/build'
export const EXPLORER_MARKER = '/data/explorer-ui/.installed-sha'
export const EXPLORER_PORT = 3000
export const EXPLORER_NETWORK = 'mainnet'
// Subspaces ships as the prebuilt horologger/subs image; binaries live at
// these paths inside it. No build-from-source step.
export const SUBSPACES_SUBS_BIN = '/usr/local/bin/subs'
@@ -88,6 +52,17 @@ export const CERTRELAY_DEFAULT_HEALTHCHECK_HANDLE =
'account-digital-useful.genesis@key'
export const CERTRELAY_DEFAULT_BOOTSTRAP = false
// Nacho (Expo dev server, horologger/nacho image). Always on; UI interface on
// 8082 (the image's exposed port, matches EXPO_DEV_PORT).
export const NACHO_PORT = 8082
export const NACHO_DIR = '/data/nacho'
// Last-resort fallback if StartOS hasn't yet populated the subs-api interface's
// address info at the moment main runs. In normal operation EXPO_PUBLIC_API_BASE_URL
// is derived dynamically from the subs-api StartOS interface — see main.ts.
export const NACHO_FALLBACK_API_BASE_URL = `http://127.0.0.1:${SUBSPACES_UI_PORT}`
export const NACHO_DEFAULT_IGNORE_NAMES = 'fold,swifty'
export const NACHO_DEFAULT_WORKSHOP_PDF_LINK_TEXT = ''
export function randomPassword() {
// bitcoind's generate-rpc-dependent action validates the password against
// /^[A-Za-z0-9_-]+$/, so the charset must stay in that set.
@@ -96,3 +71,101 @@ export function randomPassword() {
len: 32,
})
}
// Banner box rendering for the gotty terminal MOTD. Canonical width is 100
// monospaced cells: 1 cell each for the left/right side borders, plus 1-cell
// gutters on each side of text, leaving 96 cells of usable text. All public
// helpers here guarantee every line they emit is exactly BANNER_WIDTH cells.
//
// Use renderBanner() rather than hand-drawing rows so wrapping + padding stay
// correct when content changes. See bashrc in main.ts.
export const BANNER_WIDTH = 100
const BANNER_INNER = BANNER_WIDTH - 2 // 98 cells between the two side borders
const BANNER_TEXT = BANNER_INNER - 2 // 96 cells of usable text (1-cell gutters)
// Cell width assuming all code points are width-1 (true for ASCII, em-dash,
// and the box-drawing chars used here). Does NOT handle CJK or emoji — if you
// add any, switch to a wcwidth-aware count.
function cellWidth(s: string): number {
return Array.from(s).length
}
function padRight(s: string, width: number): string {
const w = cellWidth(s)
return w >= width ? s : s + ' '.repeat(width - w)
}
// Greedy word-wrap on whitespace runs. Lines that already fit pass through
// verbatim (preserving leading indent and any internal whitespace). Wrapping
// only kicks in when a line is wider than `width`; long single tokens get
// hard-broken so a long URL never overflows the box.
function wrapText(text: string, width: number): string[] {
if (cellWidth(text) <= width) return [text]
const out: string[] = []
const tokens = text.match(/\s+|\S+/g) ?? ['']
let line = ''
for (const tok of tokens) {
const isWs = /^\s/.test(tok)
const candidate = line + tok
if (cellWidth(candidate) <= width) {
line = candidate
continue
}
// Doesn't fit — flush current line and decide what to do with this token.
if (line) out.push(line)
if (isWs) {
// Discard the whitespace run at the wrap point; continuation starts flush.
line = ''
continue
}
// Non-whitespace token longer than width: hard-break.
let rest = tok
while (cellWidth(rest) > width) {
const arr = Array.from(rest)
out.push(arr.slice(0, width).join(''))
rest = arr.slice(width).join('')
}
line = rest
}
if (line) out.push(line)
if (out.length === 0) out.push('')
return out
}
function bannerTop(title?: string): string {
if (!title) return '┌' + '─'.repeat(BANNER_INNER) + '┐'
const head = '┌─ ' + title + ' '
const tail = BANNER_WIDTH - cellWidth(head) - 1
return head + '─'.repeat(Math.max(0, tail)) + '┐'
}
function bannerBottom(): string {
return '└' + '─'.repeat(BANNER_INNER) + '┘'
}
function bannerBlank(): string {
return '│' + ' '.repeat(BANNER_INNER) + '│'
}
function bannerRow(text: string): string[] {
return wrapText(text, BANNER_TEXT).map(
(line) => '│ ' + padRight(line, BANNER_TEXT) + ' │',
)
}
// Render a complete banner: top border, one or more content rows (text wrapped
// to fit; '' renders as a blank inner row), and bottom border. Every returned
// line is exactly BANNER_WIDTH cells wide.
export function renderBanner(
title: string | undefined,
lines: string[],
): string[] {
const out: string[] = [bannerTop(title)]
for (const l of lines) {
if (l === '') out.push(bannerBlank())
else out.push(...bannerRow(l))
}
out.push(bannerBottom())
return out
}