import { storeJson } from './fileModels/storeJson' import { i18n } from './i18n' import { sdk } from './sdk' import { dataDir, DEFAULT_OPERATOR_RELAY, DEFAULT_PLATFORM_MODE, SPACED_RPC_URL, SPACED_WALLETLOAD_NAME, spacesDataDir, uiPort, } from './utils' type SpacedAuth = { username: string; password: string } export const main = sdk.setupMain(async ({ effects }) => { console.info(i18n('Starting SpacesOps!')) // Read with .const() so a store.json change (e.g. from Import Operator Key or // Configure Platform) triggers an automatic service restart. const store = await storeJson.read().const(effects) if ( !store?.operatorSecretHex || !store?.operatorPublicHex || !store?.sessionSecret ) { // taskOperatorKeys + taskSessionSecret seed these in init; if they aren't // populated yet, init hasn't finished — let StartOS restart us. throw new Error( 'SpacesOps store.json is not yet populated (operator keys / session secret missing).', ) } // Mount our own volume at /data AND the Spaces 'main' volume (read-only) at // /spaces-data so we can read the spaced RPC credentials Spaces seeded there. const mounts = sdk.Mounts.of() .mountVolume({ volumeId: 'main', subpath: null, mountpoint: dataDir, readonly: false, }) .mountDependency({ dependencyId: 'spaces', volumeId: 'main', subpath: null, mountpoint: spacesDataDir, readonly: true, type: 'directory', }) const sub = await sdk.SubContainer.of( effects, { imageId: 'spacesops' }, mounts, 'spacesops-sub', ) // setupMain runs in the StartOS runtime, NOT inside the container, so it // cannot fs-read the mounted Spaces volume directly. Exec a read inside the // subcontainer instead (mirrors how spaces-startos execs space-cli for its // sync health check). Throw if the creds aren't there yet — StartOS restarts // us until Spaces is installed and its store.json is seeded. const probe = await sub.exec(['cat', `${spacesDataDir}/store.json`]) if (probe.exitCode !== 0) { throw new Error( `Spaces RPC creds not available yet: could not read ${spacesDataDir}/store.json (exit ${probe.exitCode}). Is the Spaces service installed?`, ) } let spacesStore: { spacedAuth?: SpacedAuth | null } try { spacesStore = JSON.parse((probe.stdout ?? '').toString()) } catch { throw new Error( 'Spaces RPC creds not available yet: Spaces store.json is not valid JSON.', ) } const spacedAuth = spacesStore.spacedAuth if (!spacedAuth || !spacedAuth.username || !spacedAuth.password) { throw new Error( 'Spaces RPC creds not available yet: spacedAuth missing from Spaces store.json. Wait for the Spaces service init to finish.', ) } const env: Record = { // The app defaults to 127.0.0.1:3000, which the StartOS proxy cannot reach. PLATFORM_HOST: '0.0.0.0', PLATFORM_PORT: String(uiPort), PLATFORM_DB_PATH: `${dataDir}/local.db`, PLATFORM_MODE: store.platformMode ?? DEFAULT_PLATFORM_MODE, PLATFORM_SESSION_SECRET: store.sessionSecret, OPERATOR_SECRET_HEX: store.operatorSecretHex, OPERATOR_PUBLIC_HEX: store.operatorPublicHex, OPERATOR_RELAY: store.operatorRelay ?? DEFAULT_OPERATOR_RELAY, SPACED_RPC_URL, SPACED_RPC_USER: spacedAuth.username, SPACED_RPC_PASSWORD: spacedAuth.password, SPACED_WALLETLOAD_NAME, } // Optional integrations — only injected when configured (Configure Platform). if (store.coingeckoApiKey) env.COINGECKO_API_KEY = store.coingeckoApiKey if (store.coingeckoTokenCoins) env.COINGECKO_TOKEN_COINS = store.coingeckoTokenCoins if (store.subsdUrl) env.SUBSD_URI_VALUE = store.subsdUrl if (store.subsdUser) env.SUBSD_RPC_USER = store.subsdUser if (store.subsdPassword) env.SUBSD_RPC_PASSWORD = store.subsdPassword return sdk.Daemons.of(effects).addDaemon('spacesops', { subcontainer: sub, exec: { // Keep the image entrypoint: it creates /data dirs, symlinks // /app/data -> /data, and loads defaults for any UNSET vars before // exec'ing the command. command: ['/app/docker-entrypoint.sh', 'node', 'server.js'], env, cwd: '/app', user: 'root', }, ready: { display: i18n('Web Interface'), fn: () => sdk.healthCheck.checkPortListening(effects, uiPort, { successMessage: i18n('The web interface is ready'), errorMessage: i18n('The web interface is not ready'), }), gracePeriod: 60_000, }, requires: [], }) })