Release v0.1.1:2 with certrelay, nacho, and prebuilt subspaces.
Build Service / BuildPackage (push) Has been cancelled
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:
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -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'),
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@@ -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 },
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@@ -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
@@ -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,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
+134
-263
@@ -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,11 +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)
|
let chain: any = sdk.Daemons.of(effects)
|
||||||
.addOneshot('bashrc', {
|
.addOneshot('bashrc', {
|
||||||
subcontainer: termSub,
|
subcontainer: termSub,
|
||||||
@@ -516,218 +598,7 @@ SPACES_BASHRC_EOF`],
|
|||||||
requires: ['spaced'],
|
requires: ['spaced'],
|
||||||
})
|
})
|
||||||
chain = withCertrelay(chain)
|
chain = withCertrelay(chain)
|
||||||
if (enableSubspaces) chain = withSubspaces(chain)
|
chain = withNacho(chain)
|
||||||
return chain
|
|
||||||
}
|
|
||||||
|
|
||||||
// enableExplorer = true: full chain — spaced + terminal + embedded
|
|
||||||
// PostgreSQL + Go indexer + indexer-sync health check.
|
|
||||||
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: [],
|
|
||||||
})
|
|
||||||
.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', {
|
|
||||||
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: [],
|
|
||||||
})
|
|
||||||
// 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', {
|
|
||||||
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'],
|
|
||||||
})
|
|
||||||
// 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)
|
|
||||||
if (enableSubspaces) chain = withSubspaces(chain)
|
if (enableSubspaces) chain = withSubspaces(chain)
|
||||||
return chain
|
return chain
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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\`.
|
||||||
Reference in New Issue
Block a user