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:
2026-05-26 10:28:18 -04:00
co-authored by Claude Opus 4.7
parent 1e33944be4
commit 35c1013520
38 changed files with 2071 additions and 0 deletions
+131
View File
@@ -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: [],
})
})