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>
605 lines
21 KiB
TypeScript
605 lines
21 KiB
TypeScript
import { storeJson } from './fileModels/storeJson'
|
|
import { i18n } from './i18n'
|
|
import { sdk } from './sdk'
|
|
import {
|
|
APP_USER,
|
|
BITCOIND_RPC_HOSTNAME,
|
|
BITCOIND_RPC_PORT,
|
|
CERTRELAY_ANCHOR_REFRESH,
|
|
CERTRELAY_BIN,
|
|
CERTRELAY_BIND,
|
|
CERTRELAY_CHAIN,
|
|
CERTRELAY_DATA_DIR,
|
|
CERTRELAY_DEFAULT_BOOTSTRAP,
|
|
CERTRELAY_DEFAULT_HEALTHCHECK_HANDLE,
|
|
CERTRELAY_DEFAULT_SELF_URL,
|
|
CERTRELAY_DIR,
|
|
CERTRELAY_FABRIC_BIN,
|
|
CERTRELAY_FABRIC_DEST,
|
|
CERTRELAY_PORT,
|
|
CERTRELAY_REMOTE_IP_HEADER,
|
|
dataDir,
|
|
NACHO_DEFAULT_IGNORE_NAMES,
|
|
NACHO_DEFAULT_WORKSHOP_PDF_LINK_TEXT,
|
|
NACHO_DIR,
|
|
NACHO_FALLBACK_API_BASE_URL,
|
|
NACHO_PORT,
|
|
renderBanner,
|
|
SPACED_CHAIN,
|
|
spacedRpcPort,
|
|
SUBSPACES_DATA_DIR,
|
|
SUBSPACES_DIR,
|
|
SUBSPACES_PROVER_BIN,
|
|
SUBSPACES_PROVER_DIR,
|
|
SUBSPACES_PROVER_PORT,
|
|
SUBSPACES_REGISTRY_BIN,
|
|
SUBSPACES_REGISTRY_DIR,
|
|
SUBSPACES_REGISTRY_PORT,
|
|
SUBSPACES_SUBS_BIN,
|
|
SUBSPACES_UI_PORT,
|
|
SUBSPACES_WALLET,
|
|
uiPort,
|
|
} from './utils'
|
|
|
|
export const main = sdk.setupMain(async ({ effects }) => {
|
|
console.info(i18n('Starting Spaces!'))
|
|
|
|
const store = await storeJson.read().const(effects)
|
|
if (
|
|
!store?.password ||
|
|
!store?.btcAuth ||
|
|
!store?.spacedAuth
|
|
) {
|
|
// taskSetPassword + taskBtcAuth + taskSeedSpacedAuth all seed these in
|
|
// init; if they aren't populated yet, init hasn't finished — let StartOS
|
|
// restart us.
|
|
throw new Error('Spaces store.json is not yet populated.')
|
|
}
|
|
|
|
const { password: APP_PASSWORD, btcAuth, spacedAuth } = store
|
|
const enableSubspaces = store.enableSubspaces === true
|
|
// subs-prover is independently gated and defaults OFF (null/undefined => false).
|
|
const enableSubsProver = store.enableSubsProver === true
|
|
|
|
const spacedEnv = {
|
|
SPACED_CHAIN,
|
|
SPACED_DATA_DIR: dataDir,
|
|
// Listen on all interfaces so the spaced RPC can be exposed externally via
|
|
// the "Spaces API" StartOS interface. Internal clients (subs, certrelay,
|
|
// indexer, space-cli, health checks) still connect over loopback below.
|
|
SPACED_RPC_BIND: '0.0.0.0',
|
|
SPACED_RPC_PORT: String(spacedRpcPort),
|
|
SPACED_RPC_URL: `http://127.0.0.1:${spacedRpcPort}`,
|
|
SPACED_BLOCK_INDEX: 'true',
|
|
SPACED_BITCOIN_RPC_URL: `http://${BITCOIND_RPC_HOSTNAME}:${BITCOIND_RPC_PORT}`,
|
|
SPACED_BITCOIN_RPC_USER: btcAuth.username,
|
|
SPACED_BITCOIN_RPC_PASSWORD: btcAuth.password,
|
|
SPACED_RPC_USER: spacedAuth.username,
|
|
SPACED_RPC_PASSWORD: spacedAuth.password,
|
|
// legacy aliases for `bitcoin-cli` / shell helpers that read these names
|
|
BTC_RPC_HOST: BITCOIND_RPC_HOSTNAME,
|
|
BTC_RPC_PORT: String(BITCOIND_RPC_PORT),
|
|
BTC_RPC_USER: btcAuth.username,
|
|
BTC_RPC_PASSWORD: btcAuth.password,
|
|
APP_USER,
|
|
APP_PASSWORD,
|
|
}
|
|
|
|
// Certrelay config — user-tunable bits live in store.json (set via the
|
|
// "Configure Certrelay" action); the rest are fixed package defaults.
|
|
const certrelaySelfUrl =
|
|
store.certrelaySelfUrl ?? CERTRELAY_DEFAULT_SELF_URL
|
|
const certrelayBootstrap =
|
|
store.certrelayBootstrap ?? CERTRELAY_DEFAULT_BOOTSTRAP
|
|
const certrelayHealthcheckHandle =
|
|
store.certrelayHealthcheckHandle ?? CERTRELAY_DEFAULT_HEALTHCHECK_HANDLE
|
|
const nachoWorkshopPdfLinkText =
|
|
store.nachoWorkshopPdfLinkText ?? NACHO_DEFAULT_WORKSHOP_PDF_LINK_TEXT
|
|
|
|
const mounts = sdk.Mounts.of().mountVolume({
|
|
volumeId: 'main',
|
|
subpath: null,
|
|
mountpoint: dataDir,
|
|
readonly: false,
|
|
})
|
|
|
|
const spacedSub = await sdk.SubContainer.of(
|
|
effects,
|
|
{ imageId: 'spaces' },
|
|
mounts,
|
|
'spaced-sub',
|
|
)
|
|
|
|
const termSub = await sdk.SubContainer.of(
|
|
effects,
|
|
{ imageId: 'spaces' },
|
|
mounts,
|
|
'terminal-sub',
|
|
)
|
|
|
|
const subspacesSub = await sdk.SubContainer.of(
|
|
effects,
|
|
{ imageId: 'subspaces' },
|
|
mounts,
|
|
'subspaces-sub',
|
|
)
|
|
|
|
// Optional HTTP basic auth in front of subs (Web UI + API on 7777). Only
|
|
// injected when both a stored credential pair and the enable toggle are set,
|
|
// so flipping the toggle off (or clearing creds) leaves subs unauth.
|
|
// NOTE: env var names match subs/src/main.rs clap #[arg(env=...)]:
|
|
// SUBS_BASIC_AUTH_USER and SUBS_BASIC_AUTH_PASSWORD.
|
|
const subsAuthActive = store.subsAuthEnabled === true && store.subsAuth != null
|
|
const subsAuthEnv = subsAuthActive
|
|
? {
|
|
SUBS_BASIC_AUTH_USER: store.subsAuth!.username,
|
|
SUBS_BASIC_AUTH_PASSWORD: store.subsAuth!.password,
|
|
}
|
|
: {}
|
|
|
|
// subs reads all of these from the environment (see subs/src/main.rs #[arg(env=...)]).
|
|
const subsEnv = {
|
|
SUBS_DATA_DIR: SUBSPACES_DATA_DIR,
|
|
SUBS_PORT: String(SUBSPACES_UI_PORT),
|
|
SUBS_WALLET: SUBSPACES_WALLET,
|
|
SUBS_SPACED_RPC_URL: `http://127.0.0.1:${spacedRpcPort}`,
|
|
SUBS_SPACED_RPC_USER: spacedAuth.username,
|
|
SUBS_SPACED_RPC_PASSWORD: spacedAuth.password,
|
|
SUBS_PROVER_ENDPOINT: `http://127.0.0.1:${SUBSPACES_PROVER_PORT}`,
|
|
SUBS_REGISTRY_ENDPOINT: `http://127.0.0.1:${SUBSPACES_REGISTRY_PORT}`,
|
|
HOME: SUBSPACES_DATA_DIR,
|
|
RUST_LOG: 'subs=info,subs_prover=info,registry_server=info',
|
|
...subsAuthEnv,
|
|
}
|
|
|
|
// Optional HTTP basic auth in front of subs-prover (port 8888). Same wiring
|
|
// pattern as subsAuthEnv above: only injected when both the toggle and
|
|
// credentials are set. Env var names assume subs-prover reads
|
|
// SUBS_PROVER_BASIC_AUTH_USER and SUBS_PROVER_BASIC_AUTH_PASSWORD.
|
|
const subsProverAuthActive =
|
|
store.subsProverAuthEnabled === true && store.subsProverAuth != null
|
|
const subsProverAuthEnv = subsProverAuthActive
|
|
? {
|
|
SUBS_PROVER_BASIC_AUTH_USER: store.subsProverAuth!.username,
|
|
SUBS_PROVER_BASIC_AUTH_PASSWORD: store.subsProverAuth!.password,
|
|
}
|
|
: {}
|
|
|
|
const proverEnv = {
|
|
SUBS_PROVER_SERVER: '1',
|
|
SUBS_PROVER_PORT: String(SUBSPACES_PROVER_PORT),
|
|
HOME: SUBSPACES_PROVER_DIR,
|
|
RUST_LOG: 'subs_prover=info',
|
|
...subsProverAuthEnv,
|
|
}
|
|
|
|
const registryEnv = {
|
|
REGISTRY_SERVER_PORT: String(SUBSPACES_REGISTRY_PORT),
|
|
HOME: SUBSPACES_REGISTRY_DIR,
|
|
RUST_LOG: 'registry_server=info',
|
|
}
|
|
|
|
const certrelaySub = await sdk.SubContainer.of(
|
|
effects,
|
|
{ imageId: 'certrelay' },
|
|
mounts,
|
|
'certrelay-sub',
|
|
)
|
|
|
|
const certrelayEnv = {
|
|
CERTRELAY_CHAIN,
|
|
CERTRELAY_DATA_DIR,
|
|
CERTRELAY_BIND,
|
|
CERTRELAY_PORT: String(CERTRELAY_PORT),
|
|
CERTRELAY_SELF_URL: certrelaySelfUrl,
|
|
// Credentials come from the local spaced server (store.spacedAuth).
|
|
CERTRELAY_SPACED_RPC_URL: `http://${spacedAuth.username}:${spacedAuth.password}@127.0.0.1:${spacedRpcPort}`,
|
|
CERTRELAY_REMOTE_IP_HEADER,
|
|
CERTRELAY_BOOTSTRAP: certrelayBootstrap ? 'true' : 'false',
|
|
CERTRELAY_ANCHOR_REFRESH,
|
|
CERTRELAY_HEALTHCHECK_HANDLE: certrelayHealthcheckHandle,
|
|
HOME: CERTRELAY_DATA_DIR,
|
|
RUST_LOG: 'info',
|
|
}
|
|
|
|
const nachoSub = await sdk.SubContainer.of(
|
|
effects,
|
|
{ imageId: 'nacho' },
|
|
mounts,
|
|
'nacho-sub',
|
|
)
|
|
|
|
// Derive EXPO_PUBLIC_API_BASE_URL from the subs-api StartOS interface so it
|
|
// tracks the actual host:port StartOS exposes (instead of a hardcoded
|
|
// domain). Prefer the mdns/.local URL — matches how a LAN browser reaches
|
|
// the device — then fall back to any non-local URL, then loopback.
|
|
// `.const(effects)` makes this reactive: if the interface address changes
|
|
// (clearnet enabled, Tor added, etc.), the service restarts and nacho
|
|
// picks up the new URL.
|
|
const subsApiIf = await sdk.serviceInterface
|
|
.getOwn(effects, 'subs-api')
|
|
.const()
|
|
const subsApiCandidateUrls = subsApiIf?.addressInfo
|
|
? [
|
|
...subsApiIf.addressInfo.filter({ kind: 'mdns' }).format('urlstring'),
|
|
...subsApiIf.addressInfo.nonLocal.format('urlstring'),
|
|
]
|
|
: []
|
|
const nachoApiBaseUrl =
|
|
subsApiCandidateUrls[0] ?? NACHO_FALLBACK_API_BASE_URL
|
|
|
|
// nacho is an Expo app; the image's entrypoint runs `expo start` on
|
|
// EXPO_DEV_PORT (8082). EXPO_PUBLIC_* env vars are baked into the web bundle
|
|
// at server time. The ignore list is NOT passed via env — nacho reads it at
|
|
// runtime from /data/nacho/ignore_names.txt (managed by the "Configure Nacho"
|
|
// action). nacho-setup seeds that file with the default if it's missing.
|
|
const nachoEnv = {
|
|
EXPO_PUBLIC_API_BASE_URL: nachoApiBaseUrl,
|
|
EXPO_PUBLIC_WORKSHOP_PDF_LINK_TEXT: nachoWorkshopPdfLinkText,
|
|
EXPO_DEV_PORT: String(NACHO_PORT),
|
|
NODE_ENV: 'development',
|
|
CHOKIDAR_USEPOLLING: '1',
|
|
HOME: NACHO_DIR,
|
|
}
|
|
|
|
const bashrc = [
|
|
'export PATH=/root/.cargo/bin:/data/bin:/usr/local/bin:/usr/bin:/bin',
|
|
"export PS1='spaces:\\w$ '",
|
|
`alias spaces='space-cli --chain ${SPACED_CHAIN} --rpc-user "$SPACED_RPC_USER" --rpc-password "$SPACED_RPC_PASSWORD" '`,
|
|
'cat <<EOF',
|
|
'',
|
|
// Banner rendered via renderBanner() — auto-wraps any line that exceeds the
|
|
// 96-cell text budget and pads every emitted line to exactly 100 cells, so
|
|
// the box always renders cleanly in a monospaced terminal regardless of
|
|
// edits below.
|
|
...renderBanner('Spaces', [
|
|
'spaced is managed by StartOS — do not run it manually.',
|
|
'Use the `spaces` alias to call space-cli, e.g.',
|
|
' spaces getserverinfo',
|
|
'`fabric` resolves handles against the local certrelay.',
|
|
'Docs: https://docs.spacesprotocol.org/',
|
|
]),
|
|
'',
|
|
'EOF',
|
|
].join('\n')
|
|
|
|
// Appends the always-on certrelay chain (setup oneshot + certrelay daemon) to
|
|
// any existing chain. certrelay serves cryptographic proofs binding handles to
|
|
// owner keys anchored to Bitcoin. The `fabric` CLI is copied onto /data/bin so
|
|
// the gotty terminal (which has /data/bin on PATH) can resolve handles.
|
|
const withCertrelay = (chain: any): any =>
|
|
chain
|
|
.addOneshot('certrelay-setup', {
|
|
subcontainer: certrelaySub,
|
|
exec: {
|
|
command: [
|
|
'sh',
|
|
'-c',
|
|
`set -eu; \
|
|
mkdir -p ${CERTRELAY_DATA_DIR} /data/bin; \
|
|
echo "certrelay-setup: installing fabric CLI to ${CERTRELAY_FABRIC_DEST}..."; \
|
|
cp -f ${CERTRELAY_FABRIC_BIN} ${CERTRELAY_FABRIC_DEST}; \
|
|
chmod +x ${CERTRELAY_FABRIC_DEST}; \
|
|
echo "certrelay-setup: done."`,
|
|
],
|
|
user: 'root',
|
|
},
|
|
requires: [],
|
|
})
|
|
.addDaemon('certrelay', {
|
|
subcontainer: certrelaySub,
|
|
exec: {
|
|
command: [CERTRELAY_BIN],
|
|
env: certrelayEnv,
|
|
// /data is root-owned; the image's USER certrelay can't write it.
|
|
user: 'root',
|
|
},
|
|
ready: {
|
|
display: i18n('Certrelay'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, CERTRELAY_PORT, {
|
|
successMessage: i18n('certrelay is ready'),
|
|
errorMessage: i18n('certrelay is not ready'),
|
|
}),
|
|
// Match the image's 120s start-period: anchor-refresh/checkpoint work
|
|
// on first start can delay the listener.
|
|
gracePeriod: 120_000,
|
|
trigger: sdk.trigger.cooldownTrigger(30_000),
|
|
},
|
|
requires: ['certrelay-setup', 'spaced'],
|
|
})
|
|
|
|
// Appends the always-on nacho chain: setup oneshot (mkdir data dir) + nacho
|
|
// daemon (the Expo dev server on 8082). Runs the image's own entrypoint so
|
|
// we don't have to reproduce its Expo init logic.
|
|
const withNacho = (chain: any): any =>
|
|
chain
|
|
.addOneshot('nacho-setup', {
|
|
subcontainer: nachoSub,
|
|
exec: {
|
|
command: [
|
|
'sh',
|
|
'-c',
|
|
`set -eu; mkdir -p ${NACHO_DIR}; if [ ! -f ${NACHO_DIR}/ignore_names.txt ]; then printf %s '${NACHO_DEFAULT_IGNORE_NAMES}' > ${NACHO_DIR}/ignore_names.txt; fi; echo "nacho-setup: ensured ${NACHO_DIR} and ignore_names.txt"`,
|
|
],
|
|
user: 'root',
|
|
},
|
|
requires: [],
|
|
})
|
|
.addDaemon('nacho', {
|
|
subcontainer: nachoSub,
|
|
exec: {
|
|
// The image's entrypoint runs Expo on EXPO_DEV_PORT (8082) and
|
|
// ignores CMD. Invoke it directly.
|
|
command: ['sh', '/usr/local/bin/entrypoint.sh'],
|
|
env: nachoEnv,
|
|
// The image declares no USER; default would be root inside StartOS,
|
|
// but be explicit so /data writes are unambiguous.
|
|
user: 'root',
|
|
},
|
|
ready: {
|
|
display: i18n('Nacho'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, NACHO_PORT, {
|
|
successMessage: i18n('nacho is ready'),
|
|
errorMessage: i18n('nacho is not ready'),
|
|
}),
|
|
// Expo + Metro bundler can take a while to come up on first start.
|
|
gracePeriod: 120_000,
|
|
trigger: sdk.trigger.cooldownTrigger(30_000),
|
|
},
|
|
requires: ['nacho-setup'],
|
|
})
|
|
|
|
// Appends the Subspaces chain (fetch → build → wallet ensure → subs-prover →
|
|
// subs daemon) to any existing chain. `as any` because the chain's TS type
|
|
// depends on prior IDs and we can't easily express the union here.
|
|
const withSubspaces = (chain: any): any => {
|
|
let c = chain
|
|
.addOneshot('subspaces-dirs', {
|
|
subcontainer: subspacesSub,
|
|
exec: {
|
|
command: [
|
|
'sh',
|
|
'-c',
|
|
`set -eu; \
|
|
mkdir -p ${SUBSPACES_DATA_DIR} ${SUBSPACES_PROVER_DIR} ${SUBSPACES_REGISTRY_DIR}; \
|
|
echo "subspaces-dirs: ensured data/prover/registry dirs under ${SUBSPACES_DIR}."`,
|
|
],
|
|
user: 'root',
|
|
},
|
|
requires: [],
|
|
})
|
|
.addDaemon('subs-registry', {
|
|
subcontainer: subspacesSub,
|
|
exec: {
|
|
command: [
|
|
'sh',
|
|
'-c',
|
|
`cd ${SUBSPACES_REGISTRY_DIR} && exec ${SUBSPACES_REGISTRY_BIN} --port ${SUBSPACES_REGISTRY_PORT}`,
|
|
],
|
|
env: registryEnv,
|
|
user: 'root',
|
|
},
|
|
ready: {
|
|
display: i18n('Subspaces Registry'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, SUBSPACES_REGISTRY_PORT, {
|
|
successMessage: i18n('registry-server is ready'),
|
|
errorMessage: i18n('registry-server is not ready'),
|
|
}),
|
|
gracePeriod: 60_000,
|
|
},
|
|
requires: ['subspaces-dirs'],
|
|
})
|
|
.addDaemon('subs', {
|
|
subcontainer: subspacesSub,
|
|
exec: {
|
|
command: [SUBSPACES_SUBS_BIN],
|
|
env: subsEnv,
|
|
user: 'root',
|
|
},
|
|
ready: {
|
|
display: i18n('Subspaces Web UI'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, SUBSPACES_UI_PORT, {
|
|
successMessage: i18n('subspaces UI is ready'),
|
|
errorMessage: i18n('subspaces UI is not ready'),
|
|
}),
|
|
gracePeriod: 60_000,
|
|
},
|
|
// Intentionally NOT contingent on subs-prover: the RISC Zero prover can
|
|
// take a while to become ready, and subs only needs it on-demand (proof
|
|
// generation), not at startup. subs reaches the prover via
|
|
// SUBS_PROVER_ENDPOINT once the prover is up.
|
|
requires: ['subspaces-dirs', 'spaced', 'subs-registry'],
|
|
})
|
|
|
|
// subs-prover is opt-in and DISABLED BY DEFAULT (toggle via the "Enable
|
|
// Subspaces Prover" action). Its interface (8888) stays registered either
|
|
// way (see interfaces.ts), but the daemon only starts when enabled. It's
|
|
// added LAST and depends on every other subspaces daemon; it runs lengthy
|
|
// timing tests on boot and NOTHING depends on it, so its slow startup never
|
|
// blocks anything else.
|
|
if (enableSubsProver) {
|
|
c = c.addDaemon('subs-prover', {
|
|
subcontainer: subspacesSub,
|
|
exec: {
|
|
command: [
|
|
'sh',
|
|
'-c',
|
|
`cd ${SUBSPACES_PROVER_DIR} && exec ${SUBSPACES_PROVER_BIN} --server --server-port ${SUBSPACES_PROVER_PORT}`,
|
|
],
|
|
env: proverEnv,
|
|
// Run as root: the horologger/subs image declares USER subs, but our
|
|
// /data volume + subdirs are root-owned, so the non-root user can't
|
|
// traverse/write them. Root sidesteps the ownership mismatch.
|
|
user: 'root',
|
|
},
|
|
ready: {
|
|
display: i18n('Subspaces Prover'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, SUBSPACES_PROVER_PORT, {
|
|
successMessage: i18n('subs-prover is ready'),
|
|
errorMessage: i18n('subs-prover is not ready'),
|
|
}),
|
|
gracePeriod: 60_000,
|
|
trigger: sdk.trigger.cooldownTrigger(30_000),
|
|
},
|
|
requires: ['subspaces-dirs', 'subs-registry', 'subs'],
|
|
})
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
let chain: any = sdk.Daemons.of(effects)
|
|
.addOneshot('bashrc', {
|
|
subcontainer: termSub,
|
|
exec: {
|
|
command: ['bash', '-c', `cat > /root/.bashrc <<'SPACES_BASHRC_EOF'
|
|
${bashrc}
|
|
SPACES_BASHRC_EOF`],
|
|
user: 'root',
|
|
},
|
|
requires: [],
|
|
})
|
|
.addDaemon('spaced', {
|
|
subcontainer: spacedSub,
|
|
exec: {
|
|
command: ['/root/.cargo/bin/spaced'],
|
|
env: spacedEnv,
|
|
},
|
|
ready: {
|
|
display: i18n('Spaced RPC'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, spacedRpcPort, {
|
|
successMessage: i18n('spaced RPC is ready'),
|
|
errorMessage: i18n('spaced RPC is not ready'),
|
|
}),
|
|
gracePeriod: 120_000,
|
|
},
|
|
requires: [],
|
|
})
|
|
.addDaemon('web-terminal', {
|
|
subcontainer: termSub,
|
|
exec: {
|
|
command: [
|
|
'gotty',
|
|
'--port',
|
|
String(uiPort),
|
|
'-c',
|
|
`${APP_USER}:${APP_PASSWORD}`,
|
|
'--permit-write',
|
|
'--reconnect',
|
|
'/bin/bash',
|
|
],
|
|
env: spacedEnv,
|
|
},
|
|
ready: {
|
|
display: i18n('Web Interface'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, uiPort, {
|
|
successMessage: i18n('The web terminal is ready'),
|
|
errorMessage: i18n('The web terminal is not ready'),
|
|
}),
|
|
},
|
|
requires: ['bashrc'],
|
|
})
|
|
.addHealthCheck('sync', {
|
|
ready: {
|
|
display: i18n('Spaced Sync'),
|
|
fn: async () => {
|
|
try {
|
|
const probe = await spacedSub.exec(
|
|
[
|
|
'/root/.cargo/bin/space-cli',
|
|
'--chain',
|
|
SPACED_CHAIN,
|
|
'--rpc-user',
|
|
spacedAuth.username,
|
|
'--rpc-password',
|
|
spacedAuth.password,
|
|
'--output-format',
|
|
'json',
|
|
'getserverinfo',
|
|
],
|
|
{},
|
|
)
|
|
const stderr = (probe.stderr ?? '').toString().trim()
|
|
const stdoutText = (probe.stdout ?? '').toString()
|
|
const stdoutTrimmed = stdoutText.trim()
|
|
if (probe.exitCode !== 0) {
|
|
return {
|
|
result: 'failure',
|
|
message: i18n('space-cli exited ${code}: ${error}', {
|
|
code: String(probe.exitCode),
|
|
error: (stderr || stdoutTrimmed || '<no output>').slice(0, 200),
|
|
}),
|
|
}
|
|
}
|
|
let parsed: {
|
|
ready?: boolean
|
|
progress?: number
|
|
chain?: { blocks?: number; headers?: number }
|
|
}
|
|
try {
|
|
parsed = JSON.parse(stdoutText)
|
|
} catch {
|
|
return {
|
|
result: 'failure',
|
|
message: i18n(
|
|
'getserverinfo non-JSON. stdout=${stdout} stderr=${stderr}',
|
|
{
|
|
stdout: (stdoutTrimmed || '<empty>').slice(0, 160),
|
|
stderr: (stderr || '<empty>').slice(0, 160),
|
|
},
|
|
),
|
|
}
|
|
}
|
|
const progress = Math.min(
|
|
100,
|
|
Math.max(0, Math.round((parsed.progress ?? 0) * 100)),
|
|
)
|
|
const blocks = parsed.chain?.blocks ?? 0
|
|
const headers = parsed.chain?.headers ?? 0
|
|
if (parsed.ready === true && progress >= 100) {
|
|
return {
|
|
result: 'success',
|
|
message: i18n(
|
|
'spaced is fully synced (blocks ${blocks} / headers ${headers}).',
|
|
{ blocks: String(blocks), headers: String(headers) },
|
|
),
|
|
}
|
|
}
|
|
return {
|
|
result: 'loading',
|
|
message: i18n(
|
|
'spaced is indexing: ${pct}% (blocks ${blocks} / headers ${headers}).',
|
|
{
|
|
pct: String(progress),
|
|
blocks: String(blocks),
|
|
headers: String(headers),
|
|
},
|
|
),
|
|
}
|
|
} catch (e) {
|
|
return {
|
|
result: 'failure',
|
|
message: i18n('Spaced Sync health check crashed: ${error}', {
|
|
error: (e as Error)?.message ?? String(e),
|
|
}),
|
|
}
|
|
}
|
|
},
|
|
gracePeriod: 30_000,
|
|
trigger: sdk.trigger.cooldownTrigger(30_000),
|
|
},
|
|
requires: ['spaced'],
|
|
})
|
|
chain = withCertrelay(chain)
|
|
chain = withNacho(chain)
|
|
if (enableSubspaces) chain = withSubspaces(chain)
|
|
return chain
|
|
})
|