Build SpacesOps StartOS package (v1.0.0:0)
Initial .s9pk for SpacesOps, targeting StartOS 0.4.0.x with SDK 1.5.1. - Single managed daemon from spacesops/spacesops:v1.0.0 (x86_64 + aarch64), keeping the image entrypoint (/app/docker-entrypoint.sh node server.js). Forces PLATFORM_HOST=0.0.0.0 / PLATFORM_PORT=7264 so the StartOS proxy can reach the app. Single `ui` interface on 7264. - Depends on the Spaces service (>=0.0.9:3) and auto-wires the spaced RPC creds: main.ts mounts the Spaces `main` volume read-only at /spaces-data, execs a read of its store.json inside the subcontainer, and injects SPACED_RPC_USER/ PASSWORD + SPACED_RPC_URL=http://spaces.startos:7225. Throws to retry until Spaces is installed and seeded. - Generates a Nostr operator keypair (nostr-tools, bundled by ncc) and a strong session secret in idempotent init tasks (.once() reads, allowWriteAfterConst merges). Actions: show-operator-credentials, import-operator-key, show-admin-credentials (surfaces the fixed admin/Whatever! login with a warning), configure-platform (optional relay/mode/CoinGecko/SUBSD). - Install alert warns to install Spaces first and about the fixed admin credential. Backs up the `main` volume. - Icon: icon.svg (source spacesops.svg). The `spaces` dependency uses assets/spaces-icon.png for its Marketplace metadata. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
*.s9pk
|
||||||
|
startos/*.js
|
||||||
|
node_modules/
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
docker-images
|
||||||
|
javascript
|
||||||
|
ncc-cache
|
||||||
|
NOTES.md
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
This repo packages [SpacesOps](https://git.spacesops.com/spacesops/spacesops)
|
||||||
|
for StartOS.
|
||||||
|
|
||||||
|
## Documentation — keep it in sync
|
||||||
|
|
||||||
|
- **`README.md`** — what this package is and how it's built (image, volume,
|
||||||
|
interface, dependency, actions). For developers and AI assistants.
|
||||||
|
- **`instructions.md`** — the user-facing instructions packed into the `.s9pk`
|
||||||
|
and shown on the **Instructions** tab in StartOS.
|
||||||
|
- **`CONTRIBUTING.md`** — this file.
|
||||||
|
|
||||||
|
**Any code change that warrants it must update `README.md` and `instructions.md`
|
||||||
|
in the same change** — a new or renamed action, an added or removed volume /
|
||||||
|
port / interface / dependency, a changed default, a new limitation, any altered
|
||||||
|
user-visible behavior. A package that ships with a stale README or stale
|
||||||
|
instructions is not done, even if the code is perfect. Content rules:
|
||||||
|
[Writing READMEs](https://docs.start9.com/packaging/writing-readmes.html).
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
See the [StartOS Packaging Guide](https://docs.start9.com/packaging/) for
|
||||||
|
environment setup, then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm ci # install dependencies
|
||||||
|
make # build the .s9pk for x86_64 and aarch64
|
||||||
|
```
|
||||||
|
|
||||||
|
`npm run check` runs the TypeScript type-check; `npm run build` bundles
|
||||||
|
`startos/` into `javascript/index.js` with `@vercel/ncc` (which inlines
|
||||||
|
`nostr-tools`, used to derive and encode the operator keypair).
|
||||||
|
|
||||||
|
## Updating the upstream version
|
||||||
|
|
||||||
|
SpacesOps runs the `docker.io/spacesops/spacesops` image. To track a new
|
||||||
|
upstream release:
|
||||||
|
|
||||||
|
1. Bump `dockerTag` in `startos/manifest/index.ts` to
|
||||||
|
`spacesops/spacesops:<new version>`.
|
||||||
|
2. Update `version` and `releaseNotes` in the file under `startos/versions/`,
|
||||||
|
renaming it to the new version string. A *new* version file is only needed
|
||||||
|
when the bump carries an `up`/`down` migration, or when you want the old
|
||||||
|
release notes preserved in git history — see
|
||||||
|
[Versions](https://docs.start9.com/packaging/versions.html).
|
||||||
|
3. Rebuild (`make`), sideload the `.s9pk`, and confirm it starts and connects to
|
||||||
|
Spaces.
|
||||||
|
4. Review `README.md` and `instructions.md` for anything the bump changed.
|
||||||
|
|
||||||
|
## How auto-wiring to Spaces works
|
||||||
|
|
||||||
|
`startos/main.ts` mounts the Spaces `main` volume read-only at `/spaces-data`,
|
||||||
|
execs `cat /spaces-data/store.json` inside the subcontainer, and reads
|
||||||
|
`spacedAuth.{username,password}` to populate `SPACED_RPC_USER` /
|
||||||
|
`SPACED_RPC_PASSWORD`. If the file or credentials are missing, it throws so
|
||||||
|
StartOS restarts the service until Spaces is ready. The Spaces dependency must
|
||||||
|
expose its `spaces-api` interface and seed `spacedAuth` (Spaces `>=0.0.9:3`).
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 SpacesOps
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# spacesops ships linux/amd64 + linux/arm64 only (no riscv), so build both.
|
||||||
|
ARCHES := x86 arm
|
||||||
|
# overrides to s9pk.mk must precede the include statement
|
||||||
|
include s9pk.mk
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="icon.svg" alt="SpacesOps" width="180" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# SpacesOps on StartOS
|
||||||
|
|
||||||
|
> **Upstream repo:** <https://git.spacesops.com/spacesops/spacesops>
|
||||||
|
>
|
||||||
|
> Everything not listed in this document should behave the same as upstream
|
||||||
|
> SpacesOps. If a feature, setting, or behavior is not mentioned here, the
|
||||||
|
> upstream documentation is accurate and fully applicable.
|
||||||
|
|
||||||
|
SpacesOps is a web platform for confirming [Spaces](https://spacesprotocol.org)
|
||||||
|
ownership and selling subspace names. It talks to the Spaces `spaced` daemon
|
||||||
|
over JSON-RPC, signs operator events on Nostr, and optionally integrates pricing
|
||||||
|
and subname-purchase backends. On StartOS it **requires the Spaces service** and
|
||||||
|
auto-connects to it — see [Installation and First-Run Flow](#installation-and-first-run-flow).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
- [The Spaces Dependency (auto-wiring)](#the-spaces-dependency-auto-wiring)
|
||||||
|
- [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/spacesops/spacesops:v1.0.0` |
|
||||||
|
| Base | `node:20-alpine` |
|
||||||
|
| Architectures | `x86_64` (linux/amd64), `aarch64` (linux/arm64) |
|
||||||
|
| Entrypoint | The image's `/app/docker-entrypoint.sh` is **kept**. StartOS runs `/app/docker-entrypoint.sh node server.js` as the daemon. |
|
||||||
|
| Runs as | `root`, working directory `/app` |
|
||||||
|
|
||||||
|
The image bundles Node 20 and the app's `node_modules` (including `nostr-tools`
|
||||||
|
and `bitcoinjs-lib`). The entrypoint creates `/data{,/spaces,/signed}`, symlinks
|
||||||
|
`/app/data` → `/data`, and loads defaults from `/app/setup-spacesops-env.sh` for
|
||||||
|
any environment variable StartOS leaves unset, then exec's `node server.js`.
|
||||||
|
|
||||||
|
## Volume and Data Layout
|
||||||
|
|
||||||
|
| Path | Volume | Purpose |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `/data` | `main` | SpacesOps data directory. SQLite databases (`local.db`, `sessions.db`, `prices.db`, per-tenant DBs), signed artifacts, and StartOS `store.json`. |
|
||||||
|
| `/data/local.db` | `main` | Primary SQLite database (`PLATFORM_DB_PATH`). DB migrations auto-run on startup. |
|
||||||
|
| `/data/store.json` | `main` | StartOS-managed settings: the generated Nostr operator keypair, the generated session secret, and optional Configure-Platform values. |
|
||||||
|
| `/spaces-data` | _Spaces `main`_ | The **Spaces** service's volume, mounted **read-only**. Used only at startup to read the spaced RPC credentials — see below. |
|
||||||
|
|
||||||
|
## Installation and First-Run Flow
|
||||||
|
|
||||||
|
On first install, StartOS:
|
||||||
|
|
||||||
|
1. Generates a **Nostr operator keypair** (`operatorSecretHex` /
|
||||||
|
`operatorPublicHex` in `store.json`). SpacesOps requires this keypair at
|
||||||
|
startup; the **Show Operator Credentials** action surfaces it (npub / nsec /
|
||||||
|
hex), and **Import Operator Key** replaces it with one you supply.
|
||||||
|
2. Generates a strong **session secret** (`sessionSecret`), replacing the weak
|
||||||
|
hardcoded default the upstream image ships with.
|
||||||
|
3. Reads the **spaced RPC credentials from the Spaces service** and connects to
|
||||||
|
it (see next section). The service will not finish starting until those
|
||||||
|
credentials are available.
|
||||||
|
|
||||||
|
There is no upstream setup wizard to complete — the operator keypair and session
|
||||||
|
secret are provisioned automatically.
|
||||||
|
|
||||||
|
## The Spaces Dependency (auto-wiring)
|
||||||
|
|
||||||
|
SpacesOps **requires the Spaces service** (`spaces`, version `>=0.0.9:3`).
|
||||||
|
**Install and sync Spaces first.** SpacesOps does not ask you for spaced RPC
|
||||||
|
credentials. Instead, at startup it:
|
||||||
|
|
||||||
|
1. Mounts the Spaces `main` volume read-only at `/spaces-data`.
|
||||||
|
2. Reads `spacedAuth.{username,password}` from the Spaces `store.json`.
|
||||||
|
3. Connects to spaced at `http://spaces.startos:7225` with those credentials,
|
||||||
|
exporting `SPACED_RPC_URL`, `SPACED_RPC_USER`, `SPACED_RPC_PASSWORD`, and
|
||||||
|
`SPACED_WALLETLOAD_NAME=main`.
|
||||||
|
|
||||||
|
If Spaces is not installed yet, or its credentials have not been seeded, the
|
||||||
|
service **restarts until they appear** — this is expected. (Dependency
|
||||||
|
declarations do not gate startup; SpacesOps handles readiness itself.)
|
||||||
|
|
||||||
|
## Configuration Management
|
||||||
|
|
||||||
|
| StartOS-Managed | Upstream-Managed |
|
||||||
|
| --- | --- |
|
||||||
|
| `PLATFORM_HOST` / `PLATFORM_PORT` (forced to `0.0.0.0:7264` so the proxy can reach the app) | Everything inside the SpacesOps web UI / admin area |
|
||||||
|
| `PLATFORM_DB_PATH` (`/data/local.db`) | Tenant management, space-ownership flows |
|
||||||
|
| `OPERATOR_SECRET_HEX` / `OPERATOR_PUBLIC_HEX` (generated or imported) | |
|
||||||
|
| `PLATFORM_SESSION_SECRET` (generated) | |
|
||||||
|
| `SPACED_RPC_*` (auto-wired from Spaces) | |
|
||||||
|
| Optional: `OPERATOR_RELAY`, `PLATFORM_MODE`, `COINGECKO_*`, `SUBSD_*` (via **Configure Platform**) | |
|
||||||
|
|
||||||
|
## Network Access and Interfaces
|
||||||
|
|
||||||
|
| Interface | Port | Protocol | Exposure | Notes |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| Web UI | 7264 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | The SpacesOps web platform + admin area. The app provides its own auth; the admin area uses a **fixed built-in credential** (see warning below). |
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
| ID | Name | Visibility | Availability | Inputs | Purpose |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| `show-operator-credentials` | Show Operator Credentials | Enabled | Any | — | Displays the Nostr operator keypair: npub (QR), hex public key, and the masked nsec / hex secret. |
|
||||||
|
| `import-operator-key` | Import Operator Key | Enabled | Any | 64-char hex secret key | Replaces the operator keypair. Derives the public key automatically. **Warns** that it changes the operator identity; the service restarts to apply it. |
|
||||||
|
| `show-admin-credentials` | Show Admin Credentials | Enabled | Any | — | Displays the **fixed** built-in admin login (`admin` / `Whatever!`) with a security warning. |
|
||||||
|
| `configure-platform` | Configure Platform | Enabled | Any | relay, mode, CoinGecko, SUBSD (all optional) | Sets the Nostr relay, theme mode, pricing, and SUBSD backend. Saving restarts the service. |
|
||||||
|
|
||||||
|
## Backups and Restore
|
||||||
|
|
||||||
|
`sdk.Backups.ofVolumes('main')` — the entire `/data` volume is backed up,
|
||||||
|
including all SQLite databases, signed artifacts, and `store.json` (operator
|
||||||
|
keypair, session secret, and Configure-Platform settings). On restore, the same
|
||||||
|
idempotent init runs and reuses the existing keypair and secret. The Spaces
|
||||||
|
volume is **not** part of this package's backup.
|
||||||
|
|
||||||
|
## Health Checks
|
||||||
|
|
||||||
|
| ID | Display | Grace period | Behaviour |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `spacesops` (daemon `ready`) | Web Interface | 60 s | TCP listen on `0.0.0.0:7264` |
|
||||||
|
|
||||||
|
## Limitations and Differences
|
||||||
|
|
||||||
|
1. **Requires the Spaces service.** SpacesOps auto-connects to `spaces.startos:7225`
|
||||||
|
by reading the Spaces RPC credentials from the mounted Spaces volume. Install
|
||||||
|
and sync Spaces before (or alongside) SpacesOps; the service restarts until
|
||||||
|
those credentials are available.
|
||||||
|
2. **The admin password is fixed and well-known.** Admin Basic Auth is hardcoded
|
||||||
|
in the v1.0.0 image as `admin` / `Whatever!` with **no environment override**.
|
||||||
|
It cannot be changed without rebuilding the image. **Keep this service private
|
||||||
|
(Tor-only) and never expose its admin routes to the public internet** — the
|
||||||
|
admin area can run SQL and manage tenants. See **Show Admin Credentials**.
|
||||||
|
3. **`PLATFORM_HOST` is forced to `0.0.0.0` and `PLATFORM_PORT` to `7264`** so the
|
||||||
|
StartOS reverse proxy can reach the app (it otherwise binds `127.0.0.1:3000`).
|
||||||
|
4. **Subname-purchase and pricing are off by default.** They activate only when
|
||||||
|
you fill in the relevant fields in **Configure Platform** (`COINGECKO_*` for
|
||||||
|
pricing, `SUBSD_*` for subname purchase + cert-callback flows).
|
||||||
|
5. **SUBSD is not auto-wired to the Spaces subs daemon in v1.** The Spaces subs
|
||||||
|
daemon listens on 7777 with a different API/auth than the SUBSD-on-7244 this
|
||||||
|
app expects, so SUBSD must be configured manually if used.
|
||||||
|
|
||||||
|
## What Is Unchanged from Upstream
|
||||||
|
|
||||||
|
- All SpacesOps web UI, admin, and REST API routes behave as upstream documents.
|
||||||
|
- SQLite schema and migrations are managed by the app and run on startup.
|
||||||
|
- `nostr-tools` / `bitcoinjs-lib` cryptography ships in the image and is used
|
||||||
|
exactly as upstream.
|
||||||
|
- Any environment variable not set by StartOS falls back to the image's
|
||||||
|
`setup-spacesops-env.sh` defaults.
|
||||||
|
|
||||||
|
## Quick Reference for AI Consumers
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
package_id: spacesops
|
||||||
|
upstream_version: 1.0.0
|
||||||
|
image: docker.io/spacesops/spacesops:v1.0.0
|
||||||
|
architectures: [x86_64, aarch64]
|
||||||
|
volumes:
|
||||||
|
main: /data
|
||||||
|
mounted_dependencies:
|
||||||
|
spaces: /spaces-data # read-only; used to read spaced RPC creds at startup
|
||||||
|
ports:
|
||||||
|
ui: 7264
|
||||||
|
dependencies:
|
||||||
|
- spaces # kind: running, versionRange '>=0.0.9:3', healthChecks [spaced, sync]
|
||||||
|
startos_managed_env_vars:
|
||||||
|
- PLATFORM_HOST # forced 0.0.0.0
|
||||||
|
- PLATFORM_PORT # forced 7264
|
||||||
|
- PLATFORM_DB_PATH # /data/local.db
|
||||||
|
- PLATFORM_MODE # store.platformMode ?? prod
|
||||||
|
- PLATFORM_SESSION_SECRET # generated
|
||||||
|
- OPERATOR_SECRET_HEX # generated or imported
|
||||||
|
- OPERATOR_PUBLIC_HEX # derived
|
||||||
|
- OPERATOR_RELAY # store.operatorRelay ?? wss://relay.primal.net
|
||||||
|
- SPACED_RPC_URL # http://spaces.startos:7225
|
||||||
|
- SPACED_RPC_USER # from Spaces store.json
|
||||||
|
- SPACED_RPC_PASSWORD # from Spaces store.json
|
||||||
|
- SPACED_WALLETLOAD_NAME # main
|
||||||
|
optional_env_vars: # only set when configured via configure-platform
|
||||||
|
- COINGECKO_API_KEY
|
||||||
|
- COINGECKO_TOKEN_COINS
|
||||||
|
- SUBSD_URI_VALUE
|
||||||
|
- SUBSD_RPC_USER
|
||||||
|
- SUBSD_RPC_PASSWORD
|
||||||
|
admin_credentials:
|
||||||
|
username: admin
|
||||||
|
password: Whatever! # FIXED, baked into image, cannot be changed
|
||||||
|
note: keep service private; admin area can run SQL and manage tenants
|
||||||
|
actions:
|
||||||
|
- show-operator-credentials
|
||||||
|
- import-operator-key
|
||||||
|
- show-admin-credentials
|
||||||
|
- configure-platform
|
||||||
|
```
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# assets
|
||||||
|
|
||||||
|
The package icon is `../icon.svg` at the repo root — that is what `start-cli`
|
||||||
|
packs into the `.s9pk` (and what `README.md` displays). Its editable Inkscape
|
||||||
|
source is `../spacesops.svg`; keep the two in sync.
|
||||||
|
|
||||||
|
- `spaces-icon.png` — the Spaces icon used for the `spaces` dependency's
|
||||||
|
Marketplace metadata (referenced from `startos/manifest/index.ts`).
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="100mm"
|
||||||
|
height="100mm"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
sodipodi:docname="spacesops.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="0.5"
|
||||||
|
inkscape:cx="365"
|
||||||
|
inkscape:cy="260"
|
||||||
|
inkscape:window-width="1312"
|
||||||
|
inkscape:window-height="995"
|
||||||
|
inkscape:window-x="550"
|
||||||
|
inkscape:window-y="115"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:139.331px;font-family:Outfit;-inkscape-font-specification:'Outfit, @wght=400';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'wght' 400;fill:#903369;fill-opacity:1;stroke:none;stroke-width:19.7477;stroke-linecap:round;stroke-miterlimit:3;paint-order:fill markers stroke"
|
||||||
|
x="-1.9590863"
|
||||||
|
y="82.934502"
|
||||||
|
id="text1"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan1"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:139.331px;font-family:Outfit;-inkscape-font-specification:'Outfit, @wght=400';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'wght' 400;fill:#903369;fill-opacity:1;stroke:none;stroke-width:19.7477"
|
||||||
|
x="-1.9590863"
|
||||||
|
y="82.934502">@</tspan></text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,53 @@
|
|||||||
|
# SpacesOps
|
||||||
|
|
||||||
|
SpacesOps is a web platform for confirming Spaces ownership and selling subspace
|
||||||
|
names. It connects to the Spaces `spaced` daemon, signs operator events on Nostr,
|
||||||
|
and optionally adds pricing and subname-purchase features.
|
||||||
|
|
||||||
|
## Before you start: install Spaces
|
||||||
|
|
||||||
|
SpacesOps **requires the Spaces service**. Install **Spaces** from the
|
||||||
|
Marketplace and let it sync **before** using SpacesOps. SpacesOps connects to it
|
||||||
|
automatically — you do not need to enter any spaced RPC credentials. Until
|
||||||
|
Spaces is installed and its credentials are ready, SpacesOps will keep
|
||||||
|
restarting; this is expected.
|
||||||
|
|
||||||
|
## First run
|
||||||
|
|
||||||
|
On install, StartOS automatically:
|
||||||
|
|
||||||
|
1. Generates a **Nostr operator keypair** that SpacesOps signs events with.
|
||||||
|
2. Generates a strong **session secret**.
|
||||||
|
|
||||||
|
There is no setup wizard. Once Spaces is reachable, open the **Web UI** from the
|
||||||
|
SpacesOps dashboard.
|
||||||
|
|
||||||
|
## ⚠️ Admin credentials — important
|
||||||
|
|
||||||
|
The admin area is protected by a **fixed, well-known** login that is baked into
|
||||||
|
the image and **cannot be changed**:
|
||||||
|
|
||||||
|
- **Username:** `admin`
|
||||||
|
- **Password:** `Whatever!`
|
||||||
|
|
||||||
|
The **Show Admin Credentials** action displays these. Because the admin area can
|
||||||
|
run SQL and manage tenants, **keep this service private** — use it over Tor or
|
||||||
|
your LAN, and do **not** expose its admin routes to the public internet.
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
- **Show Operator Credentials** — view your Nostr operator npub, nsec, and hex
|
||||||
|
keys.
|
||||||
|
- **Import Operator Key** — replace the operator keypair with one you provide (a
|
||||||
|
64-character hex secret key). This changes your operator identity and restarts
|
||||||
|
the service.
|
||||||
|
- **Show Admin Credentials** — view the fixed admin login (with the warning
|
||||||
|
above).
|
||||||
|
- **Configure Platform** — optionally set the Nostr relay, theme mode, CoinGecko
|
||||||
|
pricing, and a SUBSD backend. Pricing and subname-purchase features stay off
|
||||||
|
until you configure them here. Saving restarts the service.
|
||||||
|
|
||||||
|
## Backups
|
||||||
|
|
||||||
|
Backing up SpacesOps captures everything under `/data` — all databases, signed
|
||||||
|
artifacts, your operator keypair, and your settings.
|
||||||
Generated
+514
@@ -0,0 +1,514 @@
|
|||||||
|
{
|
||||||
|
"name": "spacesops-startos",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "spacesops-startos",
|
||||||
|
"dependencies": {
|
||||||
|
"@start9labs/start-sdk": "1.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.19.0",
|
||||||
|
"@vercel/ncc": "^0.38.4",
|
||||||
|
"nostr-tools": "^2.10.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/ciphers": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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/@scure/base": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip32": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/curves": "2.0.1",
|
||||||
|
"@noble/hashes": "2.0.1",
|
||||||
|
"@scure/base": "2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip32/node_modules/@noble/curves": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip32/node_modules/@noble/hashes": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip39": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "2.0.1",
|
||||||
|
"@scure/base": "2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip39/node_modules/@noble/hashes": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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/nostr-tools": {
|
||||||
|
"version": "2.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.23.5.tgz",
|
||||||
|
"integrity": "sha512-Fa7ZlUdjfUW1P4E7H3yBexhOHYi18XNyvd2n7eNHkYR085xADX6Y8V8Vm7nT/XQajaFOBrptXmVIGkJ2E4vfVw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Unlicense",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/ciphers": "2.1.1",
|
||||||
|
"@noble/curves": "2.0.1",
|
||||||
|
"@noble/hashes": "2.0.1",
|
||||||
|
"@scure/base": "2.0.0",
|
||||||
|
"@scure/bip32": "2.0.1",
|
||||||
|
"@scure/bip39": "2.0.1",
|
||||||
|
"nostr-wasm": "0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nostr-tools/node_modules/@noble/curves": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nostr-tools/node_modules/@noble/hashes": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nostr-wasm": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "spacesops-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",
|
||||||
|
"nostr-tools": "^2.10.4",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# ** 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 is the list of leaf make-targets the build matrix fans out over.
|
||||||
|
# Defaults to the arches; variant packages override (e.g. immich, ollama, vllm
|
||||||
|
# set this to a list of variant or variant-arch leaf targets).
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Generic make-variable introspection. Used by the release workflow to
|
||||||
|
# read $(TARGETS) and fan out one matrix runner per target. `make -s
|
||||||
|
# print-TARGETS` echoes the list with no other output.
|
||||||
|
print-%:
|
||||||
|
@echo '$($*)'
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="100mm"
|
||||||
|
height="100mm"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
sodipodi:docname="spacesops.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="0.5"
|
||||||
|
inkscape:cx="365"
|
||||||
|
inkscape:cy="260"
|
||||||
|
inkscape:window-width="1312"
|
||||||
|
inkscape:window-height="995"
|
||||||
|
inkscape:window-x="550"
|
||||||
|
inkscape:window-y="115"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:139.331px;font-family:Outfit;-inkscape-font-specification:'Outfit, @wght=400';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'wght' 400;fill:#903369;fill-opacity:1;stroke:none;stroke-width:19.7477;stroke-linecap:round;stroke-miterlimit:3;paint-order:fill markers stroke"
|
||||||
|
x="-1.9590863"
|
||||||
|
y="82.934502"
|
||||||
|
id="text1"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan1"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:139.331px;font-family:Outfit;-inkscape-font-specification:'Outfit, @wght=400';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'wght' 400;fill:#903369;fill-opacity:1;stroke:none;stroke-width:19.7477"
|
||||||
|
x="-1.9590863"
|
||||||
|
y="82.934502">@</tspan></text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,163 @@
|
|||||||
|
import { storeJson } from '../fileModels/storeJson'
|
||||||
|
import { i18n } from '../i18n'
|
||||||
|
import { sdk } from '../sdk'
|
||||||
|
import {
|
||||||
|
DEFAULT_COINGECKO_TOKEN_COINS,
|
||||||
|
DEFAULT_OPERATOR_RELAY,
|
||||||
|
DEFAULT_PLATFORM_MODE,
|
||||||
|
} from '../utils'
|
||||||
|
|
||||||
|
const { InputSpec, Value } = sdk
|
||||||
|
|
||||||
|
const inputSpec = InputSpec.of({
|
||||||
|
operatorRelay: Value.text({
|
||||||
|
name: i18n('Operator Nostr Relay'),
|
||||||
|
description: i18n(
|
||||||
|
'The Nostr relay SpacesOps publishes operator events to (OPERATOR_RELAY).',
|
||||||
|
),
|
||||||
|
warning: null,
|
||||||
|
footnote: null,
|
||||||
|
default: DEFAULT_OPERATOR_RELAY,
|
||||||
|
required: true,
|
||||||
|
masked: false,
|
||||||
|
placeholder: 'wss://relay.example.com',
|
||||||
|
minLength: 1,
|
||||||
|
maxLength: null,
|
||||||
|
}),
|
||||||
|
platformMode: Value.select({
|
||||||
|
name: i18n('Platform Mode'),
|
||||||
|
description: i18n(
|
||||||
|
'Sets PLATFORM_MODE. Only changes the UI theme color; "test" does not change behavior.',
|
||||||
|
),
|
||||||
|
warning: null,
|
||||||
|
footnote: null,
|
||||||
|
default: DEFAULT_PLATFORM_MODE,
|
||||||
|
values: {
|
||||||
|
prod: 'Production',
|
||||||
|
test: 'Test',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
coingeckoApiKey: Value.text({
|
||||||
|
name: i18n('CoinGecko API Key'),
|
||||||
|
description: i18n(
|
||||||
|
'Optional CoinGecko API key (COINGECKO_API_KEY) used for pricing features. Leave blank to disable pricing.',
|
||||||
|
),
|
||||||
|
warning: null,
|
||||||
|
footnote: null,
|
||||||
|
default: null,
|
||||||
|
required: false,
|
||||||
|
masked: true,
|
||||||
|
placeholder: null,
|
||||||
|
minLength: null,
|
||||||
|
maxLength: null,
|
||||||
|
}),
|
||||||
|
coingeckoTokenCoins: Value.text({
|
||||||
|
name: i18n('CoinGecko Token Coins'),
|
||||||
|
description: i18n(
|
||||||
|
'The CoinGecko coin id(s) to price against (COINGECKO_TOKEN_COINS).',
|
||||||
|
),
|
||||||
|
warning: null,
|
||||||
|
footnote: null,
|
||||||
|
default: DEFAULT_COINGECKO_TOKEN_COINS,
|
||||||
|
required: false,
|
||||||
|
masked: false,
|
||||||
|
placeholder: 'bitcoin',
|
||||||
|
minLength: null,
|
||||||
|
maxLength: null,
|
||||||
|
}),
|
||||||
|
subsdUrl: Value.text({
|
||||||
|
name: i18n('SUBSD URL'),
|
||||||
|
description: i18n(
|
||||||
|
'Optional SUBSD service URL (SUBSD_URI_VALUE) for the subname-purchase flow. Leave blank to disable.',
|
||||||
|
),
|
||||||
|
warning: null,
|
||||||
|
footnote: null,
|
||||||
|
default: null,
|
||||||
|
required: false,
|
||||||
|
masked: false,
|
||||||
|
placeholder: 'http://host:7244',
|
||||||
|
minLength: null,
|
||||||
|
maxLength: null,
|
||||||
|
}),
|
||||||
|
subsdUser: Value.text({
|
||||||
|
name: i18n('SUBSD RPC User'),
|
||||||
|
description: i18n('Optional SUBSD RPC username (SUBSD_RPC_USER).'),
|
||||||
|
warning: null,
|
||||||
|
footnote: null,
|
||||||
|
default: null,
|
||||||
|
required: false,
|
||||||
|
masked: false,
|
||||||
|
placeholder: null,
|
||||||
|
minLength: null,
|
||||||
|
maxLength: null,
|
||||||
|
}),
|
||||||
|
subsdPassword: Value.text({
|
||||||
|
name: i18n('SUBSD RPC Password'),
|
||||||
|
description: i18n('Optional SUBSD RPC password (SUBSD_RPC_PASSWORD).'),
|
||||||
|
warning: null,
|
||||||
|
footnote: null,
|
||||||
|
default: null,
|
||||||
|
required: false,
|
||||||
|
masked: true,
|
||||||
|
placeholder: null,
|
||||||
|
minLength: null,
|
||||||
|
maxLength: null,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const configurePlatform = sdk.Action.withInput(
|
||||||
|
// id
|
||||||
|
'configure-platform',
|
||||||
|
|
||||||
|
// metadata
|
||||||
|
async ({ effects }) => ({
|
||||||
|
name: i18n('Configure Platform'),
|
||||||
|
description: i18n(
|
||||||
|
'Set optional SpacesOps settings: Nostr relay, theme mode, CoinGecko pricing, and SUBSD backend. Saving restarts the service so the new settings take effect.',
|
||||||
|
),
|
||||||
|
warning: null,
|
||||||
|
allowedStatuses: 'any',
|
||||||
|
group: null,
|
||||||
|
visibility: 'enabled',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// input
|
||||||
|
inputSpec,
|
||||||
|
|
||||||
|
// prefill — load current values from store
|
||||||
|
async ({ effects }) => {
|
||||||
|
const store = await storeJson.read().once()
|
||||||
|
return {
|
||||||
|
operatorRelay: store?.operatorRelay ?? DEFAULT_OPERATOR_RELAY,
|
||||||
|
platformMode: store?.platformMode ?? DEFAULT_PLATFORM_MODE,
|
||||||
|
coingeckoApiKey: store?.coingeckoApiKey ?? null,
|
||||||
|
coingeckoTokenCoins:
|
||||||
|
store?.coingeckoTokenCoins ?? DEFAULT_COINGECKO_TOKEN_COINS,
|
||||||
|
subsdUrl: store?.subsdUrl ?? null,
|
||||||
|
subsdUser: store?.subsdUser ?? null,
|
||||||
|
subsdPassword: store?.subsdPassword ?? null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// run
|
||||||
|
async ({ effects, input }) => {
|
||||||
|
await storeJson.merge(effects, {
|
||||||
|
operatorRelay: input.operatorRelay,
|
||||||
|
platformMode: input.platformMode,
|
||||||
|
coingeckoApiKey: input.coingeckoApiKey || null,
|
||||||
|
coingeckoTokenCoins: input.coingeckoTokenCoins || null,
|
||||||
|
subsdUrl: input.subsdUrl || null,
|
||||||
|
subsdUser: input.subsdUser || null,
|
||||||
|
subsdPassword: input.subsdPassword || null,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
title: i18n('Success'),
|
||||||
|
message: i18n(
|
||||||
|
'Platform configuration saved. The service is restarting to apply the new settings.',
|
||||||
|
),
|
||||||
|
result: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { storeJson } from '../fileModels/storeJson'
|
||||||
|
import { i18n } from '../i18n'
|
||||||
|
import { isValidSecretHex, secretHexToPublicHex } from '../nostr'
|
||||||
|
import { sdk } from '../sdk'
|
||||||
|
|
||||||
|
const { InputSpec, Value } = sdk
|
||||||
|
|
||||||
|
const inputSpec = InputSpec.of({
|
||||||
|
secretHex: Value.text({
|
||||||
|
name: i18n('Operator Secret Key (hex)'),
|
||||||
|
description: i18n(
|
||||||
|
'A 64-character hex-encoded secp256k1 / Nostr secret key. The public key is derived automatically.',
|
||||||
|
),
|
||||||
|
warning: null,
|
||||||
|
footnote: null,
|
||||||
|
default: null,
|
||||||
|
required: true,
|
||||||
|
masked: true,
|
||||||
|
placeholder: '64 hexadecimal characters',
|
||||||
|
minLength: 64,
|
||||||
|
maxLength: 64,
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
regex: '^[0-9a-fA-F]{64}$',
|
||||||
|
description: i18n('Must be exactly 64 hexadecimal characters.'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const importOperatorKey = sdk.Action.withInput(
|
||||||
|
// id
|
||||||
|
'import-operator-key',
|
||||||
|
|
||||||
|
// metadata
|
||||||
|
async ({ effects }) => ({
|
||||||
|
name: i18n('Import Operator Key'),
|
||||||
|
description: i18n(
|
||||||
|
'Replace the operator keypair with one you provide (hex secret key).',
|
||||||
|
),
|
||||||
|
warning: i18n(
|
||||||
|
'This changes the operator identity SpacesOps signs events with. Events already published under the old key stay under it. The service restarts to apply the new key.',
|
||||||
|
),
|
||||||
|
allowedStatuses: 'any',
|
||||||
|
group: null,
|
||||||
|
visibility: 'enabled',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// input
|
||||||
|
inputSpec,
|
||||||
|
|
||||||
|
// prefill — never pre-populate a secret
|
||||||
|
async ({ effects }) => {},
|
||||||
|
|
||||||
|
// run
|
||||||
|
async ({ effects, input }) => {
|
||||||
|
const secretHex = input.secretHex.trim().toLowerCase()
|
||||||
|
|
||||||
|
if (!isValidSecretHex(secretHex)) {
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
title: i18n('Failure'),
|
||||||
|
message: i18n(
|
||||||
|
'The secret key must be exactly 64 hexadecimal characters.',
|
||||||
|
),
|
||||||
|
result: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let operatorPublicHex: string
|
||||||
|
try {
|
||||||
|
operatorPublicHex = secretHexToPublicHex(secretHex)
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
title: i18n('Failure'),
|
||||||
|
message: i18n(
|
||||||
|
'Could not derive a public key from that secret: ${error}',
|
||||||
|
{
|
||||||
|
error: (e as Error).message,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
result: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await storeJson.merge(effects, {
|
||||||
|
operatorSecretHex: secretHex,
|
||||||
|
operatorPublicHex,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
title: i18n('Success'),
|
||||||
|
message: i18n(
|
||||||
|
'Operator key imported. The service is restarting to apply the new identity.',
|
||||||
|
),
|
||||||
|
result: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { sdk } from '../sdk'
|
||||||
|
import { configurePlatform } from './configurePlatform'
|
||||||
|
import { importOperatorKey } from './importOperatorKey'
|
||||||
|
import { showAdminCredentials } from './showAdminCredentials'
|
||||||
|
import { showOperatorCredentials } from './showOperatorCredentials'
|
||||||
|
|
||||||
|
export const actions = sdk.Actions.of()
|
||||||
|
.addAction(showOperatorCredentials)
|
||||||
|
.addAction(importOperatorKey)
|
||||||
|
.addAction(showAdminCredentials)
|
||||||
|
.addAction(configurePlatform)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { i18n } from '../i18n'
|
||||||
|
import { sdk } from '../sdk'
|
||||||
|
import { ADMIN_PASSWORD, ADMIN_USER } from '../utils'
|
||||||
|
|
||||||
|
export const showAdminCredentials = sdk.Action.withoutInput(
|
||||||
|
// id
|
||||||
|
'show-admin-credentials',
|
||||||
|
|
||||||
|
// metadata
|
||||||
|
async ({ effects }) => ({
|
||||||
|
name: i18n('Show Admin Credentials'),
|
||||||
|
description: i18n(
|
||||||
|
'Display the built-in admin username and password for the SpacesOps admin area.',
|
||||||
|
),
|
||||||
|
warning: null,
|
||||||
|
allowedStatuses: 'any',
|
||||||
|
group: null,
|
||||||
|
visibility: 'enabled',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// run
|
||||||
|
async ({ effects }) => ({
|
||||||
|
version: '1',
|
||||||
|
title: i18n('Admin Credentials'),
|
||||||
|
message: i18n(
|
||||||
|
'WARNING: these are FIXED, well-known credentials baked into the image and cannot be changed without rebuilding it. The admin area can run SQL and manage tenants. Keep this service private (Tor-only) and never expose the admin routes to the public internet.',
|
||||||
|
),
|
||||||
|
result: {
|
||||||
|
type: 'group',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
type: 'single',
|
||||||
|
name: i18n('Username'),
|
||||||
|
description: null,
|
||||||
|
value: ADMIN_USER,
|
||||||
|
masked: false,
|
||||||
|
copyable: true,
|
||||||
|
qr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'single',
|
||||||
|
name: i18n('Password'),
|
||||||
|
description: null,
|
||||||
|
value: ADMIN_PASSWORD,
|
||||||
|
masked: true,
|
||||||
|
copyable: true,
|
||||||
|
qr: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { storeJson } from '../fileModels/storeJson'
|
||||||
|
import { i18n } from '../i18n'
|
||||||
|
import { encodeNpub, encodeNsec } from '../nostr'
|
||||||
|
import { sdk } from '../sdk'
|
||||||
|
|
||||||
|
export const showOperatorCredentials = sdk.Action.withoutInput(
|
||||||
|
// id
|
||||||
|
'show-operator-credentials',
|
||||||
|
|
||||||
|
// metadata
|
||||||
|
async ({ effects }) => ({
|
||||||
|
name: i18n('Show Operator Credentials'),
|
||||||
|
description: i18n(
|
||||||
|
'Display the Nostr operator keypair SpacesOps signs events with (npub, nsec, and hex public key).',
|
||||||
|
),
|
||||||
|
warning: null,
|
||||||
|
allowedStatuses: 'any',
|
||||||
|
group: null,
|
||||||
|
visibility: 'enabled',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// run
|
||||||
|
async ({ effects }) => {
|
||||||
|
const store = await storeJson.read().once()
|
||||||
|
const secretHex = store?.operatorSecretHex ?? null
|
||||||
|
const publicHex = store?.operatorPublicHex ?? null
|
||||||
|
|
||||||
|
if (!secretHex || !publicHex) {
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
title: i18n('Operator Credentials'),
|
||||||
|
message: i18n(
|
||||||
|
'The operator keypair has not been generated yet. Start the service once to generate it.',
|
||||||
|
),
|
||||||
|
result: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
title: i18n('Operator Credentials'),
|
||||||
|
message: i18n(
|
||||||
|
'SpacesOps signs operator events on Nostr with this keypair. Keep the secret (nsec / hex) private.',
|
||||||
|
),
|
||||||
|
result: {
|
||||||
|
type: 'group',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
type: 'single',
|
||||||
|
name: i18n('Public Key (npub)'),
|
||||||
|
description: null,
|
||||||
|
value: encodeNpub(publicHex),
|
||||||
|
masked: false,
|
||||||
|
copyable: true,
|
||||||
|
qr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'single',
|
||||||
|
name: i18n('Public Key (hex)'),
|
||||||
|
description: null,
|
||||||
|
value: publicHex,
|
||||||
|
masked: false,
|
||||||
|
copyable: true,
|
||||||
|
qr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'single',
|
||||||
|
name: i18n('Secret Key (nsec)'),
|
||||||
|
description: null,
|
||||||
|
value: encodeNsec(secretHex),
|
||||||
|
masked: true,
|
||||||
|
copyable: true,
|
||||||
|
qr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'single',
|
||||||
|
name: i18n('Secret Key (hex)'),
|
||||||
|
description: null,
|
||||||
|
value: secretHex,
|
||||||
|
masked: true,
|
||||||
|
copyable: true,
|
||||||
|
qr: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { sdk } from './sdk'
|
||||||
|
|
||||||
|
export const { createBackup, restoreInit } = sdk.setupBackups(
|
||||||
|
async ({ effects }) => sdk.Backups.ofVolumes('main'),
|
||||||
|
)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { sdk } from './sdk'
|
||||||
|
|
||||||
|
// Spaces 0.0.9:3 binds spaced to 0.0.0.0:7225 and exports the `spaces-api`
|
||||||
|
// interface with static RPC creds at the root of its `main` volume. SpacesOps
|
||||||
|
// reads those creds from the mounted Spaces volume in main.ts and connects to
|
||||||
|
// http://spaces.startos:7225. Dependency declarations do NOT gate startup —
|
||||||
|
// main.ts throws/retries until the creds are available.
|
||||||
|
export const setDependencies = sdk.setupDependencies(async ({ effects }) => ({
|
||||||
|
spaces: {
|
||||||
|
kind: 'running',
|
||||||
|
versionRange: '>=0.0.9:3',
|
||||||
|
healthChecks: ['spaced', 'sync'],
|
||||||
|
},
|
||||||
|
}))
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { FileHelper, z } from '@start9labs/start-sdk'
|
||||||
|
import { sdk } from '../sdk'
|
||||||
|
|
||||||
|
const shape = z.object({
|
||||||
|
// Nostr operator keypair — generated once in init, or replaced via the
|
||||||
|
// "Import Operator Key" action. SpacesOps signs operator events with it.
|
||||||
|
operatorSecretHex: z.string().nullable().catch(null),
|
||||||
|
operatorPublicHex: z.string().nullable().catch(null),
|
||||||
|
// Strong replacement for the app's weak hardcoded session secret.
|
||||||
|
sessionSecret: z.string().nullable().catch(null),
|
||||||
|
// Optional config set via the "Configure Platform" action.
|
||||||
|
platformMode: z.enum(['prod', 'test']).nullable().catch(null),
|
||||||
|
operatorRelay: z.string().nullable().catch(null),
|
||||||
|
coingeckoApiKey: z.string().nullable().catch(null),
|
||||||
|
coingeckoTokenCoins: z.string().nullable().catch(null),
|
||||||
|
subsdUrl: z.string().nullable().catch(null),
|
||||||
|
subsdUser: z.string().nullable().catch(null),
|
||||||
|
subsdPassword: z.string().nullable().catch(null),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const storeJson = FileHelper.json(
|
||||||
|
{ base: sdk.volumes.main, subpath: 'store.json' },
|
||||||
|
shape,
|
||||||
|
)
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
export const DEFAULT_LANG = 'en_US'
|
||||||
|
|
||||||
|
const dict = {
|
||||||
|
'Operator Nostr Relay': 0,
|
||||||
|
'The Nostr relay SpacesOps publishes operator events to (OPERATOR_RELAY).': 1,
|
||||||
|
'Platform Mode': 2,
|
||||||
|
'Sets PLATFORM_MODE. Only changes the UI theme color; "test" does not change behavior.': 3,
|
||||||
|
'CoinGecko API Key': 4,
|
||||||
|
'Optional CoinGecko API key (COINGECKO_API_KEY) used for pricing features. Leave blank to disable pricing.': 5,
|
||||||
|
'CoinGecko Token Coins': 6,
|
||||||
|
'The CoinGecko coin id(s) to price against (COINGECKO_TOKEN_COINS).': 7,
|
||||||
|
'SUBSD URL': 8,
|
||||||
|
'Optional SUBSD service URL (SUBSD_URI_VALUE) for the subname-purchase flow. Leave blank to disable.': 9,
|
||||||
|
'SUBSD RPC User': 10,
|
||||||
|
'Optional SUBSD RPC username (SUBSD_RPC_USER).': 11,
|
||||||
|
'SUBSD RPC Password': 12,
|
||||||
|
'Optional SUBSD RPC password (SUBSD_RPC_PASSWORD).': 13,
|
||||||
|
'Configure Platform': 14,
|
||||||
|
'Set optional SpacesOps settings: Nostr relay, theme mode, CoinGecko pricing, and SUBSD backend. Saving restarts the service so the new settings take effect.': 15,
|
||||||
|
Success: 16,
|
||||||
|
'Platform configuration saved. The service is restarting to apply the new settings.': 17,
|
||||||
|
'Operator Secret Key (hex)': 18,
|
||||||
|
'A 64-character hex-encoded secp256k1 / Nostr secret key. The public key is derived automatically.': 19,
|
||||||
|
'Must be exactly 64 hexadecimal characters.': 20,
|
||||||
|
'Import Operator Key': 21,
|
||||||
|
'Replace the operator keypair with one you provide (hex secret key).': 22,
|
||||||
|
'This changes the operator identity SpacesOps signs events with. Events already published under the old key stay under it. The service restarts to apply the new key.': 23,
|
||||||
|
Failure: 24,
|
||||||
|
'The secret key must be exactly 64 hexadecimal characters.': 25,
|
||||||
|
'Could not derive a public key from that secret: ${error}': 26,
|
||||||
|
'Operator key imported. The service is restarting to apply the new identity.': 27,
|
||||||
|
'Show Admin Credentials': 28,
|
||||||
|
'Display the built-in admin username and password for the SpacesOps admin area.': 29,
|
||||||
|
'Admin Credentials': 30,
|
||||||
|
'WARNING: these are FIXED, well-known credentials baked into the image and cannot be changed without rebuilding it. The admin area can run SQL and manage tenants. Keep this service private (Tor-only) and never expose the admin routes to the public internet.': 31,
|
||||||
|
Username: 32,
|
||||||
|
Password: 33,
|
||||||
|
'Show Operator Credentials': 34,
|
||||||
|
'Display the Nostr operator keypair SpacesOps signs events with (npub, nsec, and hex public key).': 35,
|
||||||
|
'Operator Credentials': 36,
|
||||||
|
'The operator keypair has not been generated yet. Start the service once to generate it.': 37,
|
||||||
|
'SpacesOps signs operator events on Nostr with this keypair. Keep the secret (nsec / hex) private.': 38,
|
||||||
|
'Public Key (npub)': 39,
|
||||||
|
'Public Key (hex)': 40,
|
||||||
|
'Secret Key (nsec)': 41,
|
||||||
|
'Secret Key (hex)': 42,
|
||||||
|
'Web UI': 43,
|
||||||
|
'SpacesOps web platform for space ownership confirmation and subspace-name purchases. The app provides its own login; the admin area uses a fixed built-in credential — see "Show Admin Credentials".': 44,
|
||||||
|
'Starting SpacesOps!': 45,
|
||||||
|
'Web Interface': 46,
|
||||||
|
'The web interface is ready': 47,
|
||||||
|
'The web interface is not ready': 48,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plumbing. DO NOT EDIT.
|
||||||
|
*/
|
||||||
|
export type I18nKey = keyof typeof dict
|
||||||
|
export type LangDict = Record<(typeof dict)[I18nKey], string>
|
||||||
|
export default dict
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { LangDict } from './default'
|
||||||
|
|
||||||
|
export default {} satisfies Record<string, LangDict>
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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 { taskOperatorKeys } from './taskOperatorKeys'
|
||||||
|
import { taskSessionSecret } from './taskSessionSecret'
|
||||||
|
|
||||||
|
export const init = sdk.setupInit(
|
||||||
|
restoreInit,
|
||||||
|
versionGraph,
|
||||||
|
setInterfaces,
|
||||||
|
setDependencies,
|
||||||
|
actions,
|
||||||
|
taskOperatorKeys,
|
||||||
|
taskSessionSecret,
|
||||||
|
)
|
||||||
|
|
||||||
|
export const uninit = sdk.setupUninit(versionGraph)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { storeJson } from '../fileModels/storeJson'
|
||||||
|
import { sdk } from '../sdk'
|
||||||
|
import { secretHexToPublicHex } from '../nostr'
|
||||||
|
import { randomOperatorSecretHex } from '../utils'
|
||||||
|
|
||||||
|
// Generate the Nostr operator keypair once, if absent. SpacesOps requires
|
||||||
|
// OPERATOR_SECRET_HEX / OPERATOR_PUBLIC_HEX at startup (it exits otherwise).
|
||||||
|
// Idempotent: only acts when the secret is not yet present. Read with .once()
|
||||||
|
// — never .const() in init (it arms a write-after-const watcher).
|
||||||
|
export const taskOperatorKeys = sdk.setupOnInit(async (effects) => {
|
||||||
|
const existing = await storeJson.read((s) => s.operatorSecretHex).once()
|
||||||
|
if (existing) return
|
||||||
|
|
||||||
|
const operatorSecretHex = randomOperatorSecretHex()
|
||||||
|
// Derive before persisting; if derivation rejects the (astronomically rare)
|
||||||
|
// out-of-range scalar, nothing is written and StartOS retries init.
|
||||||
|
const operatorPublicHex = secretHexToPublicHex(operatorSecretHex)
|
||||||
|
|
||||||
|
await storeJson.merge(
|
||||||
|
effects,
|
||||||
|
{ operatorSecretHex, operatorPublicHex },
|
||||||
|
{ allowWriteAfterConst: true },
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { storeJson } from '../fileModels/storeJson'
|
||||||
|
import { sdk } from '../sdk'
|
||||||
|
import { randomSessionSecret } from '../utils'
|
||||||
|
|
||||||
|
// Generate a strong PLATFORM_SESSION_SECRET once, if absent. The app otherwise
|
||||||
|
// falls back to a weak hardcoded default. Idempotent; reads with .once().
|
||||||
|
export const taskSessionSecret = sdk.setupOnInit(async (effects) => {
|
||||||
|
const existing = await storeJson.read((s) => s.sessionSecret).once()
|
||||||
|
if (existing) return
|
||||||
|
|
||||||
|
await storeJson.merge(
|
||||||
|
effects,
|
||||||
|
{ sessionSecret: randomSessionSecret() },
|
||||||
|
{ allowWriteAfterConst: true },
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -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(
|
||||||
|
'SpacesOps web platform for space ownership confirmation and subspace-name purchases. The app provides its own login; the admin area uses a fixed built-in credential — see "Show Admin Credentials".',
|
||||||
|
),
|
||||||
|
type: 'ui',
|
||||||
|
masked: false,
|
||||||
|
schemeOverride: null,
|
||||||
|
username: null,
|
||||||
|
path: '',
|
||||||
|
query: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
const uiReceipt = await uiMultiOrigin.export([ui])
|
||||||
|
|
||||||
|
return [uiReceipt]
|
||||||
|
})
|
||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
import { storeJson } from './fileModels/storeJson'
|
||||||
|
import { i18n } from './i18n'
|
||||||
|
import { sdk } from './sdk'
|
||||||
|
import {
|
||||||
|
dataDir,
|
||||||
|
DEFAULT_OPERATOR_RELAY,
|
||||||
|
DEFAULT_PLATFORM_MODE,
|
||||||
|
SPACED_RPC_URL,
|
||||||
|
SPACED_WALLETLOAD_NAME,
|
||||||
|
spacesDataDir,
|
||||||
|
uiPort,
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
|
type SpacedAuth = { username: string; password: string }
|
||||||
|
|
||||||
|
export const main = sdk.setupMain(async ({ effects }) => {
|
||||||
|
console.info(i18n('Starting SpacesOps!'))
|
||||||
|
|
||||||
|
// Read with .const() so a store.json change (e.g. from Import Operator Key or
|
||||||
|
// Configure Platform) triggers an automatic service restart.
|
||||||
|
const store = await storeJson.read().const(effects)
|
||||||
|
if (
|
||||||
|
!store?.operatorSecretHex ||
|
||||||
|
!store?.operatorPublicHex ||
|
||||||
|
!store?.sessionSecret
|
||||||
|
) {
|
||||||
|
// taskOperatorKeys + taskSessionSecret seed these in init; if they aren't
|
||||||
|
// populated yet, init hasn't finished — let StartOS restart us.
|
||||||
|
throw new Error(
|
||||||
|
'SpacesOps store.json is not yet populated (operator keys / session secret missing).',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mount our own volume at /data AND the Spaces 'main' volume (read-only) at
|
||||||
|
// /spaces-data so we can read the spaced RPC credentials Spaces seeded there.
|
||||||
|
const mounts = sdk.Mounts.of()
|
||||||
|
.mountVolume({
|
||||||
|
volumeId: 'main',
|
||||||
|
subpath: null,
|
||||||
|
mountpoint: dataDir,
|
||||||
|
readonly: false,
|
||||||
|
})
|
||||||
|
.mountDependency({
|
||||||
|
dependencyId: 'spaces',
|
||||||
|
volumeId: 'main',
|
||||||
|
subpath: null,
|
||||||
|
mountpoint: spacesDataDir,
|
||||||
|
readonly: true,
|
||||||
|
type: 'directory',
|
||||||
|
})
|
||||||
|
|
||||||
|
const sub = await sdk.SubContainer.of(
|
||||||
|
effects,
|
||||||
|
{ imageId: 'spacesops' },
|
||||||
|
mounts,
|
||||||
|
'spacesops-sub',
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupMain runs in the StartOS runtime, NOT inside the container, so it
|
||||||
|
// cannot fs-read the mounted Spaces volume directly. Exec a read inside the
|
||||||
|
// subcontainer instead (mirrors how spaces-startos execs space-cli for its
|
||||||
|
// sync health check). Throw if the creds aren't there yet — StartOS restarts
|
||||||
|
// us until Spaces is installed and its store.json is seeded.
|
||||||
|
const probe = await sub.exec(['cat', `${spacesDataDir}/store.json`])
|
||||||
|
if (probe.exitCode !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Spaces RPC creds not available yet: could not read ${spacesDataDir}/store.json (exit ${probe.exitCode}). Is the Spaces service installed?`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
let spacesStore: { spacedAuth?: SpacedAuth | null }
|
||||||
|
try {
|
||||||
|
spacesStore = JSON.parse((probe.stdout ?? '').toString())
|
||||||
|
} catch {
|
||||||
|
throw new Error(
|
||||||
|
'Spaces RPC creds not available yet: Spaces store.json is not valid JSON.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const spacedAuth = spacesStore.spacedAuth
|
||||||
|
if (!spacedAuth || !spacedAuth.username || !spacedAuth.password) {
|
||||||
|
throw new Error(
|
||||||
|
'Spaces RPC creds not available yet: spacedAuth missing from Spaces store.json. Wait for the Spaces service init to finish.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const env: Record<string, string> = {
|
||||||
|
// The app defaults to 127.0.0.1:3000, which the StartOS proxy cannot reach.
|
||||||
|
PLATFORM_HOST: '0.0.0.0',
|
||||||
|
PLATFORM_PORT: String(uiPort),
|
||||||
|
PLATFORM_DB_PATH: `${dataDir}/local.db`,
|
||||||
|
PLATFORM_MODE: store.platformMode ?? DEFAULT_PLATFORM_MODE,
|
||||||
|
PLATFORM_SESSION_SECRET: store.sessionSecret,
|
||||||
|
OPERATOR_SECRET_HEX: store.operatorSecretHex,
|
||||||
|
OPERATOR_PUBLIC_HEX: store.operatorPublicHex,
|
||||||
|
OPERATOR_RELAY: store.operatorRelay ?? DEFAULT_OPERATOR_RELAY,
|
||||||
|
SPACED_RPC_URL,
|
||||||
|
SPACED_RPC_USER: spacedAuth.username,
|
||||||
|
SPACED_RPC_PASSWORD: spacedAuth.password,
|
||||||
|
SPACED_WALLETLOAD_NAME,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional integrations — only injected when configured (Configure Platform).
|
||||||
|
if (store.coingeckoApiKey) env.COINGECKO_API_KEY = store.coingeckoApiKey
|
||||||
|
if (store.coingeckoTokenCoins)
|
||||||
|
env.COINGECKO_TOKEN_COINS = store.coingeckoTokenCoins
|
||||||
|
if (store.subsdUrl) env.SUBSD_URI_VALUE = store.subsdUrl
|
||||||
|
if (store.subsdUser) env.SUBSD_RPC_USER = store.subsdUser
|
||||||
|
if (store.subsdPassword) env.SUBSD_RPC_PASSWORD = store.subsdPassword
|
||||||
|
|
||||||
|
return sdk.Daemons.of(effects).addDaemon('spacesops', {
|
||||||
|
subcontainer: sub,
|
||||||
|
exec: {
|
||||||
|
// Keep the image entrypoint: it creates /data dirs, symlinks
|
||||||
|
// /app/data -> /data, and loads defaults for any UNSET vars before
|
||||||
|
// exec'ing the command.
|
||||||
|
command: ['/app/docker-entrypoint.sh', 'node', 'server.js'],
|
||||||
|
env,
|
||||||
|
cwd: '/app',
|
||||||
|
user: 'root',
|
||||||
|
},
|
||||||
|
ready: {
|
||||||
|
display: i18n('Web Interface'),
|
||||||
|
fn: () =>
|
||||||
|
sdk.healthCheck.checkPortListening(effects, uiPort, {
|
||||||
|
successMessage: i18n('The web interface is ready'),
|
||||||
|
errorMessage: i18n('The web interface is not ready'),
|
||||||
|
}),
|
||||||
|
gracePeriod: 60_000,
|
||||||
|
},
|
||||||
|
requires: [],
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export const short = {
|
||||||
|
en_US: 'Space ownership confirmation and subname sales.',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const long = {
|
||||||
|
en_US:
|
||||||
|
'SpacesOps is a web platform for confirming Spaces ownership and selling subspace names. It talks to the Spaces spaced daemon over JSON-RPC, signs operator events on Nostr, and optionally integrates pricing and subname-purchase backends. This package auto-connects to the Spaces service running on the same server.',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const depSpacesDescription = {
|
||||||
|
en_US:
|
||||||
|
'Provides the spaced JSON-RPC endpoint SpacesOps queries. SpacesOps auto-connects to it at spaces.startos:7225 using the Spaces RPC credentials, so Spaces must be installed and synced.',
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { setupManifest } from '@start9labs/start-sdk'
|
||||||
|
import { depSpacesDescription, long, short } from './i18n'
|
||||||
|
|
||||||
|
export const manifest = setupManifest({
|
||||||
|
id: 'spacesops',
|
||||||
|
title: 'SpacesOps',
|
||||||
|
license: 'MIT',
|
||||||
|
packageRepo: 'https://git.spacesops.com/spacesops/spacesops-startos',
|
||||||
|
upstreamRepo: 'https://git.spacesops.com/spacesops/spacesops',
|
||||||
|
marketingUrl: 'https://spacesops.com',
|
||||||
|
donationUrl: null,
|
||||||
|
docsUrls: [],
|
||||||
|
description: { short, long },
|
||||||
|
volumes: ['main'],
|
||||||
|
images: {
|
||||||
|
spacesops: {
|
||||||
|
source: { dockerTag: 'spacesops/spacesops:v1.0.0' },
|
||||||
|
arch: ['x86_64', 'aarch64'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
alerts: {
|
||||||
|
install: {
|
||||||
|
en_US:
|
||||||
|
'SpacesOps REQUIRES the Spaces service. Install and sync Spaces first — SpacesOps auto-connects to it at spaces.startos:7225 by reading the Spaces RPC credentials, and will not start until they are available.\n\nWARNING: the admin area is protected by a FIXED, well-known credential baked into this image (username "admin", password "Whatever!") that cannot be changed without rebuilding the image. The admin area can run SQL and manage tenants. Keep this service private (Tor-only) and do NOT expose its admin routes to the public internet. See the "Show Admin Credentials" action.',
|
||||||
|
},
|
||||||
|
update: null,
|
||||||
|
uninstall: null,
|
||||||
|
restore: null,
|
||||||
|
start: null,
|
||||||
|
stop: null,
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
spaces: {
|
||||||
|
description: depSpacesDescription,
|
||||||
|
optional: false,
|
||||||
|
metadata: {
|
||||||
|
title: 'Spaces',
|
||||||
|
icon: 'assets/spaces-icon.png',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { getPublicKey, nip19 } from 'nostr-tools'
|
||||||
|
|
||||||
|
const HEX64 = /^[0-9a-f]{64}$/
|
||||||
|
|
||||||
|
function toBytes(secretHex: string): Uint8Array {
|
||||||
|
return Uint8Array.from(Buffer.from(secretHex.trim().toLowerCase(), 'hex'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidSecretHex(hex: string): boolean {
|
||||||
|
return HEX64.test(hex.trim().toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deterministic secp256k1 x-only public key derivation (no randomness). Throws
|
||||||
|
// if the secret is not a valid curve scalar.
|
||||||
|
export function secretHexToPublicHex(secretHex: string): string {
|
||||||
|
return getPublicKey(toBytes(secretHex))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeNpub(publicHex: string): string {
|
||||||
|
return nip19.npubEncode(publicHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeNsec(secretHex: string): string {
|
||||||
|
return nip19.nsecEncode(toBytes(secretHex))
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { utils } from '@start9labs/start-sdk'
|
||||||
|
|
||||||
|
// The SpacesOps HTTP server. The app defaults to 127.0.0.1:3000; we override
|
||||||
|
// PLATFORM_HOST to 0.0.0.0 and PLATFORM_PORT to this so the StartOS reverse
|
||||||
|
// proxy can reach it.
|
||||||
|
export const uiPort = 7264
|
||||||
|
|
||||||
|
// Our own volume.
|
||||||
|
export const dataDir = '/data'
|
||||||
|
|
||||||
|
// Where the Spaces 'main' volume is mounted (read-only) so we can read the
|
||||||
|
// spaced RPC credentials Spaces seeded into its store.json.
|
||||||
|
export const spacesDataDir = '/spaces-data'
|
||||||
|
|
||||||
|
// The Spaces package this service depends on.
|
||||||
|
export const SPACES_PACKAGE_ID = 'spaces'
|
||||||
|
|
||||||
|
// Spaces exposes its spaced JSON-RPC as the `spaces-api` interface on 7225,
|
||||||
|
// reachable from a dependent package at this address.
|
||||||
|
export const SPACED_RPC_URL = 'http://spaces.startos:7225'
|
||||||
|
export const SPACED_WALLETLOAD_NAME = 'main'
|
||||||
|
|
||||||
|
// Admin Basic Auth is baked into the v1.0.0 image with NO env override. It
|
||||||
|
// cannot be changed without rebuilding the image. Surfaced (with a warning)
|
||||||
|
// via the "Show Admin Credentials" action.
|
||||||
|
export const ADMIN_USER = 'admin'
|
||||||
|
export const ADMIN_PASSWORD = 'Whatever!'
|
||||||
|
|
||||||
|
// Optional-config defaults (see the "Configure Platform" action).
|
||||||
|
export const DEFAULT_OPERATOR_RELAY = 'wss://relay.primal.net'
|
||||||
|
export const DEFAULT_PLATFORM_MODE = 'prod'
|
||||||
|
export const DEFAULT_COINGECKO_TOKEN_COINS = 'bitcoin'
|
||||||
|
|
||||||
|
// 32 random bytes as a 64-char hex string — a secp256k1/Nostr secret key. The
|
||||||
|
// odds of an out-of-range key are ~1 in 2^128; if getPublicKey rejects it,
|
||||||
|
// init throws and StartOS retries with fresh entropy.
|
||||||
|
export function randomOperatorSecretHex(): string {
|
||||||
|
return utils.getDefaultString({ charset: '0-9,a-f', len: 64 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// A strong replacement for the app's weak hardcoded PLATFORM_SESSION_SECRET.
|
||||||
|
export function randomSessionSecret(): string {
|
||||||
|
return utils.getDefaultString({ charset: 'a-z,A-Z,0-9', len: 48 })
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { VersionGraph } from '@start9labs/start-sdk'
|
||||||
|
import { v_1_0_0_0 } from './v1.0.0.0'
|
||||||
|
|
||||||
|
export const versionGraph = VersionGraph.of({
|
||||||
|
current: v_1_0_0_0,
|
||||||
|
other: [],
|
||||||
|
})
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk'
|
||||||
|
|
||||||
|
export const v_1_0_0_0 = VersionInfo.of({
|
||||||
|
version: '1.0.0:0',
|
||||||
|
releaseNotes: {
|
||||||
|
en_US: `Initial StartOS package for SpacesOps (upstream v1.0.0).
|
||||||
|
- Runs the SpacesOps web platform (Express + SQLite) from the prebuilt spacesops/spacesops:v1.0.0 image on x86_64 and aarch64.
|
||||||
|
- Depends on the Spaces service and auto-connects to its spaced JSON-RPC at spaces.startos:7225 by reading the Spaces RPC credentials from the mounted Spaces volume.
|
||||||
|
- Generates a Nostr operator keypair and a strong session secret on first install. "Show Operator Credentials" and "Import Operator Key" actions manage the keypair.
|
||||||
|
- "Show Admin Credentials" surfaces the fixed, well-known admin login baked into the image, with a warning to keep the service private.
|
||||||
|
- "Configure Platform" optionally sets the Nostr relay, theme mode, CoinGecko pricing, and SUBSD backend.`,
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
up: async ({ effects }) => {},
|
||||||
|
down: IMPOSSIBLE,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"include": ["startos/**/*.ts", "node_modules/**/startos"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2018",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user