Files
spaces-startos/startos/utils.ts
T
spacesopsandCursor 2b0224a678
Build Service / BuildPackage (push) Canceled after 0s
0.3.0:0
Update to upstream Spaces 0.3.0 and refresh bundled images (subs v0.1.2c,
certrelay v0.2.8, nacho v1.0.0b). Add Create/Show Registry API Keys for
registry-server, remove Configure Subspaces and SUBS_PUBLISH_REQUIRE_FINALIZED,
and align README/instructions with the new tags.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 19:35:15 -04:00

195 lines
7.0 KiB
TypeScript

import { utils } from '@start9labs/start-sdk'
export const uiPort = 8080
export const spacedRpcPort = 7225
export const dataDir = '/data'
export const APP_USER = 'admin'
export const BITCOIN_RPC_USER = 'spaces'
export const BITCOIND_PACKAGE_ID = 'bitcoind'
export const BITCOIND_RPC_HOSTNAME = 'bitcoind.startos'
export const BITCOIND_RPC_PORT = 8332
export const SPACED_CHAIN = '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'
export const SUBSPACES_PROVER_BIN = '/usr/local/bin/subs-prover'
export const SUBSPACES_REGISTRY_BIN = '/usr/local/bin/registry-server'
export const SUBSPACES_DIR = '/data/subspaces'
export const SUBSPACES_DATA_DIR = '/data/subspaces/data'
export const SUBSPACES_PROVER_DIR = '/data/subspaces/prover'
export const SUBSPACES_REGISTRY_DIR = '/data/subspaces/registry'
export const SUBSPACES_UI_PORT = 7777
export const SUBSPACES_PROVER_PORT = 8888
// 8081 (not the upstream default 8080) to avoid colliding with the gotty
// Space-CLI Web UI on 8080.
export const SUBSPACES_REGISTRY_PORT = 8081
export const SUBSPACES_WALLET = 'default'
// Certrelay ships as the prebuilt horologger/certrelay image; CLI binaries
// are static musl, so they run anywhere once copied onto the volume.
export const CERTRELAY_BIN = '/usr/local/bin/certrelay'
export const CERTRELAY_FABRIC_BIN = '/usr/local/bin/fabric'
export const CERTRELAY_MONITOR_BIN = '/usr/local/bin/monitor'
// Copied here so the gotty terminal (which has /data/bin on PATH) can run them.
export const CERTRELAY_FABRIC_DEST = '/data/bin/fabric'
export const CERTRELAY_MONITOR_DEST = '/data/bin/monitor'
export const CERTRELAY_DIR = '/data/certrelay'
export const CERTRELAY_DATA_DIR = '/data/certrelay/data'
export const CERTRELAY_PORT = 7778
export const CERTRELAY_CHAIN = 'mainnet'
export const CERTRELAY_BIND = '0.0.0.0'
export const CERTRELAY_REMOTE_IP_HEADER = 'x-forwarded-for'
export const CERTRELAY_ANCHOR_REFRESH = '300'
export const CERTRELAY_DEFAULT_SELF_URL = 'https://certrelay.spacesops.com'
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.
return utils.getDefaultString({
charset: 'a-z,A-Z,1-9',
len: 32,
})
}
// Matches upstream docs: `openssl rand -hex 32` → 64 lowercase hex chars.
export function randomApiKey() {
return utils.getDefaultString({
charset: 'a-f,0-9',
len: 64,
})
}
// registry-server refuses to start unless both keys are non-empty and different.
export function generateRegistryApiKeys(): {
registryApiKey: string
subsdApiKey: string
} {
const registryApiKey = randomApiKey()
let subsdApiKey = randomApiKey()
while (subsdApiKey === registryApiKey) {
subsdApiKey = randomApiKey()
}
return { registryApiKey, subsdApiKey }
}
// 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
}