Compare commits

...
9 Commits
Author SHA1 Message Date
spacesops b446cd5f69 0.3.1:0
Build Service / BuildPackage (push) Canceled after 0s
2026-08-28 16:55:15 -04:00
spacesopsandCursor 073344d4d1 0.3.0:1
Build Service / BuildPackage (push) Canceled after 0s
Group Actions and Config by category (Space-CLI, Bitcoin, Spaced,
Subspaces, Certrelay, Nacho) and build x86 + arm only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 03:34:34 -04:00
spacesopsandCursor 2b0224a678 0.3.0:0
Build Service / BuildPackage (push) Canceled after 0s
Update to upstream Spaces 0.3.0 and refresh bundled images (subs v0.1.2c,
certrelay v0.2.8, nacho v1.0.0b). Add Create/Show Registry API Keys for
registry-server, remove Configure Subspaces and SUBS_PUBLISH_REQUIRE_FINALIZED,
and align README/instructions with the new tags.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 19:35:15 -04:00
spacesopsandCursor 89f4475e99 0.2.1:5
Bump Subspaces to subs:v0.1.2b and Certrelay to certrelay:v0.2.7b;
align README and instructions with the new tags.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-09 11:43:26 -04:00
spacesops 61cd3e679a 0.2.1:4
Build Service / BuildPackage (push) Canceled after 0s
2026-07-31 21:31:22 -04:00
spacesopsandCursor 1914ad3b6a Pin spaces image to v0.2.1a and align docs
Build Service / BuildPackage (push) Canceled after 0s
The manifest now bundles horologger/spaces:v0.2.1a; README, instructions,
and 0.2.1:3 release notes match so pack and user-facing text stay in sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-31 20:34:37 -04:00
spacesops 0362388f7f 0.2.1:3
Build Service / BuildPackage (push) Canceled after 0s
2026-07-31 16:54:29 -04:00
spacesopsandCursor 5bd878ac2d Bump to 0.2.1:1
Build Service / BuildPackage (push) Canceled after 0s
Wrapper-only revision: the images are unchanged from 0.2.1:0, so only the
downstream digit moves. Release notes now describe this revision (rebuilt
against start-sdk 2.0.9, requires StartOS 0.4.0-beta.10) instead of 0.2.1:0's
image bumps.

The version file moves to versions/current.ts, where the guide keeps the latest
version — a dated file is only for a released version that carries a migration,
and this bump carries none. No `other` entry is needed: the version graph
synthesizes a range beneath current, and the packed manifest derives
canMigrateFrom <=0.2.1:1.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-30 12:03:02 -04:00
spacesopsandCursor b1d4890320 Update to @start9labs/start-sdk 2.0.9
Targets StartOS 0.4.0-beta.10 (was beta.9). Four breaking changes applied:

- Service interfaces moved onto their binding: sdk.serviceInterface.getOwn is
  gone, so nacho's EXPO_PUBLIC_API_BASE_URL now walks the subs-api interface off
  its host with sdk.host.getOwn, using the map selector so .const() re-runs only
  when that interface's address changes.
- SubContainer.of is lazy and synchronous — dropped the awaits.
- Task input split into accept/set; the Bitcoin RPC task accepts the credentials
  it sets, matching its previous behavior.
- The manifest alerts field was removed (ours was all null).

Also adopts the build plumbing the SDK now ships instead of vendoring it: the
Makefile includes node_modules/@start9labs/start-sdk/s9pk.mk, tsconfig extends
the SDK's base (ES2022, bundler resolution), and TypeScript moves to 6.0.

Verified by installing on a StartOS box: all daemons come up, nacho logs a
derived API base URL rather than the loopback fallback, and the sync health
check's space-cli exec runs on cadence against the shared subcontainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-30 11:48:09 -04:00
48 changed files with 1738 additions and 336 deletions
+2 -2
View File
@@ -1,3 +1,3 @@
ARCHES := x86
ARCHES := x86 arm
# overrides to s9pk.mk must precede the include statement
include s9pk.mk
include node_modules/@start9labs/start-sdk/s9pk.mk
+40 -27
View File
@@ -37,10 +37,10 @@ through a browser-based terminal.
| Field | Value |
| --- | --- |
| Spaces image | `docker.io/horologger/spaces` |
| Subspaces image | `docker.io/horologger/subs:v0.1.2` |
| Certrelay image | `docker.io/horologger/certrelay:v0.2.5` |
| Nacho image | `docker.io/horologger/nacho:v1.0.0` |
| Spaces image | `docker.io/horologger/spaces:v0.3.1` |
| Subspaces image | `docker.io/horologger/subs:v0.1.2d` |
| Certrelay image | `docker.io/horologger/certrelay:v0.2.8` |
| Nacho image | `docker.io/horologger/nacho:v1.0.0b` |
| Architectures | `linux/amd64`, `linux/arm64` |
| Entrypoint | StartOS-managed (image entrypoints are **not** used directly) |
@@ -54,7 +54,7 @@ image's `docker_entrypoint.sh`; daemons are defined in `startos/main.ts`.
| --- | --- | --- |
| `/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/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/store.json` | `main` | StartOS-managed credentials (Space-CLI Web UI password, bitcoind RPC user/password, spaced RPC user/password, registry API keys) and feature config (Subspaces toggles, Certrelay + Nacho settings). |
| `/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/registry` | `main` | Working directory for the `registry-server` daemon. |
@@ -87,6 +87,9 @@ On the first install, StartOS:
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.
Registry API keys (`registryApiKey` / `subsdApiKey`) are **not** seeded at
install — run **Create Registry API Keys** before (or when prompted after)
enabling Subspaces; `registry-server` will not start without them.
6. Launches `spaced` as a managed daemon (no `screen`, no shell auto-start)
and the `gotty` web terminal once the bashrc oneshot completes.
7. Runs `certrelay-setup` (creates `/data/certrelay/data`, copies `fabric` and
@@ -110,7 +113,6 @@ mainnet-only.
| Bitcoin RPC username / password (registered on bitcoind) | Wallet creation, bidding, and registration -- all driven via `space-cli` inside the terminal |
| 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) | |
| `SUBS_PUBLISH_REQUIRE_FINALIZED` (off by default; via **Configure Subspaces**) | |
| Subspaces Prover auth toggle + credentials (`SUBS_PROVER_BASIC_AUTH_USER`/`SUBS_PROVER_BASIC_AUTH_PASSWORD`, off by default) | |
| Chain selection (locked to `mainnet`) | |
| Spaced data directory and RPC bind | |
@@ -125,7 +127,7 @@ mainnet-only.
| 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 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. |
| 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. |
| 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 (walked off its host: `sdk.host.getOwn(effects, 'subspaces-multi', h => h?.bindings[7777]?.interfaces['subs-api']).const()`), preferring the `.local` URL. Falls back to `http://127.0.0.1:7777` only if no address info is available. |
| 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
@@ -145,7 +147,6 @@ mainnet-only.
| `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. |
| `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. |
| `configure-subspaces` | Configure Subspaces | Enabled | Any | Toggle `SUBS_PUBLISH_REQUIRE_FINALIZED` (default **off**). When on, subs requires finalized commitments before certificate publish. Stored in `store.subsPublishRequireFinalized`; injected into the `subs` daemon env. 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. |
@@ -154,6 +155,8 @@ mainnet-only.
| `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. |
| `create-registry-api-keys` | Create Registry API Keys | Enabled | Any | Generates (or rotates) `store.registryApiKey` + `store.subsdApiKey` as 64-char hex (`openssl rand -hex 32`). Both are required and must differ. Injected as `REGISTRY_API_KEY` / `SUBSD_API_KEY` into `registry-server`. Put `SUBSD_API_KEY` in subs Settings → Registry Server → Auth Token. Restarts so the registry picks up the new keys. When Subspaces is enabled without valid keys, a critical task points here. |
| `show-registry-api-keys` | Show Registry API Keys | Enabled | Any | Surfaces the stored `REGISTRY_API_KEY` and `SUBSD_API_KEY`. Returns blanks until Create Registry API Keys has been run. |
| `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. |
@@ -198,18 +201,22 @@ the same idempotent init logic runs and reuses the existing credentials in
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.
7. **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.2`
Subspaces** to start it. It ships as the prebuilt `horologger/subs:v0.1.2d`
image (no compile step), so enabling is fast — the daemons start as soon
as the image is on the box. Enabling runs three daemons, each with its own
StartOS interface: **subs** (Web UI + Subs API, 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.
prover's interface is always registered even when its daemon is off. Before
(or when prompted after) enabling, run **Create Registry API Keys** —
`registry-server` requires distinct `REGISTRY_API_KEY` and `SUBSD_API_KEY`
env vars at boot. Paste `SUBSD_API_KEY` into subs Settings → Registry
Server → Auth Token, then click Test. Use `REGISTRY_API_KEY` as the Bearer
token for intake/`POST /register` clients. 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.
8. **Certrelay is always on.** It ships as the prebuilt
`horologger/certrelay:v0.2.5` image (static musl binaries) and runs on
`horologger/certrelay:v0.2.8` image (static musl binaries) and runs on
7778 from service start. It talks to the local spaced over loopback using
the `store.spacedAuth` credentials. The `fabric` and `monitor` binaries are copied to
`/data/bin/` for manual use from the Space-CLI Web UI; neither is auto-started
@@ -250,10 +257,10 @@ spaces balance
package_id: spaces
upstream_version: subspacesplus
images:
spaces: docker.io/horologger/spaces:v0.2.1
subspaces: docker.io/horologger/subs:v0.1.2
certrelay: docker.io/horologger/certrelay:v0.2.5
nacho: docker.io/horologger/nacho:v1.0.0
spaces: docker.io/horologger/spaces:v0.3.1
subspaces: docker.io/horologger/subs:v0.1.2d
certrelay: docker.io/horologger/certrelay:v0.2.8
nacho: docker.io/horologger/nacho:v1.0.0b
architectures: [x86_64, aarch64]
volumes:
main: /data
@@ -285,17 +292,27 @@ spaced_env_vars:
- BTC_RPC_PASSWORD
- APP_USER
- APP_PASSWORD
spaces:
image: horologger/spaces:v0.3.1 # spaced, space-cli, gotty (terminal image)
subspaces:
default_enabled: false # opt-in via Enable Subspaces action
store_field: enableSubspaces
subs_prover_default_enabled: false # separate toggle: set-subs-prover (store.enableSubsProver)
image: horologger/subs:v0.1.2 # prebuilt; no compile step
image: horologger/subs:v0.1.2d # prebuilt; no compile step
data_dir: /data/subspaces/data
wallet: default # loaded by subs, NOT created (create via space-cli first)
daemons: # three separate daemons, each its own interface
subs: { bin: /usr/local/bin/subs, port: 7777 }
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_api_keys: # mandatory for registry-server boot; must differ
store_fields: [registryApiKey, subsdApiKey]
env_vars: [REGISTRY_API_KEY, SUBSD_API_KEY] # always injected into registry-server
generate: openssl-rand-hex-32 # 64 lowercase hex via create-registry-api-keys
subs_ui: Settings → Registry Server → Auth Token = SUBSD_API_KEY
actions:
- create-registry-api-keys
- show-registry-api-keys
auth: # optional HTTP basic auth in front of subs (Web UI + Subs API on 7777)
default_enabled: false
store_enabled_field: subsAuthEnabled
@@ -304,11 +321,6 @@ subspaces:
env_vars: # injected only when subsAuthEnabled=true AND subsAuth!=null
- SUBS_BASIC_AUTH_USER
- SUBS_BASIC_AUTH_PASSWORD
publish_require_finalized:
default: false
store_field: subsPublishRequireFinalized
env_var: SUBS_PUBLISH_REQUIRE_FINALIZED # always 'true' or 'false' on subs daemon
action: configure-subspaces
actions:
- enable-subs-auth
- disable-subs-auth
@@ -329,7 +341,7 @@ subspaces:
- set-subs-prover-credentials
certrelay:
always_on: true
image: horologger/certrelay:v0.2.5 # prebuilt; static musl binaries
image: horologger/certrelay:v0.2.8 # prebuilt; static musl binaries
server_bin: /usr/local/bin/certrelay
fabric_bin: /usr/local/bin/fabric # copied to /data/bin/fabric for the CLI
monitor_bin: /usr/local/bin/monitor # copied to /data/bin/monitor (CLI only; not started)
@@ -348,7 +360,7 @@ certrelay:
CERTRELAY_HEALTHCHECK_HANDLE: account-digital-useful.genesis@key # via action
nacho:
always_on: true
image: horologger/nacho:v1.0.0 # Expo dev server
image: horologger/nacho:v1.0.0b # Expo dev server
port: 8082
data_dir: /data/nacho
ignore_names_file: /data/nacho/ignore_names.txt # read at runtime; default 'fold,swifty' seeded by nacho-setup
@@ -372,7 +384,6 @@ actions:
- enable-subspaces
- disable-subspaces
- set-subs-prover
- configure-subspaces
- enable-subs-auth
- disable-subs-auth
- show-subs-credentials
@@ -381,6 +392,8 @@ actions:
- disable-subs-prover-auth
- show-subs-prover-credentials
- set-subs-prover-credentials
- create-registry-api-keys
- show-registry-api-keys
- reset-subspaces-state
- configure-certrelay
- configure-nacho
+13 -8
View File
@@ -1,13 +1,9 @@
# TODO — bring Spaces from beta.9 to
# TODO — bring Spaces from beta.9 to beta.10
Consult the packaging guide as you go
(`start-technologies/projects/start-sdk/docs/src/recipes.md` is the intent index). Remove items as you finish
them, and add items when you defer work.
## The service
- [ ] Update @start9labs/start-sdk to the latest version.
## Docs
- [ ] Write `README.md` (per `start-technologies/projects/start-sdk/docs/src/writing-readmes.md`).
@@ -16,8 +12,17 @@ them, and add items when you defer work.
## Build, test, ship
- [ ] First test build: `make` (or `start-cli s9pk pack`); fix any `tsc` / pack errors.
- [ ] Install on a StartOS box and verify the service runs (and is reachable, once it
exposes an interface).
- [ ] Build for arm (`ARCHES := x86 arm` in the `Makefile`). All four images declare
`aarch64` in the manifest and every tag publishes an arm64 variant, so the
package currently advertises support it never ships. Needs an arm box to verify.
- [x] First test build: `make` (or `start-cli s9pk pack`); fix any `tsc` / pack errors.
- [x] Install on a StartOS box and verify the service runs — done for 0.2.1:0 on
natural-dean.local: all daemons up, nacho's `EXPO_PUBLIC_API_BASE_URL` resolved
through the subs-api host rather than the loopback fallback.
- [ ] Verify the Bitcoin RPC task on a *fresh* install (`accept`/`set` input under SDK
2.0). `taskBtcAuth` returns early when `store.btcAuth` exists, so an update over
an existing install never exercises it.
- [ ] `npx prettier --write startos` — 9 files predate the current prettier config and
fail `--check` (untouched by the SDK bump, so left out of that diff).
- [ ] Backup / restore sanity check.
- [ ] Review the README and instructions one more time against actual behavior.
+22
View File
@@ -57,10 +57,32 @@ spaces walletbalance default
`/data/mainnet/wallets/default` to `.bakNNN` suffixes so no prior state is
lost, then calls `space-cli importwallet` and `space-cli loadwallet`.
Requires the service to be running.
- **Create Registry API Keys** — generates (or rotates) `REGISTRY_API_KEY` and
`SUBSD_API_KEY` for the Subspaces registry. Required before the registry
daemon will start.
- **Show Registry API Keys** — displays the stored registry keys.
## Subspaces registry API keys
The embedded `registry-server` will not start without two distinct bearer
secrets. Before enabling Subspaces (or when StartOS prompts you after):
1. Run **Create Registry API Keys** — generates `REGISTRY_API_KEY` and
`SUBSD_API_KEY` (same shape as `openssl rand -hex 32`).
2. In the Subspaces Web UI, open **Settings → Registry Server**, set **Auth
Token** to `SUBSD_API_KEY`, and click **Test**.
3. Give `REGISTRY_API_KEY` to any intake client that calls `POST /register`.
Use **Show Registry API Keys** to retrieve the stored values later, or re-run
**Create Registry API Keys** to rotate them (then update the Auth Token).
## Limitations
- **Mainnet only.** No testnet, signet, or regtest.
- **Container images** (see also the package manifest): the `spaced` runtime uses
`horologger/spaces:v0.3.1`; Subspaces, Certrelay, and Nacho use
`horologger/subs:v0.1.2d`, `horologger/certrelay:v0.2.8`, and
`horologger/nacho:v1.0.0b`.
- The image's `docker_entrypoint.sh` is not used. `spaced` is managed by
StartOS, not started inside a `screen` session by login.
- The only externally bound port is `8080` (gotty). Spaced RPC stays on
+1309 -13
View File
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -6,13 +6,16 @@
"check": "tsc --noEmit"
},
"dependencies": {
"@start9labs/start-sdk": "1.5.1"
"@start9labs/start-sdk": "2.0.9"
},
"overrides": {
"@start9labs/start-sdk": "$@start9labs/start-sdk"
},
"devDependencies": {
"@types/node": "^22.19.0",
"@vercel/ncc": "^0.38.4",
"prettier": "^3.6.2",
"typescript": "^5.9.3"
"typescript": "^6.0.3"
},
"prettier": {
"trailingComma": "all",
-132
View File
@@ -1,132 +0,0 @@
# ** Plumbing. DO NOT EDIT **.
# This file is imported by ./Makefile. Make edits there
PACKAGE_ID := $(shell awk -F"'" '/id:/ {print $$2}' startos/manifest/index.ts)
INGREDIENTS := $(shell start-cli s9pk list-ingredients 2>/dev/null)
# Resolve the actual git dir so this works inside git worktrees, where .git
# is a file pointing at <main>/.git/worktrees/<name> rather than a directory.
GIT_DIR := $(shell git rev-parse --git-dir 2>/dev/null)
GIT_DEPS := $(if $(GIT_DIR),$(GIT_DIR)/HEAD $(GIT_DIR)/index)
ARCHES ?= x86 arm riscv
TARGETS ?= arches
ifdef VARIANT
BASE_NAME := $(PACKAGE_ID)_$(VARIANT)
else
BASE_NAME := $(PACKAGE_ID)
endif
.PHONY: all arches aarch64 x86_64 riscv64 arm arm64 x86 riscv arch/* clean install check-deps check-init package ingredients
.DELETE_ON_ERROR:
.SECONDARY:
define SUMMARY
@manifest=$$(start-cli s9pk inspect $(1) manifest); \
size=$$(du -h $(1) | awk '{print $$1}'); \
title=$$(printf '%s' "$$manifest" | jq -r .title); \
version=$$(printf '%s' "$$manifest" | jq -r .version); \
arches=$$(printf '%s' "$$manifest" | jq -r '[.images[].arch // []] | flatten | unique | join(", ")'); \
sdkv=$$(printf '%s' "$$manifest" | jq -r .sdkVersion); \
gitHash=$$(printf '%s' "$$manifest" | jq -r .gitHash | sed -E 's/(.*-modified)$$/\x1b[0;31m\1\x1b[0m/'); \
printf "\n"; \
printf "\033[1;32m✅ Build Complete!\033[0m\n"; \
printf "\n"; \
printf "\033[1;37m📦 $$title\033[0m \033[36mv$$version\033[0m\n"; \
printf "───────────────────────────────\n"; \
printf " \033[1;36mFilename:\033[0m %s\n" "$(1)"; \
printf " \033[1;36mSize:\033[0m %s\n" "$$size"; \
printf " \033[1;36mArch:\033[0m %s\n" "$$arches"; \
printf " \033[1;36mSDK:\033[0m %s\n" "$$sdkv"; \
printf " \033[1;36mGit:\033[0m %s\n" "$$gitHash"; \
echo ""
endef
all: $(TARGETS)
arches: $(ARCHES)
universal: $(BASE_NAME).s9pk
$(call SUMMARY,$<)
arch/%: $(BASE_NAME)_%.s9pk
$(call SUMMARY,$<)
x86 x86_64: arch/x86_64
arm arm64 aarch64: arch/aarch64
riscv riscv64: arch/riscv64
$(BASE_NAME).s9pk: $(INGREDIENTS) $(GIT_DEPS)
@$(MAKE) --no-print-directory ingredients
@echo " Packing '$@'..."
start-cli s9pk pack -o $@
$(BASE_NAME)_%.s9pk: $(INGREDIENTS) $(GIT_DEPS)
@$(MAKE) --no-print-directory ingredients
@echo " Packing '$@'..."
start-cli s9pk pack --arch=$* -o $@
ingredients: $(INGREDIENTS)
@echo " Re-evaluating ingredients..."
install: | check-deps check-init
@HOST=$$(awk -F'/' '/^host:/ {print $$3}' ~/.startos/config.yaml); \
if [ -z "$$HOST" ]; then \
echo "Error: You must define \"host: http://server-name.local\" in ~/.startos/config.yaml"; \
exit 1; \
fi; \
S9PK=$$(ls -t *.s9pk 2>/dev/null | head -1); \
if [ -z "$$S9PK" ]; then \
echo "Error: No .s9pk file found. Run 'make' first."; \
exit 1; \
fi; \
printf "\n🚀 Installing %s to %s ...\n" "$$S9PK" "$$HOST"; \
start-cli package install -s "$$S9PK"
publish: | all
@REGISTRY=$$(awk -F'/' '/^registry:/ {print $$3}' ~/.startos/config.yaml); \
if [ -z "$$REGISTRY" ]; then \
echo "Error: You must define \"registry: https://my-registry.tld\" in ~/.startos/config.yaml"; \
exit 1; \
fi; \
S3BASE=$$(awk -F'/' '/^s9pk-s3base:/ {print $$3}' ~/.startos/config.yaml); \
if [ -z "$$S3BASE" ]; then \
echo "Error: You must define \"s3base: https://s9pks.my-s3-bucket.tld\" in ~/.startos/config.yaml"; \
exit 1; \
fi; \
command -v s3cmd >/dev/null || \
(echo "Error: s3cmd not found. It must be installed to publish using s3." && exit 1); \
printf "\n🚀 Publishing to %s; indexing on %s ...\n" "$$S3BASE" "$$REGISTRY"; \
for s9pk in *.s9pk; do \
age=$$(( $$(date +%s) - $$(stat -c %Y "$$s9pk") )); \
if [ "$$age" -gt 3600 ]; then \
printf "\033[1;33m⚠️ %s is %d minutes old. Publish anyway? [y/N] \033[0m" "$$s9pk" "$$((age / 60))"; \
read -r ans; \
case "$$ans" in [yY]*) ;; *) echo "Skipping $$s9pk"; continue ;; esac; \
fi; \
start-cli s9pk publish "$$s9pk"; \
done
check-deps:
@command -v start-cli >/dev/null || \
(echo "Error: start-cli not found. Please see https://docs.start9.com/latest/developer-guide/sdk/installing-the-sdk" && exit 1)
@command -v npm >/dev/null || \
(echo "Error: npm not found. Please install Node.js and npm." && exit 1)
check-init:
@if [ ! -f ~/.startos/developer.key.pem ]; then \
echo "Initializing StartOS developer environment..."; \
start-cli init-key; \
fi
javascript/index.js: $(shell find startos -type f) tsconfig.json node_modules
npm run check
npm run build
node_modules: package-lock.json
npm ci
package-lock.json: package.json
npm i
clean:
@echo "Cleaning up build artifacts..."
@rm -rf $(PACKAGE_ID).s9pk $(PACKAGE_ID)_x86_64.s9pk $(PACKAGE_ID)_aarch64.s9pk $(PACKAGE_ID)_riscv64.s9pk javascript node_modules
+14
View File
@@ -0,0 +1,14 @@
import { i18n } from './i18n'
/** Headers for the Actions and Config page (`ActionMetadata.group`). */
export const actionGroups = {
spaceCli: i18n('Space-CLI Web UI'),
bitcoin: i18n('Bitcoin'),
spaced: i18n('Spaced'),
subspaces: i18n('Subspaces'),
subspacesAuth: i18n('Subspaces Auth'),
subspacesProverAuth: i18n('Subspaces Prover Auth'),
subspacesRegistry: i18n('Subspaces Registry'),
certrelay: i18n('Certrelay'),
nacho: i18n('Nacho'),
}
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import {
@@ -61,7 +62,7 @@ export const configureCertrelay = sdk.Action.withInput(
),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.certrelay,
visibility: 'enabled',
}),
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import {
@@ -90,7 +91,7 @@ export const configureNacho = sdk.Action.withInput(
),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.nacho,
visibility: 'enabled',
}),
-65
View File
@@ -1,65 +0,0 @@
import { storeJson } from '../fileModels/storeJson'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
const { InputSpec, Value } = sdk
const inputSpec = InputSpec.of({
subsPublishRequireFinalized: Value.toggle({
name: i18n('Require Finalized Publish'),
description: i18n(
'When on, subs blocks certificate publish until commitments are finalized (150 confirmations). Maps to SUBS_PUBLISH_REQUIRE_FINALIZED in the subs daemon environment. Off by default.',
),
warning: null,
footnote: null,
default: false,
}),
})
export const configureSubspaces = sdk.Action.withInput(
// id
'configure-subspaces',
// metadata
async ({ effects }) => ({
name: i18n('Configure Subspaces'),
description: i18n(
'Set user-tunable subs options. Saving restarts the service so the subs daemon picks up the new environment.',
),
warning: null,
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
// input
inputSpec,
// prefill — current value (unset/null => false)
async ({ effects }) => {
const enabled = await storeJson
.read((s) => s.subsPublishRequireFinalized)
.once()
return { subsPublishRequireFinalized: enabled === true }
},
// run
async ({ effects, input }) => {
await storeJson.merge(effects, {
subsPublishRequireFinalized: input.subsPublishRequireFinalized,
})
return {
version: '1',
title: i18n('Success'),
message: input.subsPublishRequireFinalized
? i18n(
'Subspaces configuration saved. SUBS_PUBLISH_REQUIRE_FINALIZED is enabled; the service is restarting.',
)
: i18n(
'Subspaces configuration saved. SUBS_PUBLISH_REQUIRE_FINALIZED is disabled; the service is restarting.',
),
result: null,
}
},
)
+63
View File
@@ -0,0 +1,63 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { generateRegistryApiKeys } from '../utils'
export const createRegistryApiKeys = sdk.Action.withoutInput(
// id
'create-registry-api-keys',
// metadata
async ({ effects }) => ({
name: i18n('Create Registry API Keys'),
description: i18n(
'Generate (or rotate) REGISTRY_API_KEY and SUBSD_API_KEY for the embedded registry-server. Both are required and must differ. Put SUBSD_API_KEY in subs Settings → Registry Server → Auth Token. REGISTRY_API_KEY is for intake clients calling POST /register. Saving restarts the service so registry-server picks up the new keys.',
),
warning: null,
allowedStatuses: 'any',
group: actionGroups.subspacesRegistry,
visibility: 'enabled',
}),
// run
async ({ effects }) => {
const { registryApiKey, subsdApiKey } = generateRegistryApiKeys()
await storeJson.merge(effects, { registryApiKey, subsdApiKey })
return {
version: '1',
title: i18n('Success'),
message: i18n(
'Registry API keys saved. Paste SUBSD_API_KEY into subs Settings → Registry Server → Auth Token, then click Test. Use REGISTRY_API_KEY as the Bearer token for POST /register clients. The service is restarting so registry-server picks up the new keys.',
),
result: {
type: 'group',
value: [
{
type: 'single',
name: i18n('REGISTRY_API_KEY'),
description: i18n(
'Bearer secret for intake/POST /register clients.',
),
value: registryApiKey,
masked: true,
copyable: true,
qr: false,
},
{
type: 'single',
name: i18n('SUBSD_API_KEY'),
description: i18n(
'Paste into subs Settings → Registry Server → Auth Token (subs↔registry channel).',
),
value: subsdApiKey,
masked: true,
copyable: true,
qr: false,
},
],
},
}
},
)
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
@@ -16,7 +17,7 @@ export const disableSubsAuth = sdk.Action.withoutInput(
),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.subspacesAuth,
visibility: enabled === true ? 'enabled' : 'hidden',
}
},
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
@@ -16,7 +17,7 @@ export const disableSubsProverAuth = sdk.Action.withoutInput(
),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.subspacesProverAuth,
visibility: enabled === true ? 'enabled' : 'hidden',
}
},
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
@@ -16,7 +17,7 @@ export const disableSubspaces = sdk.Action.withoutInput(
),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.subspaces,
visibility: current === true ? 'enabled' : 'hidden',
}
},
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { randomPassword } from '../utils'
@@ -19,7 +20,7 @@ export const enableSubsAuth = sdk.Action.withoutInput(
),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.subspacesAuth,
visibility: enabled === true ? 'hidden' : 'enabled',
}
},
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { randomPassword } from '../utils'
@@ -19,7 +20,7 @@ export const enableSubsProverAuth = sdk.Action.withoutInput(
),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.subspacesProverAuth,
visibility: enabled === true ? 'hidden' : 'enabled',
}
},
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
@@ -16,7 +17,7 @@ export const enableSubspaces = sdk.Action.withoutInput(
),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.subspaces,
visibility: current === true ? 'hidden' : 'enabled',
}
},
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { dataDir, SPACED_CHAIN } from '../utils'
@@ -18,7 +19,7 @@ export const exportWallet = sdk.Action.withoutInput(
),
warning: null,
allowedStatuses: 'only-running',
group: null,
group: actionGroups.spaced,
visibility: 'enabled',
}),
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { dataDir, SPACED_CHAIN } from '../utils'
@@ -37,7 +38,7 @@ export const importWallet = sdk.Action.withInput(
'Existing /data/mainnet/wallets_backup/default.json and /data/mainnet/wallets/default will be renamed with .bakNNN suffixes before the import. The active spaced daemon will load the imported wallet on success.',
),
allowedStatuses: 'only-running',
group: null,
group: actionGroups.spaced,
visibility: 'enabled',
}),
+4 -2
View File
@@ -1,7 +1,7 @@
import { sdk } from '../sdk'
import { configureCertrelay } from './configureCertrelay'
import { configureNacho } from './configureNacho'
import { configureSubspaces } from './configureSubspaces'
import { createRegistryApiKeys } from './createRegistryApiKeys'
import { disableSubsAuth } from './disableSubsAuth'
import { disableSubsProverAuth } from './disableSubsProverAuth'
import { disableSubspaces } from './disableSubspaces'
@@ -19,6 +19,7 @@ import { setSubsProver } from './setSubsProver'
import { setSubsProverCredentials } from './setSubsProverCredentials'
import { showCredentials } from './showCredentials'
import { showPassword } from './showPassword'
import { showRegistryApiKeys } from './showRegistryApiKeys'
import { showSpacedCredentials } from './showSpacedCredentials'
import { showSubsCredentials } from './showSubsCredentials'
import { showSubsProverCredentials } from './showSubsProverCredentials'
@@ -49,5 +50,6 @@ export const actions = sdk.Actions.of()
.addAction(resetSubspacesState)
.addAction(configureCertrelay)
.addAction(configureNacho)
.addAction(configureSubspaces)
.addAction(createRegistryApiKeys)
.addAction(showRegistryApiKeys)
.addAction(uploadSupportPdf)
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { APP_USER, randomPassword } from '../utils'
@@ -15,7 +16,7 @@ export const resetPassword = sdk.Action.withoutInput(
),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.spaceCli,
visibility: 'enabled',
}),
+2 -1
View File
@@ -1,3 +1,4 @@
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { dataDir, SPACED_CHAIN } from '../utils'
@@ -16,7 +17,7 @@ export const resetSpacedState = sdk.Action.withoutInput(
"This deletes spaced's on-disk index. The next start will resync from spaces' anchor and can take a while. store.json (passwords + RPC credentials) is preserved.",
),
allowedStatuses: 'any',
group: null,
group: actionGroups.spaced,
visibility: 'enabled',
}),
+2 -1
View File
@@ -1,3 +1,4 @@
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { dataDir, SUBSPACES_DIR } from '../utils'
@@ -16,7 +17,7 @@ export const resetSubspacesState = sdk.Action.withoutInput(
'This deletes all local Subspaces state — any handles/proofs stored on disk will be lost. The binaries ship in the image, so nothing needs to be re-downloaded. The spaces wallet on spaced is preserved.',
),
allowedStatuses: 'any',
group: null,
group: actionGroups.subspaces,
visibility: 'enabled',
}),
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import {
@@ -17,7 +18,7 @@ export const setBitcoinRpc = sdk.Action.withoutInput(
description: i18n('Re-run the bitcoind RPC credential setup for Spaces'),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.bitcoin,
visibility: 'enabled',
}),
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { randomPassword } from '../utils'
@@ -50,7 +51,7 @@ export const setSubsCredentials = sdk.Action.withInput(
),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.subspacesAuth,
visibility: 'enabled',
}),
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
@@ -28,7 +29,7 @@ export const setSubsProver = sdk.Action.withInput(
),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.subspaces,
visibility: 'enabled',
}),
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { randomPassword } from '../utils'
@@ -50,7 +51,7 @@ export const setSubsProverCredentials = sdk.Action.withInput(
),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.subspacesProverAuth,
visibility: 'enabled',
}),
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { APP_USER } from '../utils'
@@ -15,7 +16,7 @@ export const showCredentials = sdk.Action.withoutInput(
),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.spaceCli,
visibility: 'hidden',
}),
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { APP_USER } from '../utils'
@@ -15,7 +16,7 @@ export const showPassword = sdk.Action.withoutInput(
),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.spaceCli,
visibility: 'enabled',
}),
+75
View File
@@ -0,0 +1,75 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
export const showRegistryApiKeys = sdk.Action.withoutInput(
// id
'show-registry-api-keys',
// metadata
async ({ effects }) => ({
name: i18n('Show Registry API Keys'),
description: i18n(
'Display the stored REGISTRY_API_KEY and SUBSD_API_KEY used by registry-server. Returns blanks if Create Registry API Keys has not been run yet.',
),
warning: null,
allowedStatuses: 'any',
group: actionGroups.subspacesRegistry,
visibility: 'enabled',
}),
// run
async ({ effects }) => {
const [registryApiKey, subsdApiKey] = await Promise.all([
storeJson.read((s) => s.registryApiKey).once(),
storeJson.read((s) => s.subsdApiKey).once(),
])
const ready =
!!registryApiKey &&
!!subsdApiKey &&
registryApiKey.length > 0 &&
subsdApiKey.length > 0 &&
registryApiKey !== subsdApiKey
return {
version: '1',
title: i18n('Show Registry API Keys'),
message: ready
? i18n(
'Paste SUBSD_API_KEY into subs Settings → Registry Server → Auth Token. Use REGISTRY_API_KEY as the Bearer token for POST /register clients.',
)
: i18n(
'No valid registry API keys are stored yet. Run Create Registry API Keys first (both keys must be non-empty and different).',
),
result: {
type: 'group',
value: [
{
type: 'single',
name: i18n('REGISTRY_API_KEY'),
description: i18n(
'Bearer secret for intake/POST /register clients.',
),
value: registryApiKey ?? '',
masked: true,
copyable: true,
qr: false,
},
{
type: 'single',
name: i18n('SUBSD_API_KEY'),
description: i18n(
'Paste into subs Settings → Registry Server → Auth Token (subs↔registry channel).',
),
value: subsdApiKey ?? '',
masked: true,
copyable: true,
qr: false,
},
],
},
}
},
)
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { spacedRpcPort } from '../utils'
@@ -15,7 +16,7 @@ export const showSpacedCredentials = sdk.Action.withoutInput(
),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.spaced,
visibility: 'enabled',
}),
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { SUBSPACES_UI_PORT } from '../utils'
@@ -15,7 +16,7 @@ export const showSubsCredentials = sdk.Action.withoutInput(
),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.subspacesAuth,
visibility: 'enabled',
}),
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { SUBSPACES_PROVER_PORT } from '../utils'
@@ -15,7 +16,7 @@ export const showSubsProverCredentials = sdk.Action.withoutInput(
),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.subspacesProverAuth,
visibility: 'enabled',
}),
+2 -1
View File
@@ -1,4 +1,5 @@
import { storeJson } from '../fileModels/storeJson'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { dataDir, SPACED_CHAIN } from '../utils'
@@ -15,7 +16,7 @@ export const syncStatus = sdk.Action.withoutInput(
),
warning: null,
allowedStatuses: 'only-running',
group: null,
group: actionGroups.spaced,
visibility: 'enabled',
}),
+2 -1
View File
@@ -1,4 +1,5 @@
import { readFile } from 'fs/promises'
import { actionGroups } from '../actionGroups'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { dataDir } from '../utils'
@@ -36,7 +37,7 @@ export const uploadSupportPdf = sdk.Action.withInput(
),
warning: null,
allowedStatuses: 'any',
group: null,
group: actionGroups.nacho,
visibility: 'enabled',
}),
+6 -1
View File
@@ -39,7 +39,12 @@ const shape = z.object({
certrelayBootstrap: z.boolean().nullable().catch(null),
certrelayHealthcheckHandle: z.string().nullable().catch(null),
nachoWorkshopPdfLinkText: z.string().nullable().catch(null),
subsPublishRequireFinalized: z.boolean().nullable().catch(null),
// Bearer secrets for the embedded registry-server (examples/registry-server).
// REGISTRY_API_KEY guards POST /register; SUBSD_API_KEY is the Auth Token
// for the subs↔registry channel (Settings → Registry Server → Auth Token).
// Both are required and must differ or registry-server refuses to start.
registryApiKey: z.string().nullable().catch(null),
subsdApiKey: z.string().nullable().catch(null),
})
export const storeJson = FileHelper.json(
+24 -10
View File
@@ -289,16 +289,30 @@ const dict = {
205,
'Subspaces Prover Auth credentials saved. Auth is currently DISABLED — enable it with "Enable Subspaces Prover Auth" to enforce these credentials.':
206,
'Configure Subspaces': 207,
'Set user-tunable subs options. Saving restarts the service so the subs daemon picks up the new environment.':
208,
'Require Finalized Publish': 209,
'When on, subs blocks certificate publish until commitments are finalized (150 confirmations). Maps to SUBS_PUBLISH_REQUIRE_FINALIZED in the subs daemon environment. Off by default.':
210,
'Subspaces configuration saved. SUBS_PUBLISH_REQUIRE_FINALIZED is enabled; the service is restarting.':
211,
'Subspaces configuration saved. SUBS_PUBLISH_REQUIRE_FINALIZED is disabled; the service is restarting.':
212,
'Create Registry API Keys': 213,
'Generate (or rotate) REGISTRY_API_KEY and SUBSD_API_KEY for the embedded registry-server. Both are required and must differ. Put SUBSD_API_KEY in subs Settings → Registry Server → Auth Token. REGISTRY_API_KEY is for intake clients calling POST /register. Saving restarts the service so registry-server picks up the new keys.':
214,
'Registry API keys saved. Paste SUBSD_API_KEY into subs Settings → Registry Server → Auth Token, then click Test. Use REGISTRY_API_KEY as the Bearer token for POST /register clients. The service is restarting so registry-server picks up the new keys.':
215,
REGISTRY_API_KEY: 216,
SUBSD_API_KEY: 217,
'Bearer secret for intake/POST /register clients.': 218,
'Paste into subs Settings → Registry Server → Auth Token (subs↔registry channel).':
219,
'Show Registry API Keys': 220,
'Display the stored REGISTRY_API_KEY and SUBSD_API_KEY used by registry-server. Returns blanks if Create Registry API Keys has not been run yet.':
221,
'Paste SUBSD_API_KEY into subs Settings → Registry Server → Auth Token. Use REGISTRY_API_KEY as the Bearer token for POST /register clients.':
222,
'No valid registry API keys are stored yet. Run Create Registry API Keys first (both keys must be non-empty and different).':
223,
'Subspaces registry-server requires REGISTRY_API_KEY and SUBSD_API_KEY before it can start':
224,
Bitcoin: 225,
Spaced: 226,
Subspaces: 227,
'Subspaces Auth': 228,
'Subspaces Prover Auth': 229,
} as const
/**
+2
View File
@@ -5,6 +5,7 @@ import { setInterfaces } from '../interfaces'
import { sdk } from '../sdk'
import { versionGraph } from '../versions'
import { taskBtcAuth } from './taskBtcAuth'
import { taskRegistryApiKeys } from './taskRegistryApiKeys'
import { taskSeedCertrelay } from './taskSeedCertrelay'
import { taskSeedEnableSubspaces } from './taskSeedEnableSubspaces'
import { taskSeedNacho } from './taskSeedNacho'
@@ -20,6 +21,7 @@ export const init = sdk.setupInit(
taskBtcAuth,
taskSeedSpacedAuth,
taskSeedEnableSubspaces,
taskRegistryApiKeys,
taskSeedCertrelay,
taskSeedNacho,
taskSetPassword,
+2 -1
View File
@@ -22,7 +22,8 @@ export const taskBtcAuth = sdk.setupOnInit(async (effects) => {
reason: i18n('Spaces needs RPC credentials in Bitcoin'),
input: {
kind: 'partial',
value: { username, password },
accept: [{ username, password }],
set: { username, password },
},
})
+28
View File
@@ -0,0 +1,28 @@
import { createRegistryApiKeys } from '../actions/createRegistryApiKeys'
import { storeJson } from '../fileModels/storeJson'
import { i18n } from '../i18n'
import { sdk } from '../sdk'
// registry-server refuses to start without both keys (and they must differ).
// Only prompt when Subspaces is enabled so installs that leave it off are not
// blocked; enabling Subspaces without keys surfaces this critical task.
export const taskRegistryApiKeys = sdk.setupOnInit(async (effects) => {
const store = await storeJson.read().once()
if (store?.enableSubspaces !== true) return
const registryApiKey = store?.registryApiKey
const subsdApiKey = store?.subsdApiKey
const ready =
!!registryApiKey &&
!!subsdApiKey &&
registryApiKey.length > 0 &&
subsdApiKey.length > 0 &&
registryApiKey !== subsdApiKey
if (ready) return
await sdk.action.createOwnTask(effects, createRegistryApiKeys, 'critical', {
reason: i18n(
'Subspaces registry-server requires REGISTRY_API_KEY and SUBSD_API_KEY before it can start',
),
})
})
+38 -13
View File
@@ -105,21 +105,21 @@ export const main = sdk.setupMain(async ({ effects }) => {
readonly: false,
})
const spacedSub = await sdk.SubContainer.of(
const spacedSub = sdk.SubContainer.of(
effects,
{ imageId: 'spaces' },
mounts,
'spaced-sub',
)
const termSub = await sdk.SubContainer.of(
const termSub = sdk.SubContainer.of(
effects,
{ imageId: 'spaces' },
mounts,
'terminal-sub',
)
const subspacesSub = await sdk.SubContainer.of(
const subspacesSub = sdk.SubContainer.of(
effects,
{ imageId: 'subspaces' },
mounts,
@@ -149,8 +149,6 @@ export const main = sdk.setupMain(async ({ effects }) => {
SUBS_SPACED_RPC_PASSWORD: spacedAuth.password,
SUBS_PROVER_ENDPOINT: `http://127.0.0.1:${SUBSPACES_PROVER_PORT}`,
SUBS_REGISTRY_ENDPOINT: `http://127.0.0.1:${SUBSPACES_REGISTRY_PORT}`,
SUBS_PUBLISH_REQUIRE_FINALIZED:
store.subsPublishRequireFinalized === true ? 'true' : 'false',
HOME: SUBSPACES_DATA_DIR,
RUST_LOG: 'subs=info,subs_prover=info,registry_server=info',
...subsAuthEnv,
@@ -181,9 +179,12 @@ export const main = sdk.setupMain(async ({ effects }) => {
REGISTRY_SERVER_PORT: String(SUBSPACES_REGISTRY_PORT),
HOME: SUBSPACES_REGISTRY_DIR,
RUST_LOG: 'registry_server=info',
// examples/registry-server requires both at boot; they must differ.
REGISTRY_API_KEY: store.registryApiKey ?? '',
SUBSD_API_KEY: store.subsdApiKey ?? '',
}
const certrelaySub = await sdk.SubContainer.of(
const certrelaySub = sdk.SubContainer.of(
effects,
{ imageId: 'certrelay' },
mounts,
@@ -206,7 +207,7 @@ export const main = sdk.setupMain(async ({ effects }) => {
RUST_LOG: 'info',
}
const nachoSub = await sdk.SubContainer.of(
const nachoSub = sdk.SubContainer.of(
effects,
{ imageId: 'nacho' },
mounts,
@@ -217,11 +218,17 @@ export const main = sdk.setupMain(async ({ effects }) => {
// 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')
// The interface is reached by walking its host ('subspaces-multi', the
// MultiHost that bound 7777 in interfaces.ts). `.const()` makes this
// reactive, and the `map` selector narrows the watch to this one interface:
// if its address changes (clearnet enabled, Tor added, etc.), the service
// restarts and nacho picks up the new URL.
const subsApiIf = await sdk.host
.getOwn(
effects,
'subspaces-multi',
(host) => host?.bindings[SUBSPACES_UI_PORT]?.interfaces['subs-api'],
)
.const()
const subsApiCandidateUrls = subsApiIf?.addressInfo
? [
@@ -250,7 +257,9 @@ export const main = sdk.setupMain(async ({ effects }) => {
'export PATH=/root/.cargo/bin:/data/bin:/usr/local/bin:/usr/bin:/bin',
"export PS1='spaces:\\w$ '",
`alias spaces='space-cli --chain ${SPACED_CHAIN} --rpc-user "$SPACED_RPC_USER" --rpc-password "$SPACED_RPC_PASSWORD" '`,
'cat <<EOF',
// Quoted delimiter: banner copy uses backticks around CLI names; an unquoted
// <<EOF would run command substitution (including `monitor` on PATH).
'cat <<\'EOF\'',
'',
// Banner rendered via renderBanner() — auto-wraps any line that exceeds the
// 96-cell text budget and pads every emitted line to exactly 100 cells, so
@@ -363,6 +372,22 @@ export const main = sdk.setupMain(async ({ effects }) => {
// 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.
const withSubspaces = (chain: any): any => {
const registryApiKey = store.registryApiKey
const subsdApiKey = store.subsdApiKey
if (
!registryApiKey ||
!subsdApiKey ||
registryApiKey.length === 0 ||
subsdApiKey.length === 0 ||
registryApiKey === subsdApiKey
) {
// taskRegistryApiKeys surfaces Create Registry API Keys when Subspaces is
// on without valid keys; refuse to start the registry half-configured.
throw new Error(
'Registry API keys are not configured. Run Create Registry API Keys.',
)
}
let c = chain
.addOneshot('subspaces-dirs', {
subcontainer: subspacesSub,
+4 -12
View File
@@ -14,30 +14,22 @@ export const manifest = setupManifest({
volumes: ['main'],
images: {
spaces: {
source: { dockerTag: 'horologger/spaces:v0.2.1' },
source: { dockerTag: 'horologger/spaces:v0.3.1' },
arch: ['x86_64', 'aarch64'],
},
subspaces: {
source: { dockerTag: 'horologger/subs:v0.1.2' },
source: { dockerTag: 'horologger/subs:v0.1.2d' },
arch: ['x86_64', 'aarch64'],
},
certrelay: {
source: { dockerTag: 'horologger/certrelay:v0.2.5' },
source: { dockerTag: 'horologger/certrelay:v0.2.8' },
arch: ['x86_64', 'aarch64'],
},
nacho: {
source: { dockerTag: 'horologger/nacho:v1.0.0' },
source: { dockerTag: 'horologger/nacho:v1.0.0b' },
arch: ['x86_64', 'aarch64'],
},
},
alerts: {
install: null,
update: null,
uninstall: null,
restore: null,
start: null,
stop: null,
},
dependencies: {
bitcoind: {
description: depBitcoindDescription,
+21
View File
@@ -74,6 +74,27 @@ export function randomPassword() {
})
}
// Matches upstream docs: `openssl rand -hex 32` → 64 lowercase hex chars.
export function randomApiKey() {
return utils.getDefaultString({
charset: 'a-f,0-9',
len: 64,
})
}
// registry-server refuses to start unless both keys are non-empty and different.
export function generateRegistryApiKeys(): {
registryApiKey: string
subsdApiKey: string
} {
const registryApiKey = randomApiKey()
let subsdApiKey = randomApiKey()
while (subsdApiKey === registryApiKey) {
subsdApiKey = randomApiKey()
}
return { registryApiKey, subsdApiKey }
}
// 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
+14
View File
@@ -0,0 +1,14 @@
import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk'
export const current = VersionInfo.of({
version: '0.3.1:0',
releaseNotes: {
en_US: `Update to upstream Spaces **0.3.1** (\`horologger/spaces:v0.3.1\`). Also bundles \`subs:v0.1.2d\` (\`certrelay:v0.2.8\` and \`nacho:v1.0.0b\` unchanged).
Requires StartOS 0.4.0-beta.10 (\`start-sdk\` 2.0.9).`,
},
migrations: {
up: async ({ effects }) => {},
down: IMPOSSIBLE,
},
})
+2 -2
View File
@@ -1,7 +1,7 @@
import { VersionGraph } from '@start9labs/start-sdk'
import { v_0_2_1_0 } from './v0.2.1.0'
import { current } from './current'
export const versionGraph = VersionGraph.of({
current: v_0_2_1_0,
current,
other: [],
})
-13
View File
@@ -1,13 +0,0 @@
import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_1_0 = VersionInfo.of({
version: '0.2.1:0',
releaseNotes: {
en_US: `- **spaced image** updated to \`horologger/spaces:v0.2.1\` (from v0.0.9x).
- **Certrelay image** updated to \`horologger/certrelay:v0.2.5\` (from v0.2.3).`,
},
migrations: {
up: async ({ effects }) => {},
down: IMPOSSIBLE,
},
})
+2 -9
View File
@@ -1,11 +1,4 @@
{
"include": ["startos/**/*.ts", "node_modules/**/startos"],
"compilerOptions": {
"target": "ES2018",
"module": "CommonJS",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
}
"extends": "@start9labs/start-sdk/tsconfig.base.json",
"include": ["startos/**/*.ts", "node_modules/**/startos"]
}