From 172e3f63ac46cd31f304ffdb25adc3a008a9477f Mon Sep 17 00:00:00 2001 From: spacesops Date: Thu, 14 May 2026 05:39:56 -0400 Subject: [PATCH] working --- LICENSE | 21 ++ Makefile | 3 + README.md | 192 ++++++++++++ assets/icon.png | Bin 0 -> 53043 bytes instructions.md | 64 ++++ package-lock.json | 359 ++++++++++++++++++++++ package.json | 23 ++ s9pk.mk | 132 ++++++++ startos/actions/index.ts | 13 + startos/actions/resetPassword.ts | 58 ++++ startos/actions/resetSpacedState.ts | 59 ++++ startos/actions/setBitcoinRpc.ts | 60 ++++ startos/actions/showCredentials.ts | 57 ++++ startos/actions/syncStatus.ts | 69 +++++ startos/backups.ts | 5 + startos/dependencies.ts | 9 + startos/fileModels/storeJson.ts | 18 ++ startos/i18n/dictionaries/default.ts | 63 ++++ startos/i18n/dictionaries/translations.ts | 3 + startos/i18n/index.ts | 8 + startos/index.ts | 11 + startos/init/index.ts | 20 ++ startos/init/taskBtcAuth.ts | 34 ++ startos/init/taskSetPassword.ts | 20 ++ startos/interfaces.ts | 27 ++ startos/main.ts | 187 +++++++++++ startos/manifest/i18n.ts | 12 + startos/manifest/index.ts | 39 +++ startos/sdk.ts | 9 + startos/utils.ts | 26 ++ startos/versions/index.ts | 7 + startos/versions/v0.0.9.0.a1.ts | 14 + tsconfig.json | 11 + 33 files changed, 1633 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 assets/icon.png create mode 100644 instructions.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 s9pk.mk create mode 100644 startos/actions/index.ts create mode 100644 startos/actions/resetPassword.ts create mode 100644 startos/actions/resetSpacedState.ts create mode 100644 startos/actions/setBitcoinRpc.ts create mode 100644 startos/actions/showCredentials.ts create mode 100644 startos/actions/syncStatus.ts create mode 100644 startos/backups.ts create mode 100644 startos/dependencies.ts create mode 100644 startos/fileModels/storeJson.ts create mode 100644 startos/i18n/dictionaries/default.ts create mode 100644 startos/i18n/dictionaries/translations.ts create mode 100644 startos/i18n/index.ts create mode 100644 startos/index.ts create mode 100644 startos/init/index.ts create mode 100644 startos/init/taskBtcAuth.ts create mode 100644 startos/init/taskSetPassword.ts create mode 100644 startos/interfaces.ts create mode 100644 startos/main.ts create mode 100644 startos/manifest/i18n.ts create mode 100644 startos/manifest/index.ts create mode 100644 startos/sdk.ts create mode 100644 startos/utils.ts create mode 100644 startos/versions/index.ts create mode 100644 startos/versions/v0.0.9.0.a1.ts create mode 100644 tsconfig.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9ffebe4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 horologger + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..927e1fc --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +ARCHES := x86 +# overrides to s9pk.mk must precede the include statement +include s9pk.mk diff --git a/README.md b/README.md new file mode 100644 index 0000000..d5bf9a1 --- /dev/null +++ b/README.md @@ -0,0 +1,192 @@ +

+ Spaces +

+ +# Spaces on StartOS + +> **Upstream docs:** +> +> Everything not listed in this document should behave the same as upstream +> `spaced` / `space-cli`. If a feature, setting, or behavior is not mentioned +> here, the upstream documentation is accurate and fully applicable. + +Spaces is a permissionless protocol for sovereign Bitcoin-anchored identities. +This package runs the [`spaced`](https://github.com/spacesops/spaced) daemon +against Bitcoin **mainnet** and exposes [`space-cli`](https://github.com/spacesops/spaced) +through a browser-based terminal. + +--- + +## Table of Contents + +- [Image and Container Runtime](#image-and-container-runtime) +- [Volume and Data Layout](#volume-and-data-layout) +- [Installation and First-Run Flow](#installation-and-first-run-flow) +- [Configuration Management](#configuration-management) +- [Network Access and Interfaces](#network-access-and-interfaces) +- [Actions](#actions) +- [Backups and Restore](#backups-and-restore) +- [Health Checks](#health-checks) +- [Limitations and Differences](#limitations-and-differences) +- [What Is Unchanged from Upstream](#what-is-unchanged-from-upstream) +- [Quick Reference for AI Consumers](#quick-reference-for-ai-consumers) + +--- + +## Image and Container Runtime + +| Field | Value | +| --- | --- | +| Image | `docker.io/horologger/spaces` | +| Architectures | `linux/amd64`, `linux/arm64` | +| Entrypoint | StartOS-managed (image entrypoint is **not** used) | + +The image bundles `spaced`, `space-cli`, `bitcoin-cli`, `gotty`, `node`, `npm`, +`screen`, and a small shell environment. StartOS ignores the image's +`docker_entrypoint.sh`; daemons are defined in `startos/main.ts`. + +## Volume and Data Layout + +| Path | Volume | Purpose | +| --- | --- | --- | +| `/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 (web-UI password + bitcoind RPC user/password) | + +## Installation and First-Run Flow + +On the first install, StartOS: + +1. Seeds `store.json.password` with a random 32-char alphanumeric admin password + and creates a critical task that points the user at **Show Web UI Credentials** + so the password can be copied before login. +2. Seeds `store.json.btcAuth` with a random `spaces:` RPC credential + pair and creates a critical cross-service task on **Bitcoin** that runs + bitcoind's `generate-rpc-dependent` action to register the credentials in + `bitcoin.conf`. +3. Launches `spaced` as a managed daemon (no `screen`, no shell auto-start). +4. Launches the `gotty` web terminal once `spaced` is up. + +The user is **not** prompted to choose a chain or RPC mode -- this package is +mainnet-only. + +## Configuration Management + +| StartOS-Managed | Upstream-Managed | +| --- | --- | +| 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 | +| Chain selection (locked to `mainnet`) | `space-cli` flags and subcommands | +| Spaced data directory and RPC bind | | + +## Network Access and Interfaces + +| Interface | Port | Protocol | Exposure | Notes | +| --- | --- | --- | --- | --- | +| Web UI (gotty terminal) | 8080 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | Basic auth: `admin:` | +| Spaced RPC | 7225 | HTTP JSON-RPC | **Loopback only** | Internal use, cookie-authenticated | + +## Actions + +| ID | Name | Visibility | Availability | Purpose | +| --- | --- | --- | --- | --- | +| `reset-password` | Reset Web UI Password | Enabled | Any | Regenerates the web-UI password and restarts the terminal daemon | +| `show-credentials` | Show Web UI Credentials | Hidden | Any | Surfaces the current `admin` username + masked password (launched by the first-install task) | +| `set-bitcoin-rpc` | Set up Bitcoin RPC | Enabled | Any | Re-invokes bitcoind's `generate-rpc-dependent` with the stored credentials. Safe to call repeatedly. | +| `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. | + +## Backups and Restore + +`sdk.Backups.ofVolumes('main')` -- the entire `/data` volume is backed up, +including spaced state, wallets, the block index, and `store.json`. On restore, +the same idempotent init logic runs and reuses the existing credentials in +`store.json`. + +## Health Checks + +| ID | Display | Grace period | Behaviour | +| --- | --- | --- | --- | +| `spaced` (daemon `ready`) | Spaced RPC | 120 s | TCP listen on `127.0.0.1:7225` | +| `web-terminal` (daemon `ready`) | Web Interface | default | TCP listen on `0.0.0.0:8080` | +| `sync` (standalone) | Spaced Sync | 300 s | Exec `space-cli getserverinfo`; reports `success` when `ready=true`, otherwise `loading` with progress percentage | + +## Limitations and Differences + +1. **Mainnet only.** Testnet, testnet4, signet, and regtest are not exposed. +2. **The image's `docker_entrypoint.sh` is not used.** Its auto-start of + `spaced` inside `screen` would conflict with the StartOS-managed daemon. +3. **Web UI is a terminal, not a graphical app.** All Spaces operations happen + via `space-cli` (aliased as `spaces` inside the shell). +4. **Only port 8080 (gotty) is exposed.** Spaced RPC, and any other ports + present in the image (e.g. 22253, 3000, 5173, 8081) are not bound. +5. **Bitcoin Core 31.x is the only supported dependency.** Earlier majors are + not allowed by the manifest version range. +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. + +## What Is Unchanged from Upstream + +- `space-cli` subcommands and flags work exactly as documented upstream. +- `spaced` honours all `SPACED_*` environment variables not otherwise set by + StartOS. +- Wallet files, the spaces database, and the block index are managed by + `spaced` itself; StartOS only provides the volume. +- bitcoind connectivity follows the standard StartOS dependency-service model + (`bitcoind.startos:8332`). + +## Using the Web Terminal + +After install: + +1. Run the **Show Web UI Credentials** action (the install task surfaces it). +2. Open the Web UI from the StartOS dashboard. +3. Log in with `admin` and the displayed password. +4. Use `spaces ` -- it expands to + `space-cli --chain mainnet --rpc-cookie /data/mainnet/.cookie `. + +Examples: + +```bash +spaces getserverinfo +spaces walletcreate default +spaces walletbalance default +``` + +## Quick Reference for AI Consumers + +```yaml +package_id: spaces +upstream_version: subspacesplus +image: docker.io/horologger/spaces:v0.0.9s +architectures: [x86_64, aarch64] +volumes: + main: /data +ports: + ui: 8080 + spaced_rpc: 7225 # loopback only +dependencies: + - bitcoind +startos_managed_env_vars: + - SPACED_CHAIN + - SPACED_DATA_DIR + - SPACED_RPC_BIND + - SPACED_RPC_PORT + - SPACED_RPC_URL + - SPACED_BLOCK_INDEX + - SPACED_BITCOIN_RPC_URL + - SPACED_BITCOIN_RPC_USER + - SPACED_BITCOIN_RPC_PASSWORD + - BTC_RPC_HOST + - BTC_RPC_PORT + - BTC_RPC_USER + - BTC_RPC_PASSWORD + - APP_USER + - APP_PASSWORD +actions: + - reset-password + - show-credentials + - set-bitcoin-rpc + - sync-status + - reset-spaced-state +``` diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8e51a5db95701705b5dae7a879fee4bb6b8e0fd2 GIT binary patch literal 53043 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%0DYJf=DaKoj&`s{;N<2;yl2~(EM+6^^+JpifR5?N( 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 +- Upstream docs: +- Source: +- Package source: diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..62fa0aa --- /dev/null +++ b/package-lock.json @@ -0,0 +1,359 @@ +{ + "name": "spaces-startos", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "spaces-startos", + "dependencies": { + "@start9labs/start-sdk": "1.5.1" + }, + "devDependencies": { + "@types/node": "^22.19.0", + "@vercel/ncc": "^0.38.4", + "prettier": "^3.6.2", + "typescript": "^5.9.3" + } + }, + "node_modules/@iarna/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==", + "license": "ISC" + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@start9labs/start-sdk/-/start-sdk-1.5.1.tgz", + "integrity": "sha512-iztLiOCtHuTfUCd2JOWio4OvBk5qFGa0NI+G+ZB/dQ1sWtunYEnzqMcF6N/Ss4L6+7bBOMAMU4VuhyxeZoHyIw==", + "license": "MIT", + "dependencies": { + "@iarna/toml": "^3.0.0", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0", + "@types/ini": "^4.1.1", + "deep-equality-data-structures": "^2.0.0", + "fast-xml-parser": "~5.7.0", + "ini": "^5.0.0", + "isomorphic-fetch": "^3.0.0", + "mime": "^4.1.0", + "yaml": "^2.8.3", + "zod": "4.3.6", + "zod-deep-partial": "^1.2.0" + } + }, + "node_modules/@types/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vercel/ncc": { + "version": "0.38.4", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz", + "integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==", + "dev": true, + "license": "MIT", + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/deep-equality-data-structures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/deep-equality-data-structures/-/deep-equality-data-structures-2.0.0.tgz", + "integrity": "sha512-qgrUr7MKXq7VRN+WUpQ48QlXVGL0KdibAoTX8KRg18lgOgqbEKMAW1WZsVCtakY4+XX42pbAJzTz/DlXEFM2Fg==", + "license": "MIT", + "dependencies": { + "object-hash": "^3.0.0" + } + }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/mime": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-deep-partial": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/zod-deep-partial/-/zod-deep-partial-1.4.4.tgz", + "integrity": "sha512-aWkPl7hVStgE01WzbbSxCgX4O+sSpgt8JOjvFUtMTF75VgL6MhWQbiZi+AWGN85SfSTtI9gsOtL1vInoqfDVaA==", + "license": "MIT", + "peerDependencies": { + "zod": "^4.1.13" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c9dfbbb --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "spaces-startos", + "scripts": { + "build": "rm -rf ./javascript && ncc build startos/index.ts -o ./javascript", + "prettier": "prettier --write startos", + "check": "tsc --noEmit" + }, + "dependencies": { + "@start9labs/start-sdk": "1.5.1" + }, + "devDependencies": { + "@types/node": "^22.19.0", + "@vercel/ncc": "^0.38.4", + "prettier": "^3.6.2", + "typescript": "^5.9.3" + }, + "prettier": { + "trailingComma": "all", + "tabWidth": 2, + "semi": false, + "singleQuote": true + } +} diff --git a/s9pk.mk b/s9pk.mk new file mode 100644 index 0000000..978a059 --- /dev/null +++ b/s9pk.mk @@ -0,0 +1,132 @@ +# ** 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
/.git/worktrees/ 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 diff --git a/startos/actions/index.ts b/startos/actions/index.ts new file mode 100644 index 0000000..7c4c832 --- /dev/null +++ b/startos/actions/index.ts @@ -0,0 +1,13 @@ +import { sdk } from '../sdk' +import { resetPassword } from './resetPassword' +import { resetSpacedState } from './resetSpacedState' +import { setBitcoinRpc } from './setBitcoinRpc' +import { showCredentials } from './showCredentials' +import { syncStatus } from './syncStatus' + +export const actions = sdk.Actions.of() + .addAction(resetPassword) + .addAction(showCredentials) + .addAction(setBitcoinRpc) + .addAction(syncStatus) + .addAction(resetSpacedState) diff --git a/startos/actions/resetPassword.ts b/startos/actions/resetPassword.ts new file mode 100644 index 0000000..1d4169a --- /dev/null +++ b/startos/actions/resetPassword.ts @@ -0,0 +1,58 @@ +import { storeJson } from '../fileModels/storeJson' +import { i18n } from '../i18n' +import { sdk } from '../sdk' +import { APP_USER, randomPassword } from '../utils' + +export const resetPassword = sdk.Action.withoutInput( + // id + 'reset-password', + + // metadata + async ({ effects }) => ({ + name: i18n('Reset Web UI Password'), + description: i18n( + 'Generate a new admin password for the Spaces web terminal', + ), + warning: null, + allowedStatuses: 'any', + group: null, + visibility: 'enabled', + }), + + // run + async ({ effects }) => { + const password = randomPassword() + await storeJson.merge(effects, { password }) + + return { + version: '1', + title: i18n('Success'), + message: i18n( + 'Use these credentials to log in to the Spaces web terminal.', + ), + result: { + type: 'group', + value: [ + { + type: 'single', + name: i18n('Username'), + description: null, + value: APP_USER, + masked: false, + copyable: true, + qr: false, + }, + { + type: 'single', + name: i18n('Password'), + description: null, + value: password, + masked: true, + copyable: true, + qr: false, + }, + ], + }, + } + }, +) diff --git a/startos/actions/resetSpacedState.ts b/startos/actions/resetSpacedState.ts new file mode 100644 index 0000000..d1f9f0e --- /dev/null +++ b/startos/actions/resetSpacedState.ts @@ -0,0 +1,59 @@ +import { i18n } from '../i18n' +import { sdk } from '../sdk' +import { dataDir, SPACED_CHAIN } from '../utils' + +export const resetSpacedState = sdk.Action.withoutInput( + // id + 'reset-spaced-state', + + // metadata + async ({ effects }) => ({ + name: i18n('Reset Spaced State'), + description: i18n( + "Wipe /data/mainnet/ so spaced resyncs its index from spaces' anchor.", + ), + warning: i18n( + "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, + 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-state', + (subc) => + subc.exec(['rm', '-rf', `${dataDir}/${SPACED_CHAIN}`], { user: 'root' }), + ) + + if (res.exitCode !== 0) { + return { + version: '1', + title: i18n('Failure'), + message: i18n('Could not wipe spaced state: ${error}', { + error: (res.stderr ?? '').toString() || `exit ${res.exitCode}`, + }), + result: null, + } + } + + return { + version: '1', + title: i18n('Success'), + message: i18n( + 'Spaced state has been wiped. Start (or restart) the service to resync.', + ), + result: null, + } + }, +) diff --git a/startos/actions/setBitcoinRpc.ts b/startos/actions/setBitcoinRpc.ts new file mode 100644 index 0000000..a03ffa7 --- /dev/null +++ b/startos/actions/setBitcoinRpc.ts @@ -0,0 +1,60 @@ +import { storeJson } from '../fileModels/storeJson' +import { i18n } from '../i18n' +import { sdk } from '../sdk' +import { + BITCOIN_RPC_USER, + BITCOIND_PACKAGE_ID, + randomPassword, +} from '../utils' + +export const setBitcoinRpc = sdk.Action.withoutInput( + // id + 'set-bitcoin-rpc', + + // metadata + async ({ effects }) => ({ + name: i18n('Set up Bitcoin RPC'), + description: i18n('Re-run the bitcoind RPC credential setup for Spaces'), + warning: null, + allowedStatuses: 'any', + group: null, + visibility: 'enabled', + }), + + // run + async ({ effects }) => { + const existing = await storeJson.read((s) => s.btcAuth).once() + const username = existing?.username ?? BITCOIN_RPC_USER + const password = existing?.password ?? randomPassword() + + try { + await effects.action.run({ + packageId: BITCOIND_PACKAGE_ID, + actionId: 'generate-rpc-dependent', + input: { username, password }, + }) + } catch (err) { + return { + version: '1', + title: i18n('Failure'), + message: i18n('Could not call bitcoind. Is Bitcoin installed and running?'), + result: null, + } + } + + if (!existing) { + await storeJson.merge( + effects, + { btcAuth: { username, password } }, + { allowWriteAfterConst: true }, + ) + } + + return { + version: '1', + title: i18n('Success'), + message: i18n('Bitcoin RPC credentials have been re-sent to bitcoind.'), + result: null, + } + }, +) diff --git a/startos/actions/showCredentials.ts b/startos/actions/showCredentials.ts new file mode 100644 index 0000000..4cb9d80 --- /dev/null +++ b/startos/actions/showCredentials.ts @@ -0,0 +1,57 @@ +import { storeJson } from '../fileModels/storeJson' +import { i18n } from '../i18n' +import { sdk } from '../sdk' +import { APP_USER } from '../utils' + +export const showCredentials = sdk.Action.withoutInput( + // id + 'show-credentials', + + // metadata + async ({ effects }) => ({ + name: i18n('Show Web UI Credentials'), + description: i18n( + 'Display the username and password for the Spaces web terminal', + ), + warning: null, + allowedStatuses: 'any', + group: null, + visibility: 'hidden', + }), + + // run + async ({ effects }) => { + const password = await storeJson.read((s) => s.password).once() + + return { + version: '1', + title: i18n('Show Web UI Credentials'), + message: i18n( + 'Use these credentials to log in to the Spaces web terminal.', + ), + result: { + type: 'group', + value: [ + { + type: 'single', + name: i18n('Username'), + description: null, + value: APP_USER, + masked: false, + copyable: true, + qr: false, + }, + { + type: 'single', + name: i18n('Password'), + description: null, + value: password ?? '', + masked: true, + copyable: true, + qr: false, + }, + ], + }, + } + }, +) diff --git a/startos/actions/syncStatus.ts b/startos/actions/syncStatus.ts new file mode 100644 index 0000000..c256a93 --- /dev/null +++ b/startos/actions/syncStatus.ts @@ -0,0 +1,69 @@ +import { i18n } from '../i18n' +import { sdk } from '../sdk' +import { dataDir, SPACED_CHAIN } from '../utils' + +export const syncStatus = sdk.Action.withoutInput( + // id + 'sync-status', + + // metadata + async ({ effects }) => ({ + name: i18n('Sync Status'), + description: i18n( + "Query spaced's getserverinfo RPC and report sync progress", + ), + warning: null, + allowedStatuses: 'only-running', + 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-sync-status', + (subc) => + subc.exec([ + '/root/.cargo/bin/space-cli', + '--chain', + SPACED_CHAIN, + '--rpc-cookie', + `${dataDir}/${SPACED_CHAIN}/.cookie`, + 'getserverinfo', + ]), + ) + + if (res.exitCode !== 0) { + return { + version: '1', + title: i18n('Failure'), + message: i18n('Could not query spaced. Is the service running?'), + result: null, + } + } + + const stdout = (res.stdout ?? '').toString().trim() + return { + version: '1', + title: i18n('Sync Status'), + message: stdout || i18n('spaced is fully synced.'), + result: { + type: 'single', + name: 'getserverinfo', + description: null, + value: stdout, + masked: false, + copyable: true, + qr: false, + }, + } + }, +) diff --git a/startos/backups.ts b/startos/backups.ts new file mode 100644 index 0000000..0a90b1e --- /dev/null +++ b/startos/backups.ts @@ -0,0 +1,5 @@ +import { sdk } from './sdk' + +export const { createBackup, restoreInit } = sdk.setupBackups( + async ({ effects }) => sdk.Backups.ofVolumes('main'), +) diff --git a/startos/dependencies.ts b/startos/dependencies.ts new file mode 100644 index 0000000..9d90098 --- /dev/null +++ b/startos/dependencies.ts @@ -0,0 +1,9 @@ +import { sdk } from './sdk' + +export const setDependencies = sdk.setupDependencies(async ({ effects }) => ({ + bitcoind: { + kind: 'running', + versionRange: '>=31.0:0 <32.0:0', + healthChecks: ['bitcoind'], + }, +})) diff --git a/startos/fileModels/storeJson.ts b/startos/fileModels/storeJson.ts new file mode 100644 index 0000000..b5e3ba0 --- /dev/null +++ b/startos/fileModels/storeJson.ts @@ -0,0 +1,18 @@ +import { FileHelper, z } from '@start9labs/start-sdk' +import { sdk } from '../sdk' + +const shape = z.object({ + password: z.string().nullable().catch(null), + btcAuth: z + .object({ + username: z.string(), + password: z.string(), + }) + .nullable() + .catch(null), +}) + +export const storeJson = FileHelper.json( + { base: sdk.volumes.main, subpath: 'store.json' }, + shape, +) diff --git a/startos/i18n/dictionaries/default.ts b/startos/i18n/dictionaries/default.ts new file mode 100644 index 0000000..83a9abf --- /dev/null +++ b/startos/i18n/dictionaries/default.ts @@ -0,0 +1,63 @@ +export const DEFAULT_LANG = 'en_US' + +const dict = { + // main.ts + 'Starting Spaces!': 0, + 'Web Interface': 1, + 'The web terminal is ready': 2, + 'The web terminal is not ready': 3, + 'Spaced RPC': 4, + 'spaced RPC is ready': 5, + 'spaced RPC is not ready': 6, + 'Spaced Sync': 7, + 'spaced is querying Bitcoin and indexing — this can take a while on first run.': + 8, + 'spaced is fully synced.': 9, + 'spaced is indexing. Progress: ${pct}%': 10, + 'spaced RPC did not respond.': 11, + + // interfaces.ts + 'Web UI': 12, + 'Browser terminal that exposes space-cli inside the Spaces container.': 13, + + // dependencies.ts / tasks + 'Spaces needs RPC credentials in Bitcoin': 14, + 'Spaces needs an admin password for the web terminal': 15, + + // actions + 'Reset Web UI Password': 16, + 'Generate a new admin password for the Spaces web terminal': 17, + 'Show Web UI Credentials': 18, + 'Display the username and password for the Spaces web terminal': 19, + 'Set up Bitcoin RPC': 20, + 'Re-run the bitcoind RPC credential setup for Spaces': 21, + 'Sync Status': 22, + "Query spaced's getserverinfo RPC and report sync progress": 23, + Username: 24, + Password: 25, + Success: 26, + Failure: 27, + 'Bitcoin RPC credentials have been re-sent to bitcoind.': 28, + 'Could not call bitcoind. Is Bitcoin installed and running?': 29, + 'Could not query spaced. Is the service running?': 30, + 'Use these credentials to log in to the Spaces web terminal.': 31, + + // manifest description fields + 'Sovereign Bitcoin identities.': 32, + + // misc + 'Configure your Spaces web terminal password': 33, + 'Reset Spaced State': 34, + "Wipe /data/mainnet/ so spaced resyncs its index from spaces' anchor.": 35, + "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.": + 36, + 'Spaced state has been wiped. Start (or restart) the service to resync.': 37, + 'Could not wipe spaced state: ${error}': 38, +} as const + +/** + * Plumbing. DO NOT EDIT. + */ +export type I18nKey = keyof typeof dict +export type LangDict = Record<(typeof dict)[I18nKey], string> +export default dict diff --git a/startos/i18n/dictionaries/translations.ts b/startos/i18n/dictionaries/translations.ts new file mode 100644 index 0000000..98f9348 --- /dev/null +++ b/startos/i18n/dictionaries/translations.ts @@ -0,0 +1,3 @@ +import { LangDict } from './default' + +export default {} satisfies Record diff --git a/startos/i18n/index.ts b/startos/i18n/index.ts new file mode 100644 index 0000000..04cea20 --- /dev/null +++ b/startos/i18n/index.ts @@ -0,0 +1,8 @@ +/** + * Plumbing. DO NOT EDIT this file. + */ +import { setupI18n } from '@start9labs/start-sdk' +import defaultDict, { DEFAULT_LANG } from './dictionaries/default' +import translations from './dictionaries/translations' + +export const i18n = setupI18n(defaultDict, translations, DEFAULT_LANG) diff --git a/startos/index.ts b/startos/index.ts new file mode 100644 index 0000000..7af589b --- /dev/null +++ b/startos/index.ts @@ -0,0 +1,11 @@ +/** + * Plumbing. DO NOT EDIT. + */ +export { createBackup } from './backups' +export { main } from './main' +export { init, uninit } from './init' +export { actions } from './actions' +import { buildManifest } from '@start9labs/start-sdk' +import { manifest as sdkManifest } from './manifest' +import { versionGraph } from './versions' +export const manifest = buildManifest(versionGraph, sdkManifest) diff --git a/startos/init/index.ts b/startos/init/index.ts new file mode 100644 index 0000000..3dd81a2 --- /dev/null +++ b/startos/init/index.ts @@ -0,0 +1,20 @@ +import { actions } from '../actions' +import { restoreInit } from '../backups' +import { setDependencies } from '../dependencies' +import { setInterfaces } from '../interfaces' +import { sdk } from '../sdk' +import { versionGraph } from '../versions' +import { taskBtcAuth } from './taskBtcAuth' +import { taskSetPassword } from './taskSetPassword' + +export const init = sdk.setupInit( + restoreInit, + versionGraph, + setInterfaces, + setDependencies, + actions, + taskBtcAuth, + taskSetPassword, +) + +export const uninit = sdk.setupUninit(versionGraph) diff --git a/startos/init/taskBtcAuth.ts b/startos/init/taskBtcAuth.ts new file mode 100644 index 0000000..2615a95 --- /dev/null +++ b/startos/init/taskBtcAuth.ts @@ -0,0 +1,34 @@ +import { storeJson } from '../fileModels/storeJson' +import { i18n } from '../i18n' +import { sdk } from '../sdk' +import { + BITCOIN_RPC_USER, + BITCOIND_PACKAGE_ID, + randomPassword, +} from '../utils' + +export const taskBtcAuth = sdk.setupOnInit(async (effects) => { + const existing = await storeJson.read((s) => s.btcAuth).once() + if (existing) return + + const username = BITCOIN_RPC_USER + const password = randomPassword() + + await effects.action.createTask({ + replayId: 'spaces-rpc-auth', + packageId: BITCOIND_PACKAGE_ID, + actionId: 'generate-rpc-dependent', + severity: 'critical', + reason: i18n('Spaces needs RPC credentials in Bitcoin'), + input: { + kind: 'partial', + value: { username, password }, + }, + }) + + await storeJson.merge( + effects, + { btcAuth: { username, password } }, + { allowWriteAfterConst: true }, + ) +}) diff --git a/startos/init/taskSetPassword.ts b/startos/init/taskSetPassword.ts new file mode 100644 index 0000000..1933311 --- /dev/null +++ b/startos/init/taskSetPassword.ts @@ -0,0 +1,20 @@ +import { showCredentials } from '../actions/showCredentials' +import { storeJson } from '../fileModels/storeJson' +import { i18n } from '../i18n' +import { sdk } from '../sdk' +import { randomPassword } from '../utils' + +export const taskSetPassword = sdk.setupOnInit(async (effects) => { + const existing = await storeJson.read((s) => s.password).once() + if (existing) return + + await storeJson.merge( + effects, + { password: randomPassword() }, + { allowWriteAfterConst: true }, + ) + + await sdk.action.createOwnTask(effects, showCredentials, 'critical', { + reason: i18n('Spaces needs an admin password for the web terminal'), + }) +}) diff --git a/startos/interfaces.ts b/startos/interfaces.ts new file mode 100644 index 0000000..79a9719 --- /dev/null +++ b/startos/interfaces.ts @@ -0,0 +1,27 @@ +import { i18n } from './i18n' +import { sdk } from './sdk' +import { uiPort } from './utils' + +export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => { + const uiMulti = sdk.MultiHost.of(effects, 'ui-multi') + const uiMultiOrigin = await uiMulti.bindPort(uiPort, { + protocol: 'http', + }) + const ui = sdk.createInterface(effects, { + name: i18n('Web UI'), + id: 'ui', + description: i18n( + 'Browser terminal that exposes space-cli inside the Spaces container.', + ), + type: 'ui', + masked: false, + schemeOverride: null, + username: null, + path: '', + query: {}, + }) + + const uiReceipt = await uiMultiOrigin.export([ui]) + + return [uiReceipt] +}) diff --git a/startos/main.ts b/startos/main.ts new file mode 100644 index 0000000..94a3cc4 --- /dev/null +++ b/startos/main.ts @@ -0,0 +1,187 @@ +import { storeJson } from './fileModels/storeJson' +import { i18n } from './i18n' +import { sdk } from './sdk' +import { + APP_USER, + BITCOIND_RPC_HOSTNAME, + BITCOIND_RPC_PORT, + dataDir, + SPACED_CHAIN, + spacedRpcPort, + uiPort, +} from './utils' + +export const main = sdk.setupMain(async ({ effects }) => { + console.info(i18n('Starting Spaces!')) + + const store = await storeJson.read().const(effects) + if (!store?.password || !store?.btcAuth) { + // taskSetPassword + taskBtcAuth both seed these in init; if they aren't + // populated yet, init hasn't finished — let StartOS restart us. + throw new Error('Spaces store.json is not yet populated.') + } + + const { password: APP_PASSWORD, btcAuth } = store + + const spacedEnv = { + SPACED_CHAIN, + SPACED_DATA_DIR: dataDir, + SPACED_RPC_BIND: '127.0.0.1', + SPACED_RPC_PORT: String(spacedRpcPort), + SPACED_RPC_URL: `http://127.0.0.1:${spacedRpcPort}`, + SPACED_BLOCK_INDEX: 'true', + SPACED_BITCOIN_RPC_URL: `http://${BITCOIND_RPC_HOSTNAME}:${BITCOIND_RPC_PORT}`, + SPACED_BITCOIN_RPC_USER: btcAuth.username, + SPACED_BITCOIN_RPC_PASSWORD: btcAuth.password, + // legacy aliases for `bitcoin-cli` / shell helpers that read these names + BTC_RPC_HOST: BITCOIND_RPC_HOSTNAME, + BTC_RPC_PORT: String(BITCOIND_RPC_PORT), + BTC_RPC_USER: btcAuth.username, + BTC_RPC_PASSWORD: btcAuth.password, + APP_USER, + APP_PASSWORD, + } + + const mounts = sdk.Mounts.of().mountVolume({ + volumeId: 'main', + subpath: null, + mountpoint: dataDir, + readonly: false, + }) + + const spacedSub = await sdk.SubContainer.of( + effects, + { imageId: 'spaces' }, + mounts, + 'spaced-sub', + ) + + const termSub = await sdk.SubContainer.of( + effects, + { imageId: 'spaces' }, + mounts, + 'terminal-sub', + ) + + const bashrc = [ + '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-cookie ${dataDir}/${SPACED_CHAIN}/.cookie '`, + 'cat < /root/.bashrc <<'SPACES_BASHRC_EOF' +${bashrc} +SPACES_BASHRC_EOF`], + user: 'root', + }, + requires: [], + }) + .addDaemon('spaced', { + subcontainer: spacedSub, + exec: { + command: ['/root/.cargo/bin/spaced'], + env: spacedEnv, + }, + ready: { + display: i18n('Spaced RPC'), + fn: () => + sdk.healthCheck.checkPortListening(effects, spacedRpcPort, { + successMessage: i18n('spaced RPC is ready'), + errorMessage: i18n('spaced RPC is not ready'), + }), + gracePeriod: 120_000, + }, + requires: [], + }) + .addDaemon('web-terminal', { + subcontainer: termSub, + exec: { + command: [ + 'gotty', + '--port', + String(uiPort), + '-c', + `${APP_USER}:${APP_PASSWORD}`, + '--permit-write', + '--reconnect', + '/bin/bash', + ], + env: spacedEnv, + }, + ready: { + display: i18n('Web Interface'), + fn: () => + sdk.healthCheck.checkPortListening(effects, uiPort, { + successMessage: i18n('The web terminal is ready'), + errorMessage: i18n('The web terminal is not ready'), + }), + }, + requires: ['bashrc'], + }) + .addHealthCheck('sync', { + ready: { + display: i18n('Spaced Sync'), + fn: async () => { + const probe = await spacedSub.exec( + [ + 'bash', + '-c', + `/root/.cargo/bin/space-cli --chain ${SPACED_CHAIN} --rpc-cookie ${dataDir}/${SPACED_CHAIN}/.cookie getserverinfo`, + ], + {}, + ) + + if (probe.exitCode !== 0) { + return { + result: 'starting', + message: i18n( + 'spaced is querying Bitcoin and indexing — this can take a while on first run.', + ), + } + } + + const stdout = (probe.stdout ?? '').toString() + let progress = 0 + try { + const parsed = JSON.parse(stdout) as { + ready?: boolean + progress?: number + chain?: { blocks?: number; headers?: number } + } + if (parsed.ready === true) { + return { + result: 'success', + message: i18n('spaced is fully synced.'), + } + } + progress = Math.floor((parsed.progress ?? 0) * 100) + } catch { + // fall through to loading + } + + return { + result: 'loading', + message: i18n('spaced is indexing. Progress: ${pct}%', { + pct: String(progress), + }), + } + }, + gracePeriod: 300_000, + }, + requires: ['spaced'], + }) +}) diff --git a/startos/manifest/i18n.ts b/startos/manifest/i18n.ts new file mode 100644 index 0000000..a7d75a5 --- /dev/null +++ b/startos/manifest/i18n.ts @@ -0,0 +1,12 @@ +export const short = { + en_US: 'Sovereign Bitcoin identities.', +} + +export const long = { + en_US: + 'Spaces is a permissionless protocol for sovereign Bitcoin-anchored identities. This package runs the spaced daemon against Bitcoin mainnet and exposes space-cli through a browser-based terminal.', +} + +export const depBitcoindDescription = { + en_US: 'Provides the Bitcoin block source spaced indexes.', +} diff --git a/startos/manifest/index.ts b/startos/manifest/index.ts new file mode 100644 index 0000000..cd73a3e --- /dev/null +++ b/startos/manifest/index.ts @@ -0,0 +1,39 @@ +import { setupManifest } from '@start9labs/start-sdk' +import { depBitcoindDescription, long, short } from './i18n' + +export const manifest = setupManifest({ + id: 'spaces', + title: 'Spaces', + license: 'MIT', + packageRepo: 'https://github.com/horologger/spaces-startos', + upstreamRepo: 'https://github.com/spacesops/spaced', + marketingUrl: 'https://spacesprotocol.org', + donationUrl: null, + docsUrls: ['https://docs.spacesprotocol.org/'], + description: { short, long }, + volumes: ['main'], + images: { + spaces: { + source: { dockerTag: 'horologger/spaces:v0.0.9s' }, + arch: ['x86_64', 'aarch64'], + }, + }, + alerts: { + install: null, + update: null, + uninstall: null, + restore: null, + start: null, + stop: null, + }, + dependencies: { + bitcoind: { + description: depBitcoindDescription, + optional: false, + metadata: { + title: 'Bitcoin', + icon: 'https://raw.githubusercontent.com/Start9Labs/bitcoin-core-startos/feec0b1dae42961a257948fe39b40caf8672fce1/dep-icon.svg', + }, + }, + }, +}) diff --git a/startos/sdk.ts b/startos/sdk.ts new file mode 100644 index 0000000..04ae4b1 --- /dev/null +++ b/startos/sdk.ts @@ -0,0 +1,9 @@ +import { StartSdk } from '@start9labs/start-sdk' +import { manifest } from './manifest' + +/** + * Plumbing. DO NOT EDIT. + * + * The exported "sdk" const is used throughout this package codebase. + */ +export const sdk = StartSdk.of().withManifest(manifest).build(true) diff --git a/startos/utils.ts b/startos/utils.ts new file mode 100644 index 0000000..8466b84 --- /dev/null +++ b/startos/utils.ts @@ -0,0 +1,26 @@ +import { utils } from '@start9labs/start-sdk' + +export const uiPort = 8080 + +export const spacedRpcPort = 7225 + +export const dataDir = '/data' + +export const APP_USER = 'admin' + +export const BITCOIN_RPC_USER = 'spaces' + +export const BITCOIND_PACKAGE_ID = 'bitcoind' +export const BITCOIND_RPC_HOSTNAME = 'bitcoind.startos' +export const BITCOIND_RPC_PORT = 8332 + +export const SPACED_CHAIN = 'mainnet' + +export function randomPassword() { + // bitcoind's generate-rpc-dependent action validates the password against + // /^[A-Za-z0-9_-]+$/, so the charset must stay in that set. + return utils.getDefaultString({ + charset: 'a-z,A-Z,1-9', + len: 32, + }) +} diff --git a/startos/versions/index.ts b/startos/versions/index.ts new file mode 100644 index 0000000..7893a04 --- /dev/null +++ b/startos/versions/index.ts @@ -0,0 +1,7 @@ +import { VersionGraph } from '@start9labs/start-sdk' +import { v_0_0_9_0_a1 } from './v0.0.9.0.a1' + +export const versionGraph = VersionGraph.of({ + current: v_0_0_9_0_a1, + other: [], +}) diff --git a/startos/versions/v0.0.9.0.a1.ts b/startos/versions/v0.0.9.0.a1.ts new file mode 100644 index 0000000..eb301e0 --- /dev/null +++ b/startos/versions/v0.0.9.0.a1.ts @@ -0,0 +1,14 @@ +import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' + +export const v_0_0_9_0_a1 = VersionInfo.of({ + version: '0.0.9:0-alpha.1', + releaseNotes: { + en_US: `- Add "Reset Spaced State" action so a corrupt /data/mainnet/ index can be wiped from the UI. +- Web terminal no longer requires spaced to be healthy, so gotty stays reachable when spaced crash-loops. +- Initial build bundles spaced + space-cli from horologger/spaces:v0.0.9s, managed spaced daemon (mainnet only), gotty browser terminal with admin basic auth, and the Bitcoin Core 31.x dependency.`, + }, + migrations: { + up: async ({ effects }) => {}, + down: IMPOSSIBLE, + }, +}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a2945a5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "include": ["startos/**/*.ts", "node_modules/**/startos"], + "compilerOptions": { + "target": "ES2018", + "module": "CommonJS", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true + } +}