From f6f6a5bf2496034325a441bd4ad5855b8726cae7 Mon Sep 17 00:00:00 2001 From: helix-nine <267227783+helix-nine@users.noreply.github.com> Date: Sat, 20 Jun 2026 14:34:28 -0600 Subject: [PATCH] Review pass: SDK conformity, credential + relay UX, docs, i18n Audit and update of the community-contributed keep-startos against start-docs/packaging and the start9-registry packages. Conformity: - Rename the version file to current.ts (export `current`); keep version 0.4.0:0. - Add AGENTS.md, CLAUDE.md, TODO.md; sync s9pk.mk, .gitignore, .dockerignore, CONTRIBUTING.md to the registry canon. - Add tagAndRelease.yml; enable FREE_DISK_SPACE in release.yml (source build). - Renumber the i18n dictionary to close a stale index gap. Credentials (recipe-admin-credentials): - Drop the generate-in-init + "Show Login Credentials" reveal anti-pattern. A setupOnInit watcher now raises a critical task pointing at one Set Web Admin Password action that generates, stores, and returns the token (first-set and rotation). The vault password stays an internal secret. Remove credentials.ts and actions/showLoginCredentials.ts. Relays: - Seed upstream keep-core's canonical default relay set instead of a single relay. keep-web's bunker crashes with zero relays, so require >=1 (max 10, 256-char URLs), tighten the wss:// pattern, and fall back to the default set in main.ts. Docs: - Rewrite README.md and instructions.md to the writing-readmes / writing-instructions conventions; sync UPDATING.md. i18n: - Translate all dictionary strings and the release notes into es_ES, de_DE, pl_PL, fr_FR. Verified on a dev StartOS box: builds, installs, runs healthy; the credential task, password set/rotate, and Configure form work; i18n resolves per locale. Co-Authored-By: Claude Opus 4.8 (1M context) --- .dockerignore | 8 +- .github/workflows/release.yml | 1 + .github/workflows/tagAndRelease.yml | 25 +++ .gitignore | 8 +- AGENTS.md | 9 + CLAUDE.md | 1 + CONTRIBUTING.md | 15 +- README.md | 238 ++++++++++++++++++++-- TODO.md | 1 + UPDATING.md | 2 +- instructions.md | 57 ++++-- s9pk.mk | 9 +- startos/actions/configure.ts | 38 +++- startos/actions/index.ts | 4 +- startos/actions/setWebAdminPassword.ts | 64 ++++++ startos/actions/showLoginCredentials.ts | 49 ----- startos/credentials.ts | 25 --- startos/fileModels/store.json.ts | 14 +- startos/i18n/dictionaries/default.ts | 25 ++- startos/i18n/dictionaries/translations.ts | 99 ++++++++- startos/init/index.ts | 2 + startos/init/initializeService.ts | 18 +- startos/init/watchCredentials.ts | 17 ++ startos/main.ts | 25 ++- startos/utils.ts | 22 +- startos/versions/current.ts | 21 ++ startos/versions/index.ts | 13 +- startos/versions/v0.4.0_0.ts | 13 -- startos/versions/v0.4.1_0.ts | 13 -- startos/versions/v0.4.2_0.ts | 13 -- startos/versions/v0.4.3_0.ts | 13 -- startos/versions/v0.4.5_0.ts | 13 -- startos/versions/v0.4.6_0.ts | 13 -- startos/versions/v0.4.7_0.ts | 13 -- startos/versions/v0.4.8_0.ts | 13 -- 35 files changed, 606 insertions(+), 308 deletions(-) create mode 100644 .github/workflows/tagAndRelease.yml create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 TODO.md create mode 100644 startos/actions/setWebAdminPassword.ts delete mode 100644 startos/actions/showLoginCredentials.ts delete mode 100644 startos/credentials.ts create mode 100644 startos/init/watchCredentials.ts create mode 100644 startos/versions/current.ts delete mode 100644 startos/versions/v0.4.0_0.ts delete mode 100644 startos/versions/v0.4.1_0.ts delete mode 100644 startos/versions/v0.4.2_0.ts delete mode 100644 startos/versions/v0.4.3_0.ts delete mode 100644 startos/versions/v0.4.5_0.ts delete mode 100644 startos/versions/v0.4.6_0.ts delete mode 100644 startos/versions/v0.4.7_0.ts delete mode 100644 startos/versions/v0.4.8_0.ts diff --git a/.dockerignore b/.dockerignore index ddb2a65..0a17f46 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,2 @@ .git -.gitmodules -node_modules -javascript -*.s9pk -keep/target -keep/keep-web/ui/node_modules -keep/keep-web/ui/dist +.gitmodules \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75c6036..ced5f49 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,7 @@ jobs: release: uses: start9labs/shared-workflows/.github/workflows/release.yml@master with: + FREE_DISK_SPACE: true # keep-web compiles Rust + the Vite UI from source RELEASE_REGISTRY: ${{ vars.RELEASE_REGISTRY }} S3_S9PKS_BASE_URL: ${{ vars.S3_S9PKS_BASE_URL }} secrets: diff --git a/.github/workflows/tagAndRelease.yml b/.github/workflows/tagAndRelease.yml new file mode 100644 index 0000000..c82d597 --- /dev/null +++ b/.github/workflows/tagAndRelease.yml @@ -0,0 +1,25 @@ +name: Tag and Release + +on: + push: + branches: ['main'] + paths-ignore: ['*.md'] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tag: + uses: start9labs/shared-workflows/.github/workflows/tagAndRelease.yml@master + with: + REFERENCE_REGISTRY: ${{ vars.REFERENCE_REGISTRY }} + FREE_DISK_SPACE: true # keep-web compiles Rust + the Vite UI from source + RELEASE_REGISTRY: ${{ vars.RELEASE_REGISTRY }} + S3_S9PKS_BASE_URL: ${{ vars.S3_S9PKS_BASE_URL }} + secrets: + DEV_KEY: ${{ secrets.DEV_KEY }} + S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} + S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} + permissions: + contents: write diff --git a/.gitignore b/.gitignore index 7faf44f..4dad48c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ -node_modules/ -javascript/ *.s9pk +startos/*.js +node_modules/ .DS_Store +.vscode/ +docker-images +javascript +ncc-cache \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6df0a76 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ +# AGENTS.md + +This is a StartOS service-package repository — it builds a `.s9pk` for StartOS. + +Develop it inside a StartOS packaging workspace created by `start-cli s9pk init-workspace`, +which provides the packaging guide and agent context one level up. If you're reading this in a +bare clone with no workspace, the full guide is at . + +Work this package's `TODO.md` from top to bottom. Keep `README.md` (architecture, for developers and LLMs) and `instructions.md` (end-user docs) in sync with your changes. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8022836..c5ace59 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,8 +4,9 @@ - **[`README.md`](./README.md)** — what this package is and how it's built (image, volumes, interfaces, CI). Technical reference for developers and AI assistants. - **[`instructions.md`](./instructions.md)** — the user-facing instructions packed into the `.s9pk` and shown on the **Instructions** tab in StartOS, for the person running the service. +- **[`TODO.md`](./TODO.md)** — pending work on this package. -Any code change that affects user-visible behavior must update `README.md` and `instructions.md` in the same change. Content rules: [Writing READMEs](https://docs.start9.com/packaging/writing-readmes.html), [Writing Instructions](https://docs.start9.com/packaging/writing-instructions.html). +**Read all three before starting any work.** Any code change that affects user-visible behavior must update `README.md` and `instructions.md` in the same change; add to `TODO.md` when you defer work, and remove items when complete. Content rules: [Writing READMEs](https://docs.start9.com/packaging/writing-readmes.html), [Writing Instructions](https://docs.start9.com/packaging/writing-instructions.html). ## Environment setup @@ -17,7 +18,7 @@ See [Environment Setup](https://docs.start9.com/packaging/environment-setup.html git clone --recurse-submodules https://github.com/privkeyio/keep-startos cd keep-startos npm ci # install the TypeScript SDK -make x86 # build keep_x86_64.s9pk (x86_64 only — see Makefile / manifest) +make x86 # build keep_x86_64.s9pk (x86_64 only — see Makefile / manifest) make install # sideload to the host in ~/.startos/config.yaml ``` @@ -25,19 +26,21 @@ The `keep/` submodule is the upstream source; the root `Dockerfile` compiles `ke ## Updating the upstream version -See [UPDATING.md](./UPDATING.md) to bump the `keep` submodule, then update `version` and `releaseNotes` in the file under `startos/versions/`, renaming it to the new version string (a new version file is only needed for a migration or to preserve old release notes — see [Versions](https://docs.start9.com/packaging/versions.html)). +1. Apply the upstream bump per [UPDATING.md](./UPDATING.md) (fast-forward the `keep` submodule and stage the new pointer). +2. Update `version` and `releaseNotes` in `startos/versions/current.ts` — the latest version always lives in that file, so an in-place edit is all most bumps need. A new file is spun off only when the bump requires a migration — see [Versions](https://docs.start9.com/packaging/versions.html). ## CI/CD -Two workflows under `.github/workflows/` wrap reusable workflows in [`start9labs/shared-workflows`](https://github.com/Start9Labs/shared-workflows): +Three workflows under `.github/workflows/` wrap reusable workflows in [`start9labs/shared-workflows`](https://github.com/Start9Labs/shared-workflows): - **`build.yml`** — on PR to `main` and manual dispatch, builds the `.s9pk` to verify it packs. Requires repo secret `DEV_KEY` (a StartOS developer key from `start-cli init-key`). - **`release.yml`** — on a `v*.*` tag, builds and publishes. Requires `DEV_KEY` plus registry/S3 vars (`RELEASE_REGISTRY`, `S3_S9PKS_BASE_URL`) and secrets (`S3_ACCESS_KEY`, `S3_SECRET_KEY`) when publishing to your own registry. +- **`tagAndRelease.yml`** — on push to `main`, tags `v` and runs the release, skipping if that version is already in production. ### Cutting a release -1. Update `startos/versions/` (version + release notes) and bump the `keep` submodule if needed. -2. `git tag v0.3.0 && git push --tags` → `release.yml` builds and publishes. +1. Update `startos/versions/current.ts` (version + release notes) and bump the `keep` submodule if needed. +2. `git tag v0.4.0 && git push --tags` → `release.yml` builds and publishes (or merge to `main` and let `tagAndRelease.yml` tag and publish automatically). ## How to contribute diff --git a/README.md b/README.md index 3815283..0b015d4 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,229 @@ -# keep-startos +

+ Keep Logo +

-StartOS package for [Keep](https://github.com/privkeyio/keep) — an always-on FROST threshold co-signer for Nostr and Bitcoin. +# Keep on StartOS -The server holds one share of a multi-device FROST key and coordinates with your other devices over Nostr relays to produce signatures, so no single device ever holds the whole key. It exposes a NIP-46 bunker for Nostr clients and a web admin UI for importing your share and watching signing activity. +> **Upstream docs:** +> +> Everything not listed in this document should behave the same as upstream +> Keep. If a feature, setting, or behavior is not mentioned here, the upstream +> documentation is accurate and fully applicable. -Built on the [`keep-web`](https://github.com/privkeyio/keep/tree/main/keep-web) daemon (Rust axum API + embedded FROST node + Svelte admin SPA). +[Keep](https://github.com/privkeyio/keep) is a self-custodial key manager for Nostr and Bitcoin. This package runs its [`keep-web`](https://github.com/privkeyio/keep/tree/main/keep-web) daemon as an always-on FROST threshold co-signer: it holds one share of a multi-device key and coordinates signatures with your other devices over Nostr relays, so no single device ever holds the whole key. -## Building +--- -This package targets **StartOS 0.4.x** and uses the StartOS TypeScript SDK. +## Table of Contents -```sh -git clone --recurse-submodules https://github.com/privkeyio/keep-startos -cd keep-startos -make # produces keep_x86_64.s9pk (x86_64 only) -make install # installs to the host in ~/.startos/config.yaml -``` +- [Image and Container Runtime](#image-and-container-runtime) +- [Volume and Data Layout](#volume-and-data-layout) +- [Installation and First-Run Flow](#installation-and-first-run-flow) +- [Configuration Management](#configuration-management) +- [Network Access and Interfaces](#network-access-and-interfaces) +- [Actions (StartOS UI)](#actions-startos-ui) +- [Dependencies](#dependencies) +- [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) +- [Contributing](#contributing) +- [Quick Reference for AI Consumers](#quick-reference-for-ai-consumers) + +--- + +## Image and Container Runtime + +| Property | Value | +|----------|-------| +| Image | `keep` — built from source via the root `Dockerfile` | +| Source | `keep` git submodule (upstream `keep-web` crate + Svelte admin SPA) | +| Build | Rust release (`keep-web`, default features = wss-only) + Vite/Svelte UI, on a slim Debian runtime | +| Architectures | x86_64 | +| Entrypoint | `/app/keep-web` | + +--- + +## Volume and Data Layout + +| Volume | Mount Point | Purpose | +|--------|-------------|---------| +| `main` | `/data` | Encrypted vault and StartOS settings | + +**Key paths on the `main` volume:** + +- `/data/vault` — the encrypted Keep vault: your FROST share(s), signing state, and the persisted co-signing kill-switch flag +- `/data/start9/store.json` — StartOS persistent settings: vault password, Web Admin token, bunker/FROST relays, optional group + +--- + +## Installation and First-Run Flow + +| Step | Upstream | StartOS | +|------|----------|---------| +| Vault creation | Manual (`keep` CLI) | Auto-created at `/data/vault` on first start | +| Vault password | User-supplied | Auto-generated internal secret (never shown) | +| Web Admin login | Token logged at startup | Generated and rotated via the Set Web Admin Password action | +| Share import | CLI / app | Pasted into the Web Admin in setup mode | + +**First-run steps:** + +1. A critical task prompts you to run **Set Web Admin Password**; copy the generated password and sign in (username `admin`). +2. Open the **Web Admin** and import your FROST share (a `kshare1…` export from the device that created the group). +3. Restart the service so `keep-web` loads the share and starts the co-signer. +4. Flip the co-signing kill switch in the Web Admin (ships off, fail-closed). + +See [instructions.md](instructions.md) for the user-facing walkthrough. + +--- + +## Configuration Management + +| StartOS-Managed | Upstream-Managed (Web Admin) | +|-----------------|------------------------------| +| Vault path, listen address, UI dir (env) | FROST share import | +| Vault password (auto-generated internal secret) | Co-signing kill switch | +| Web Admin token (Set Web Admin Password action) | Signing policy / per-request approvals | +| Bunker & FROST relays, optional group (Configure action) | Active-group selection (with shares for multiple groups) | + +**Environment variables set by StartOS** (`startos/main.ts`): + +| Variable | Value | Purpose | +|----------|-------|---------| +| `KEEP_PATH` | `/data/vault` | Vault location (on the backed-up `main` volume) | +| `KEEP_WEB_LISTEN` | `0.0.0.0:8080` | Web Admin bind address | +| `KEEP_WEB_UI_DIR` | `/app/ui` | Built Svelte admin SPA | +| `KEEP_PASSWORD` | (auto-generated) | Vault encryption password | +| `KEEP_WEB_AUTH_TOKEN` | (set by action) | Web Admin bearer token; omitted until set | +| `KEEP_BUNKER_RELAY` | (Configure) | NIP-46 bunker relays, comma-separated | +| `KEEP_FROST_RELAY` | (Configure) | FROST coordination relays, comma-separated | +| `KEEP_FROST_GROUP` | (Configure, optional) | Pins a specific group npub; omitted → auto-select (switch the active group in the Web Admin) | + +Single-key mode (`KEEP_ALLOW_SINGLE_KEY`) and env-level auto-approve (`KEEP_FROST_AUTO_APPROVE`) are intentionally left unset — Keep runs as a fail-closed FROST co-signer. + +Relays default to a single `wss://bucket.coracle.social` (upstream keep-core's default): it reliably delivers the rapid ephemeral kind-24242 events FROST coordination needs, which most general-purpose relays drop. The Configure action requires at least one relay (the bunker will not start with none) and accepts up to 10 — add more known-good relays there for redundancy. + +--- + +## Network Access and Interfaces + +| Interface | Port | Protocol | Purpose | +|-----------|------|----------|---------| +| Web Admin (`ui`) | 8080 | HTTP | Admin SPA + bearer-authenticated `/api` | + +The NIP-46 bunker and FROST signing are **not** bound ports: `keep-web` reaches Nostr relays over outbound WebSocket connections, and clients reach the bunker through those relays using the connection string shown in the Web Admin. + +**Access methods:** + +- LAN IP with unique port +- `.local` with unique port +- Tor `.onion` address (if added) +- Custom domains (if configured) -The `keep` git submodule pins the upstream source built into the image (`Dockerfile` builds `keep-web` + the SPA). +--- -## Structure +## Actions (StartOS UI) -- `startos/` — package definition (manifest, main daemon, interfaces, config store, actions, i18n). -- `Dockerfile` — multi-stage build: Rust (`keep-web`, release, wss-only) + Node (SPA) → slim runtime image. -- `keep/` — upstream source as a git submodule. +### Set Web Admin Password -## CI +Generate or rotate the Web Admin bearer token. On install a critical task prompts you to run it before signing in; re-run any time to rotate. -- **Build** (`.github/workflows/build.yml`): on PRs to `main` and manual dispatch, builds the `.s9pk` via Start9's shared workflow to verify it packs. Requires repo secret **`DEV_KEY`** (a StartOS developer key, `start-cli init-key`). -- **Release** (`.github/workflows/release.yml`): on `v*.*` tags, builds and publishes. Requires `DEV_KEY` plus the registry/S3 vars (`RELEASE_REGISTRY`, `S3_S9PKS_BASE_URL`) and secrets (`S3_ACCESS_KEY`, `S3_SECRET_KEY`) when publishing to your own registry. +| Property | Value | +|----------|-------| +| Availability | Any status | +| Visibility | Always visible (also a critical task on install) | +| Inputs | None | +| Outputs | Username (`admin`) and the generated password | -See [`instructions.md`](instructions.md) for setup and usage. +### Configure + +Set the relays and group Keep uses, then restart to apply. + +| Property | Value | +|----------|-------| +| Availability | Any status | +| Visibility | Always visible | +| Inputs | Bunker Relays, FROST Relays, Group (npub, optional) | +| Outputs | Confirmation; restarts the service | + +--- + +## Dependencies + +None. + +--- + +## Backups and Restore + +**Included in backup:** + +- `main` volume — the encrypted vault (`/data/vault`) and `store.json` + +**Restore behavior:** + +- The vault and its password are restored together, so your share, kill-switch state, relays, and Web Admin token come back as-is — no re-import or reconfiguration. Init regenerates the vault password only on a fresh install, never on restore. + +--- + +## Health Checks + +| Check | Display Name | Method | Messages | +|-------|--------------|--------|----------| +| `primary` | Web Admin | Port-listening check on 8080 | "The Keep admin UI is ready" / "The Keep admin UI is not responding" | + +--- + +## Limitations and Differences + +1. **x86_64 only** — `keep-web` is built for x86_64; there is no aarch64 or riscv64 image. +2. **Requires an existing FROST group** — this package imports shares; it does not run key generation. Create the group and export a share elsewhere (e.g. Keep Android) first. +3. **Co-signing is off by default** — fail-closed; enable it with the Web Admin kill switch. +4. **Share import is Web-Admin-only** — the upstream CLI and enclave paths are not exposed; import in setup mode, then restart so the co-signer starts. + +--- + +## What Is Unchanged from Upstream + +- FROST threshold signing and distributed key generation coordinated over Nostr relays +- NIP-46 bunker remote signing for any compatible Nostr client +- The `keep-web` admin SPA and its bearer-authenticated `/api` surface +- Argon2id + XChaCha20-Poly1305 vault encryption with keys zeroized in RAM +- The co-signing kill switch and live signing-activity feed + +--- + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for environment setup, the submodule and `make x86` build, and the release workflow. + +--- + +## Quick Reference for AI Consumers + +```yaml +package_id: keep +architectures: [x86_64] +image: keep (built from source; keep-web crate + Svelte SPA) +volumes: + main: /data +ports: + ui: 8080 +dependencies: none +startos_managed_env_vars: + - KEEP_PATH + - KEEP_WEB_LISTEN + - KEEP_WEB_UI_DIR + - KEEP_PASSWORD + - KEEP_WEB_AUTH_TOKEN + - KEEP_BUNKER_RELAY + - KEEP_FROST_RELAY + - KEEP_FROST_GROUP +actions: + - set-web-admin-password + - configure +health_checks: + - primary: port_check 8080 +backup_volumes: + - main +``` diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..4640904 --- /dev/null +++ b/TODO.md @@ -0,0 +1 @@ +# TODO diff --git a/UPDATING.md b/UPDATING.md index 7202924..b688904 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -21,7 +21,7 @@ cd keep && awk -F'"' '/^version/ {print $2; exit}' Cargo.toml cd .. && git add keep ``` -2. Update `startos/versions/` — set `version` (and `releaseNotes`) to match. Rename the version file to the new version string if you want the old notes preserved in git history. +2. Update `startos/versions/current.ts` — set `version` (and `releaseNotes`) to match. The latest version always lives in `current.ts`, so an in-place edit is all most bumps need; spin off a new version file only when the bump carries a migration (see [Versions](https://docs.start9.com/packaging/versions.html)). 3. Rebuild and sideload to smoke-test: diff --git a/instructions.md b/instructions.md index 25618b4..41d3c6a 100644 --- a/instructions.md +++ b/instructions.md @@ -1,41 +1,54 @@ # Keep -Keep turns your StartOS server into an always-on member of your FROST signing quorum. It holds one share of a multi-device key and coordinates with your other devices (phone, laptop) over Nostr relays to produce signatures. No single device, including this one, ever holds the whole key. +> **Before you start:** Keep co-signs for a FROST group you created **somewhere else** (for example, the Keep Android app). This package holds and imports your share(s) of one or more such groups — it does not generate keys on its own. Have your share export ready. -> **Important:** Keep only works as part of a multi-device FROST group you already created elsewhere (for example, the Keep Android app). This package imports one share of that group; it does not create keys on its own. +## Documentation -## Why run it on StartOS +- [Keep usage guide](https://github.com/privkeyio/keep/blob/main/docs/USAGE.md) — exporting and importing shares, bunker mode, and signing. +- [Keep security model](https://github.com/privkeyio/keep/blob/main/docs/SECURITY.md) — how the vault, keys, and FROST shares are protected. +- [Keep upstream README](https://github.com/privkeyio/keep#readme) — project overview and features. -Threshold signing needs enough devices online at the same moment. Phones sleep and drop connectivity, so two of them rarely line up. An always-on server fixes that: it is the reliably online co-signer, so signing from your other device works on demand instead of needing two devices awake at once. It also keeps signing nonces pre-staged for instant signatures. +## What you get on StartOS -## First-time setup +- A **Web Admin** interface (the `ui` interface, reachable on your LAN) for importing your share, reading the bunker connection string, toggling co-signing, and watching signing activity. +- An always-on **FROST co-signer** — the reliably online member of your quorum, so signing from your phone or laptop no longer needs two devices awake at once. +- A **NIP-46 bunker** any Nostr client can point at to request signatures; Keep coordinates each one with your other devices over Nostr relays. +- Your FROST share — one per group — held in an encrypted vault on the backed-up `main` volume. -1. **Get your login.** After install, run the **Show Login Credentials** action (also surfaced as a task) to see your Web Admin username and password. Open the Web Admin and sign in with them. -2. **Import your share.** On a fresh install the Web Admin starts in setup mode, with no share loaded. Paste your FROST share export (a `kshare1…` string or JSON) and its passphrase. You create this export on the device that holds the group, for example Keep Android, Export Share, Show text or QR. -3. **Restart the service.** Once a share is present, restarting starts the co-signer. It announces itself on the FROST relay, but stays in standby until you enable co-signing. -4. **Enable co-signing.** Use the kill switch in the Web Admin to turn co-signing on (it ships off, fail-closed). The setting persists across restarts. +## Getting set up -After that, the Web Admin shows the bunker connection string (npub), the group, the threshold, and a live feed of signing activity. If you import shares for more than one group, the Shares section marks the active group and lets you switch which one the co-signer serves. +You need an existing FROST group and a share export from the device that created it (e.g. Keep Android → Export Share → show text or QR). -## Configuration +1. **Set your login.** On install, StartOS prompts you with a critical **Set Web Admin Password** action. Run it, save the generated password to your password manager, and use it to sign in — the username is `admin`. Run the same action any time to rotate the password. +2. **Import your share.** Open the **Web Admin**. On a fresh install it starts in setup mode with no share loaded. Paste your FROST share export (a `kshare1…` string or JSON) and its passphrase. +3. **Restart Keep.** The co-signer loads your share at startup, so restart the service once the share is imported. It then announces itself on your FROST relay and waits in standby. +4. **Enable co-signing.** Co-signing ships **off** (fail-closed). Flip the kill switch in the Web Admin to turn it on; the setting persists across restarts. -Use the **Configure** action to set: +Once it's running, the Web Admin shows your bunker connection string (npub), the group, the threshold, and a live feed of signing activity. If you import shares for more than one group, the Shares section marks the active group and lets you switch which one the co-signer serves. -- **Bunker Relays:** where Nostr clients reach this signer over NIP-46. Default `wss://bucket.coracle.social`. -- **FROST Relays:** where signing rounds are coordinated with your other devices. These must match the relays your other devices use. Default `wss://bucket.coracle.social`. -- **Group (npub):** optional override. Leave blank and the co-signer serves your share's group automatically. If the vault holds shares for more than one group it auto-selects one (no crash) and you can switch which group is served from the Web Admin's Shares section. Set this only to pin a specific group. +## Using Keep -Saving the configuration restarts the service. +### Web Admin -## Turning co-signing on and off +Sign in with the username `admin` and the password from **Set Web Admin Password**. The Web Admin is where you import your share(s), read your bunker connection string, flip the co-signing kill switch, switch the active group in the Shares section, and watch signing activity. -Co-signing is controlled by the kill switch in the Web Admin, not the Configure action. A fresh install starts with co-signing **off** (fail-closed), so the node won't sign until you turn it on. Toggle it live in the Web Admin with no restart; the setting persists across restarts. While it's on, the node takes part in signing rounds automatically within policy, and the human approval happens on your other device. +### Co-signing kill switch -## Connecting a Nostr client +Co-signing is controlled by the kill switch in the Web Admin, not by an action. Toggle it live with no restart; it persists across restarts. While it's on, Keep joins signing rounds automatically within policy — the human approval still happens on your other device. -Point any NIP-46 ("bunker") capable Nostr client at the bunker connection string shown in the Web Admin. When the client requests a signature, this node coordinates a FROST round with your other devices to produce it. +### Connecting a Nostr client + +Point any NIP-46 ("bunker") capable Nostr client at the connection string shown in the Web Admin. When the client requests a signature, Keep coordinates a FROST round with your other devices to produce it. + +### Actions + +- **Set Web Admin Password** — generate or rotate the Web Admin password. Surfaced as a critical task on a fresh install. +- **Configure** — set the relays and group Keep uses: + - **Bunker Relays** — where Nostr clients reach this signer over NIP-46. Pre-filled with `wss://bucket.coracle.social`; keep at least one. + - **FROST Relays** — where signing rounds are coordinated with your other devices; these must match the relays your other devices use. Pre-filled with `wss://bucket.coracle.social`, which reliably carries the rapid FROST signing traffic that most general-purpose relays drop; keep at least one. + - **Group (npub)** — optional override. Leave blank and the co-signer serves your share's group automatically; with shares for more than one group it auto-selects one and you can switch which is served from the Web Admin's Shares section. Set this only to pin a specific group. ## Security notes -- The vault is encrypted at rest with a password generated at install and stored in this package's data volume, which is captured by StartOS backups. Because the service runs unattended, the share is decrypted in memory while it runs. That is the trade-off for an always-on signer. -- This node holds only one share. Compromising it alone cannot sign or reconstruct your key; an attacker would still need to reach the threshold using your other devices. +- Because Keep runs unattended, your share is decrypted in memory while the service is running. That is the trade-off for an always-on signer. +- This server holds only one share of any group — compromising it alone cannot sign or reconstruct a key; an attacker would still need to reach the threshold using your other devices. diff --git a/s9pk.mk b/s9pk.mk index 25538af..675ebc9 100644 --- a/s9pk.mk +++ b/s9pk.mk @@ -82,11 +82,11 @@ install: | check-deps check-init 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 \ + if [ -z "$$(ls *.s9pk 2>/dev/null)" ]; then \ echo "Error: No .s9pk file found. Run 'make' first."; \ exit 1; \ fi; \ + S9PK=$$(start-cli s9pk select) || exit 1; \ printf "\n🚀 Installing %s to %s ...\n" "$$S9PK" "$$HOST"; \ start-cli package install -s "$$S9PK" @@ -130,12 +130,9 @@ javascript/index.js: $(shell find startos -type f) tsconfig.json node_modules npm run check npm run build -node_modules: package-lock.json +node_modules: package-lock.json package.json npm ci -package-lock.json: package.json - npm i - clean: @echo "Cleaning up build artifacts..." @rm -rf $(PACKAGE_ID).s9pk $(PACKAGE_ID)_x86_64.s9pk $(PACKAGE_ID)_aarch64.s9pk $(PACKAGE_ID)_riscv64.s9pk javascript node_modules diff --git a/startos/actions/configure.ts b/startos/actions/configure.ts index 371d165..15d9282 100644 --- a/startos/actions/configure.ts +++ b/startos/actions/configure.ts @@ -1,13 +1,17 @@ import { sdk } from '../sdk' import { i18n } from '../i18n' import { storeJson } from '../fileModels/store.json' -import { defaultBunkerRelay, defaultFrostRelay } from '../utils' +import { defaultRelays, maxRelays, maxRelayUrlLength } from '../utils' const { InputSpec, Value, List } = sdk +// Mirror upstream keep-core `validate_relay_url`'s key rules: wss:// scheme, a +// non-empty host, and no whitespace or userinfo (a malformed relay would crash +// the co-signer when keep-web calls add_relay). Private/loopback hosts can't be +// caught in a regex; upstream rejects those at connect time. const relayPattern = { - regex: '^wss://.+', - description: 'Relay URLs must start with wss://', + regex: '^wss://[^\\s@]+$', + description: 'Must be a wss:// relay URL with no spaces, e.g. wss://relay.example.com', } const inputSpec = InputSpec.of({ @@ -16,11 +20,17 @@ const inputSpec = InputSpec.of({ { name: i18n('Bunker Relays'), description: i18n( - 'Relays used for the NIP-46 bunker (where Nostr clients reach this signer)', + 'Relays used for the NIP-46 bunker (where Nostr clients reach this signer). At least one is required.', ), - default: [defaultBunkerRelay], + default: defaultRelays, + minLength: 1, + maxLength: maxRelays, + }, + { + patterns: [relayPattern], + placeholder: 'wss://bucket.coracle.social', + maxLength: maxRelayUrlLength, }, - { patterns: [relayPattern], placeholder: 'wss://bucket.coracle.social' }, ), ), frostRelays: Value.list( @@ -28,11 +38,17 @@ const inputSpec = InputSpec.of({ { name: i18n('FROST Relays'), description: i18n( - 'Relays used to coordinate FROST signing rounds with your other devices. These must match the relays your other share-holders use.', + 'Relays used to coordinate FROST signing rounds with your other devices. These must match the relays your other share-holders use. At least one is required.', ), - default: [defaultFrostRelay], + default: defaultRelays, + minLength: 1, + maxLength: maxRelays, + }, + { + patterns: [relayPattern], + placeholder: 'wss://bucket.coracle.social', + maxLength: maxRelayUrlLength, }, - { patterns: [relayPattern], placeholder: 'wss://bucket.coracle.social' }, ), ), frostGroup: Value.text({ @@ -59,8 +75,8 @@ export const configure = sdk.Action.withInput( async ({ effects }) => { const s = await storeJson.read().once() return { - bunkerRelays: s?.bunkerRelays ?? [defaultBunkerRelay], - frostRelays: s?.frostRelays ?? [defaultFrostRelay], + bunkerRelays: s?.bunkerRelays?.length ? s.bunkerRelays : defaultRelays, + frostRelays: s?.frostRelays?.length ? s.frostRelays : defaultRelays, frostGroup: s?.frostGroup || null, } }, diff --git a/startos/actions/index.ts b/startos/actions/index.ts index 4d2b558..561c7cf 100644 --- a/startos/actions/index.ts +++ b/startos/actions/index.ts @@ -1,7 +1,7 @@ import { sdk } from '../sdk' import { configure } from './configure' -import { showLoginCredentials } from './showLoginCredentials' +import { setWebAdminPassword } from './setWebAdminPassword' export const actions = sdk.Actions.of() .addAction(configure) - .addAction(showLoginCredentials) + .addAction(setWebAdminPassword) diff --git a/startos/actions/setWebAdminPassword.ts b/startos/actions/setWebAdminPassword.ts new file mode 100644 index 0000000..c3201c5 --- /dev/null +++ b/startos/actions/setWebAdminPassword.ts @@ -0,0 +1,64 @@ +import { utils } from '@start9labs/start-sdk' +import { storeJson } from '../fileModels/store.json' +import { i18n } from '../i18n' +import { sdk } from '../sdk' +import { ADMIN_USERNAME } from '../utils' + +// Single source of truth for the Web Admin credential: this action generates, +// stores, and returns it — covering both first-set and rotation. The init +// watcher (init/watchCredentials.ts) only decides whether to surface the task. +// See recipe-admin-credentials. +export const setWebAdminPassword = sdk.Action.withoutInput( + 'set-web-admin-password', + + async () => ({ + name: i18n('Set Web Admin Password'), + description: i18n( + 'Generate a new random password to sign in to the Keep Web Admin. Run this again at any time to rotate it; the current password is replaced.', + ), + warning: null, + // Runnable while running or stopped: main reads the token reactively, so a + // running service restarts onto the new token; a stopped one loads it on + // next start. + allowedStatuses: 'any', + group: null, + visibility: 'enabled', + }), + + async ({ effects }) => { + const password = utils.getDefaultString({ + charset: 'a-z,A-Z,0-9', + len: 32, + }) + await storeJson.merge(effects, { webAuthToken: password }) + + return { + version: '1' as const, + title: i18n('Web Admin Password'), + message: i18n('Use this password to sign in to the Keep Web Admin.'), + result: { + type: 'group', + value: [ + { + type: 'single', + name: i18n('Username'), + description: null, + value: ADMIN_USERNAME, + masked: false, + copyable: true, + qr: false, + }, + { + type: 'single', + name: i18n('Password'), + description: null, + value: password, + masked: true, + copyable: true, + qr: false, + }, + ], + }, + } + }, +) diff --git a/startos/actions/showLoginCredentials.ts b/startos/actions/showLoginCredentials.ts deleted file mode 100644 index c3dc6c7..0000000 --- a/startos/actions/showLoginCredentials.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ensureCredentials } from '../credentials' -import { i18n } from '../i18n' -import { sdk } from '../sdk' - -export const showLoginCredentials = sdk.Action.withoutInput( - 'show-login-credentials', - - async ({ effects }) => ({ - name: i18n('Show Login Credentials'), - description: i18n('Reveal the username and password for the Web Admin'), - warning: null, - allowedStatuses: 'any', - group: null, - visibility: 'enabled', - }), - - async ({ effects }) => { - const credentials = await ensureCredentials(effects) - - return { - version: '1' as const, - title: i18n('Web Admin Login'), - message: i18n('Use these credentials to sign in to the Keep Web Admin.'), - result: { - type: 'group', - value: [ - { - name: i18n('Username'), - description: null, - type: 'single', - value: credentials.username, - copyable: true, - qr: false, - masked: false, - }, - { - name: i18n('Password'), - description: null, - type: 'single', - value: credentials.password, - copyable: true, - qr: false, - masked: true, - }, - ], - }, - } - }, -) diff --git a/startos/credentials.ts b/startos/credentials.ts deleted file mode 100644 index 5cfb2db..0000000 --- a/startos/credentials.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { T, utils } from '@start9labs/start-sdk' -import { storeJson } from './fileModels/store.json' - -export const ADMIN_USERNAME = 'admin' - -export type KeepCredentials = { - username: string - password: string -} - -// Generate-if-missing on every start (not install-only), so the token is -// stable across upgrades and always matches what the action reveals. -export async function ensureCredentials( - effects: T.Effects, -): Promise { - const store = await storeJson.read().once() - const password = - store?.webAuthToken || utils.getDefaultString({ charset: 'a-z,A-Z,0-9', len: 32 }) - - if (store?.webAuthToken !== password) { - await storeJson.merge(effects, { webAuthToken: password }) - } - - return { username: ADMIN_USERNAME, password } -} diff --git a/startos/fileModels/store.json.ts b/startos/fileModels/store.json.ts index 0bd4503..a3e08fd 100644 --- a/startos/fileModels/store.json.ts +++ b/startos/fileModels/store.json.ts @@ -1,18 +1,20 @@ import { z, FileHelper } from '@start9labs/start-sdk' import { sdk } from '../sdk' -import { defaultBunkerRelay, defaultFrostRelay } from '../utils' +import { defaultRelays } from '../utils' // Package-internal state. Written only by our init + actions, so .const() // gives automatic restart-on-change. const storeConfigSchema = z.object({ // Generated once at install; encrypts the Keep vault at rest. vaultPassword: z.string().catch(''), - // Generated once at install; bearer token for the Web Admin login. - webAuthToken: z.string().catch(''), - // Relays where Nostr clients reach the NIP-46 bunker. - bunkerRelays: z.array(z.string()).catch([defaultBunkerRelay]), + // Bearer token for the Web Admin login. Unset until the user runs the + // Set/Reset Web Admin Password action (recipe-admin-credentials). + webAuthToken: z.string().optional().catch(undefined), + // Relays where Nostr clients reach the NIP-46 bunker. At least one is + // required — keep-web's bunker will not start with an empty list. + bunkerRelays: z.array(z.string()).catch(defaultRelays), // Relays used to coordinate FROST signing rounds with peer devices. - frostRelays: z.array(z.string()).catch([defaultFrostRelay]), + frostRelays: z.array(z.string()).catch(defaultRelays), // The group npub to co-sign for. Empty = auto-detect the single imported // share's group. frostGroup: z.string().catch(''), diff --git a/startos/i18n/dictionaries/default.ts b/startos/i18n/dictionaries/default.ts index 2496569..fe9332c 100644 --- a/startos/i18n/dictionaries/default.ts +++ b/startos/i18n/dictionaries/default.ts @@ -14,21 +14,24 @@ const dict = { Configure: 5, 'Configure the relays and group for the FROST co-signer': 6, 'Bunker Relays': 7, - 'Relays used for the NIP-46 bunker (where Nostr clients reach this signer)': 8, + 'Relays used for the NIP-46 bunker (where Nostr clients reach this signer). At least one is required.': 8, 'FROST Relays': 9, - 'Relays used to coordinate FROST signing rounds with your other devices. These must match the relays your other share-holders use.': 10, + 'Relays used to coordinate FROST signing rounds with your other devices. These must match the relays your other share-holders use. At least one is required.': 10, 'Group (npub)': 11, 'Optional: the FROST group to co-sign for. Leave blank to auto-detect from the imported share.': 12, - 'Configuration saved': 15, - 'The service is restarting with the new settings.': 16, + 'Configuration saved': 13, + 'The service is restarting with the new settings.': 14, - // actions/showLoginCredentials.ts - 'Show Login Credentials': 17, - 'Reveal the username and password for the Web Admin': 18, - 'Web Admin Login': 19, - 'Use these credentials to sign in to the Keep Web Admin.': 20, - Username: 21, - Password: 22, + // actions/setWebAdminPassword.ts + 'Set Web Admin Password': 15, + 'Generate a new random password to sign in to the Keep Web Admin. Run this again at any time to rotate it; the current password is replaced.': 16, + 'Web Admin Password': 17, + 'Use this password to sign in to the Keep Web Admin.': 18, + Username: 19, + Password: 20, + + // init/watchCredentials.ts + 'Set your Keep Web Admin password, then sign in.': 21, } as const /** diff --git a/startos/i18n/dictionaries/translations.ts b/startos/i18n/dictionaries/translations.ts index 98f9348..0394a5b 100644 --- a/startos/i18n/dictionaries/translations.ts +++ b/startos/i18n/dictionaries/translations.ts @@ -1,3 +1,100 @@ import { LangDict } from './default' -export default {} satisfies Record +export default { + es_ES: { + 0: 'Iniciando el firmante conjunto Keep', + 1: 'Administración web', + 2: 'La interfaz de administración de Keep está lista', + 3: 'La interfaz de administración de Keep no responde', + 4: 'Importa tu fragmento FROST, consulta la conexión del bunker y observa la actividad de firma', + 5: 'Configurar', + 6: 'Configura los relés y el grupo del firmante conjunto FROST', + 7: 'Relés del bunker', + 8: 'Relés usados para el bunker NIP-46 (donde los clientes Nostr contactan con este firmante). Se requiere al menos uno.', + 9: 'Relés FROST', + 10: 'Relés usados para coordinar las rondas de firma FROST con tus otros dispositivos. Deben coincidir con los relés que usan los demás poseedores de fragmentos. Se requiere al menos uno.', + 11: 'Grupo (npub)', + 12: 'Opcional: el grupo FROST para el que co-firmar. Déjalo en blanco para detectarlo automáticamente a partir del fragmento importado.', + 13: 'Configuración guardada', + 14: 'El servicio se está reiniciando con la nueva configuración.', + 15: 'Establecer la contraseña de administración web', + 16: 'Genera una nueva contraseña aleatoria para iniciar sesión en la administración web de Keep. Ejecuta esta acción de nuevo en cualquier momento para rotarla; la contraseña actual se reemplaza.', + 17: 'Contraseña de administración web', + 18: 'Usa esta contraseña para iniciar sesión en la administración web de Keep.', + 19: 'Usuario', + 20: 'Contraseña', + 21: 'Establece tu contraseña de administración web de Keep y luego inicia sesión.', + }, + de_DE: { + 0: 'Keep-Mitunterzeichner wird gestartet', + 1: 'Web-Administration', + 2: 'Die Keep-Administrationsoberfläche ist bereit', + 3: 'Die Keep-Administrationsoberfläche antwortet nicht', + 4: 'Importieren Sie Ihren FROST-Anteil, sehen Sie die Bunker-Verbindung ein und verfolgen Sie die Signieraktivität', + 5: 'Konfigurieren', + 6: 'Konfigurieren Sie die Relays und die Gruppe für den FROST-Mitunterzeichner', + 7: 'Bunker-Relays', + 8: 'Relays für den NIP-46-Bunker (über die Nostr-Clients diesen Unterzeichner erreichen). Mindestens eines ist erforderlich.', + 9: 'FROST-Relays', + 10: 'Relays zur Koordination der FROST-Signierrunden mit Ihren anderen Geräten. Sie müssen mit den Relays übereinstimmen, die Ihre anderen Anteilsinhaber verwenden. Mindestens eines ist erforderlich.', + 11: 'Gruppe (npub)', + 12: 'Optional: die FROST-Gruppe, für die mitsigniert werden soll. Leer lassen, um sie automatisch aus dem importierten Anteil zu erkennen.', + 13: 'Konfiguration gespeichert', + 14: 'Der Dienst wird mit den neuen Einstellungen neu gestartet.', + 15: 'Web-Administrationspasswort festlegen', + 16: 'Generieren Sie ein neues zufälliges Passwort für die Anmeldung an der Keep-Web-Administration. Führen Sie diese Aktion jederzeit erneut aus, um es zu rotieren; das aktuelle Passwort wird ersetzt.', + 17: 'Web-Administrationspasswort', + 18: 'Verwenden Sie dieses Passwort, um sich an der Keep-Web-Administration anzumelden.', + 19: 'Benutzername', + 20: 'Passwort', + 21: 'Legen Sie Ihr Keep-Web-Administrationspasswort fest und melden Sie sich dann an.', + }, + pl_PL: { + 0: 'Uruchamianie współsygnatariusza Keep', + 1: 'Panel administracyjny', + 2: 'Panel administracyjny Keep jest gotowy', + 3: 'Panel administracyjny Keep nie odpowiada', + 4: 'Zaimportuj swój udział FROST, zobacz połączenie bunkra i obserwuj aktywność podpisywania', + 5: 'Konfiguruj', + 6: 'Skonfiguruj przekaźniki i grupę dla współsygnatariusza FROST', + 7: 'Przekaźniki bunkra', + 8: 'Przekaźniki używane do bunkra NIP-46 (przez które klienci Nostr łączą się z tym sygnatariuszem). Wymagany jest co najmniej jeden.', + 9: 'Przekaźniki FROST', + 10: 'Przekaźniki używane do koordynacji rund podpisywania FROST z Twoimi innymi urządzeniami. Muszą być zgodne z przekaźnikami używanymi przez pozostałych posiadaczy udziałów. Wymagany jest co najmniej jeden.', + 11: 'Grupa (npub)', + 12: 'Opcjonalnie: grupa FROST, dla której współpodpisywać. Pozostaw puste, aby wykryć ją automatycznie z zaimportowanego udziału.', + 13: 'Konfiguracja zapisana', + 14: 'Usługa jest ponownie uruchamiana z nowymi ustawieniami.', + 15: 'Ustaw hasło panelu administracyjnego', + 16: 'Wygeneruj nowe losowe hasło do logowania w panelu administracyjnym Keep. Uruchom tę akcję ponownie w dowolnym momencie, aby je zmienić; obecne hasło zostanie zastąpione.', + 17: 'Hasło panelu administracyjnego', + 18: 'Użyj tego hasła, aby zalogować się do panelu administracyjnego Keep.', + 19: 'Nazwa użytkownika', + 20: 'Hasło', + 21: 'Ustaw hasło panelu administracyjnego Keep, a następnie zaloguj się.', + }, + fr_FR: { + 0: 'Démarrage du cosignataire Keep', + 1: 'Administration web', + 2: "L'interface d'administration de Keep est prête", + 3: "L'interface d'administration de Keep ne répond pas", + 4: "Importez votre fragment FROST, consultez la connexion bunker et suivez l'activité de signature", + 5: 'Configurer', + 6: 'Configurez les relais et le groupe du cosignataire FROST', + 7: 'Relais bunker', + 8: 'Relais utilisés pour le bunker NIP-46 (par lesquels les clients Nostr joignent ce signataire). Au moins un est requis.', + 9: 'Relais FROST', + 10: 'Relais utilisés pour coordonner les cycles de signature FROST avec vos autres appareils. Ils doivent correspondre aux relais utilisés par les autres détenteurs de fragments. Au moins un est requis.', + 11: 'Groupe (npub)', + 12: 'Facultatif : le groupe FROST pour lequel cosigner. Laissez vide pour le détecter automatiquement à partir du fragment importé.', + 13: 'Configuration enregistrée', + 14: 'Le service redémarre avec les nouveaux paramètres.', + 15: "Définir le mot de passe d'administration web", + 16: "Générez un nouveau mot de passe aléatoire pour vous connecter à l'administration web de Keep. Relancez cette action à tout moment pour le renouveler ; le mot de passe actuel est remplacé.", + 17: "Mot de passe d'administration web", + 18: "Utilisez ce mot de passe pour vous connecter à l'administration web de Keep.", + 19: "Nom d'utilisateur", + 20: 'Mot de passe', + 21: "Définissez votre mot de passe d'administration web de Keep, puis connectez-vous.", + }, +} satisfies Record diff --git a/startos/init/index.ts b/startos/init/index.ts index a07ab38..53a7b89 100644 --- a/startos/init/index.ts +++ b/startos/init/index.ts @@ -5,6 +5,7 @@ import { versionGraph } from '../versions' import { actions } from '../actions' import { restoreInit } from '../backups' import { initializeService } from './initializeService' +import { watchCredentials } from './watchCredentials' export const init = sdk.setupInit( restoreInit, @@ -13,6 +14,7 @@ export const init = sdk.setupInit( setDependencies, actions, initializeService, + watchCredentials, ) export const uninit = sdk.setupUninit(versionGraph) diff --git a/startos/init/initializeService.ts b/startos/init/initializeService.ts index 710f7db..a7c4445 100644 --- a/startos/init/initializeService.ts +++ b/startos/init/initializeService.ts @@ -1,6 +1,4 @@ import { utils } from '@start9labs/start-sdk' -import { showLoginCredentials } from '../actions/showLoginCredentials' -import { ensureCredentials } from '../credentials' import { storeJson } from '../fileModels/store.json' import { sdk } from '../sdk' @@ -10,21 +8,15 @@ export const initializeService = sdk.setupOnInit(async (effects, kind) => { if (kind !== 'install') return - // Generate the vault password once at install. It encrypts the Keep vault at - // rest; it lives in the (backed-up) main volume's store.json. The Web Admin - // token is handled lazily by ensureCredentials so it survives upgrades. + // Generate the vault password once, at install only. It is an internal secret + // (recipe-internal-secrets): it encrypts the Keep vault at rest and is never + // shown to the user. It lives in the backed-up main volume's store.json, so it + // survives upgrades/restarts and is restored alongside the vault — gating on + // `install` keeps a restore from overwriting the restored password. await storeJson.merge(effects, { vaultPassword: utils.getDefaultString({ charset: 'a-z,A-Z,0-9', len: 32, }), }) - - // Mint the Web Admin credentials now and surface a task so the operator saves - // them before opening the UI (no log-digging, no env vars). - await ensureCredentials(effects) - await sdk.action.createOwnTask(effects, showLoginCredentials, 'important', { - reason: 'Save your Keep Web Admin username and password, then sign in.', - replayId: 'save-login-credentials', - }) }) diff --git a/startos/init/watchCredentials.ts b/startos/init/watchCredentials.ts new file mode 100644 index 0000000..72708dc --- /dev/null +++ b/startos/init/watchCredentials.ts @@ -0,0 +1,17 @@ +import { setWebAdminPassword } from '../actions/setWebAdminPassword' +import { storeJson } from '../fileModels/store.json' +import { i18n } from '../i18n' +import { sdk } from '../sdk' + +// Surface a critical task whenever the Web Admin token is unset, pointing at the +// Set/Reset Web Admin Password action. Reactive (.const) so it re-evaluates on +// every init and stops nagging once the action has set a token. +export const watchCredentials = sdk.setupOnInit(async (effects) => { + const store = await storeJson.read().const(effects) + + if (!store?.webAuthToken) { + await sdk.action.createOwnTask(effects, setWebAdminPassword, 'critical', { + reason: i18n('Set your Keep Web Admin password, then sign in.'), + }) + } +}) diff --git a/startos/main.ts b/startos/main.ts index 418a643..87b1d94 100644 --- a/startos/main.ts +++ b/startos/main.ts @@ -1,8 +1,7 @@ -import { ensureCredentials } from './credentials' import { storeJson } from './fileModels/store.json' import { i18n } from './i18n' import { sdk } from './sdk' -import { uiPort } from './utils' +import { defaultRelays, uiPort } from './utils' export const main = sdk.setupMain(async ({ effects }) => { console.info(i18n('Starting Keep co-signer')) @@ -10,8 +9,16 @@ export const main = sdk.setupMain(async ({ effects }) => { const store = await storeJson.read().const(effects) if (!store) throw new Error('no store.json') - // Generate-if-missing so the token survives upgrades and matches the action. - const credentials = await ensureCredentials(effects) + // keep-web's bunker refuses to start with no relays (it would crash the + // co-signer), so never hand it an empty list — fall back to the default set + // if the store somehow holds one. The form enforces at least one, so this is + // belt-and-suspenders against a hand-edited or corrupted store. + const bunkerRelays = store.bunkerRelays.length + ? store.bunkerRelays + : defaultRelays + const frostRelays = store.frostRelays.length + ? store.frostRelays + : defaultRelays // keep-web is configured entirely through env. The vault lives under the // mounted volume so it is captured by StartOS backups. @@ -20,11 +27,15 @@ export const main = sdk.setupMain(async ({ effects }) => { KEEP_WEB_LISTEN: `0.0.0.0:${uiPort}`, KEEP_WEB_UI_DIR: '/app/ui', KEEP_PASSWORD: store.vaultPassword, - KEEP_WEB_AUTH_TOKEN: credentials.password, // keep-web splits these on commas into a relay list. - KEEP_BUNKER_RELAY: store.bunkerRelays.join(','), - KEEP_FROST_RELAY: store.frostRelays.join(','), + KEEP_BUNKER_RELAY: bunkerRelays.join(','), + KEEP_FROST_RELAY: frostRelays.join(','), } + // The Web Admin bearer token is set by the Set/Reset Web Admin Password + // action. Until then it is unset and keep-web stays fail-closed (it mints a + // throwaway token); the critical task drives the user to set a known one. + // Reactive store read above means setting it restarts the daemon onto it. + if (store.webAuthToken) env.KEEP_WEB_AUTH_TOKEN = store.webAuthToken // Optional explicit group; otherwise keep-web auto-resolves from the share. if (store.frostGroup) env.KEEP_FROST_GROUP = store.frostGroup diff --git a/startos/utils.ts b/startos/utils.ts index 6f5ab0e..831a779 100644 --- a/startos/utils.ts +++ b/startos/utils.ts @@ -1,6 +1,20 @@ export const uiPort = 8080 -// coracle's relay reliably delivers the rapid ephemeral (kind 24242) FROST -// signing traffic; nos.lol drops it, stalling signing rounds. -export const defaultBunkerRelay = 'wss://bucket.coracle.social' -export const defaultFrostRelay = 'wss://bucket.coracle.social' +// keep-web auth is a single bearer token; the Web Admin login form pairs it +// with a fixed, readonly "admin" username (for password managers). +export const ADMIN_USERNAME = 'admin' + +// Default relay(s) we hand keep-web. keep-web sources relays ONLY from env +// (KEEP_BUNKER_RELAY / KEEP_FROST_RELAY) — never the vault's RelayConfig or the +// imported share — and its bunker refuses to start with zero relays, so the +// package must supply at least one. We mirror upstream keep-core's default: a +// single coracle relay, which reliably delivers the rapid ephemeral (kind 24242) +// events FROST coordination depends on. Most general-purpose relays (nos.lol, +// damus, …) drop those, stalling signing rounds — so this is NOT a place to add +// "redundancy" with arbitrary relays; operators add more known-good relays via +// the Configure action. Re-sync if upstream changes its default. +export const defaultRelays = ['wss://bucket.coracle.social'] + +// Upstream caps from keep-core `relay.rs` (MAX_RELAYS, MAX_RELAY_URL_LENGTH). +export const maxRelays = 10 +export const maxRelayUrlLength = 256 diff --git a/startos/versions/current.ts b/startos/versions/current.ts new file mode 100644 index 0000000..a3b5e1c --- /dev/null +++ b/startos/versions/current.ts @@ -0,0 +1,21 @@ +import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' + +export const current = VersionInfo.of({ + version: '0.4.8:0', + releaseNotes: { + en_US: + 'Initial release of Keep, the always-on FROST co-signer, for StartOS.', + es_ES: + 'Versión inicial de Keep, el firmante conjunto FROST siempre activo, para StartOS.', + de_DE: + 'Erste Version von Keep, dem stets aktiven FROST-Mitunterzeichner, für StartOS.', + pl_PL: + 'Pierwsze wydanie Keep, zawsze aktywnego współsygnatariusza FROST, dla StartOS.', + fr_FR: + 'Première version de Keep, le cosignataire FROST toujours actif, pour StartOS.', + }, + migrations: { + up: async ({ effects }) => {}, + down: IMPOSSIBLE, + }, +}) diff --git a/startos/versions/index.ts b/startos/versions/index.ts index d858674..e596b0c 100644 --- a/startos/versions/index.ts +++ b/startos/versions/index.ts @@ -1,14 +1,7 @@ import { VersionGraph } from '@start9labs/start-sdk' -import { v0_4_0_0 } from './v0.4.0_0' -import { v0_4_1_0 } from './v0.4.1_0' -import { v0_4_2_0 } from './v0.4.2_0' -import { v0_4_3_0 } from './v0.4.3_0' -import { v0_4_5_0 } from './v0.4.5_0' -import { v0_4_6_0 } from './v0.4.6_0' -import { v0_4_7_0 } from './v0.4.7_0' -import { v0_4_8_0 } from './v0.4.8_0' +import { current } from './current' export const versionGraph = VersionGraph.of({ - current: v0_4_8_0, - other: [v0_4_0_0, v0_4_1_0, v0_4_2_0, v0_4_3_0, v0_4_5_0, v0_4_6_0, v0_4_7_0], + current, + other: [], }) diff --git a/startos/versions/v0.4.0_0.ts b/startos/versions/v0.4.0_0.ts deleted file mode 100644 index 50e04f7..0000000 --- a/startos/versions/v0.4.0_0.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' - -export const v0_4_0_0 = VersionInfo.of({ - version: '0.4.0:0', - releaseNotes: { - en_US: - 'Initial release of Keep, the always-on FROST co-signer, for StartOS.', - }, - migrations: { - up: async ({ effects }) => {}, - down: IMPOSSIBLE, - }, -}) diff --git a/startos/versions/v0.4.1_0.ts b/startos/versions/v0.4.1_0.ts deleted file mode 100644 index 7c09e4e..0000000 --- a/startos/versions/v0.4.1_0.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' - -export const v0_4_1_0 = VersionInfo.of({ - version: '0.4.1:0', - releaseNotes: { - en_US: - 'Peer presence (online/offline) now appears in the Web Admin activity feed.', - }, - migrations: { - up: async ({ effects }) => {}, - down: IMPOSSIBLE, - }, -}) diff --git a/startos/versions/v0.4.2_0.ts b/startos/versions/v0.4.2_0.ts deleted file mode 100644 index 097f1a3..0000000 --- a/startos/versions/v0.4.2_0.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' - -export const v0_4_2_0 = VersionInfo.of({ - version: '0.4.2:0', - releaseNotes: { - en_US: - 'Reliable repeated bunker signing: imported shares now aggregate correctly (no more "Unknown identifier"), and a spurious "No nonces stored" failure is gone. Credential files are written atomically and fail closed if corrupted, so the bunker URL stays stable across restarts.', - }, - migrations: { - up: async ({ effects }) => {}, - down: IMPOSSIBLE, - }, -}) diff --git a/startos/versions/v0.4.3_0.ts b/startos/versions/v0.4.3_0.ts deleted file mode 100644 index 774edf9..0000000 --- a/startos/versions/v0.4.3_0.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' - -export const v0_4_3_0 = VersionInfo.of({ - version: '0.4.3:0', - releaseNotes: { - en_US: - 'Web Admin UX: live co-signer presence with a readiness indicator (Ready to sign / Waiting for co-signers), a prominent approval bar with browser-tab and opt-in desktop alerts, a decluttered activity feed (repeated events collapse into one line), relative timestamps, and a signing log grouped by session with expandable detail.', - }, - migrations: { - up: async ({ effects }) => {}, - down: IMPOSSIBLE, - }, -}) diff --git a/startos/versions/v0.4.5_0.ts b/startos/versions/v0.4.5_0.ts deleted file mode 100644 index 9b72854..0000000 --- a/startos/versions/v0.4.5_0.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' - -export const v0_4_5_0 = VersionInfo.of({ - version: '0.4.5:0', - releaseNotes: { - en_US: - 'Co-signer reliability: peers now report accurate online/offline presence, and signing rounds gracefully fall back to interactive mode when a pre-exchanged nonce is stale, preventing stuck or failed co-sign requests.', - }, - migrations: { - up: async ({ effects }) => {}, - down: IMPOSSIBLE, - }, -}) diff --git a/startos/versions/v0.4.6_0.ts b/startos/versions/v0.4.6_0.ts deleted file mode 100644 index e8971ed..0000000 --- a/startos/versions/v0.4.6_0.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' - -export const v0_4_6_0 = VersionInfo.of({ - version: '0.4.6:0', - releaseNotes: { - en_US: - 'Multiple key groups: the co-signer no longer crashes when the vault holds more than one FROST group. It serves one group at a time (auto-selected by default) and you can switch which group is served from the Web Admin. Default relay is now wss://bucket.coracle.social, which reliably delivers FROST coordination traffic.', - }, - migrations: { - up: async ({ effects }) => {}, - down: IMPOSSIBLE, - }, -}) diff --git a/startos/versions/v0.4.7_0.ts b/startos/versions/v0.4.7_0.ts deleted file mode 100644 index e73b003..0000000 --- a/startos/versions/v0.4.7_0.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' - -export const v0_4_7_0 = VersionInfo.of({ - version: '0.4.7:0', - releaseNotes: { - en_US: - 'Bounded multi-event pre-approval cache (cap 100, TTL 5 min) and single-party FROST sign/ECDH refinements, so wallet descriptor coordination and multi-event signing flows no longer stall after a single pre-approval. Includes the automated fund sweep on descriptor migration.', - }, - migrations: { - up: async ({ effects }) => {}, - down: IMPOSSIBLE, - }, -}) diff --git a/startos/versions/v0.4.8_0.ts b/startos/versions/v0.4.8_0.ts deleted file mode 100644 index abee57b..0000000 --- a/startos/versions/v0.4.8_0.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' - -export const v0_4_8_0 = VersionInfo.of({ - version: '0.4.8:0', - releaseNotes: { - en_US: - 'Production race fixes in ECDH and signing coordination (subscribe-before-send), persistent NIP-46 grants CLI with hidden-vault support, bunker auto-approval + transport-key persistence fix, and an audit-log expansion covering rotate_password / rotate_data_key and rate-limit trips. Includes the closing-out mutation-testing campaign on signing / ECDH / descriptor / PSBT / NIP-46 modules and a dependency bump to frost 3.0.', - }, - migrations: { - up: async ({ effects }) => {}, - down: IMPOSSIBLE, - }, -})