Bump the image to spacesops/spacesops:v1.0.1, always inject NODE_EXTRA_CA_CERTS from sdk.getSslCertificate so the upstream entrypoint no longer crashes on unset _SCRIPT_DIR, and document the same-host HTTPS behavior in the README. Co-authored-by: Cursor <cursoragent@cursor.com>
154 lines
5.5 KiB
TypeScript
154 lines
5.5 KiB
TypeScript
import { mkdir, writeFile } from 'fs/promises'
|
|
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,
|
|
startOsHostnameFromUrl,
|
|
nodeExtraCaCertContainerPath,
|
|
nodeExtraCaCertVolumeSubpath,
|
|
SPACES_PACKAGE_ID,
|
|
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
|
|
|
|
// v1.0.1's entrypoint sources setup-spacesops-env.sh with set -u. That file
|
|
// exports NODE_EXTRA_CA_CERTS using ${_SCRIPT_DIR}, but _SCRIPT_DIR is set on
|
|
// a separate non-export line the entrypoint never evals — startup crashes
|
|
// unless NODE_EXTRA_CA_CERTS is already set. Fetch the StartOS root CA for
|
|
// outbound *.startos HTTPS (SUBS_URI host when configured, else Spaces).
|
|
const caHostname =
|
|
(store.subsUrl && startOsHostnameFromUrl(store.subsUrl)) ||
|
|
`${SPACES_PACKAGE_ID}.startos`
|
|
const certs = await sdk.getSslCertificate(effects, [caHostname]).const()
|
|
const [rootCa] = certs.slice(-1)
|
|
await mkdir(sdk.volumes.main.subpath('.startos'), { recursive: true })
|
|
await writeFile(
|
|
sdk.volumes.main.subpath(nodeExtraCaCertVolumeSubpath),
|
|
rootCa,
|
|
)
|
|
env.NODE_EXTRA_CA_CERTS = nodeExtraCaCertContainerPath
|
|
|
|
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: [],
|
|
})
|
|
})
|