Files
spacesops-startos/startos/main.ts
T
spacesopsandClaude Opus 4.7 147f51d710 Rename SUBSD_* env vars to SUBS_* to match upstream refactor
The upstream SpacesOps app refactored its SUBSD_* environment variables to
SUBS_* (verified against server.js in spacesops/spacesops:v1.0.0). Update the
package to match so the configured values reach the app:

- main.ts: inject SUBS_URI_VALUE / SUBS_RPC_USER / SUBS_RPC_PASSWORD.
- configurePlatform action + storeJson: rename store keys subsd*→subs* and the
  field labels/descriptions (SUBS URL / SUBS RPC User / SUBS RPC Password).
- README, instructions, release notes: SUBSD→SUBS.

Note: the image's /app/setup-spacesops-env.sh still exports SUBSD_* defaults
(partial upstream refactor) — see report; vars the package does not set (e.g.
SUBS_RPC_URL) currently have no in-image default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 12:19:51 -04:00

132 lines
4.5 KiB
TypeScript

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<string, string> = {
// 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.subsUrl) env.SUBS_URI_VALUE = store.subsUrl
if (store.subsUser) env.SUBS_RPC_USER = store.subsUser
if (store.subsPassword) env.SUBS_RPC_PASSWORD = store.subsPassword
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: [],
})
})