Release v0.1.1:2 with certrelay, nacho, and prebuilt subspaces.
Build Service / BuildPackage (push) Has been cancelled

Remove the embedded explorer/indexer stack, switch subspaces to prebuilt images, add certrelay and nacho as always-on services, expose Spaces and Subs APIs, and wire optional HTTP basic auth for subs and subs-prover with the correct SUBS_BASIC_AUTH_PASSWORD env var.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-23 12:06:16 -04:00
co-authored by Cursor
parent 931becd274
commit ad64a47d42
30 changed files with 1364 additions and 831 deletions
+111 -155
View File
@@ -38,27 +38,15 @@ through a browser-based terminal.
| Field | Value | | Field | Value |
| --- | --- | | --- | --- |
| Spaces image | `docker.io/horologger/spaces` | | Spaces image | `docker.io/horologger/spaces` |
| PostgreSQL image | `docker.io/postgres:16.3` | | Subspaces image | `docker.io/horologger/subs:v0.1.2` |
| Subspaces image | `docker.io/horologger/subs:v0.1.0` |
| Certrelay image | `docker.io/horologger/certrelay:v0.2.3` | | Certrelay image | `docker.io/horologger/certrelay:v0.2.3` |
| Indexer image | _**TODO** — prebuilt explorer-indexer image, not yet produced_ | | Nacho image | `docker.io/horologger/nacho:v1.0.0` |
| Explorer image | _**TODO** — prebuilt explorer (SvelteKit) image_ |
| Architectures | `linux/amd64`, `linux/arm64` | | Architectures | `linux/amd64`, `linux/arm64` |
| Entrypoint | StartOS-managed (image entrypoints are **not** used directly) | | Entrypoint | StartOS-managed (image entrypoints are **not** used directly) |
The Spaces image bundles `spaced`, `space-cli`, `bitcoin-cli`, `gotty`, `node`, The Spaces image bundles `spaced`, `space-cli`, `bitcoin-cli`, `gotty`, `node`,
`npm`, `screen`, and a small shell environment. StartOS ignores the Spaces `npm`, `screen`, and a small shell environment. StartOS ignores the Spaces
image's `docker_entrypoint.sh`; daemons are defined in `startos/main.ts`. The image's `docker_entrypoint.sh`; daemons are defined in `startos/main.ts`.
PostgreSQL image is the upstream `postgres:16.3`, launched via its official
`docker-entrypoint.sh`.
> **Note (build-from-source removed):** earlier builds compiled the Go indexer
> and the SvelteKit explorer from source inside `golang:1.23-alpine` and
> `node:20-alpine` builder images. Those images and their build chains were
> removed to shrink the `.s9pk` and eliminate the multi-minute first-enable
> compile. They will be replaced with prebuilt images (see the **TODO** rows
> above). Until then, enabling the embedded explorer starts **PostgreSQL only**
> — the indexer and explorer-UI daemons are stubbed out in `startos/main.ts`.
## Volume and Data Layout ## Volume and Data Layout
@@ -66,15 +54,14 @@ PostgreSQL image is the upstream `postgres:16.3`, launched via its official
| --- | --- | --- | | --- | --- | --- |
| `/data` | `main` | Spaces data directory (`SPACED_DATA_DIR`), wallets, indexes, and `store.json` | | `/data` | `main` | Spaces data directory (`SPACED_DATA_DIR`), wallets, indexes, and `store.json` |
| `/data/mainnet/.cookie` | `main` | Spaced RPC cookie (auto-generated by `spaced` at startup) | | `/data/mainnet/.cookie` | `main` | Spaced RPC cookie (auto-generated by `spaced` at startup) |
| `/data/store.json` | `main` | StartOS-managed credentials (web-UI password, bitcoind RPC user/password, PostgreSQL user/password/database) | | `/data/store.json` | `main` | StartOS-managed credentials (Space-CLI Web UI password, bitcoind RPC user/password, spaced RPC user/password) and feature config (Subspaces toggles, Certrelay + Nacho settings). |
| `/data/postgres` | `main` | PostgreSQL data directory (`PGDATA`). Owned by uid 999 (`postgres`). The whole `main` volume is backed up, so the database is included. |
| `/data/explorer-indexer` | `main` | **TODO** — reserved for the prebuilt indexer's runtime data. The build-from-source layout (source, `bin/sync`, `bin/goose`, marker) is gone; `reset-indexer-state` still wipes this path. |
| `/data/explorer-ui` | `main` | **TODO** — reserved for the prebuilt explorer's runtime data. The build-from-source layout (source, `build/`, `node_modules`, marker) is gone; `reset-explorer-state` still wipes this path. |
| `/data/subspaces/data` | `main` | `subs` runtime data (`SUBS_DATA_DIR`) — handles, proofs, config.db. Persists across restarts and enable/disable toggles. | | `/data/subspaces/data` | `main` | `subs` runtime data (`SUBS_DATA_DIR`) — handles, proofs, config.db. Persists across restarts and enable/disable toggles. |
| `/data/subspaces/prover` | `main` | Working directory for the `subs-prover` daemon. | | `/data/subspaces/prover` | `main` | Working directory for the `subs-prover` daemon. |
| `/data/subspaces/registry` | `main` | Working directory for the `registry-server` daemon. | | `/data/subspaces/registry` | `main` | Working directory for the `registry-server` daemon. |
| `/data/certrelay/data` | `main` | Certrelay data directory (`CERTRELAY_DATA_DIR`) — anchors, peer state, sqlite. | | `/data/certrelay/data` | `main` | Certrelay data directory (`CERTRELAY_DATA_DIR`) — anchors, peer state, sqlite. |
| `/data/bin/fabric` | `main` | The `fabric` CLI, copied from the certrelay image on each start so the gotty terminal (which has `/data/bin` on PATH) can resolve handles. | | `/data/bin/fabric` | `main` | The `fabric` CLI, copied from the certrelay image on each start so the gotty terminal (which has `/data/bin` on PATH) can resolve handles. |
| `/data/support.pdf` | `main` | Workshop PDF served by nacho. Uploaded/replaced via the **Upload Support PDF** action. Missing until an upload has been performed. |
| `/data/nacho/ignore_names.txt` | `main` | Comma-separated names hidden from the nacho UI (read at runtime). Created by the `nacho-setup` oneshot with the default (`fold,swifty`) on first start; managed by the **Configure Nacho** action. |
## Installation and First-Run Flow ## Installation and First-Run Flow
@@ -87,60 +74,41 @@ On the first install, StartOS:
pair and creates a critical cross-service task on **Bitcoin** that runs pair and creates a critical cross-service task on **Bitcoin** that runs
bitcoind's `generate-rpc-dependent` action to register the credentials in bitcoind's `generate-rpc-dependent` action to register the credentials in
`bitcoin.conf`. `bitcoin.conf`.
3. Seeds `store.json.dbAuth` with a random PostgreSQL password (username 3. Seeds `store.json.spacedAuth` with a random `spaces:<random>` credential
`postgres`, database `spacesprotocol_explorer`). No task is created — the
credentials are only used internally by the embedded daemon.
4. Runs the `postgres-chown` oneshot to create `/data/postgres` with the right
ownership, then launches the PostgreSQL daemon on loopback 5432.
5. Seeds `store.json.spacedAuth` with a random `spaces:<random>` credential
pair. Spaced is configured to require these via `SPACED_RPC_USER` / pair. Spaced is configured to require these via `SPACED_RPC_USER` /
`SPACED_RPC_PASSWORD` (cookie auth is **not** used in this package), so the `SPACED_RPC_PASSWORD` (cookie auth is **not** used in this package), so the
gotty terminal's `spaces` alias and the indexer share a single auth path. gotty terminal's `spaces` alias and other clients share a single auth path.
6. Seeds `store.json.enableExplorer = false`. The embedded explorer 4. Seeds `store.json` certrelay defaults: `certrelaySelfUrl =
(PostgreSQL + Go indexer) is **opt-in**.
7. Seeds `store.json` certrelay defaults: `certrelaySelfUrl =
https://certrelay.spacesops.com`, `certrelayBootstrap = false`, https://certrelay.spacesops.com`, `certrelayBootstrap = false`,
`certrelayHealthcheckHandle = account-digital-useful.genesis@key`. `certrelayHealthcheckHandle = account-digital-useful.genesis@key`.
8. Launches `spaced` as a managed daemon (no `screen`, no shell auto-start) 5. Seeds `store.json` nacho defaults (`nachoWorkshopPdfLinkText = ''`) and
`store.json.enableSubspaces = false`, `store.json.enableSubsProver = false`.
The nacho ignore-names list lives in `/data/nacho/ignore_names.txt` (not
in the store); the `nacho-setup` oneshot creates that file with the
default (`fold,swifty`) on first daemon start if it doesn't exist.
6. Launches `spaced` as a managed daemon (no `screen`, no shell auto-start)
and the `gotty` web terminal once the bashrc oneshot completes. and the `gotty` web terminal once the bashrc oneshot completes.
9. Runs `certrelay-setup` (creates `/data/certrelay/data`, copies the `fabric` 7. Runs `certrelay-setup` (creates `/data/certrelay/data`, copies the `fabric`
CLI to `/data/bin/fabric`) and launches the **certrelay** daemon on 7778. CLI to `/data/bin/fabric`) and launches the **certrelay** daemon on 7778.
Certrelay is **always on**; its `CERTRELAY_SPACED_RPC_URL` uses the local Certrelay is **always on**; its `CERTRELAY_SPACED_RPC_URL` uses the local
spaced credentials from `store.spacedAuth`. Configure its self URL etc. via spaced credentials from `store.spacedAuth`. Configure its self URL etc. via
the **Configure Certrelay** action. the **Configure Certrelay** action.
8. Runs `nacho-setup` (mkdir `/data/nacho`; seeds `ignore_names.txt` with the
default `fold,swifty` if missing) and launches the **nacho** Expo daemon
on 8082. Always on.
The user is **not** prompted to choose a chain or RPC mode — this package is The user is **not** prompted to choose a chain or RPC mode — this package is
mainnet-only. mainnet-only.
### Enabling the embedded explorer
Run the **Enable Embedded Explorer** action. That flips
`store.json.enableExplorer = true` and the service auto-restarts:
8. Postgres-chown oneshot creates `/data/postgres` with correct ownership.
9. PostgreSQL daemon starts on loopback `127.0.0.1:5432`.
> **TODO (prebuilt images pending):** the indexer and explorer-UI daemons are
> currently **stubbed out** in `startos/main.ts`. The build-from-source chain
> (`indexer-fetch` → `indexer-build` → `indexer-migrate` → `indexer`, and
> `explorer-fetch` → `explorer-install` → `explorer-ui`) was removed to shrink
> the `.s9pk` and cut startup time. Until prebuilt images are wired in,
> enabling the explorer starts **PostgreSQL only**; nothing populates the DB
> and the Explorer Web UI (port 3000) will not resolve. The `show-db-credentials`,
> `reset-db-state`, `reset-indexer-state`, and `reset-explorer-state` actions
> remain available.
Run **Disable Embedded Explorer** to turn it back off; on-disk data at
`/data/postgres`, `/data/explorer-indexer`, and `/data/explorer-ui` is
preserved.
## Configuration Management ## Configuration Management
| StartOS-Managed | Upstream-Managed | | StartOS-Managed | Upstream-Managed |
| --- | --- | | --- | --- |
| Web-UI username / password (`admin` + generated password) | `spaced` runtime tuning via `SPACED_*` env vars in the image | | Web-UI username / password (`admin` + generated password) | `spaced` runtime tuning via `SPACED_*` env vars in the image |
| Bitcoin RPC username / password (registered on bitcoind) | Wallet creation, bidding, and registration -- all driven via `space-cli` inside the terminal | | Bitcoin RPC username / password (registered on bitcoind) | Wallet creation, bidding, and registration -- all driven via `space-cli` inside the terminal |
| PostgreSQL username / password / database (loopback only) | `space-cli` flags and subcommands | | Spaced RPC username / password (`SPACED_RPC_USER`/`SPACED_RPC_PASSWORD`) | `space-cli` flags and subcommands |
| Subspaces auth toggle + credentials (`SUBS_BASIC_AUTH_USER`/`SUBS_BASIC_AUTH_PASSWORD`, off by default) | |
| Subspaces Prover auth toggle + credentials (`SUBS_PROVER_BASIC_AUTH_USER`/`SUBS_PROVER_BASIC_AUTH_PASSWORD`, off by default) | |
| Chain selection (locked to `mainnet`) | | | Chain selection (locked to `mainnet`) | |
| Spaced data directory and RPC bind | | | Spaced data directory and RPC bind | |
@@ -149,14 +117,13 @@ preserved.
| Interface | Port | Protocol | Exposure | Notes | | Interface | Port | Protocol | Exposure | Notes |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| Space-CLI Web UI (gotty terminal) | 8080 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | Basic auth: `admin:<store.password>` | | Space-CLI Web UI (gotty terminal) | 8080 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | Basic auth: `admin:<store.password>` |
| Explorer Web UI | 3000 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | SvelteKit explorer. **TODO** — daemon stubbed pending the prebuilt explorer image; the interface is still registered but nothing listens yet. | | Subspaces Web UI | 7777 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | `subs` daemon (subsd). Optional HTTP basic auth — off by default, gated by **Enable Subspaces Auth**. When on, both this UI and the Subs API are walled behind `SUBS_BASIC_AUTH_USER` / `SUBS_BASIC_AUTH_PASSWORD`. Only useful while Subspaces is enabled. |
| Subspaces Web UI | 7777 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | `subs` daemon (subsd). No built-in auth. Only useful while Subspaces is enabled. | | Subs API | 7777 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | The subsd REST API (same daemon/port as above), exposed as a distinct `api` interface separate from the prover and registry. Shares the optional Subspaces Auth wall with the Web UI (same port, same process). Only useful while Subspaces is enabled. |
| Subs API | 7777 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | The subsd REST API (same daemon/port as above), exposed as a distinct `api` interface separate from the prover and registry. Only useful while Subspaces is enabled. | | Subspaces Prover | 8888 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | `subs-prover` RISC Zero prover server (no GPU). Its own StartOS interface. Optional HTTP basic auth — off by default, gated by **Enable Subspaces Prover Auth**. When on, `SUBS_PROVER_BASIC_AUTH_USER`/`SUBS_PROVER_BASIC_AUTH_PASSWORD` are enforced. Only useful while Subspaces (and the prover toggle) is enabled. |
| Subspaces Prover | 8888 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | `subs-prover` RISC Zero prover server (no GPU). Its own StartOS interface. Only useful while Subspaces is enabled. |
| Subspaces Registry | 8081 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | `registry-server` for publishing/resolving handles (remapped from its upstream default 8080 to avoid colliding with the gotty terminal on 8080). Its own StartOS interface. Only useful while Subspaces is enabled. | | Subspaces Registry | 8081 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | `registry-server` for publishing/resolving handles (remapped from its upstream default 8080 to avoid colliding with the gotty terminal on 8080). Its own StartOS interface. Only useful while Subspaces is enabled. |
| Certrelay | 7778 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | `certrelay` server (always on). Serves cryptographic proofs binding handles to owner keys. **Setup:** run **Configure Certrelay** and set `CERTRELAY_SELF_URL` to the publicly visible URL StartOS exposes for this interface. | | Certrelay | 7778 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | `certrelay` server (always on). Serves cryptographic proofs binding handles to owner keys. **Setup:** run **Configure Certrelay** and set `CERTRELAY_SELF_URL` to the publicly visible URL StartOS exposes for this interface. |
| Spaces API (spaced RPC) | 7225 | HTTP JSON-RPC | LAN / `.local` / Tor / clearnet (via StartOS) | spaced JSON-RPC, now bound to `0.0.0.0` and exposed as the **Spaces API** interface. Static-cred-authenticated via `SPACED_RPC_USER` / `SPACED_RPC_PASSWORD` from `store.json.spacedAuth`. Internal clients still connect over loopback. | | Nacho | 8082 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | nacho Expo dev server (always on). The ignore list is read at runtime from `/data/nacho/ignore_names.txt` (managed by **Configure Nacho**). `EXPO_PUBLIC_API_BASE_URL` is derived dynamically from the Subs API StartOS interface (`sdk.serviceInterface.getOwn('subs-api').const()`), preferring the `.local` URL. Falls back to `http://127.0.0.1:7777` only if no address info is available. |
| PostgreSQL | 5432 | Postgres wire protocol | **Loopback only** (`listen_addresses=127.0.0.1`) | Username/password from `store.json.dbAuth`; full `DB_URL` exported into the web terminal | | Spaces API (spaced RPC) | 7225 | HTTP JSON-RPC | LAN / `.local` / Tor / clearnet (via StartOS) | spaced JSON-RPC, bound to `0.0.0.0` and exposed as the **Spaces API** interface. Static-cred-authenticated via `SPACED_RPC_USER` / `SPACED_RPC_PASSWORD` from `store.json.spacedAuth`. Internal clients still connect over loopback. |
## Actions ## Actions
@@ -166,21 +133,26 @@ preserved.
| `show-credentials` | Show Space-CLI Web UI Credentials | Hidden | Any | Surfaces the current `admin` username + masked password (launched by the first-install task) | | `show-credentials` | Show Space-CLI Web UI Credentials | Hidden | Any | Surfaces the current `admin` username + masked password (launched by the first-install task) |
| `show-password` | Show Space-CLI Web UI Password | Enabled | Any | Same as `show-credentials` but visible in the actions list, for routine re-display of the current admin credentials | | `show-password` | Show Space-CLI Web UI Password | Enabled | Any | Same as `show-credentials` but visible in the actions list, for routine re-display of the current admin credentials |
| `set-bitcoin-rpc` | Set up Bitcoin RPC | Enabled | Any | Re-invokes bitcoind's `generate-rpc-dependent` with the stored credentials. Safe to call repeatedly. | | `set-bitcoin-rpc` | Set up Bitcoin RPC | Enabled | Any | Re-invokes bitcoind's `generate-rpc-dependent` with the stored credentials. Safe to call repeatedly. |
| `show-spaced-credentials` | Show Spaces API Credentials | Enabled | Any | Surfaces the spaced RPC `SPACED_RPC_USER` / `SPACED_RPC_PASSWORD` (from `store.spacedAuth`) plus a loopback connection URL (password masked, all copyable). |
| `sync-status` | Sync Status | Enabled | Only running | Runs `space-cli getserverinfo` inside the daemon container and returns the JSON output | | `sync-status` | Sync Status | Enabled | Only running | Runs `space-cli getserverinfo` inside the daemon container and returns the JSON output |
| `reset-spaced-state` | Reset Spaced State | Enabled | Any | Deletes `/data/mainnet/` so spaced resyncs its index from spaces' anchor. Preserves `store.json` (passwords + RPC credentials). Use when spaced crash-loops on a stale or corrupt index. | | `reset-spaced-state` | Reset Spaced State | Enabled | Any | Deletes `/data/mainnet/` so spaced resyncs its index from spaces' anchor. Preserves `store.json` (passwords + RPC credentials). Use when spaced crash-loops on a stale or corrupt index. |
| `export-wallet` | Export Wallet | Enabled | Only running | Runs `space-cli exportwallet /data/mainnet/wallets_backup/default.json` and surfaces the resulting JSON as a masked/copyable result. The file is also persisted inside the volume at that path. | | `export-wallet` | Export Wallet | Enabled | Only running | Runs `space-cli exportwallet /data/mainnet/wallets_backup/default.json` and surfaces the resulting JSON as a masked/copyable result. The file is also persisted inside the volume at that path. |
| `import-wallet` | Import Wallet | Enabled | Only running | Accepts a pasted JSON payload (textarea), writes it to `/data/mainnet/wallets_backup/default.json` (rotating the existing file to `.bakNNN`), rotates `/data/mainnet/wallets/default` to `.bakNNN`, then runs `space-cli importwallet` + `loadwallet`. | | `import-wallet` | Import Wallet | Enabled | Only running | Accepts a pasted JSON payload (textarea), writes it to `/data/mainnet/wallets_backup/default.json` (rotating the existing file to `.bakNNN`), rotates `/data/mainnet/wallets/default` to `.bakNNN`, then runs `space-cli importwallet` + `loadwallet`. |
| `enable-explorer` | Enable Embedded Explorer | Enabled (hidden when already on) | Any | Sets `store.enableExplorer = true` and triggers a service restart so the full daemon graph (postgres + indexer + indexer-sync HC) takes effect. |
| `disable-explorer` | Disable Embedded Explorer | Enabled (hidden when already off) | Any | Sets `store.enableExplorer = false` and triggers a service restart so the indexer and postgres daemons stop. On-disk data at `/data/postgres` and `/data/explorer-indexer` is preserved. |
| `show-db-credentials` | Show Database Credentials | Enabled | Any | Surfaces the PostgreSQL username, password, database, and full `DB_URL` (all copyable; secrets masked). Credentials exist regardless of whether the explorer is currently enabled. |
| `reset-db-state` | Reset Database State | Enabled | Any | Deletes `/data/postgres` so the next start re-initializes an empty database. Warning-gated. `store.json` is preserved. |
| `reset-indexer-state` | Reset Indexer State | Enabled | Any | Deletes `/data/explorer-indexer` so the next start re-fetches the indexer source and rebuilds the sync + goose binaries. Useful when bumping the pinned commit. PostgreSQL data is preserved. Warning-gated. |
| `reset-explorer-state` | Reset Explorer UI State | Enabled | Any | Deletes `/data/explorer-ui` so the next start re-fetches the SvelteKit source and rebuilds the bundle. PostgreSQL data is preserved. Warning-gated. |
| `enable-subspaces` | Enable Subspaces | Enabled (hidden when already on) | Any | Sets `store.enableSubspaces = true` and triggers a service restart so the Subspaces chain (build → wallet → subs-prover → subs) takes effect. | | `enable-subspaces` | Enable Subspaces | Enabled (hidden when already on) | Any | Sets `store.enableSubspaces = true` and triggers a service restart so the Subspaces chain (build → wallet → subs-prover → subs) takes effect. |
| `disable-subspaces` | Disable Subspaces | Enabled (hidden when already off) | Any | Sets `store.enableSubspaces = false` and triggers a service restart so the subs and subs-prover daemons stop. On-disk data at `/data/subspaces` is preserved. | | `disable-subspaces` | Disable Subspaces | Enabled (hidden when already off) | Any | Sets `store.enableSubspaces = false` and triggers a service restart so the subs and subs-prover daemons stop. On-disk data at `/data/subspaces` is preserved. |
| `reset-subspaces-state` | Reset Subspaces State | Enabled | Any | Deletes `/data/subspaces` (runtime data only — binaries ship in the image). Use to wipe local handles/proofs. The spaces wallet on spaced is preserved. Warning-gated. | | `reset-subspaces-state` | Reset Subspaces State | Enabled | Any | Deletes `/data/subspaces` (runtime data only — binaries ship in the image). Use to wipe local handles/proofs. The spaces wallet on spaced is preserved. Warning-gated. |
| `set-subs-prover` | Enable / Disable Subspaces Prover | Enabled | Any | Toggle (`store.enableSubsProver`, default **off**) that starts/stops only the `subs-prover` daemon, independent of the overall Subspaces toggle. When off, the daemon doesn't start but its interface (8888) stays registered. Saving restarts the service. | | `set-subs-prover` | Enable / Disable Subspaces Prover | Enabled | Any | Toggle (`store.enableSubsProver`, default **off**) that starts/stops only the `subs-prover` daemon, independent of the overall Subspaces toggle. When off, the daemon doesn't start but its interface (8888) stays registered. Saving restarts the service. |
| `enable-subs-auth` | Enable Subspaces Auth | Enabled (hidden when already on) | Any | Sets `store.subsAuthEnabled = true`. If no credentials exist yet, generates `{ username: 'spaces', password: random32 }` and stores them in `store.subsAuth`. Restart re-launches `subs` with `SUBS_BASIC_AUTH_USER`/`SUBS_BASIC_AUTH_PASSWORD` env vars set. |
| `disable-subs-auth` | Disable Subspaces Auth | Enabled (hidden when already off) | Any | Sets `store.subsAuthEnabled = false`. Stored credentials are preserved so a later re-enable does not regenerate them. Restart re-launches `subs` without the auth env vars. |
| `show-subs-credentials` | Show Subspaces Auth Credentials | Enabled | Any | Surfaces the current `SUBS_BASIC_AUTH_USER`/`SUBS_BASIC_AUTH_PASSWORD` (from `store.subsAuth`) plus a loopback connection URL. Reports whether auth is currently enforced. |
| `set-subs-credentials` | Set Subspaces Auth Credentials | Enabled | Any | Set or rotate `store.subsAuth` and enable auth (`store.subsAuthEnabled = true`). Username is prefilled with the current value; password field is blank — leave it blank to auto-generate a random 32-char password, or paste your own. Restarts the service so subs picks up `SUBS_BASIC_AUTH_USER`/`SUBS_BASIC_AUTH_PASSWORD`. |
| `enable-subs-prover-auth` | Enable Subspaces Prover Auth | Enabled (hidden when already on) | Any | Sets `store.subsProverAuthEnabled = true`. If no credentials exist yet, generates `{ username: 'spaces', password: random32 }` and stores them in `store.subsProverAuth`. Restart re-launches `subs-prover` with `SUBS_PROVER_BASIC_AUTH_USER`/`SUBS_PROVER_BASIC_AUTH_PASSWORD` env vars set. |
| `disable-subs-prover-auth` | Disable Subspaces Prover Auth | Enabled (hidden when already off) | Any | Sets `store.subsProverAuthEnabled = false`. Stored credentials are preserved. Restart re-launches `subs-prover` without the auth env vars. |
| `show-subs-prover-credentials` | Show Subspaces Prover Auth Credentials | Enabled | Any | Surfaces the current `SUBS_PROVER_BASIC_AUTH_USER`/`SUBS_PROVER_BASIC_AUTH_PASSWORD` (from `store.subsProverAuth`) plus a loopback connection URL. Reports whether auth is currently enforced. |
| `set-subs-prover-credentials` | Set Subspaces Prover Auth Credentials | Enabled | Any | Set or rotate `store.subsProverAuth`. Same UX as `set-subs-credentials` (blank password → auto-generated). Restarts the service only if prover auth is currently enabled. |
| `configure-certrelay` | Configure Certrelay | Enabled | Any | Sets `CERTRELAY_SELF_URL` (text), `CERTRELAY_BOOTSTRAP` (toggle), and `CERTRELAY_HEALTHCHECK_HANDLE` (text) in `store.json`; prefilled with current values. Saving restarts the service so certrelay picks up the new config. | | `configure-certrelay` | Configure Certrelay | Enabled | Any | Sets `CERTRELAY_SELF_URL` (text), `CERTRELAY_BOOTSTRAP` (toggle), and `CERTRELAY_HEALTHCHECK_HANDLE` (text) in `store.json`; prefilled with current values. Saving restarts the service so certrelay picks up the new config. |
| `configure-nacho` | Configure Nacho | Enabled | Any | Writes the Ignore Names list to `/data/nacho/ignore_names.txt` (read at runtime by nacho — survives daemon restarts independent of the store) and sets `EXPO_PUBLIC_WORKSHOP_PDF_LINK_TEXT` in `store.json`. Prefill reads the file (with the package default as fallback) plus the current store value. Saving restarts the service. |
| `upload-support-pdf` | Upload Support PDF | Enabled | Any | Required `.pdf` file upload that overwrites `/data/support.pdf` on the main volume (read with `fs/promises`, piped via stdin into a temp `spaces` subcontainer's `cat > /data/support.pdf`). Split out from Configure Nacho because StartOS's form serializes an unselected `Value.file` as `{}` rather than `null`, which `Value.file(..., required: false).nullable()` rejects; a `required: true` dedicated action avoids the empty-form path entirely. |
## Backups and Restore ## Backups and Restore
@@ -193,15 +165,14 @@ the same idempotent init logic runs and reuses the existing credentials in
| ID | Display | Grace period | Behaviour | | ID | Display | Grace period | Behaviour |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `postgres` (daemon `ready`) | Database | 60 s | TCP listen on `127.0.0.1:5432` |
| `spaced` (daemon `ready`) | Spaced RPC | 120 s | TCP listen on `0.0.0.0:7225` (exposed as the Spaces API interface) | | `spaced` (daemon `ready`) | Spaced RPC | 120 s | TCP listen on `0.0.0.0:7225` (exposed as the Spaces API interface) |
| `subs-prover` (daemon `ready`) | Subspaces Prover | 60 s | TCP listen on `0.0.0.0:8888`. Only present when Subspaces **and** the `set-subs-prover` toggle are enabled (toggle defaults off). Polled every 30 s; nothing depends on it. | | `subs-prover` (daemon `ready`) | Subspaces Prover | 60 s | TCP listen on `0.0.0.0:8888`. Only present when Subspaces **and** the `set-subs-prover` toggle are enabled (toggle defaults off). Polled every 30 s; nothing depends on it. |
| `subs-registry` (daemon `ready`) | Subspaces Registry | 60 s | TCP listen on `0.0.0.0:8081`. Only present when Subspaces is enabled. | | `subs-registry` (daemon `ready`) | Subspaces Registry | 60 s | TCP listen on `0.0.0.0:8081`. Only present when Subspaces is enabled. |
| `subs` (daemon `ready`) | Subspaces Web UI | 60 s | TCP listen on `0.0.0.0:7777`. Only present when Subspaces is enabled. | | `subs` (daemon `ready`) | Subspaces Web UI | 60 s | TCP listen on `0.0.0.0:7777`. Only present when Subspaces is enabled. |
| `certrelay` (daemon `ready`) | Certrelay | 120 s | TCP listen on `0.0.0.0:7778`. Always on. Polled every 30 s. | | `certrelay` (daemon `ready`) | Certrelay | 120 s | TCP listen on `0.0.0.0:7778`. Always on. Polled every 30 s. |
| `nacho` (daemon `ready`) | Nacho | 120 s | TCP listen on `0.0.0.0:8082`. Always on. Polled every 30 s. |
| `web-terminal` (daemon `ready`) | Web Interface | default | TCP listen on `0.0.0.0:8080` | | `web-terminal` (daemon `ready`) | Web Interface | default | TCP listen on `0.0.0.0:8080` |
| `sync` (standalone) | Spaced Sync | 30 s | Exec `space-cli --output-format json getserverinfo`; reports `success` when `ready=true && progress=100%`, otherwise `loading` with progress percentage | | `sync` (standalone) | Spaced Sync | 30 s | Exec `space-cli --output-format json getserverinfo`; reports `success` when `ready=true && progress=100%`, otherwise `loading` with progress percentage |
| `indexer` / `explorer-ui` / `indexer-sync` | — | — | **TODO** — removed with the build-from-source chain. Will return when the prebuilt indexer + explorer images are wired in. |
## Limitations and Differences ## Limitations and Differences
@@ -212,47 +183,28 @@ the same idempotent init logic runs and reuses the existing credentials in
via `space-cli` (aliased as `spaces` inside the shell). via `space-cli` (aliased as `spaces` inside the shell).
4. **Externally exposed ports** (each a StartOS interface): 8080 (gotty 4. **Externally exposed ports** (each a StartOS interface): 8080 (gotty
Space-CLI Web UI, always on), 7225 (Spaces API / spaced RPC, always on), Space-CLI Web UI, always on), 7225 (Spaces API / spaced RPC, always on),
7778 (Certrelay, always on), and — only while their feature is enabled — 7778 (Certrelay, always on), 8082 (Nacho, always on), and — only while
3000 (Explorer Web UI, currently stubbed), 7777 (Subspaces Web UI), 8888 Subspaces is enabled — 7777 (Subspaces Web UI + Subs API), 8888 (Subspaces
(Subspaces Prover), 8081 (Subspaces Registry). **Loopback-only:** PostgreSQL Prover, only if its sub-toggle is on), 8081 (Subspaces Registry). The
(5432). The spaced RPC is authenticated (static creds) even though it's now spaced RPC is authenticated (static creds). The Subspaces registry is
externally exposed. The Subspaces registry is remapped from its upstream remapped from its upstream default 8080 to 8081 to avoid colliding with the
default 8080 to 8081 to avoid colliding with the gotty terminal. gotty terminal.
5. **Bitcoin Core 31.x is the only supported dependency.** Earlier majors are 5. **Bitcoin Core 31.x is the only supported dependency.** Earlier majors are
not allowed by the manifest version range. not allowed by the manifest version range.
6. **The web terminal is independent of spaced.** Gotty stays reachable even 6. **The web terminal is independent of spaced.** Gotty stays reachable even
when `spaced` is crash-looping, so you can always shell in to diagnose. when `spaced` is crash-looping, so you can always shell in to diagnose.
7. **The embedded explorer is opt-in — and currently PostgreSQL-only.** Fresh 7. **Subspaces is opt-in.** Fresh installs do not run Subspaces. Run **Enable
installs run in spaces-only mode (spaced + gotty). Run **Enable Embedded Subspaces** to start it. It ships as the prebuilt `horologger/subs:v0.1.2`
Explorer** to start PostgreSQL; **Disable Embedded Explorer** stops it. The image (no compile step), so enabling is fast — the daemons start as soon
indexer and explorer-UI daemons are **stubbed out** pending prebuilt images as the image is on the box. Enabling runs three daemons, each with its own
(the build-from-source chain was removed to shrink the `.s9pk` and cut StartOS interface: **subs** (Web UI + Subs API, 7777), **registry-server**
startup time). So enabling the explorer today brings up an empty database (8081), and — only if the separate **Enable / Disable Subspaces Prover**
and nothing else. PostgreSQL data is preserved across toggles. toggle is on (default **off**) — **subs-prover** (8888, no GPU). The
8. **PostgreSQL is embedded, not a separate service.** There is no prover's interface is always registered even when its daemon is off. subs
cross-service dependency on a postgres .s9pk; the database is local to this loads the existing `default` spaces wallet at startup and does **not**
package, lives at `/data/postgres`, and is intended for the (pending) create it — create it first from the Space-CLI Web UI with `spaces
prebuilt indexer + explorer UI. Connect to it from inside the gotty createwallet` if you haven't.
terminal with `psql $DB_URL`. 8. **Certrelay is always on.** It ships as the prebuilt
9. **Subspaces pointers data note.** When the prebuilt indexer is wired back
in, be aware our spaced binary predates the spaces pointers feature; the
prior build-from-source indexer was patched to treat `ptrs_root` as optional
and to skip the `getptrblockmeta` RPC. A future rebuild of
`horologger/spaces` with a newer spaced will let us drop those patches.
10. **Subspaces is opt-in.** Fresh installs do not run Subspaces. Run **Enable
Subspaces** to start it. It ships as the prebuilt `horologger/subs:v0.1.0`
image (no compile step), so enabling is fast — the three daemons start as
soon as the image is on the box. Enabling runs three daemons, each with its
own StartOS interface: **subs** (Web UI, 7777), **registry-server** (8081),
and — only if the separate **Enable / Disable Subspaces Prover** toggle is
on (default **off**) — **subs-prover** (8888, no GPU). The prover's
interface is always registered even when its daemon is off. subs loads the
existing `default` spaces
wallet at startup and does **not** create it — create it first from the
Space-CLI Web UI with `spaces createwallet` if you haven't. Subspaces is
independent of the Embedded Explorer toggle — you can run any combination
of {explorer, subspaces}.
11. **Certrelay is always on.** It ships as the prebuilt
`horologger/certrelay:v0.2.3` image (static musl binaries) and runs on `horologger/certrelay:v0.2.3` image (static musl binaries) and runs on
7778 from service start. It talks to the local spaced over loopback using 7778 from service start. It talks to the local spaced over loopback using
the `store.spacedAuth` credentials. The bundled `fabric` CLI is copied to the `store.spacedAuth` credentials. The bundled `fabric` CLI is copied to
@@ -294,23 +246,20 @@ package_id: spaces
upstream_version: subspacesplus upstream_version: subspacesplus
images: images:
spaces: docker.io/horologger/spaces:v0.0.9s spaces: docker.io/horologger/spaces:v0.0.9s
postgres: docker.io/postgres:16.3 subspaces: docker.io/horologger/subs:v0.1.2
subspaces: docker.io/horologger/subs:v0.1.0
certrelay: docker.io/horologger/certrelay:v0.2.3 certrelay: docker.io/horologger/certrelay:v0.2.3
# TODO: prebuilt indexer image (was golang:1.23-alpine builder, removed) nacho: docker.io/horologger/nacho:v1.0.0
# TODO: prebuilt explorer image (was node:20-alpine builder, removed)
architectures: [x86_64, aarch64] architectures: [x86_64, aarch64]
volumes: volumes:
main: /data main: /data
ports: ports:
ui: 8080 ui: 8080
explorer_ui: 3000 # interface registered; daemon stubbed (TODO prebuilt image)
subspaces_ui: 7777 # external, only useful while subspaces enabled subspaces_ui: 7777 # external, only useful while subspaces enabled
subspaces_prover: 8888 # external (its own interface), subspaces only subspaces_prover: 8888 # external (its own interface), subspaces only
subspaces_registry: 8081 # external (its own interface), subspaces only; remapped from upstream 8080 subspaces_registry: 8081 # external (its own interface), subspaces only; remapped from upstream 8080
certrelay: 7778 # external (its own interface), always on certrelay: 7778 # external (its own interface), always on
nacho: 8082 # external (its own interface), always on
spaced_rpc: 7225 # external (Spaces API interface); spaced binds 0.0.0.0, auth-gated spaced_rpc: 7225 # external (Spaces API interface); spaced binds 0.0.0.0, auth-gated
postgres: 5432 # loopback only
dependencies: dependencies:
- bitcoind - bitcoind
spaced_env_vars: spaced_env_vars:
@@ -331,37 +280,43 @@ spaced_env_vars:
- BTC_RPC_PASSWORD - BTC_RPC_PASSWORD
- APP_USER - APP_USER
- APP_PASSWORD - APP_PASSWORD
- DB_URL
postgres_env_vars:
- POSTGRES_USER
- POSTGRES_PASSWORD
- POSTGRES_DB
- PGDATA
explorer:
default_enabled: false # opt-in via Enable Embedded Explorer action
store_field: enableExplorer
status: postgres_only # indexer + explorer-ui daemons stubbed; see TODOs
reset_actions: [reset-db-state, reset-indexer-state, reset-explorer-state]
indexer:
status: TODO # build-from-source (Go) removed; prebuilt image pending
prior_source_repo: spacesprotocol/explorer-indexer
data_dir: /data/explorer-indexer # reset-indexer-state still wipes this
explorer_ui:
status: TODO # build-from-source (SvelteKit) removed; prebuilt image pending
prior_source_repo: randomlogin/explorer
data_dir: /data/explorer-ui # reset-explorer-state still wipes this
port: 3000 # interface registered; daemon stubbed
subspaces: subspaces:
default_enabled: false # opt-in via Enable Subspaces action default_enabled: false # opt-in via Enable Subspaces action
store_field: enableSubspaces store_field: enableSubspaces
subs_prover_default_enabled: false # separate toggle: set-subs-prover (store.enableSubsProver) subs_prover_default_enabled: false # separate toggle: set-subs-prover (store.enableSubsProver)
image: horologger/subs:v0.1.0 # prebuilt; no compile step image: horologger/subs:v0.1.2 # prebuilt; no compile step
data_dir: /data/subspaces/data data_dir: /data/subspaces/data
wallet: default # loaded by subs, NOT created (create via space-cli first) wallet: default # loaded by subs, NOT created (create via space-cli first)
daemons: # three separate daemons, each its own interface daemons: # three separate daemons, each its own interface
subs: { bin: /usr/local/bin/subs, port: 7777 } subs: { bin: /usr/local/bin/subs, port: 7777 }
subs-prover: { bin: /usr/local/bin/subs-prover, port: 8888, gpu: none } subs-prover: { bin: /usr/local/bin/subs-prover, port: 8888, gpu: none }
registry-server: { bin: /usr/local/bin/registry-server, port: 8081 } # remapped from upstream 8080 registry-server: { bin: /usr/local/bin/registry-server, port: 8081 } # remapped from upstream 8080
auth: # optional HTTP basic auth in front of subs (Web UI + Subs API on 7777)
default_enabled: false
store_enabled_field: subsAuthEnabled
store_creds_field: subsAuth # { username, password }; persists across enable/disable
default_username: spaces
env_vars: # injected only when subsAuthEnabled=true AND subsAuth!=null
- SUBS_BASIC_AUTH_USER
- SUBS_BASIC_AUTH_PASSWORD
actions:
- enable-subs-auth
- disable-subs-auth
- show-subs-credentials
- set-subs-credentials
prover_auth: # optional HTTP basic auth in front of subs-prover (8888)
default_enabled: false
store_enabled_field: subsProverAuthEnabled
store_creds_field: subsProverAuth
default_username: spaces
env_vars: # injected only when subsProverAuthEnabled=true AND subsProverAuth!=null
- SUBS_PROVER_BASIC_AUTH_USER
- SUBS_PROVER_BASIC_AUTH_PASSWORD
actions:
- enable-subs-prover-auth
- disable-subs-prover-auth
- show-subs-prover-credentials
- set-subs-prover-credentials
certrelay: certrelay:
always_on: true always_on: true
image: horologger/certrelay:v0.2.3 # prebuilt; static musl binaries image: horologger/certrelay:v0.2.3 # prebuilt; static musl binaries
@@ -380,41 +335,42 @@ certrelay:
CERTRELAY_SELF_URL: https://certrelay.spacesops.com # default; set to your public URL CERTRELAY_SELF_URL: https://certrelay.spacesops.com # default; set to your public URL
CERTRELAY_BOOTSTRAP: 'false' # toggle via action CERTRELAY_BOOTSTRAP: 'false' # toggle via action
CERTRELAY_HEALTHCHECK_HANDLE: account-digital-useful.genesis@key # via action CERTRELAY_HEALTHCHECK_HANDLE: account-digital-useful.genesis@key # via action
managed_env_vars: nacho:
- POSTGRES_URI always_on: true
- BITCOIN_NODE_URI image: horologger/nacho:v1.0.0 # Expo dev server
- BITCOIN_NODE_USER port: 8082
- BITCOIN_NODE_PASSWORD data_dir: /data/nacho
- SPACES_NODE_URI ignore_names_file: /data/nacho/ignore_names.txt # read at runtime; default 'fold,swifty' seeded by nacho-setup
- RPC_USER configurable_via_actions:
- RPC_PASSWORD - configure-nacho # ignore_names.txt + EXPO_PUBLIC_WORKSHOP_PDF_LINK_TEXT
- ACTIVATION_BLOCK_HEIGHT - upload-support-pdf # writes /data/support.pdf
- FAST_SYNC_BLOCK_HEIGHT env:
- UPDATE_DB_INTERVAL EXPO_PUBLIC_API_BASE_URL: <derived from subs-api StartOS interface>
- MEMPOOL_CHUNK_SIZE EXPO_PUBLIC_WORKSHOP_PDF_LINK_TEXT: '' # set via configure-nacho
defaults: EXPO_DEV_PORT: '8082'
ACTIVATION_BLOCK_HEIGHT: '871222' # spaces mainnet activation
FAST_SYNC_BLOCK_HEIGHT: '864000'
UPDATE_DB_INTERVAL: '5'
MEMPOOL_CHUNK_SIZE: '200'
actions: actions:
- reset-password - reset-password
- show-credentials - show-credentials
- show-password - show-password
- set-bitcoin-rpc - set-bitcoin-rpc
- show-spaced-credentials
- sync-status - sync-status
- reset-spaced-state - reset-spaced-state
- export-wallet - export-wallet
- import-wallet - import-wallet
- enable-explorer
- disable-explorer
- show-db-credentials
- reset-db-state
- reset-indexer-state
- reset-explorer-state
- enable-subspaces - enable-subspaces
- disable-subspaces - disable-subspaces
- set-subs-prover - set-subs-prover
- enable-subs-auth
- disable-subs-auth
- show-subs-credentials
- set-subs-credentials
- enable-subs-prover-auth
- disable-subs-prover-auth
- show-subs-prover-credentials
- set-subs-prover-credentials
- reset-subspaces-state - reset-subspaces-state
- configure-certrelay - configure-certrelay
- configure-nacho
- upload-support-pdf
``` ```
+167
View File
@@ -0,0 +1,167 @@
import { storeJson } from '../fileModels/storeJson'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import {
dataDir,
NACHO_DEFAULT_IGNORE_NAMES,
NACHO_DEFAULT_WORKSHOP_PDF_LINK_TEXT,
NACHO_DIR,
} from '../utils'
const { InputSpec, Value } = sdk
const IGNORE_NAMES_PATH = `${NACHO_DIR}/ignore_names.txt`
const mainMount = sdk.Mounts.of().mountVolume({
volumeId: 'main',
subpath: null,
mountpoint: dataDir,
readonly: false,
})
// Reads /data/nacho/ignore_names.txt off the main volume via a temp
// subcontainer. Returns null if the file is missing/empty/unreadable so the
// prefill falls back to the package default.
async function readIgnoreNamesFile(effects: any): Promise<string | null> {
try {
const res = await sdk.SubContainer.withTemp(
effects,
{ imageId: 'spaces' },
mainMount,
'nacho-read-ignore-names',
(subc) =>
subc.exec(
[
'sh',
'-c',
`if [ -f ${IGNORE_NAMES_PATH} ]; then cat ${IGNORE_NAMES_PATH}; fi`,
],
{ user: 'root' },
),
)
if (res.exitCode !== 0) return null
const value = (res.stdout ?? '').toString().trim()
return value.length > 0 ? value : null
} catch {
return null
}
}
const inputSpec = InputSpec.of({
ignoreNames: Value.text({
name: i18n('Ignore Names'),
description: i18n(
'Comma-separated list of names for the nacho UI to ignore (EXPO_PUBLIC_IGNORE_NAMES).',
),
warning: null,
footnote: null,
default: NACHO_DEFAULT_IGNORE_NAMES,
required: true,
masked: false,
placeholder: 'fold,swifty',
minLength: 0,
maxLength: null,
}),
workshopPdfLinkText: Value.text({
name: i18n('Workshop PDF Link Text'),
description: i18n(
'Display text for the workshop PDF link in the nacho UI (EXPO_PUBLIC_WORKSHOP_PDF_LINK_TEXT). Leave empty to suppress the label.',
),
warning: null,
footnote: null,
default: NACHO_DEFAULT_WORKSHOP_PDF_LINK_TEXT,
required: false,
masked: false,
placeholder: 'Workshop PDF',
minLength: 0,
maxLength: null,
}),
})
export const configureNacho = sdk.Action.withInput(
// id
'configure-nacho',
// metadata
async ({ effects }) => ({
name: i18n('Configure Nacho'),
description: i18n(
'Set the user-tunable nacho settings. The Ignore Names list is written to /data/nacho/ignore_names.txt (read by nacho at runtime). The Workshop PDF link text is set via EXPO_PUBLIC_WORKSHOP_PDF_LINK_TEXT. Saving restarts the service. To upload a new Workshop PDF, use the separate "Upload Support PDF" action.',
),
warning: null,
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
// input
inputSpec,
// prefill — current values. Ignore Names is sourced from the file on disk
// (the source of truth nacho actually reads), with the default as a fallback.
async ({ effects }) => {
const [store, fileValue] = await Promise.all([
storeJson.read().once(),
readIgnoreNamesFile(effects),
])
return {
ignoreNames: fileValue ?? NACHO_DEFAULT_IGNORE_NAMES,
workshopPdfLinkText:
store?.nachoWorkshopPdfLinkText ??
NACHO_DEFAULT_WORKSHOP_PDF_LINK_TEXT,
}
},
// run
async ({ effects, input }) => {
// Persist the workshop PDF link text in the store (so it stays an
// EXPO_PUBLIC_* env var) and write the ignore names to its file on the
// main volume. mkdir -p first in case /data/nacho doesn't yet exist
// (e.g., the user runs this action before nacho-setup has executed).
await storeJson.merge(effects, {
nachoWorkshopPdfLinkText: input.workshopPdfLinkText ?? '',
})
const ignoreNames = input.ignoreNames
const res = await sdk.SubContainer.withTemp(
effects,
{ imageId: 'spaces' },
mainMount,
'nacho-write-ignore-names',
(subc) =>
subc.exec(
[
'sh',
'-c',
`set -eu; mkdir -p ${NACHO_DIR}; cat > ${IGNORE_NAMES_PATH} && chmod 644 ${IGNORE_NAMES_PATH}`,
],
{ input: ignoreNames, user: 'root' },
),
)
if (res.exitCode !== 0) {
return {
version: '1',
title: i18n('Failure'),
message: i18n('Could not write ignore_names.txt: ${error}', {
error:
((res.stderr ?? '').toString() || `exit ${res.exitCode}`).slice(
0,
300,
),
}),
result: null,
}
}
return {
version: '1',
title: i18n('Success'),
message: i18n(
'Nacho configuration saved. Ignore Names written to /data/nacho/ignore_names.txt; the service is restarting to apply the new values.',
),
result: null,
}
},
)
-37
View File
@@ -1,37 +0,0 @@
import { storeJson } from '../fileModels/storeJson'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
export const disableExplorer = sdk.Action.withoutInput(
// id
'disable-explorer',
// metadata
async ({ effects }) => {
const current = await storeJson.read((s) => s.enableExplorer).once()
return {
name: i18n('Disable Embedded Explorer'),
description: i18n(
'Stop the embedded PostgreSQL + Go indexer. Indexed data on disk (/data/postgres, /data/explorer-indexer) is preserved and will be reused if the explorer is re-enabled later. Service restarts automatically.',
),
warning: null,
allowedStatuses: 'any',
group: null,
visibility: current === true ? 'enabled' : 'hidden',
}
},
// run
async ({ effects }) => {
await storeJson.merge(effects, { enableExplorer: false })
return {
version: '1',
title: i18n('Success'),
message: i18n(
'Embedded explorer disabled. The service is restarting in spaces-only mode (spaced + web terminal). Run "Enable Embedded Explorer" to turn it back on.',
),
result: null,
}
},
)
+37
View File
@@ -0,0 +1,37 @@
import { storeJson } from '../fileModels/storeJson'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
export const disableSubsAuth = sdk.Action.withoutInput(
// id
'disable-subs-auth',
// metadata
async ({ effects }) => {
const enabled = await storeJson.read((s) => s.subsAuthEnabled).once()
return {
name: i18n('Disable Subspaces Auth'),
description: i18n(
'Turn off HTTP basic auth in front of the Subspaces Web UI / Subs API. Stored credentials are preserved so re-enabling does not generate new ones; use "Set Subspaces Auth Credentials" to rotate. Service restarts.',
),
warning: null,
allowedStatuses: 'any',
group: null,
visibility: enabled === true ? 'enabled' : 'hidden',
}
},
// run
async ({ effects }) => {
await storeJson.merge(effects, { subsAuthEnabled: false })
return {
version: '1',
title: i18n('Success'),
message: i18n(
'Subspaces Auth disabled. The service is restarting; subs will serve unauthenticated again on port 7777. Stored credentials are kept for the next enable.',
),
result: null,
}
},
)
+37
View File
@@ -0,0 +1,37 @@
import { storeJson } from '../fileModels/storeJson'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
export const disableSubsProverAuth = sdk.Action.withoutInput(
// id
'disable-subs-prover-auth',
// metadata
async ({ effects }) => {
const enabled = await storeJson.read((s) => s.subsProverAuthEnabled).once()
return {
name: i18n('Disable Subspaces Prover Auth'),
description: i18n(
'Turn off HTTP basic auth in front of the Subspaces Prover. Stored credentials are preserved so re-enabling does not generate new ones; use "Set Subspaces Prover Auth Credentials" to rotate. Service restarts.',
),
warning: null,
allowedStatuses: 'any',
group: null,
visibility: enabled === true ? 'enabled' : 'hidden',
}
},
// run
async ({ effects }) => {
await storeJson.merge(effects, { subsProverAuthEnabled: false })
return {
version: '1',
title: i18n('Success'),
message: i18n(
'Subspaces Prover Auth disabled. The service is restarting; subs-prover will serve unauthenticated again on port 8888. Stored credentials are kept for the next enable.',
),
result: null,
}
},
)
-37
View File
@@ -1,37 +0,0 @@
import { storeJson } from '../fileModels/storeJson'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
export const enableExplorer = sdk.Action.withoutInput(
// id
'enable-explorer',
// metadata
async ({ effects }) => {
const current = await storeJson.read((s) => s.enableExplorer).once()
return {
name: i18n('Enable Embedded Explorer'),
description: i18n(
'Turn on the embedded PostgreSQL + Go indexer. Spaces protocol data (blocks, transactions, spaces, rollouts) will be indexed locally for use by the future explorer web UI. Service restarts automatically.',
),
warning: null,
allowedStatuses: 'any',
group: null,
visibility: current === true ? 'hidden' : 'enabled',
}
},
// run
async ({ effects }) => {
await storeJson.merge(effects, { enableExplorer: true })
return {
version: '1',
title: i18n('Success'),
message: i18n(
'Embedded explorer enabled. The service is restarting; the indexer will begin fetching blocks shortly. First-time start can take several minutes for the Go build.',
),
result: null,
}
},
)
+53
View File
@@ -0,0 +1,53 @@
import { storeJson } from '../fileModels/storeJson'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { randomPassword } from '../utils'
const DEFAULT_USERNAME = 'spaces'
export const enableSubsAuth = sdk.Action.withoutInput(
// id
'enable-subs-auth',
// metadata
async ({ effects }) => {
const enabled = await storeJson.read((s) => s.subsAuthEnabled).once()
return {
name: i18n('Enable Subspaces Auth'),
description: i18n(
'Turn on HTTP basic auth in front of the Subspaces Web UI and the Subs API (both on port 7777). If no credentials have been set yet, a random password is generated (username defaults to "spaces"). Use "Show Subspaces Auth Credentials" afterwards to retrieve them. Service restarts so subs picks up the auth env vars.',
),
warning: null,
allowedStatuses: 'any',
group: null,
visibility: enabled === true ? 'hidden' : 'enabled',
}
},
// run
async ({ effects }) => {
const existing = await storeJson.read((s) => s.subsAuth).once()
const subsAuth = existing ?? {
username: DEFAULT_USERNAME,
password: randomPassword(),
}
await storeJson.merge(effects, {
subsAuth,
subsAuthEnabled: true,
})
return {
version: '1',
title: i18n('Success'),
message: existing
? i18n(
'Subspaces Auth enabled with the existing stored credentials. The service is restarting; use "Show Subspaces Auth Credentials" to view them.',
)
: i18n(
'Subspaces Auth enabled and a fresh credential pair was generated. The service is restarting; use "Show Subspaces Auth Credentials" to view them.',
),
result: null,
}
},
)
+53
View File
@@ -0,0 +1,53 @@
import { storeJson } from '../fileModels/storeJson'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { randomPassword } from '../utils'
const DEFAULT_USERNAME = 'spaces'
export const enableSubsProverAuth = sdk.Action.withoutInput(
// id
'enable-subs-prover-auth',
// metadata
async ({ effects }) => {
const enabled = await storeJson.read((s) => s.subsProverAuthEnabled).once()
return {
name: i18n('Enable Subspaces Prover Auth'),
description: i18n(
'Turn on HTTP basic auth in front of the Subspaces Prover (port 8888). If no credentials have been set yet, a random password is generated (username defaults to "spaces"). Use "Show Subspaces Prover Auth Credentials" afterwards to retrieve them. Service restarts so subs-prover picks up the auth env vars.',
),
warning: null,
allowedStatuses: 'any',
group: null,
visibility: enabled === true ? 'hidden' : 'enabled',
}
},
// run
async ({ effects }) => {
const existing = await storeJson.read((s) => s.subsProverAuth).once()
const subsProverAuth = existing ?? {
username: DEFAULT_USERNAME,
password: randomPassword(),
}
await storeJson.merge(effects, {
subsProverAuth,
subsProverAuthEnabled: true,
})
return {
version: '1',
title: i18n('Success'),
message: existing
? i18n(
'Subspaces Prover Auth enabled with the existing stored credentials. The service is restarting; use "Show Subspaces Prover Auth Credentials" to view them.',
)
: i18n(
'Subspaces Prover Auth enabled and a fresh credential pair was generated. The service is restarting; use "Show Subspaces Prover Auth Credentials" to view them.',
),
result: null,
}
},
)
+22 -12
View File
@@ -1,41 +1,51 @@
import { sdk } from '../sdk' import { sdk } from '../sdk'
import { configureCertrelay } from './configureCertrelay' import { configureCertrelay } from './configureCertrelay'
import { disableExplorer } from './disableExplorer' import { configureNacho } from './configureNacho'
import { disableSubsAuth } from './disableSubsAuth'
import { disableSubsProverAuth } from './disableSubsProverAuth'
import { disableSubspaces } from './disableSubspaces' import { disableSubspaces } from './disableSubspaces'
import { enableExplorer } from './enableExplorer' import { enableSubsAuth } from './enableSubsAuth'
import { enableSubsProverAuth } from './enableSubsProverAuth'
import { enableSubspaces } from './enableSubspaces' import { enableSubspaces } from './enableSubspaces'
import { exportWallet } from './exportWallet' import { exportWallet } from './exportWallet'
import { importWallet } from './importWallet' import { importWallet } from './importWallet'
import { resetDbState } from './resetDbState'
import { resetExplorerState } from './resetExplorerState'
import { resetIndexerState } from './resetIndexerState'
import { resetPassword } from './resetPassword' import { resetPassword } from './resetPassword'
import { resetSpacedState } from './resetSpacedState' import { resetSpacedState } from './resetSpacedState'
import { resetSubspacesState } from './resetSubspacesState' import { resetSubspacesState } from './resetSubspacesState'
import { setBitcoinRpc } from './setBitcoinRpc' import { setBitcoinRpc } from './setBitcoinRpc'
import { setSubsCredentials } from './setSubsCredentials'
import { setSubsProver } from './setSubsProver' import { setSubsProver } from './setSubsProver'
import { setSubsProverCredentials } from './setSubsProverCredentials'
import { showCredentials } from './showCredentials' import { showCredentials } from './showCredentials'
import { showDbCredentials } from './showDbCredentials'
import { showPassword } from './showPassword' import { showPassword } from './showPassword'
import { showSpacedCredentials } from './showSpacedCredentials'
import { showSubsCredentials } from './showSubsCredentials'
import { showSubsProverCredentials } from './showSubsProverCredentials'
import { syncStatus } from './syncStatus' import { syncStatus } from './syncStatus'
import { uploadSupportPdf } from './uploadSupportPdf'
export const actions = sdk.Actions.of() export const actions = sdk.Actions.of()
.addAction(resetPassword) .addAction(resetPassword)
.addAction(showCredentials) .addAction(showCredentials)
.addAction(showPassword) .addAction(showPassword)
.addAction(setBitcoinRpc) .addAction(setBitcoinRpc)
.addAction(showSpacedCredentials)
.addAction(syncStatus) .addAction(syncStatus)
.addAction(resetSpacedState) .addAction(resetSpacedState)
.addAction(exportWallet) .addAction(exportWallet)
.addAction(importWallet) .addAction(importWallet)
.addAction(enableExplorer)
.addAction(disableExplorer)
.addAction(showDbCredentials)
.addAction(resetDbState)
.addAction(resetIndexerState)
.addAction(resetExplorerState)
.addAction(enableSubspaces) .addAction(enableSubspaces)
.addAction(disableSubspaces) .addAction(disableSubspaces)
.addAction(setSubsProver) .addAction(setSubsProver)
.addAction(enableSubsAuth)
.addAction(disableSubsAuth)
.addAction(showSubsCredentials)
.addAction(setSubsCredentials)
.addAction(enableSubsProverAuth)
.addAction(disableSubsProverAuth)
.addAction(showSubsProverCredentials)
.addAction(setSubsProverCredentials)
.addAction(resetSubspacesState) .addAction(resetSubspacesState)
.addAction(configureCertrelay) .addAction(configureCertrelay)
.addAction(configureNacho)
.addAction(uploadSupportPdf)
-58
View File
@@ -1,58 +0,0 @@
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { dataDir, pgDataDir } from '../utils'
export const resetDbState = sdk.Action.withoutInput(
// id
'reset-db-state',
// metadata
async ({ effects }) => ({
name: i18n('Reset Database State'),
description: i18n(
'Wipe /data/postgres so PostgreSQL re-initializes from scratch.',
),
warning: i18n(
'This deletes all PostgreSQL data on disk. The next start will re-create an empty database. store.json (passwords + RPC credentials) is preserved.',
),
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
// run
async ({ effects }) => {
const res = await sdk.SubContainer.withTemp(
effects,
{ imageId: 'spaces' },
sdk.Mounts.of().mountVolume({
volumeId: 'main',
subpath: null,
mountpoint: dataDir,
readonly: false,
}),
'spaces-reset-db',
(subc) => subc.exec(['rm', '-rf', pgDataDir], { user: 'root' }),
)
if (res.exitCode !== 0) {
return {
version: '1',
title: i18n('Failure'),
message: i18n('Could not wipe PostgreSQL state: ${error}', {
error: (res.stderr ?? '').toString() || `exit ${res.exitCode}`,
}),
result: null,
}
}
return {
version: '1',
title: i18n('Success'),
message: i18n(
'PostgreSQL data has been wiped. Start (or restart) the service to re-initialize the database.',
),
result: null,
}
},
)
-60
View File
@@ -1,60 +0,0 @@
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { dataDir, EXPLORER_DIR } from '../utils'
export const resetExplorerState = sdk.Action.withoutInput(
// id
'reset-explorer-state',
// metadata
async ({ effects }) => ({
name: i18n('Reset Explorer UI State'),
description: i18n(
'Wipe /data/explorer-ui so the next start re-fetches the explorer source and rebuilds it from scratch.',
),
warning: i18n(
'This deletes the cached explorer source and the built SvelteKit bundle. The next start will need internet access to re-download from GitHub and to fetch npm dependencies. PostgreSQL data is preserved.',
),
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
// run
async ({ effects }) => {
const res = await sdk.SubContainer.withTemp(
effects,
// TODO(prebuilt-explorer): switch back to the explorer image once it
// exists. Any image with `rm` works for the wipe; `spaces` is always present.
{ imageId: 'spaces' },
sdk.Mounts.of().mountVolume({
volumeId: 'main',
subpath: null,
mountpoint: dataDir,
readonly: false,
}),
'spaces-reset-explorer',
(subc) => subc.exec(['rm', '-rf', EXPLORER_DIR], { user: 'root' }),
)
if (res.exitCode !== 0) {
return {
version: '1',
title: i18n('Failure'),
message: i18n('Could not wipe explorer state: ${error}', {
error: (res.stderr ?? '').toString() || `exit ${res.exitCode}`,
}),
result: null,
}
}
return {
version: '1',
title: i18n('Success'),
message: i18n(
'Explorer cache has been wiped. Start (or restart) the service to re-fetch and rebuild from GitHub.',
),
result: null,
}
},
)
-58
View File
@@ -1,58 +0,0 @@
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { dataDir, INDEXER_DIR } from '../utils'
export const resetIndexerState = sdk.Action.withoutInput(
// id
'reset-indexer-state',
// metadata
async ({ effects }) => ({
name: i18n('Reset Indexer State'),
description: i18n(
'Wipe /data/explorer-indexer so the next start re-fetches the indexer source and rebuilds the sync + goose binaries.',
),
warning: i18n(
'This deletes the cached indexer source and compiled binaries. The next start will need internet access to re-download from GitHub and to fetch Go modules. PostgreSQL data (the indexed blocks themselves) is preserved — use Reset Database State if you also want to clear that.',
),
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
// run
async ({ effects }) => {
const res = await sdk.SubContainer.withTemp(
effects,
{ imageId: 'spaces' },
sdk.Mounts.of().mountVolume({
volumeId: 'main',
subpath: null,
mountpoint: dataDir,
readonly: false,
}),
'spaces-reset-indexer',
(subc) => subc.exec(['rm', '-rf', INDEXER_DIR], { user: 'root' }),
)
if (res.exitCode !== 0) {
return {
version: '1',
title: i18n('Failure'),
message: i18n('Could not wipe indexer state: ${error}', {
error: (res.stderr ?? '').toString() || `exit ${res.exitCode}`,
}),
result: null,
}
}
return {
version: '1',
title: i18n('Success'),
message: i18n(
'Indexer cache has been wiped. Start (or restart) the service to re-fetch and rebuild from GitHub.',
),
result: null,
}
},
)
+91
View File
@@ -0,0 +1,91 @@
import { storeJson } from '../fileModels/storeJson'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { randomPassword } from '../utils'
const { InputSpec, Value } = sdk
const DEFAULT_USERNAME = 'spaces'
const inputSpec = InputSpec.of({
username: Value.text({
name: i18n('Username'),
description: i18n(
'Username for HTTP basic auth in front of subs (SUBS_BASIC_AUTH_USER).',
),
warning: null,
footnote: null,
default: DEFAULT_USERNAME,
required: true,
masked: false,
placeholder: 'spaces',
minLength: 1,
maxLength: null,
}),
password: Value.text({
name: i18n('Password'),
description: i18n(
'Password for HTTP basic auth in front of subs (SUBS_BASIC_AUTH_PASSWORD). Leave blank to auto-generate a random one.',
),
warning: null,
footnote: null,
default: null,
required: false,
masked: true,
placeholder: '(leave blank to auto-generate)',
minLength: 0,
maxLength: null,
}),
})
export const setSubsCredentials = sdk.Action.withInput(
// id
'set-subs-credentials',
// metadata
async ({ effects }) => ({
name: i18n('Set Subspaces Auth Credentials'),
description: i18n(
'Set or rotate the HTTP basic auth credentials enforced in front of the Subspaces Web UI and Subs API. Auth is enabled automatically. Saving restarts the service.',
),
warning: null,
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
// input
inputSpec,
// prefill — current username, never the password (the user explicitly types
// a new password or leaves blank to auto-generate)
async ({ effects }) => {
const subsAuth = await storeJson.read((s) => s.subsAuth).once()
return {
username: subsAuth?.username ?? DEFAULT_USERNAME,
password: null,
}
},
// run
async ({ effects, input }) => {
const password =
input.password && input.password.length > 0
? input.password
: randomPassword()
await storeJson.merge(effects, {
subsAuth: { username: input.username, password },
subsAuthEnabled: true,
})
return {
version: '1',
title: i18n('Success'),
message: i18n(
'Subspaces Auth credentials saved. The service is restarting so subs picks up the new credentials.',
),
result: null,
}
},
)
@@ -0,0 +1,96 @@
import { storeJson } from '../fileModels/storeJson'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { randomPassword } from '../utils'
const { InputSpec, Value } = sdk
const DEFAULT_USERNAME = 'spaces'
const inputSpec = InputSpec.of({
username: Value.text({
name: i18n('Username'),
description: i18n(
'Username for HTTP basic auth in front of subs-prover (SUBS_PROVER_BASIC_AUTH_USER).',
),
warning: null,
footnote: null,
default: DEFAULT_USERNAME,
required: true,
masked: false,
placeholder: 'spaces',
minLength: 1,
maxLength: null,
}),
password: Value.text({
name: i18n('Password'),
description: i18n(
'Password for HTTP basic auth in front of subs-prover (SUBS_PROVER_BASIC_AUTH_PASSWORD). Leave blank to auto-generate a random one.',
),
warning: null,
footnote: null,
default: null,
required: false,
masked: true,
placeholder: '(leave blank to auto-generate)',
minLength: 0,
maxLength: null,
}),
})
export const setSubsProverCredentials = sdk.Action.withInput(
// id
'set-subs-prover-credentials',
// metadata
async ({ effects }) => ({
name: i18n('Set Subspaces Prover Auth Credentials'),
description: i18n(
'Set or rotate the HTTP basic auth credentials enforced in front of the Subspaces Prover. Saving restarts the service if auth is currently enabled.',
),
warning: null,
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
// input
inputSpec,
// prefill — current username, never the password (the user explicitly types
// a new password or leaves blank to auto-generate)
async ({ effects }) => {
const subsProverAuth = await storeJson.read((s) => s.subsProverAuth).once()
return {
username: subsProverAuth?.username ?? DEFAULT_USERNAME,
password: null,
}
},
// run
async ({ effects, input }) => {
const password =
input.password && input.password.length > 0
? input.password
: randomPassword()
await storeJson.merge(effects, {
subsProverAuth: { username: input.username, password },
})
const enabled = await storeJson.read((s) => s.subsProverAuthEnabled).once()
return {
version: '1',
title: i18n('Success'),
message: enabled
? i18n(
'Subspaces Prover Auth credentials saved. The service is restarting so subs-prover picks up the new credentials.',
)
: i18n(
'Subspaces Prover Auth credentials saved. Auth is currently DISABLED — enable it with "Enable Subspaces Prover Auth" to enforce these credentials.',
),
result: null,
}
},
)
@@ -1,17 +1,17 @@
import { storeJson } from '../fileModels/storeJson' import { storeJson } from '../fileModels/storeJson'
import { i18n } from '../i18n' import { i18n } from '../i18n'
import { sdk } from '../sdk' import { sdk } from '../sdk'
import { POSTGRES_PORT } from '../utils' import { spacedRpcPort } from '../utils'
export const showDbCredentials = sdk.Action.withoutInput( export const showSpacedCredentials = sdk.Action.withoutInput(
// id // id
'show-db-credentials', 'show-spaced-credentials',
// metadata // metadata
async ({ effects }) => ({ async ({ effects }) => ({
name: i18n('Show Database Credentials'), name: i18n('Show Spaces API Credentials'),
description: i18n( description: i18n(
'Display the PostgreSQL username, password, database, and connection URL.', 'Display the spaced RPC username and password (SPACED_RPC_USER / SPACED_RPC_PASSWORD) used to authenticate against the Spaces API.',
), ),
warning: null, warning: null,
allowedStatuses: 'any', allowedStatuses: 'any',
@@ -21,26 +21,25 @@ export const showDbCredentials = sdk.Action.withoutInput(
// run // run
async ({ effects }) => { async ({ effects }) => {
const dbAuth = await storeJson.read((s) => s.dbAuth).once() const spacedAuth = await storeJson.read((s) => s.spacedAuth).once()
const username = dbAuth?.username ?? '' const username = spacedAuth?.username ?? ''
const password = dbAuth?.password ?? '' const password = spacedAuth?.password ?? ''
const database = dbAuth?.database ?? '' const url = spacedAuth
const url = dbAuth ? `http://${spacedAuth.username}:${spacedAuth.password}@127.0.0.1:${spacedRpcPort}`
? `postgres://${dbAuth.username}:${dbAuth.password}@127.0.0.1:${POSTGRES_PORT}/${dbAuth.database}`
: '' : ''
return { return {
version: '1', version: '1',
title: i18n('Show Database Credentials'), title: i18n('Show Spaces API Credentials'),
message: i18n( message: i18n(
'Use these credentials to connect to the Spaces PostgreSQL database (loopback only inside the container).', 'Use these credentials to authenticate against the spaced JSON-RPC (the Spaces API). The connection URL shown uses the loopback address; for external access, substitute the host StartOS exposes for the Spaces API interface.',
), ),
result: { result: {
type: 'group', type: 'group',
value: [ value: [
{ {
type: 'single', type: 'single',
name: i18n('Username'), name: i18n('SPACED_RPC_USER'),
description: null, description: null,
value: username, value: username,
masked: false, masked: false,
@@ -49,22 +48,13 @@ export const showDbCredentials = sdk.Action.withoutInput(
}, },
{ {
type: 'single', type: 'single',
name: i18n('Password'), name: i18n('SPACED_RPC_PASSWORD'),
description: null, description: null,
value: password, value: password,
masked: true, masked: true,
copyable: true, copyable: true,
qr: false, qr: false,
}, },
{
type: 'single',
name: i18n('Database'),
description: null,
value: database,
masked: false,
copyable: true,
qr: false,
},
{ {
type: 'single', type: 'single',
name: i18n('Connection URL'), name: i18n('Connection URL'),
+79
View File
@@ -0,0 +1,79 @@
import { storeJson } from '../fileModels/storeJson'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { SUBSPACES_UI_PORT } from '../utils'
export const showSubsCredentials = sdk.Action.withoutInput(
// id
'show-subs-credentials',
// metadata
async ({ effects }) => ({
name: i18n('Show Subspaces Auth Credentials'),
description: i18n(
'Display the HTTP basic auth username and password used in front of the Subspaces Web UI and Subs API. Returns blanks if no credentials have been set yet.',
),
warning: null,
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
// run
async ({ effects }) => {
const [subsAuth, enabled] = await Promise.all([
storeJson.read((s) => s.subsAuth).once(),
storeJson.read((s) => s.subsAuthEnabled).once(),
])
const username = subsAuth?.username ?? ''
const password = subsAuth?.password ?? ''
const status =
enabled === true
? i18n('Auth is ENABLED — these credentials are enforced.')
: i18n(
'Auth is DISABLED — subs is serving unauthenticated. These credentials will take effect when enabled.',
)
const url = subsAuth
? `http://${subsAuth.username}:${subsAuth.password}@127.0.0.1:${SUBSPACES_UI_PORT}`
: ''
return {
version: '1',
title: i18n('Show Subspaces Auth Credentials'),
message: status,
result: {
type: 'group',
value: [
{
type: 'single',
name: i18n('SUBS_BASIC_AUTH_USER'),
description: null,
value: username,
masked: false,
copyable: true,
qr: false,
},
{
type: 'single',
name: i18n('SUBS_BASIC_AUTH_PASSWORD'),
description: null,
value: password,
masked: true,
copyable: true,
qr: false,
},
{
type: 'single',
name: i18n('Connection URL'),
description: null,
value: url,
masked: true,
copyable: true,
qr: false,
},
],
},
}
},
)
@@ -0,0 +1,79 @@
import { storeJson } from '../fileModels/storeJson'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { SUBSPACES_PROVER_PORT } from '../utils'
export const showSubsProverCredentials = sdk.Action.withoutInput(
// id
'show-subs-prover-credentials',
// metadata
async ({ effects }) => ({
name: i18n('Show Subspaces Prover Auth Credentials'),
description: i18n(
'Display the HTTP basic auth username and password used in front of the Subspaces Prover. Returns blanks if no credentials have been set yet.',
),
warning: null,
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
// run
async ({ effects }) => {
const [subsProverAuth, enabled] = await Promise.all([
storeJson.read((s) => s.subsProverAuth).once(),
storeJson.read((s) => s.subsProverAuthEnabled).once(),
])
const username = subsProverAuth?.username ?? ''
const password = subsProverAuth?.password ?? ''
const status =
enabled === true
? i18n('Auth is ENABLED — these credentials are enforced.')
: i18n(
'Auth is DISABLED — subs-prover is serving unauthenticated. These credentials will take effect when enabled.',
)
const url = subsProverAuth
? `http://${subsProverAuth.username}:${subsProverAuth.password}@127.0.0.1:${SUBSPACES_PROVER_PORT}`
: ''
return {
version: '1',
title: i18n('Show Subspaces Prover Auth Credentials'),
message: status,
result: {
type: 'group',
value: [
{
type: 'single',
name: i18n('SUBS_PROVER_BASIC_AUTH_USER'),
description: null,
value: username,
masked: false,
copyable: true,
qr: false,
},
{
type: 'single',
name: i18n('SUBS_PROVER_BASIC_AUTH_PASSWORD'),
description: null,
value: password,
masked: true,
copyable: true,
qr: false,
},
{
type: 'single',
name: i18n('Connection URL'),
description: null,
value: url,
masked: true,
copyable: true,
qr: false,
},
],
},
}
},
)
+108
View File
@@ -0,0 +1,108 @@
import { readFile } from 'fs/promises'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { dataDir } from '../utils'
const { InputSpec, Value } = sdk
// Where to drop the uploaded PDF on the main volume. nacho (and any other
// daemon that mounts /data) can read it from this path.
const SUPPORT_PDF_PATH = `${dataDir}/support.pdf`
const inputSpec = InputSpec.of({
// `required: true` so the form refuses to submit without a file selected —
// works around the StartOS quirk where an empty file picker submits `{}`
// (which Value.file's nullable parser rejects).
pdf: Value.file({
name: i18n('Workshop PDF'),
description: i18n(
'PDF to write to /data/support.pdf (overwrites any existing file).',
),
warning: null,
extensions: ['.pdf'],
required: true,
}),
})
export const uploadSupportPdf = sdk.Action.withInput(
// id
'upload-support-pdf',
// metadata
async ({ effects }) => ({
name: i18n('Upload Support PDF'),
description: i18n(
'Upload a PDF that replaces /data/support.pdf on the main volume (overwrites if it exists). Used by nacho as the workshop PDF.',
),
warning: null,
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
// input
inputSpec,
// prefill — file pickers can't be prefilled
async ({ effects }) => {},
// run
async ({ effects, input }) => {
try {
const pdfBuffer = await readFile(input.pdf.path)
const res = await sdk.SubContainer.withTemp(
effects,
{ imageId: 'spaces' },
sdk.Mounts.of().mountVolume({
volumeId: 'main',
subpath: null,
mountpoint: dataDir,
readonly: false,
}),
'upload-support-pdf',
(subc) =>
subc.exec(
[
'sh',
'-c',
`cat > ${SUPPORT_PDF_PATH} && chmod 644 ${SUPPORT_PDF_PATH}`,
],
{ input: pdfBuffer, user: 'root' },
),
)
if (res.exitCode !== 0) {
return {
version: '1',
title: i18n('Failure'),
message: i18n('Could not write support PDF: ${error}', {
error:
((res.stderr ?? '').toString() ||
`exit ${res.exitCode}`).slice(0, 300),
}),
result: null,
}
}
return {
version: '1',
title: i18n('Success'),
message: i18n(
'Workshop PDF written to /data/support.pdf (${size} bytes).',
{ size: String(input.pdf.commitment.size) },
),
result: null,
}
} catch (e) {
return {
version: '1',
title: i18n('Failure'),
message: i18n('Could not write support PDF: ${error}', {
error: (e as Error)?.message ?? String(e),
}),
result: null,
}
}
},
)
+17 -9
View File
@@ -10,14 +10,6 @@ const shape = z.object({
}) })
.nullable() .nullable()
.catch(null), .catch(null),
dbAuth: z
.object({
username: z.string(),
password: z.string(),
database: z.string(),
})
.nullable()
.catch(null),
spacedAuth: z spacedAuth: z
.object({ .object({
username: z.string(), username: z.string(),
@@ -25,12 +17,28 @@ const shape = z.object({
}) })
.nullable() .nullable()
.catch(null), .catch(null),
enableExplorer: z.boolean().nullable().catch(null),
enableSubspaces: z.boolean().nullable().catch(null), enableSubspaces: z.boolean().nullable().catch(null),
enableSubsProver: z.boolean().nullable().catch(null), enableSubsProver: z.boolean().nullable().catch(null),
subsAuth: z
.object({
username: z.string(),
password: z.string(),
})
.nullable()
.catch(null),
subsAuthEnabled: z.boolean().nullable().catch(null),
subsProverAuth: z
.object({
username: z.string(),
password: z.string(),
})
.nullable()
.catch(null),
subsProverAuthEnabled: z.boolean().nullable().catch(null),
certrelaySelfUrl: z.string().nullable().catch(null), certrelaySelfUrl: z.string().nullable().catch(null),
certrelayBootstrap: z.boolean().nullable().catch(null), certrelayBootstrap: z.boolean().nullable().catch(null),
certrelayHealthcheckHandle: z.string().nullable().catch(null), certrelayHealthcheckHandle: z.string().nullable().catch(null),
nachoWorkshopPdfLinkText: z.string().nullable().catch(null),
}) })
export const storeJson = FileHelper.json( export const storeJson = FileHelper.json(
+91
View File
@@ -144,6 +144,37 @@ const dict = {
'Spaces API': 143, 'Spaces API': 143,
'JSON-RPC API served by the spaced daemon. Authenticated with the spaced RPC credentials (SPACED_RPC_USER / SPACED_RPC_PASSWORD from store.spacedAuth). spaced binds 0.0.0.0 so external processes can reach this endpoint.': 'JSON-RPC API served by the spaced daemon. Authenticated with the spaced RPC credentials (SPACED_RPC_USER / SPACED_RPC_PASSWORD from store.spacedAuth). spaced binds 0.0.0.0 so external processes can reach this endpoint.':
144, 144,
'Show Spaces API Credentials': 145,
'Display the spaced RPC username and password (SPACED_RPC_USER / SPACED_RPC_PASSWORD) used to authenticate against the Spaces API.':
146,
'Use these credentials to authenticate against the spaced JSON-RPC (the Spaces API). The connection URL shown uses the loopback address; for external access, substitute the host StartOS exposes for the Spaces API interface.':
147,
SPACED_RPC_USER: 148,
SPACED_RPC_PASSWORD: 149,
Nacho: 150,
'nacho is ready': 151,
'nacho is not ready': 152,
'Nacho Expo dev server. Configure the EXPO_PUBLIC_IGNORE_NAMES list via the "Configure Nacho" action. EXPO_PUBLIC_API_BASE_URL is derived dynamically from the Subs API StartOS interface (the .local URL is preferred).':
153,
'Configure Nacho': 154,
'Set the user-tunable nacho settings. The Ignore Names list is written to /data/nacho/ignore_names.txt (read by nacho at runtime). The Workshop PDF link text is set via EXPO_PUBLIC_WORKSHOP_PDF_LINK_TEXT. Saving restarts the service. To upload a new Workshop PDF, use the separate "Upload Support PDF" action.':
155,
'Ignore Names': 156,
'Comma-separated list of names for the nacho UI to ignore (EXPO_PUBLIC_IGNORE_NAMES).':
157,
'Nacho configuration saved. Ignore Names written to /data/nacho/ignore_names.txt; the service is restarting to apply the new values.':
158,
'Could not write ignore_names.txt: ${error}': 167,
'Workshop PDF Link Text': 159,
'Display text for the workshop PDF link in the nacho UI (EXPO_PUBLIC_WORKSHOP_PDF_LINK_TEXT). Leave empty to suppress the label.':
160,
'Workshop PDF': 161,
'PDF to write to /data/support.pdf (overwrites any existing file).': 162,
'Upload Support PDF': 163,
'Upload a PDF that replaces /data/support.pdf on the main volume (overwrites if it exists). Used by nacho as the workshop PDF.':
164,
'Could not write support PDF: ${error}': 165,
'Workshop PDF written to /data/support.pdf (${size} bytes).': 166,
// interfaces.ts // interfaces.ts
'Space-CLI Web UI': 12, 'Space-CLI Web UI': 12,
@@ -198,6 +229,66 @@ const dict = {
'Could not parse wallet JSON: ${error}': 49, 'Could not parse wallet JSON: ${error}': 49,
'Wallet imported and loaded.': 50, 'Wallet imported and loaded.': 50,
'Could not import wallet: ${error}': 51, 'Could not import wallet: ${error}': 51,
'Enable Subspaces Auth': 168,
'Turn on HTTP basic auth in front of the Subspaces Web UI and the Subs API (both on port 7777). If no credentials have been set yet, a random password is generated (username defaults to "spaces"). Use "Show Subspaces Auth Credentials" afterwards to retrieve them. Service restarts so subs picks up the auth env vars.':
169,
'Subspaces Auth enabled with the existing stored credentials. The service is restarting; use "Show Subspaces Auth Credentials" to view them.':
170,
'Subspaces Auth enabled and a fresh credential pair was generated. The service is restarting; use "Show Subspaces Auth Credentials" to view them.':
171,
'Disable Subspaces Auth': 172,
'Turn off HTTP basic auth in front of the Subspaces Web UI / Subs API. Stored credentials are preserved so re-enabling does not generate new ones; use "Set Subspaces Auth Credentials" to rotate. Service restarts.':
173,
'Subspaces Auth disabled. The service is restarting; subs will serve unauthenticated again on port 7777. Stored credentials are kept for the next enable.':
174,
'Show Subspaces Auth Credentials': 175,
'Display the HTTP basic auth username and password used in front of the Subspaces Web UI and Subs API. Returns blanks if no credentials have been set yet.':
176,
'Auth is ENABLED — these credentials are enforced.': 177,
'Auth is DISABLED — subs is serving unauthenticated. These credentials will take effect when enabled.':
178,
SUBS_BASIC_AUTH_USER: 179,
SUBS_BASIC_AUTH_PASSWORD: 180,
'Set Subspaces Auth Credentials': 181,
'Username for HTTP basic auth in front of subs (SUBS_BASIC_AUTH_USER).': 183,
'Password for HTTP basic auth in front of subs (SUBS_BASIC_AUTH_PASSWORD). Leave blank to auto-generate a random one.':
185,
'Set or rotate the HTTP basic auth credentials enforced in front of the Subspaces Web UI and Subs API. Auth is enabled automatically. Saving restarts the service.':
186,
'Subspaces Auth credentials saved. The service is restarting so subs picks up the new credentials.':
187,
'Subspaces Auth credentials saved. Auth is currently DISABLED — enable it with "Enable Subspaces Auth" to enforce these credentials.':
188,
'Enable Subspaces Prover Auth': 189,
'Turn on HTTP basic auth in front of the Subspaces Prover (port 8888). If no credentials have been set yet, a random password is generated (username defaults to "spaces"). Use "Show Subspaces Prover Auth Credentials" afterwards to retrieve them. Service restarts so subs-prover picks up the auth env vars.':
190,
'Subspaces Prover Auth enabled with the existing stored credentials. The service is restarting; use "Show Subspaces Prover Auth Credentials" to view them.':
191,
'Subspaces Prover Auth enabled and a fresh credential pair was generated. The service is restarting; use "Show Subspaces Prover Auth Credentials" to view them.':
192,
'Disable Subspaces Prover Auth': 193,
'Turn off HTTP basic auth in front of the Subspaces Prover. Stored credentials are preserved so re-enabling does not generate new ones; use "Set Subspaces Prover Auth Credentials" to rotate. Service restarts.':
194,
'Subspaces Prover Auth disabled. The service is restarting; subs-prover will serve unauthenticated again on port 8888. Stored credentials are kept for the next enable.':
195,
'Show Subspaces Prover Auth Credentials': 196,
'Display the HTTP basic auth username and password used in front of the Subspaces Prover. Returns blanks if no credentials have been set yet.':
197,
'Auth is DISABLED — subs-prover is serving unauthenticated. These credentials will take effect when enabled.':
198,
SUBS_PROVER_BASIC_AUTH_USER: 199,
SUBS_PROVER_BASIC_AUTH_PASSWORD: 200,
'Set Subspaces Prover Auth Credentials': 201,
'Username for HTTP basic auth in front of subs-prover (SUBS_PROVER_BASIC_AUTH_USER).':
202,
'Password for HTTP basic auth in front of subs-prover (SUBS_PROVER_BASIC_AUTH_PASSWORD). Leave blank to auto-generate a random one.':
203,
'Set or rotate the HTTP basic auth credentials enforced in front of the Subspaces Prover. Saving restarts the service if auth is currently enabled.':
204,
'Subspaces Prover Auth credentials saved. The service is restarting so subs-prover picks up the new credentials.':
205,
'Subspaces Prover Auth credentials saved. Auth is currently DISABLED — enable it with "Enable Subspaces Prover Auth" to enforce these credentials.':
206,
} as const } as const
/** /**
+2 -4
View File
@@ -6,9 +6,8 @@ import { sdk } from '../sdk'
import { versionGraph } from '../versions' import { versionGraph } from '../versions'
import { taskBtcAuth } from './taskBtcAuth' import { taskBtcAuth } from './taskBtcAuth'
import { taskSeedCertrelay } from './taskSeedCertrelay' import { taskSeedCertrelay } from './taskSeedCertrelay'
import { taskSeedDb } from './taskSeedDb'
import { taskSeedEnableExplorer } from './taskSeedEnableExplorer'
import { taskSeedEnableSubspaces } from './taskSeedEnableSubspaces' import { taskSeedEnableSubspaces } from './taskSeedEnableSubspaces'
import { taskSeedNacho } from './taskSeedNacho'
import { taskSeedSpacedAuth } from './taskSeedSpacedAuth' import { taskSeedSpacedAuth } from './taskSeedSpacedAuth'
import { taskSetPassword } from './taskSetPassword' import { taskSetPassword } from './taskSetPassword'
@@ -19,11 +18,10 @@ export const init = sdk.setupInit(
setDependencies, setDependencies,
actions, actions,
taskBtcAuth, taskBtcAuth,
taskSeedDb,
taskSeedSpacedAuth, taskSeedSpacedAuth,
taskSeedEnableExplorer,
taskSeedEnableSubspaces, taskSeedEnableSubspaces,
taskSeedCertrelay, taskSeedCertrelay,
taskSeedNacho,
taskSetPassword, taskSetPassword,
) )
-20
View File
@@ -1,20 +0,0 @@
import { storeJson } from '../fileModels/storeJson'
import { sdk } from '../sdk'
import { POSTGRES_DB, POSTGRES_USER, randomPassword } from '../utils'
export const taskSeedDb = sdk.setupOnInit(async (effects) => {
const existing = await storeJson.read((s) => s.dbAuth).once()
if (existing) return
await storeJson.merge(
effects,
{
dbAuth: {
username: POSTGRES_USER,
password: randomPassword(),
database: POSTGRES_DB,
},
},
{ allowWriteAfterConst: true },
)
})
-13
View File
@@ -1,13 +0,0 @@
import { storeJson } from '../fileModels/storeJson'
import { sdk } from '../sdk'
export const taskSeedEnableExplorer = sdk.setupOnInit(async (effects) => {
const existing = await storeJson.read((s) => s.enableExplorer).once()
if (existing !== null && existing !== undefined) return
await storeJson.merge(
effects,
{ enableExplorer: false },
{ allowWriteAfterConst: true },
)
})
+21
View File
@@ -0,0 +1,21 @@
import { storeJson } from '../fileModels/storeJson'
import { sdk } from '../sdk'
import { NACHO_DEFAULT_WORKSHOP_PDF_LINK_TEXT } from '../utils'
// Note: the nacho Ignore Names list is NOT seeded here. It lives in
// /data/nacho/ignore_names.txt and is created with the default on first
// daemon start by the nacho-setup oneshot in main.ts.
export const taskSeedNacho = sdk.setupOnInit(async (effects) => {
const store = await storeJson.read().once()
if (
store?.nachoWorkshopPdfLinkText === null ||
store?.nachoWorkshopPdfLinkText === undefined
) {
await storeJson.merge(
effects,
{ nachoWorkshopPdfLinkText: NACHO_DEFAULT_WORKSHOP_PDF_LINK_TEXT },
{ allowWriteAfterConst: true },
)
}
})
+22 -22
View File
@@ -2,7 +2,7 @@ import { i18n } from './i18n'
import { sdk } from './sdk' import { sdk } from './sdk'
import { import {
CERTRELAY_PORT, CERTRELAY_PORT,
EXPLORER_PORT, NACHO_PORT,
spacedRpcPort, spacedRpcPort,
SUBSPACES_PROVER_PORT, SUBSPACES_PROVER_PORT,
SUBSPACES_REGISTRY_PORT, SUBSPACES_REGISTRY_PORT,
@@ -31,26 +31,6 @@ export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => {
const uiReceipt = await uiMultiOrigin.export([ui]) const uiReceipt = await uiMultiOrigin.export([ui])
const explorerMulti = sdk.MultiHost.of(effects, 'explorer-multi')
const explorerMultiOrigin = await explorerMulti.bindPort(EXPLORER_PORT, {
protocol: 'http',
})
const explorer = sdk.createInterface(effects, {
name: i18n('Explorer Web UI'),
id: 'explorer',
description: i18n(
'SvelteKit explorer for the Spaces protocol. Reads from the embedded PostgreSQL populated by the indexer. Only useful while the embedded explorer is enabled.',
),
type: 'ui',
masked: false,
schemeOverride: null,
username: null,
path: '',
query: {},
})
const explorerReceipt = await explorerMultiOrigin.export([explorer])
const subspacesMulti = sdk.MultiHost.of(effects, 'subspaces-multi') const subspacesMulti = sdk.MultiHost.of(effects, 'subspaces-multi')
const subspacesMultiOrigin = await subspacesMulti.bindPort(SUBSPACES_UI_PORT, { const subspacesMultiOrigin = await subspacesMulti.bindPort(SUBSPACES_UI_PORT, {
protocol: 'http', protocol: 'http',
@@ -172,13 +152,33 @@ export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => {
const spacedApiReceipt = await spacedMultiOrigin.export([spacedApi]) const spacedApiReceipt = await spacedMultiOrigin.export([spacedApi])
const nachoMulti = sdk.MultiHost.of(effects, 'nacho-multi')
const nachoMultiOrigin = await nachoMulti.bindPort(NACHO_PORT, {
protocol: 'http',
})
const nacho = sdk.createInterface(effects, {
name: i18n('Nacho'),
id: 'nacho',
description: i18n(
'Nacho Expo dev server. Configure the EXPO_PUBLIC_IGNORE_NAMES list via the "Configure Nacho" action. EXPO_PUBLIC_API_BASE_URL is derived dynamically from the Subs API StartOS interface (the .local URL is preferred).',
),
type: 'ui',
masked: false,
schemeOverride: null,
username: null,
path: '',
query: {},
})
const nachoReceipt = await nachoMultiOrigin.export([nacho])
return [ return [
uiReceipt, uiReceipt,
explorerReceipt,
subspacesReceipt, subspacesReceipt,
proverReceipt, proverReceipt,
registryReceipt, registryReceipt,
certrelayReceipt, certrelayReceipt,
spacedApiReceipt, spacedApiReceipt,
nachoReceipt,
] ]
}) })
+137 -266
View File
@@ -19,8 +19,12 @@ import {
CERTRELAY_PORT, CERTRELAY_PORT,
CERTRELAY_REMOTE_IP_HEADER, CERTRELAY_REMOTE_IP_HEADER,
dataDir, dataDir,
pgDataDir, NACHO_DEFAULT_IGNORE_NAMES,
POSTGRES_PORT, NACHO_DEFAULT_WORKSHOP_PDF_LINK_TEXT,
NACHO_DIR,
NACHO_FALLBACK_API_BASE_URL,
NACHO_PORT,
renderBanner,
SPACED_CHAIN, SPACED_CHAIN,
spacedRpcPort, spacedRpcPort,
SUBSPACES_DATA_DIR, SUBSPACES_DATA_DIR,
@@ -44,17 +48,15 @@ export const main = sdk.setupMain(async ({ effects }) => {
if ( if (
!store?.password || !store?.password ||
!store?.btcAuth || !store?.btcAuth ||
!store?.dbAuth ||
!store?.spacedAuth !store?.spacedAuth
) { ) {
// taskSetPassword + taskBtcAuth + taskSeedDb + taskSeedSpacedAuth all seed // taskSetPassword + taskBtcAuth + taskSeedSpacedAuth all seed these in
// these in init; if they aren't populated yet, init hasn't finished — let // init; if they aren't populated yet, init hasn't finished — let StartOS
// StartOS restart us. // restart us.
throw new Error('Spaces store.json is not yet populated.') throw new Error('Spaces store.json is not yet populated.')
} }
const { password: APP_PASSWORD, btcAuth, dbAuth, spacedAuth } = store const { password: APP_PASSWORD, btcAuth, spacedAuth } = store
const enableExplorer = store.enableExplorer === true
const enableSubspaces = store.enableSubspaces === true const enableSubspaces = store.enableSubspaces === true
// subs-prover is independently gated and defaults OFF (null/undefined => false). // subs-prover is independently gated and defaults OFF (null/undefined => false).
const enableSubsProver = store.enableSubsProver === true const enableSubsProver = store.enableSubsProver === true
@@ -81,7 +83,6 @@ export const main = sdk.setupMain(async ({ effects }) => {
BTC_RPC_PASSWORD: btcAuth.password, BTC_RPC_PASSWORD: btcAuth.password,
APP_USER, APP_USER,
APP_PASSWORD, APP_PASSWORD,
DB_URL: `postgres://${dbAuth.username}:${dbAuth.password}@127.0.0.1:${POSTGRES_PORT}/${dbAuth.database}`,
} }
// Certrelay config — user-tunable bits live in store.json (set via the // Certrelay config — user-tunable bits live in store.json (set via the
@@ -92,6 +93,8 @@ export const main = sdk.setupMain(async ({ effects }) => {
store.certrelayBootstrap ?? CERTRELAY_DEFAULT_BOOTSTRAP store.certrelayBootstrap ?? CERTRELAY_DEFAULT_BOOTSTRAP
const certrelayHealthcheckHandle = const certrelayHealthcheckHandle =
store.certrelayHealthcheckHandle ?? CERTRELAY_DEFAULT_HEALTHCHECK_HANDLE store.certrelayHealthcheckHandle ?? CERTRELAY_DEFAULT_HEALTHCHECK_HANDLE
const nachoWorkshopPdfLinkText =
store.nachoWorkshopPdfLinkText ?? NACHO_DEFAULT_WORKSHOP_PDF_LINK_TEXT
const mounts = sdk.Mounts.of().mountVolume({ const mounts = sdk.Mounts.of().mountVolume({
volumeId: 'main', volumeId: 'main',
@@ -114,36 +117,6 @@ export const main = sdk.setupMain(async ({ effects }) => {
'terminal-sub', 'terminal-sub',
) )
const postgresSub = await sdk.SubContainer.of(
effects,
{ imageId: 'postgres' },
mounts,
'postgres-sub',
)
const postgresEnv = {
POSTGRES_USER: dbAuth.username,
POSTGRES_PASSWORD: dbAuth.password,
POSTGRES_DB: dbAuth.database,
PGDATA: pgDataDir,
}
// TODO(prebuilt-indexer / prebuilt-explorer): the Go indexer and SvelteKit
// explorer used to be built from source in dedicated builder images
// (golang:1.23-alpine, node:20-alpine). Those images + their build chains
// were removed to shrink the .s9pk and cut first-enable latency. When the
// prebuilt images exist, re-add their SubContainers + env here, e.g.:
// const indexerSub = await sdk.SubContainer.of(
// effects, { imageId: '<prebuilt-indexer-image>' }, mounts, 'indexer-sub')
// const explorerSub = await sdk.SubContainer.of(
// effects, { imageId: '<prebuilt-explorer-image>' }, mounts, 'explorer-sub')
// const indexerEnv = { POSTGRES_URI, BITCOIN_NODE_URI/USER/PASSWORD,
// SPACES_NODE_URI, RPC_USER/PASSWORD, ACTIVATION_BLOCK_HEIGHT,
// FAST_SYNC_BLOCK_HEIGHT, UPDATE_DB_INTERVAL, MEMPOOL_CHUNK_SIZE }
// const explorerEnv = { DB_URL (?sslmode=disable), PUBLIC_BTC_NETWORK, PORT }
// Also add the image ids to startos/manifest/index.ts and re-add the daemon
// definitions in the explorer branch below. See git history for prior shapes.
const subspacesSub = await sdk.SubContainer.of( const subspacesSub = await sdk.SubContainer.of(
effects, effects,
{ imageId: 'subspaces' }, { imageId: 'subspaces' },
@@ -151,6 +124,19 @@ export const main = sdk.setupMain(async ({ effects }) => {
'subspaces-sub', 'subspaces-sub',
) )
// Optional HTTP basic auth in front of subs (Web UI + API on 7777). Only
// injected when both a stored credential pair and the enable toggle are set,
// so flipping the toggle off (or clearing creds) leaves subs unauth.
// NOTE: env var names match subs/src/main.rs clap #[arg(env=...)]:
// SUBS_BASIC_AUTH_USER and SUBS_BASIC_AUTH_PASSWORD.
const subsAuthActive = store.subsAuthEnabled === true && store.subsAuth != null
const subsAuthEnv = subsAuthActive
? {
SUBS_BASIC_AUTH_USER: store.subsAuth!.username,
SUBS_BASIC_AUTH_PASSWORD: store.subsAuth!.password,
}
: {}
// subs reads all of these from the environment (see subs/src/main.rs #[arg(env=...)]). // subs reads all of these from the environment (see subs/src/main.rs #[arg(env=...)]).
const subsEnv = { const subsEnv = {
SUBS_DATA_DIR: SUBSPACES_DATA_DIR, SUBS_DATA_DIR: SUBSPACES_DATA_DIR,
@@ -163,13 +149,28 @@ export const main = sdk.setupMain(async ({ effects }) => {
SUBS_REGISTRY_ENDPOINT: `http://127.0.0.1:${SUBSPACES_REGISTRY_PORT}`, SUBS_REGISTRY_ENDPOINT: `http://127.0.0.1:${SUBSPACES_REGISTRY_PORT}`,
HOME: SUBSPACES_DATA_DIR, HOME: SUBSPACES_DATA_DIR,
RUST_LOG: 'subs=info,subs_prover=info,registry_server=info', RUST_LOG: 'subs=info,subs_prover=info,registry_server=info',
...subsAuthEnv,
} }
// Optional HTTP basic auth in front of subs-prover (port 8888). Same wiring
// pattern as subsAuthEnv above: only injected when both the toggle and
// credentials are set. Env var names assume subs-prover reads
// SUBS_PROVER_BASIC_AUTH_USER and SUBS_PROVER_BASIC_AUTH_PASSWORD.
const subsProverAuthActive =
store.subsProverAuthEnabled === true && store.subsProverAuth != null
const subsProverAuthEnv = subsProverAuthActive
? {
SUBS_PROVER_BASIC_AUTH_USER: store.subsProverAuth!.username,
SUBS_PROVER_BASIC_AUTH_PASSWORD: store.subsProverAuth!.password,
}
: {}
const proverEnv = { const proverEnv = {
SUBS_PROVER_SERVER: '1', SUBS_PROVER_SERVER: '1',
SUBS_PROVER_PORT: String(SUBSPACES_PROVER_PORT), SUBS_PROVER_PORT: String(SUBSPACES_PROVER_PORT),
HOME: SUBSPACES_PROVER_DIR, HOME: SUBSPACES_PROVER_DIR,
RUST_LOG: 'subs_prover=info', RUST_LOG: 'subs_prover=info',
...subsProverAuthEnv,
} }
const registryEnv = { const registryEnv = {
@@ -201,19 +202,63 @@ export const main = sdk.setupMain(async ({ effects }) => {
RUST_LOG: 'info', RUST_LOG: 'info',
} }
const nachoSub = await sdk.SubContainer.of(
effects,
{ imageId: 'nacho' },
mounts,
'nacho-sub',
)
// Derive EXPO_PUBLIC_API_BASE_URL from the subs-api StartOS interface so it
// tracks the actual host:port StartOS exposes (instead of a hardcoded
// domain). Prefer the mdns/.local URL — matches how a LAN browser reaches
// the device — then fall back to any non-local URL, then loopback.
// `.const(effects)` makes this reactive: if the interface address changes
// (clearnet enabled, Tor added, etc.), the service restarts and nacho
// picks up the new URL.
const subsApiIf = await sdk.serviceInterface
.getOwn(effects, 'subs-api')
.const()
const subsApiCandidateUrls = subsApiIf?.addressInfo
? [
...subsApiIf.addressInfo.filter({ kind: 'mdns' }).format('urlstring'),
...subsApiIf.addressInfo.nonLocal.format('urlstring'),
]
: []
const nachoApiBaseUrl =
subsApiCandidateUrls[0] ?? NACHO_FALLBACK_API_BASE_URL
// nacho is an Expo app; the image's entrypoint runs `expo start` on
// EXPO_DEV_PORT (8082). EXPO_PUBLIC_* env vars are baked into the web bundle
// at server time. The ignore list is NOT passed via env — nacho reads it at
// runtime from /data/nacho/ignore_names.txt (managed by the "Configure Nacho"
// action). nacho-setup seeds that file with the default if it's missing.
const nachoEnv = {
EXPO_PUBLIC_API_BASE_URL: nachoApiBaseUrl,
EXPO_PUBLIC_WORKSHOP_PDF_LINK_TEXT: nachoWorkshopPdfLinkText,
EXPO_DEV_PORT: String(NACHO_PORT),
NODE_ENV: 'development',
CHOKIDAR_USEPOLLING: '1',
HOME: NACHO_DIR,
}
const bashrc = [ const bashrc = [
'export PATH=/root/.cargo/bin:/data/bin:/usr/local/bin:/usr/bin:/bin', 'export PATH=/root/.cargo/bin:/data/bin:/usr/local/bin:/usr/bin:/bin',
"export PS1='spaces:\\w$ '", "export PS1='spaces:\\w$ '",
`alias spaces='space-cli --chain ${SPACED_CHAIN} --rpc-user "$SPACED_RPC_USER" --rpc-password "$SPACED_RPC_PASSWORD" '`, `alias spaces='space-cli --chain ${SPACED_CHAIN} --rpc-user "$SPACED_RPC_USER" --rpc-password "$SPACED_RPC_PASSWORD" '`,
'cat <<EOF', 'cat <<EOF',
'', '',
'┌─ Spaces ─────────────────────────────────────────────────┐', // Banner rendered via renderBanner() — auto-wraps any line that exceeds the
'│ spaced is managed by StartOS — do not run it manually. │', // 96-cell text budget and pads every emitted line to exactly 100 cells, so
'│ Use the `spaces` alias to call space-cli, e.g. │', // the box always renders cleanly in a monospaced terminal regardless of
'│ spaces getserverinfo │', // edits below.
'│ `fabric` resolves handles against the local certrelay. │', ...renderBanner('Spaces', [
'│ Docs: https://docs.spacesprotocol.org/ │', 'spaced is managed by StartOS — do not run it manually.',
'└──────────────────────────────────────────────────────────┘', 'Use the `spaces` alias to call space-cli, e.g.',
' spaces getserverinfo',
'`fabric` resolves handles against the local certrelay.',
'Docs: https://docs.spacesprotocol.org/',
]),
'', '',
'EOF', 'EOF',
].join('\n') ].join('\n')
@@ -264,6 +309,48 @@ export const main = sdk.setupMain(async ({ effects }) => {
requires: ['certrelay-setup', 'spaced'], requires: ['certrelay-setup', 'spaced'],
}) })
// Appends the always-on nacho chain: setup oneshot (mkdir data dir) + nacho
// daemon (the Expo dev server on 8082). Runs the image's own entrypoint so
// we don't have to reproduce its Expo init logic.
const withNacho = (chain: any): any =>
chain
.addOneshot('nacho-setup', {
subcontainer: nachoSub,
exec: {
command: [
'sh',
'-c',
`set -eu; mkdir -p ${NACHO_DIR}; if [ ! -f ${NACHO_DIR}/ignore_names.txt ]; then printf %s '${NACHO_DEFAULT_IGNORE_NAMES}' > ${NACHO_DIR}/ignore_names.txt; fi; echo "nacho-setup: ensured ${NACHO_DIR} and ignore_names.txt"`,
],
user: 'root',
},
requires: [],
})
.addDaemon('nacho', {
subcontainer: nachoSub,
exec: {
// The image's entrypoint runs Expo on EXPO_DEV_PORT (8082) and
// ignores CMD. Invoke it directly.
command: ['sh', '/usr/local/bin/entrypoint.sh'],
env: nachoEnv,
// The image declares no USER; default would be root inside StartOS,
// but be explicit so /data writes are unambiguous.
user: 'root',
},
ready: {
display: i18n('Nacho'),
fn: () =>
sdk.healthCheck.checkPortListening(effects, NACHO_PORT, {
successMessage: i18n('nacho is ready'),
errorMessage: i18n('nacho is not ready'),
}),
// Expo + Metro bundler can take a while to come up on first start.
gracePeriod: 120_000,
trigger: sdk.trigger.cooldownTrigger(30_000),
},
requires: ['nacho-setup'],
})
// Appends the Subspaces chain (fetch → build → wallet ensure → subs-prover → // Appends the Subspaces chain (fetch → build → wallet ensure → subs-prover →
// subs daemon) to any existing chain. `as any` because the chain's TS type // subs daemon) to any existing chain. `as any` because the chain's TS type
// depends on prior IDs and we can't easily express the union here. // depends on prior IDs and we can't easily express the union here.
@@ -366,162 +453,6 @@ export const main = sdk.setupMain(async ({ effects }) => {
return c return c
} }
if (!enableExplorer) {
// Spaces-only mode: spaced + gotty terminal. No PostgreSQL, no indexer.
// User can flip the toggle via the Enable Embedded Explorer action;
// store.json.enableExplorer is read via .const() so the merge triggers
// an automatic service restart and the full chain takes over.
let chain: any = sdk.Daemons.of(effects)
.addOneshot('bashrc', {
subcontainer: termSub,
exec: {
command: ['bash', '-c', `cat > /root/.bashrc <<'SPACES_BASHRC_EOF'
${bashrc}
SPACES_BASHRC_EOF`],
user: 'root',
},
requires: [],
})
.addDaemon('spaced', {
subcontainer: spacedSub,
exec: {
command: ['/root/.cargo/bin/spaced'],
env: spacedEnv,
},
ready: {
display: i18n('Spaced RPC'),
fn: () =>
sdk.healthCheck.checkPortListening(effects, spacedRpcPort, {
successMessage: i18n('spaced RPC is ready'),
errorMessage: i18n('spaced RPC is not ready'),
}),
gracePeriod: 120_000,
},
requires: [],
})
.addDaemon('web-terminal', {
subcontainer: termSub,
exec: {
command: [
'gotty',
'--port',
String(uiPort),
'-c',
`${APP_USER}:${APP_PASSWORD}`,
'--permit-write',
'--reconnect',
'/bin/bash',
],
env: spacedEnv,
},
ready: {
display: i18n('Web Interface'),
fn: () =>
sdk.healthCheck.checkPortListening(effects, uiPort, {
successMessage: i18n('The web terminal is ready'),
errorMessage: i18n('The web terminal is not ready'),
}),
},
requires: ['bashrc'],
})
.addHealthCheck('sync', {
ready: {
display: i18n('Spaced Sync'),
fn: async () => {
try {
const probe = await spacedSub.exec(
[
'/root/.cargo/bin/space-cli',
'--chain',
SPACED_CHAIN,
'--rpc-user',
spacedAuth.username,
'--rpc-password',
spacedAuth.password,
'--output-format',
'json',
'getserverinfo',
],
{},
)
const stderr = (probe.stderr ?? '').toString().trim()
const stdoutText = (probe.stdout ?? '').toString()
const stdoutTrimmed = stdoutText.trim()
if (probe.exitCode !== 0) {
return {
result: 'failure',
message: i18n('space-cli exited ${code}: ${error}', {
code: String(probe.exitCode),
error: (stderr || stdoutTrimmed || '<no output>').slice(0, 200),
}),
}
}
let parsed: {
ready?: boolean
progress?: number
chain?: { blocks?: number; headers?: number }
}
try {
parsed = JSON.parse(stdoutText)
} catch {
return {
result: 'failure',
message: i18n(
'getserverinfo non-JSON. stdout=${stdout} stderr=${stderr}',
{
stdout: (stdoutTrimmed || '<empty>').slice(0, 160),
stderr: (stderr || '<empty>').slice(0, 160),
},
),
}
}
const progress = Math.min(
100,
Math.max(0, Math.round((parsed.progress ?? 0) * 100)),
)
const blocks = parsed.chain?.blocks ?? 0
const headers = parsed.chain?.headers ?? 0
if (parsed.ready === true && progress >= 100) {
return {
result: 'success',
message: i18n(
'spaced is fully synced (blocks ${blocks} / headers ${headers}).',
{ blocks: String(blocks), headers: String(headers) },
),
}
}
return {
result: 'loading',
message: i18n(
'spaced is indexing: ${pct}% (blocks ${blocks} / headers ${headers}).',
{
pct: String(progress),
blocks: String(blocks),
headers: String(headers),
},
),
}
} catch (e) {
return {
result: 'failure',
message: i18n('Spaced Sync health check crashed: ${error}', {
error: (e as Error)?.message ?? String(e),
}),
}
}
},
gracePeriod: 30_000,
trigger: sdk.trigger.cooldownTrigger(30_000),
},
requires: ['spaced'],
})
chain = withCertrelay(chain)
if (enableSubspaces) chain = withSubspaces(chain)
return chain
}
// enableExplorer = true: full chain — spaced + terminal + embedded
// PostgreSQL + Go indexer + indexer-sync health check.
let chain: any = sdk.Daemons.of(effects) let chain: any = sdk.Daemons.of(effects)
.addOneshot('bashrc', { .addOneshot('bashrc', {
subcontainer: termSub, subcontainer: termSub,
@@ -533,40 +464,6 @@ SPACES_BASHRC_EOF`],
}, },
requires: [], requires: [],
}) })
.addOneshot('postgres-chown', {
subcontainer: postgresSub,
exec: {
command: [
'bash',
'-c',
`mkdir -p ${pgDataDir} && chown -R postgres:postgres ${pgDataDir} && chmod 700 ${pgDataDir}`,
],
user: 'root',
},
requires: [],
})
.addDaemon('postgres', {
subcontainer: postgresSub,
exec: {
command: [
'docker-entrypoint.sh',
'postgres',
'-c',
'listen_addresses=127.0.0.1',
],
env: postgresEnv,
},
ready: {
display: i18n('Database'),
fn: () =>
sdk.healthCheck.checkPortListening(effects, POSTGRES_PORT, {
successMessage: i18n('postgres is ready'),
errorMessage: i18n('postgres is not ready'),
}),
gracePeriod: 60_000,
},
requires: ['postgres-chown'],
})
.addDaemon('spaced', { .addDaemon('spaced', {
subcontainer: spacedSub, subcontainer: spacedSub,
exec: { exec: {
@@ -584,22 +481,6 @@ SPACES_BASHRC_EOF`],
}, },
requires: [], requires: [],
}) })
// TODO(prebuilt-indexer): the Go indexer build-from-source chain
// (indexer-fetch + indexer-build + indexer-cleanup-legacy +
// indexer-migrate + `indexer` daemon) was removed to shrink the .s9pk
// and cut first-enable latency. Re-add an `indexer` daemon here that runs
// the prebuilt `sync` binary from the future indexer image (image id +
// binary path TBD) against PostgreSQL + spaced. Env it needs is the old
// indexerEnv (POSTGRES_URI, BITCOIN_NODE_*, SPACES_NODE_URI, RPC_USER/
// PASSWORD, ACTIVATION_BLOCK_HEIGHT, FAST_SYNC_BLOCK_HEIGHT,
// UPDATE_DB_INTERVAL, MEMPOOL_CHUNK_SIZE). A goose migration step must
// run before it. See git history (build-from-source) for the prior shape.
//
// TODO(prebuilt-explorer): the SvelteKit explorer build chain
// (explorer-fetch + explorer-install + `explorer-ui` daemon) was removed
// for the same reason. Re-add an `explorer-ui` daemon that runs the
// prebuilt explorer server from the provided explorer image (image id +
// command TBD) on port 3000, reading DB_URL + PUBLIC_BTC_NETWORK.
.addDaemon('web-terminal', { .addDaemon('web-terminal', {
subcontainer: termSub, subcontainer: termSub,
exec: { exec: {
@@ -645,11 +526,9 @@ SPACES_BASHRC_EOF`],
], ],
{}, {},
) )
const stderr = (probe.stderr ?? '').toString().trim() const stderr = (probe.stderr ?? '').toString().trim()
const stdoutText = (probe.stdout ?? '').toString() const stdoutText = (probe.stdout ?? '').toString()
const stdoutTrimmed = stdoutText.trim() const stdoutTrimmed = stdoutText.trim()
if (probe.exitCode !== 0) { if (probe.exitCode !== 0) {
return { return {
result: 'failure', result: 'failure',
@@ -659,7 +538,6 @@ SPACES_BASHRC_EOF`],
}), }),
} }
} }
let parsed: { let parsed: {
ready?: boolean ready?: boolean
progress?: number progress?: number
@@ -679,14 +557,12 @@ SPACES_BASHRC_EOF`],
), ),
} }
} }
const progress = Math.min( const progress = Math.min(
100, 100,
Math.max(0, Math.round((parsed.progress ?? 0) * 100)), Math.max(0, Math.round((parsed.progress ?? 0) * 100)),
) )
const blocks = parsed.chain?.blocks ?? 0 const blocks = parsed.chain?.blocks ?? 0
const headers = parsed.chain?.headers ?? 0 const headers = parsed.chain?.headers ?? 0
if (parsed.ready === true && progress >= 100) { if (parsed.ready === true && progress >= 100) {
return { return {
result: 'success', result: 'success',
@@ -696,7 +572,6 @@ SPACES_BASHRC_EOF`],
), ),
} }
} }
return { return {
result: 'loading', result: 'loading',
message: i18n( message: i18n(
@@ -711,10 +586,9 @@ SPACES_BASHRC_EOF`],
} catch (e) { } catch (e) {
return { return {
result: 'failure', result: 'failure',
message: i18n( message: i18n('Spaced Sync health check crashed: ${error}', {
'Spaced Sync health check crashed: ${error}', error: (e as Error)?.message ?? String(e),
{ error: (e as Error)?.message ?? String(e) }, }),
),
} }
} }
}, },
@@ -723,11 +597,8 @@ SPACES_BASHRC_EOF`],
}, },
requires: ['spaced'], requires: ['spaced'],
}) })
// TODO(prebuilt-indexer): re-add the `indexer-sync` standalone health
// check once the prebuilt indexer daemon is wired back in. It queried
// MAX(blocks.height) (non-orphan) via psql and compared against spaced's
// tip from `space-cli getserverinfo`. See git history for the prior body.
chain = withCertrelay(chain) chain = withCertrelay(chain)
chain = withNacho(chain)
if (enableSubspaces) chain = withSubspaces(chain) if (enableSubspaces) chain = withSubspaces(chain)
return chain return chain
}) })
+5 -14
View File
@@ -17,27 +17,18 @@ export const manifest = setupManifest({
source: { dockerTag: 'horologger/spaces:v0.0.9s' }, source: { dockerTag: 'horologger/spaces:v0.0.9s' },
arch: ['x86_64', 'aarch64'], arch: ['x86_64', 'aarch64'],
}, },
postgres: {
source: { dockerTag: 'postgres:16.3' },
arch: ['x86_64', 'aarch64'],
},
// TODO(prebuilt-indexer): add the prebuilt explorer-indexer image here once
// it's produced, e.g. 'indexer-go': { source: { dockerTag:
// 'horologger/explorer-indexer:<tag>' }, arch: ['x86_64','aarch64'] }.
// The golang:1.23-alpine builder image + build-from-source chain were
// removed to shrink the .s9pk and cut first-enable latency.
//
// TODO(prebuilt-explorer): add the prebuilt explorer (SvelteKit) image here,
// replacing the node:20-alpine builder, e.g. 'explorer-ui': { source: {
// dockerTag: 'horologger/explorer:<tag>' }, arch: ['x86_64','aarch64'] }.
subspaces: { subspaces: {
source: { dockerTag: 'horologger/subs:v0.1.0' }, source: { dockerTag: 'horologger/subs:v0.1.2' },
arch: ['x86_64', 'aarch64'], arch: ['x86_64', 'aarch64'],
}, },
certrelay: { certrelay: {
source: { dockerTag: 'horologger/certrelay:v0.2.3' }, source: { dockerTag: 'horologger/certrelay:v0.2.3' },
arch: ['x86_64', 'aarch64'], arch: ['x86_64', 'aarch64'],
}, },
nacho: {
source: { dockerTag: 'horologger/nacho:v1.0.0' },
arch: ['x86_64', 'aarch64'],
},
}, },
alerts: { alerts: {
install: null, install: null,
+109 -36
View File
@@ -16,42 +16,6 @@ export const BITCOIND_RPC_PORT = 8332
export const SPACED_CHAIN = 'mainnet' export const SPACED_CHAIN = 'mainnet'
export const POSTGRES_PORT = 5432
export const POSTGRES_USER = 'postgres'
export const POSTGRES_DB = 'spacesprotocol_explorer'
export const pgDataDir = '/data/postgres'
export const INDEXER_REPO = 'spacesprotocol/explorer-indexer'
export const INDEXER_GIT_SHA = '00ae1e548734d93f1a8bb9f48d2290f459e12b35'
// Bump the suffix to force a full re-fetch + rebuild of /data/explorer-indexer.
export const INDEXER_BUILD_ID = `${INDEXER_GIT_SHA}-g3`
export const INDEXER_TARBALL_URL = `https://github.com/${INDEXER_REPO}/archive/${INDEXER_GIT_SHA}.tar.gz`
export const INDEXER_DIR = '/data/explorer-indexer'
export const INDEXER_BIN_DIR = '/data/explorer-indexer/bin'
export const INDEXER_SYNC_BIN = '/data/explorer-indexer/bin/sync'
export const INDEXER_GOOSE_BIN = '/data/explorer-indexer/bin/goose'
export const INDEXER_SCHEMA_DIR = '/data/explorer-indexer/sql/schema'
export const INDEXER_MARKER = '/data/explorer-indexer/.installed-sha'
// Spaces protocol mainnet activation block (per spacesprotocol/explorer-indexer
// env.example). Indexer skips fast-sync below FAST_SYNC and starts indexing
// spaces data at ACTIVATION.
export const INDEXER_ACTIVATION_HEIGHT = '871222'
export const INDEXER_FAST_SYNC_HEIGHT = '864000'
export const INDEXER_UPDATE_INTERVAL = '5'
export const INDEXER_MEMPOOL_CHUNK_SIZE = '200'
export const EXPLORER_REPO = 'randomlogin/explorer'
export const EXPLORER_GIT_SHA = 'c827da1754c3cba5c5507d2c29f21b8fa231344d'
// Bump suffix to force re-fetch + rebuild of /data/explorer-ui.
export const EXPLORER_BUILD_ID = `${EXPLORER_GIT_SHA}-e1`
export const EXPLORER_TARBALL_URL = `https://github.com/${EXPLORER_REPO}/archive/${EXPLORER_GIT_SHA}.tar.gz`
export const EXPLORER_DIR = '/data/explorer-ui'
export const EXPLORER_BUILD_DIR = '/data/explorer-ui/build'
export const EXPLORER_MARKER = '/data/explorer-ui/.installed-sha'
export const EXPLORER_PORT = 3000
export const EXPLORER_NETWORK = 'mainnet'
// Subspaces ships as the prebuilt horologger/subs image; binaries live at // Subspaces ships as the prebuilt horologger/subs image; binaries live at
// these paths inside it. No build-from-source step. // these paths inside it. No build-from-source step.
export const SUBSPACES_SUBS_BIN = '/usr/local/bin/subs' export const SUBSPACES_SUBS_BIN = '/usr/local/bin/subs'
@@ -88,6 +52,17 @@ export const CERTRELAY_DEFAULT_HEALTHCHECK_HANDLE =
'account-digital-useful.genesis@key' 'account-digital-useful.genesis@key'
export const CERTRELAY_DEFAULT_BOOTSTRAP = false export const CERTRELAY_DEFAULT_BOOTSTRAP = false
// Nacho (Expo dev server, horologger/nacho image). Always on; UI interface on
// 8082 (the image's exposed port, matches EXPO_DEV_PORT).
export const NACHO_PORT = 8082
export const NACHO_DIR = '/data/nacho'
// Last-resort fallback if StartOS hasn't yet populated the subs-api interface's
// address info at the moment main runs. In normal operation EXPO_PUBLIC_API_BASE_URL
// is derived dynamically from the subs-api StartOS interface — see main.ts.
export const NACHO_FALLBACK_API_BASE_URL = `http://127.0.0.1:${SUBSPACES_UI_PORT}`
export const NACHO_DEFAULT_IGNORE_NAMES = 'fold,swifty'
export const NACHO_DEFAULT_WORKSHOP_PDF_LINK_TEXT = ''
export function randomPassword() { export function randomPassword() {
// bitcoind's generate-rpc-dependent action validates the password against // bitcoind's generate-rpc-dependent action validates the password against
// /^[A-Za-z0-9_-]+$/, so the charset must stay in that set. // /^[A-Za-z0-9_-]+$/, so the charset must stay in that set.
@@ -96,3 +71,101 @@ export function randomPassword() {
len: 32, len: 32,
}) })
} }
// Banner box rendering for the gotty terminal MOTD. Canonical width is 100
// monospaced cells: 1 cell each for the left/right side borders, plus 1-cell
// gutters on each side of text, leaving 96 cells of usable text. All public
// helpers here guarantee every line they emit is exactly BANNER_WIDTH cells.
//
// Use renderBanner() rather than hand-drawing rows so wrapping + padding stay
// correct when content changes. See bashrc in main.ts.
export const BANNER_WIDTH = 100
const BANNER_INNER = BANNER_WIDTH - 2 // 98 cells between the two side borders
const BANNER_TEXT = BANNER_INNER - 2 // 96 cells of usable text (1-cell gutters)
// Cell width assuming all code points are width-1 (true for ASCII, em-dash,
// and the box-drawing chars used here). Does NOT handle CJK or emoji — if you
// add any, switch to a wcwidth-aware count.
function cellWidth(s: string): number {
return Array.from(s).length
}
function padRight(s: string, width: number): string {
const w = cellWidth(s)
return w >= width ? s : s + ' '.repeat(width - w)
}
// Greedy word-wrap on whitespace runs. Lines that already fit pass through
// verbatim (preserving leading indent and any internal whitespace). Wrapping
// only kicks in when a line is wider than `width`; long single tokens get
// hard-broken so a long URL never overflows the box.
function wrapText(text: string, width: number): string[] {
if (cellWidth(text) <= width) return [text]
const out: string[] = []
const tokens = text.match(/\s+|\S+/g) ?? ['']
let line = ''
for (const tok of tokens) {
const isWs = /^\s/.test(tok)
const candidate = line + tok
if (cellWidth(candidate) <= width) {
line = candidate
continue
}
// Doesn't fit — flush current line and decide what to do with this token.
if (line) out.push(line)
if (isWs) {
// Discard the whitespace run at the wrap point; continuation starts flush.
line = ''
continue
}
// Non-whitespace token longer than width: hard-break.
let rest = tok
while (cellWidth(rest) > width) {
const arr = Array.from(rest)
out.push(arr.slice(0, width).join(''))
rest = arr.slice(width).join('')
}
line = rest
}
if (line) out.push(line)
if (out.length === 0) out.push('')
return out
}
function bannerTop(title?: string): string {
if (!title) return '┌' + '─'.repeat(BANNER_INNER) + '┐'
const head = '┌─ ' + title + ' '
const tail = BANNER_WIDTH - cellWidth(head) - 1
return head + '─'.repeat(Math.max(0, tail)) + '┐'
}
function bannerBottom(): string {
return '└' + '─'.repeat(BANNER_INNER) + '┘'
}
function bannerBlank(): string {
return '│' + ' '.repeat(BANNER_INNER) + '│'
}
function bannerRow(text: string): string[] {
return wrapText(text, BANNER_TEXT).map(
(line) => '│ ' + padRight(line, BANNER_TEXT) + ' │',
)
}
// Render a complete banner: top border, one or more content rows (text wrapped
// to fit; '' renders as a blank inner row), and bottom border. Every returned
// line is exactly BANNER_WIDTH cells wide.
export function renderBanner(
title: string | undefined,
lines: string[],
): string[] {
const out: string[] = [bannerTop(title)]
for (const l of lines) {
if (l === '') out.push(bannerBlank())
else out.push(...bannerRow(l))
}
out.push(bannerBottom())
return out
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { VersionGraph } from '@start9labs/start-sdk' import { VersionGraph } from '@start9labs/start-sdk'
import { v_0_0_9_3 } from './v0.0.9.3' import { v_0_1_1_2 } from './v0.1.1.2'
export const versionGraph = VersionGraph.of({ export const versionGraph = VersionGraph.of({
current: v_0_0_9_3, current: v_0_1_1_2,
other: [], other: [],
}) })
@@ -1,14 +1,21 @@
import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk'
export const v_0_0_9_3 = VersionInfo.of({ export const v_0_1_1_2 = VersionInfo.of({
version: '0.0.9:3', version: '0.1.1:2',
releaseNotes: { releaseNotes: {
en_US: `- Exposed the spaced JSON-RPC API as a new "Spaces API" StartOS interface (port 7225) and changed SPACED_RPC_BIND from 127.0.0.1 to 0.0.0.0 so external processes can reach it. It remains authenticated with the spaced RPC credentials (store.spacedAuth); internal clients still connect over loopback. en_US: `- **Optional HTTP basic auth in front of the Subspaces Prover (port 8888).** Same pattern as the subs auth: off by default, gated by \`store.subsProverAuthEnabled\` + \`store.subsProverAuth\`, wired via \`SUBS_PROVER_BASIC_AUTH_USER\` / \`SUBS_PROVER_BASIC_AUTH_PASSWORD\` env vars on the \`subs-prover\` daemon. Four new actions: **Enable Subspaces Prover Auth**, **Disable Subspaces Prover Auth**, **Show Subspaces Prover Auth Credentials**, **Set Subspaces Prover Auth Credentials**. Independent of subs auth — each daemon has its own toggle and credential store.
- **Optional HTTP basic auth in front of the Subspaces Web UI and Subs API.** Both share port 7777 on the \`subs\` daemon, so a single toggle gates both. Off by default. Wired via \`SUBS_BASIC_AUTH_USER\` / \`SUBS_BASIC_AUTH_PASSWORD\` env vars injected into subs's environment only when the toggle is on AND credentials are stored. Four new actions manage the lifecycle: **Enable Subspaces Auth** (auto-generates a \`spaces\` / random32 credential pair on first enable; preserves them on subsequent enables), **Disable Subspaces Auth** (preserves credentials across the toggle), **Show Subspaces Auth Credentials** (surfaces user/password + a loopback URL, reports current enforcement state), **Set Subspaces Auth Credentials** (rotate; blank password auto-generates a random one; also enables auth). Stored as \`store.subsAuth\` and \`store.subsAuthEnabled\`. NOTE: the in-package nacho client will break against an authed subs until separately wired — that integration is deferred.
- **Nacho ignore list is now file-backed at \`/data/nacho/ignore_names.txt\`** instead of \`EXPO_PUBLIC_IGNORE_NAMES\`. Expo inlines \`EXPO_PUBLIC_*\` into the bundle at build time and connected clients cache that bundle aggressively, so env-var changes only landed after a hard reload. Moving the list to a runtime-read file means nacho picks up changes without a bundle rebuild. The \`nacho-setup\` oneshot creates the file with the default (\`fold,swifty\`) on first start; the **Configure Nacho** action writes the file directly and prefills the form by reading it back (with the default as fallback). The \`nachoIgnoreNames\` field has been removed from \`store.json\`.
- **Milestone release: this version ships with Nacho bundled in.** The \`horologger/nacho:v1.0.0\` Expo dev server runs alongside spaced as an always-on UI on port 8082, wired to the Subs API and configurable via the "Configure Nacho" and "Upload Support PDF" actions.
- **Removed the embedded explorer feature and its indexer entirely.** Deleted the Explorer Web UI interface (port 3000), the postgres image, the PostgreSQL daemon + chown oneshot, and all stubs for the prebuilt indexer / explorer-UI images. Dropped the actions: enable-explorer, disable-explorer, show-db-credentials, reset-db-state, reset-indexer-state, reset-explorer-state. Removed the \`dbAuth\` and \`enableExplorer\` fields from store.json + their seed tasks (taskSeedDb, taskSeedEnableExplorer). main.ts collapsed to a single always-on chain (no more enableExplorer branching). Drop the related EXPLORER_*, INDEXER_*, POSTGRES_* constants from utils.ts.
- Added the nacho service (prebuilt \`horologger/nacho:v1.0.0\`, Expo dev server). Always on; runs the image's entrypoint and exposes a UI interface on port 8082. Env: \`EXPO_PUBLIC_API_BASE_URL\` is derived dynamically from the Subs API StartOS interface (\`sdk.serviceInterface.getOwn('subs-api').const()\`) — preferring the .local URL — so it tracks whatever host:port StartOS exposes for the subs daemon; \`EXPO_PUBLIC_IGNORE_NAMES\` (defaults to "fold,swifty") and \`EXPO_PUBLIC_WORKSHOP_PDF_LINK_TEXT\` (defaults to empty) are user-configurable via the new "Configure Nacho" action; the workshop PDF is uploaded via a separate "Upload Support PDF" action that overwrites /data/support.pdf on the main volume (split from Configure Nacho because the StartOS form serializes an unselected file input as {} rather than null, which Value.file's nullable parser rejects — a required-true dedicated action sidesteps the problem). Service auto-restarts when either the subs-api address info or the ignore-names value changes.
- Exposed the spaced JSON-RPC API as a new "Spaces API" StartOS interface (port 7225) and changed SPACED_RPC_BIND from 127.0.0.1 to 0.0.0.0 so external processes can reach it. It remains authenticated with the spaced RPC credentials (store.spacedAuth); internal clients still connect over loopback.
- New "Show Spaces API Credentials" action surfaces SPACED_RPC_USER / SPACED_RPC_PASSWORD (and a loopback connection URL) so you can authenticate external clients against the Spaces API.
- Added a dedicated "Subs API" interface (type api) on the subs daemon's port (7777), distinct from the Subspaces Prover (8888) and Subspaces Registry (8081) interfaces. The subs daemon is "subsd — an HTTP REST API server"; this surfaces that REST API as its own dashboard entry. - Added a dedicated "Subs API" interface (type api) on the subs daemon's port (7777), distinct from the Subspaces Prover (8888) and Subspaces Registry (8081) interfaces. The subs daemon is "subsd — an HTTP REST API server"; this surfaces that REST API as its own dashboard entry.
- New "Enable / Disable Subspaces Prover" action independently gates just the subs-prover daemon (store.enableSubsProver), **disabled by default**. When off, subs-prover does not start (it runs lengthy boot-time timing tests), but its interface on 8888 stays registered. It is added last and nothing depends on it, so its slow startup never blocks subs, the registry, or certrelay. - New "Enable / Disable Subspaces Prover" action independently gates just the subs-prover daemon (store.enableSubsProver), **disabled by default**. When off, subs-prover does not start (it runs lengthy boot-time timing tests), but its interface on 8888 stays registered. It is added last and nothing depends on it, so its slow startup never blocks subs, the registry, or certrelay.
- Embedded Certrelay (prebuilt \`horologger/certrelay:v0.2.3\` image, static musl binaries). Always on — runs the \`certrelay\` server on port 7778 from service start, as its own StartOS interface. It serves cryptographic proofs binding Bitcoin-anchored handles to owner keys, talking to the local spaced over loopback using the store.spacedAuth credentials. The bundled \`fabric\` CLI is copied to /data/bin/fabric so it's runnable from the Space-CLI Web UI. New "Configure Certrelay" action sets CERTRELAY_SELF_URL, CERTRELAY_BOOTSTRAP, and CERTRELAY_HEALTHCHECK_HANDLE (stored in store.json; saving restarts the service). Set CERTRELAY_SELF_URL to the publicly visible URL StartOS exposes for the Certrelay interface. - Embedded Certrelay (prebuilt \`horologger/certrelay:v0.2.3\` image, static musl binaries). Always on — runs the \`certrelay\` server on port 7778 from service start, as its own StartOS interface. It serves cryptographic proofs binding Bitcoin-anchored handles to owner keys, talking to the local spaced over loopback using the store.spacedAuth credentials. The bundled \`fabric\` CLI is copied to /data/bin/fabric so it's runnable from the Space-CLI Web UI. New "Configure Certrelay" action sets CERTRELAY_SELF_URL, CERTRELAY_BOOTSTRAP, and CERTRELAY_HEALTHCHECK_HANDLE (stored in store.json; saving restarts the service). Set CERTRELAY_SELF_URL to the publicly visible URL StartOS exposes for the Certrelay interface.
- Removed the build-from-source chains for the Go indexer and the SvelteKit explorer, and dropped their builder images (\`golang:1.23-alpine\`, \`node:20-alpine\`). This shrinks the .s9pk and removes the multi-minute first-enable compile. The indexer + explorer-UI daemons are now stubbed with TODOs in startos/main.ts pending prebuilt images (indexer image not yet produced; explorer image to be provided). Enabling the embedded explorer currently starts PostgreSQL only. All explorer-indexer actions (show-db-credentials, reset-db-state, reset-indexer-state, reset-explorer-state) are retained. - Removed the build-from-source chains for the Go indexer and the SvelteKit explorer, and dropped their builder images (\`golang:1.23-alpine\`, \`node:20-alpine\`). This shrinks the .s9pk and removes the multi-minute first-enable compile. The indexer + explorer-UI daemons are now stubbed with TODOs in startos/main.ts pending prebuilt images (indexer image not yet produced; explorer image to be provided). Enabling the embedded explorer currently starts PostgreSQL only. All explorer-indexer actions (show-db-credentials, reset-db-state, reset-indexer-state, reset-explorer-state) are retained.
- Embedded Subspaces support (prebuilt \`horologger/subs:v0.1.0\` image). Opt-in via the new "Enable Subspaces" action; "Disable Subspaces" stops it. No compile step — enabling starts three prebuilt daemons, each on its own StartOS interface: **subs** (Web UI, 7777), **subs-prover** (RISC Zero prover, no GPU, 8888), and **registry-server** (handle registry, 8081). subs loads the existing \`default\` spaces wallet at startup (it does not create one). Runtime data persists at /data/subspaces/data across restarts and toggles. New "Reset Subspaces State" action wipes local data. Adds a fifth manifest image (\`horologger/subs:v0.1.0\`, ~84 MB) — switched from building \`spacesops/subs\` from source with \`rust:1-slim\` to shrink the .s9pk and eliminate the multi-minute first-enable compile. - Embedded Subspaces support (prebuilt \`horologger/subs:\` image). Opt-in via the new "Enable Subspaces" action; "Disable Subspaces" stops it. No compile step — enabling starts three prebuilt daemons, each on its own StartOS interface: **subs** (Web UI, 7777), **subs-prover** (RISC Zero prover, no GPU, 8888), and **registry-server** (handle registry, 8081). subs loads the existing \`default\` spaces wallet at startup (it does not create one). Runtime data persists at /data/subspaces/data across restarts and toggles. New "Reset Subspaces State" action wipes local data. Adds a fifth manifest image (\`horologger/subs:\`, ~84 MB) — switched from building \`spacesops/subs\` from source with \`rust:1-slim\` to shrink the .s9pk and eliminate the multi-minute first-enable compile.
- Five distinct interfaces now appear in the dashboard: **Space-CLI Web UI** (gotty terminal, 8080), **Explorer Web UI** (3000, opt-in), **Subspaces Web UI** (7777, opt-in), **Subspaces Prover** (8888, opt-in), **Subspaces Registry** (8081, opt-in). Internal "Web UI" references renamed to **Space-CLI Web UI** for clarity. - Five distinct interfaces now appear in the dashboard: **Space-CLI Web UI** (gotty terminal, 8080), **Explorer Web UI** (3000, opt-in), **Subspaces Web UI** (7777, opt-in), **Subspaces Prover** (8888, opt-in), **Subspaces Registry** (8081, opt-in). Internal "Web UI" references renamed to **Space-CLI Web UI** for clarity.
- Embedded explorer (PostgreSQL + Go indexer + SvelteKit web UI) is now **opt-in**. Fresh installs run in spaces-only mode (spaced + gotty terminal). New "Enable Embedded Explorer" action turns the whole bundle on; "Disable Embedded Explorer" turns it back off. Both auto-restart the service so the new daemon graph takes effect. Indexed data on disk is preserved across toggles. - Embedded explorer (PostgreSQL + Go indexer + SvelteKit web UI) is now **opt-in**. Fresh installs run in spaces-only mode (spaced + gotty terminal). New "Enable Embedded Explorer" action turns the whole bundle on; "Disable Embedded Explorer" turns it back off. Both auto-restart the service so the new daemon graph takes effect. Indexed data on disk is preserved across toggles.
- Embedded SvelteKit explorer web UI (randomlogin/explorer @ c827da175). Fetched into /data/explorer-ui on first enable, \`npm install\` + \`npm run build\` produce a SvelteKit node-adapter bundle, and \`node build\` runs as a managed daemon on port 3000. Exposed as a StartOS interface so it appears alongside the gotty terminal in the dashboard. Reads exclusively from the embedded PostgreSQL; does not talk to spaced or bitcoind directly. New "Reset Explorer UI State" action wipes the cache for a clean rebuild. Adds a fourth manifest image \`node:20-alpine\`. - Embedded SvelteKit explorer web UI (randomlogin/explorer @ c827da175). Fetched into /data/explorer-ui on first enable, \`npm install\` + \`npm run build\` produce a SvelteKit node-adapter bundle, and \`node build\` runs as a managed daemon on port 3000. Exposed as a StartOS interface so it appears alongside the gotty terminal in the dashboard. Reads exclusively from the embedded PostgreSQL; does not talk to spaced or bitcoind directly. New "Reset Explorer UI State" action wipes the cache for a clean rebuild. Adds a fourth manifest image \`node:20-alpine\`.