Build SpacesOps StartOS package (v1.0.0:0)
Initial .s9pk for SpacesOps, targeting StartOS 0.4.0.x with SDK 1.5.1. - Single managed daemon from spacesops/spacesops:v1.0.0 (x86_64 + aarch64), keeping the image entrypoint (/app/docker-entrypoint.sh node server.js). Forces PLATFORM_HOST=0.0.0.0 / PLATFORM_PORT=7264 so the StartOS proxy can reach the app. Single `ui` interface on 7264. - Depends on the Spaces service (>=0.0.9:3) and auto-wires the spaced RPC creds: main.ts mounts the Spaces `main` volume read-only at /spaces-data, execs a read of its store.json inside the subcontainer, and injects SPACED_RPC_USER/ PASSWORD + SPACED_RPC_URL=http://spaces.startos:7225. Throws to retry until Spaces is installed and seeded. - Generates a Nostr operator keypair (nostr-tools, bundled by ncc) and a strong session secret in idempotent init tasks (.once() reads, allowWriteAfterConst merges). Actions: show-operator-credentials, import-operator-key, show-admin-credentials (surfaces the fixed admin/Whatever! login with a warning), configure-platform (optional relay/mode/CoinGecko/SUBSD). - Install alert warns to install Spaces first and about the fixed admin credential. Backs up the `main` volume. - Icon: icon.svg (source spacesops.svg). The `spaces` dependency uses assets/spaces-icon.png for its Marketplace metadata. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+131
@@ -0,0 +1,131 @@
|
||||
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.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: [],
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user