app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 8b54d3730b0e374e61165e3c7b53ee556f92ce73
parent 5c2d89a37594bffbb1ed77bd1ed0d385ed0e9cd8
Author: triesap <tyson@radroots.org>
Date:   Sun,  2 Aug 2026 17:56:59 +0000

docs: audit current accounts proof and Nostr references

- record the live Kotlin account proof and repository baseline
- pin reviewed upstream commits, paths, and license boundaries
- establish the architecture, security, testing, and ADR skeletons
- preserve runtime behavior while closing the research checkpoint

Diffstat:
Adocs/adr/0001-rust-core-and-uniffi.md | 18++++++++++++++++++
Adocs/adr/0002-nostr-library-selection.md | 22++++++++++++++++++++++
Adocs/adr/0003-account-metadata-and-secret-storage.md | 23+++++++++++++++++++++++
Adocs/architecture/nostr-accounts.md | 36++++++++++++++++++++++++++++++++++++
Adocs/architecture/reference-research.md | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/architecture/rust-ffi-boundary.md | 22++++++++++++++++++++++
Mdocs/implementation/nostr-runtime-rcld.md | 8++++----
Adocs/runbooks/local-relay-development.md | 13+++++++++++++
Adocs/security/key-management.md | 25+++++++++++++++++++++++++
Adocs/testing/nostr-accounts-test-plan.md | 26++++++++++++++++++++++++++
10 files changed, 302 insertions(+), 4 deletions(-)

diff --git a/docs/adr/0001-rust-core-and-uniffi.md b/docs/adr/0001-rust-core-and-uniffi.md @@ -0,0 +1,18 @@ +# ADR 0001: Rust core and UniFFI + +## Status + +Accepted. + +## Decision + +Implement the canonical Radroots Studio account and application runtime in a +Rust workspace under `core/**`. Expose immutable snapshots, explicit commands, +safe errors, and closeable observer subscriptions to Kotlin through UniFFI. +Compose Desktop remains a thin native JVM 21 lifecycle and presentation shell. + +## Consequences + +Account behavior is reusable by future shells and cannot diverge in Kotlin. +The build must generate Kotlin bindings, stage a platform native library, test +callback and object lifetime behavior, and package that library with the app. diff --git a/docs/adr/0002-nostr-library-selection.md b/docs/adr/0002-nostr-library-selection.md @@ -0,0 +1,22 @@ +# ADR 0002: Nostr library selection + +## Status + +Accepted dependency direction; implementation compatibility is verified when +the Cargo lockfile is introduced. + +## Decision + +Use stable `nostr` and `nostr-sdk` `0.44.1` for key generation, NIP-19, +event/signature verification, and relay client behavior. Keep SDK secret types +inside a narrow adapter guarded by Radroots secret wrappers. + +Do not use Notedeck source or nostrdb. Both reviewed references are GPL and the +MVP already requires migration-managed SQLite. Do not adopt the current +`0.45.0-alpha` SDK line without a superseding compatibility ADR. + +## Consequences + +Local relay tests may use a controlled WebSocket fixture rather than depending +on alpha-only SDK relay helpers. Dependency versions and licenses are pinned +and audited before final acceptance. diff --git a/docs/adr/0003-account-metadata-and-secret-storage.md b/docs/adr/0003-account-metadata-and-secret-storage.md @@ -0,0 +1,23 @@ +# ADR 0003: Account metadata and secret storage + +## Status + +Accepted. + +## Decision + +Store migration-managed, non-secret account metadata and profile cache in +bundled SQLite at an injectable OS application-data location. Store local +secret keys only in the OS credential store through Rust `SecretStore`, using +service `org.radroots.studio.nostr` and canonical public-key hex as the account +key. + +Use a non-secret operation journal because SQLite and the credential store +cannot share an atomic transaction. Publish snapshots only after durable +success. There is no ordinary-file secret fallback. + +## Consequences + +Startup recovery is mandatory. Add/import and removal need failure injection at +every cross-resource boundary. Generated nsec is a one-time receipt exception; +imported key text has a documented JVM String limitation. diff --git a/docs/architecture/nostr-accounts.md b/docs/architecture/nostr-accounts.md @@ -0,0 +1,36 @@ +# Nostr accounts architecture + +## Status + +Initial architecture contract. Update this document as each implemented +checkpoint makes paths and behavior concrete. + +## Ownership + +Rust `AppCore` owns the saved-account registry, selected account, active +signer/session, persistence, recovery, relay/profile work, immutable snapshots, +and safe errors. Kotlin is a thin lifecycle and presentation shell over UniFFI. + +## Identity and state + +Account identity is canonical lowercase Nostr public-key hex. Npub is a display +encoding, and an optional local label is not identity. Saved, selected, and +active are separate states. Startup restores saved accounts and selection but +starts signed out. + +Activation prepares a candidate signer/session before atomically replacing the +working session. Sign out retains the saved account and credential. Confirmed +removal deletes account-private state and credential, then chooses a +deterministic selected fallback without activating it. + +## Data partitioning + +Public Nostr events may be cached by event ID and subject pubkey. Local account +metadata and typed private preferences are keyed by owner pubkey. Secret keys +are never stored in SQLite. + +## Relay and profile flow + +Relays are global WebSocket endpoints from `RADROOTS_NOSTR_RELAYS`. Activation +emits cached profile data before a verified kind-0 refresh. Offline or invalid +relay data retains the cache and produces a nonfatal state. diff --git a/docs/architecture/reference-research.md b/docs/architecture/reference-research.md @@ -0,0 +1,113 @@ +# Nostr account runtime reference research + +## Status + +Baseline recorded on 2026-08-02 before runtime implementation. The dependency +and source audit will be reconciled again before final acceptance. + +## Local baseline + +- Repository: standalone `oss/studio_app` capsule. +- Branch: `master`. +- Baseline commit: `9cd07ced24d62eca4cc89f09c82a3539c7fe9dcf`. +- Runtime: Kotlin JVM 21 and Compose Desktop. +- Current proof: UUID account IDs, user-entered display names, HTTP/HTTPS server + URLs, Kotlin `LoginStatus`, and a Kotlin-owned reducer/store. +- Current persistence, credential store, relay client, Rust core, and FFI: none. +- Baseline verification: `./gradlew --no-daemon :app:desktop:test` passed. + +## Reviewed references + +### Nostr protocol + +- Repository: `nostr-protocol/nips`. +- Commit: `c53877571f96eb423661fc23c620d629d37b8f19`. +- Paths: `01.md`, `19.md`. +- Adopt: lowercase 32-byte public-key hex in protocol/storage, WebSocket relay + transport, kind-0 metadata, signed-event verification, replaceable-event + ordering, and npub/nsec as human-facing encodings. + +### Amethyst + +- Repository: `vitorpamplona/amethyst`. +- Commit: `bf41e75b78b3891e5434baa4e5778a93988ba2ca`. +- Reviewed paths: `README.md`, `docs/secure-key-storage-migration.md`, + `docs/plans/archive/2026-04-23-feat-desktop-multi-account-support-plan.md`, + `docs/plans/archive/2026-04-28-multi-account-testing-sheet.md`, + `docs/plans/archive/2026-05-14-fix-account-security-hardening-plan.md`, + JVM `SecureKeyStorage.kt`, `AccountManager.kt`, + `DesktopAccountStorage.kt`, and their account/keyring tests. +- Adopt: pubkey identity, multiple saved accounts, explicit corruption state, + account-scoped resources, and transition tests for missing credentials, + logout, and account replacement. +- Reject: encrypted ordinary-file secret fallback, publishing in-memory cache + before durable writes, mutating the replacement session before cleaning the + captured old session, swallowed deletion failures, and silent conversion of + a missing local credential into a read-only account. + +### Notedeck + +- Repository: `damus-io/notedeck`. +- Commit: `b41ffeb57636f9de3147803c1313d99dda6cffa2`. +- Reviewed paths: `README.md`, `LICENSE`, + `crates/notedeck/src/account/accounts.rs`, + `crates/notedeck/src/account/cache.rs`, + `crates/notedeck/src/storage/account_storage.rs`, + `crates/notedeck/src/storage/keyring_store.rs`, and + `crates/notedeck/src/user_account.rs`. +- License: GPL-3.0-or-later. Architectural comparison is clean-room only; no + source is copied. +- Adopt conceptually: pubkey-keyed account cache, selected-account persistence, + in-memory credential fakes, and account-specific resources. +- Reject: constructors that permit secrets on disk, file-first deletion without + recovery, cache mutation before storage success, a synthetic fallback + account, hash-map-order selection, and cloneable secret-bearing account types. + +### rust-nostr and nostr-sdk + +- Repository: `nostrdevkit/nostr`. +- Commit reviewed: `7834dd624dcc8bdce9610988eb0b5e888b73c2eb`. +- Reviewed paths: workspace manifests, `nostr-keyring/**`, key types, + `nostr-sdk/src/local_relay/**`, and SDK client/profile APIs. +- License: MIT. +- Dependency baseline: pin stable `nostr` and `nostr-sdk` `0.44.1`; do not adopt + the reviewed repository's `0.45.0-alpha` line without a later ADR change. +- Adopt: maintained key/event/NIP implementations and SDK relay behavior. +- Contain: upstream secret/key types may implement `Clone` or `Debug`; keep them + behind a non-cloneable, redacted Radroots adapter boundary. + +### nostrdb + +- Repository: `damus-io/nostrdb`. +- Commit: `f4591db9524bc4936af76af4750ec425e67700be`. +- Reviewed paths: `README.md`, `LICENSE`, and build/storage entry points. +- License: GPL-3.0-or-later. +- Decision: do not use. The handoff requires migration-managed non-secret + SQLite, and the additional database and license are unnecessary for the MVP. + +### UniFFI + +- Repository: `mozilla/uniffi-rs`. +- Commit: `2ccd07e219161c51a5642b4d7be8f174a846462f`. +- Reviewed paths: `README.md`, `LICENSE`, async internals/overview, + `docs/manual/src/kotlin/**`, callback-interface documentation, Kotlin future + and callback binding tests, and JNI thread-attachment runtime code. +- Release baseline: `0.32.0`, MPL-2.0. +- Adopt: generated Kotlin bindings, asynchronous operations, explicit object + disposal, and callback handles. +- Guard: invoke no observer while holding Rust locks; serialize revisions; + verify JVM thread attachment, cancellation, deregistration, and close races. + +## Dependency baseline + +- `nostr` and `nostr-sdk` `0.44.1`. +- `uniffi` `0.32.0`. +- `keyring` `4.1.6` behind the Radroots `SecretStore`. +- `rusqlite` `0.40.1` with bundled SQLite. +- `refinery` `0.9.2`. +- `secrecy` `0.10.3`. +- `zeroize` `1.9.0`. +- `directories` `6.0.0`. + +Exact transitive resolution is committed through `core/Cargo.lock`. Platform +features and license metadata must be recorded before final acceptance. diff --git a/docs/architecture/rust-ffi-boundary.md b/docs/architecture/rust-ffi-boundary.md @@ -0,0 +1,22 @@ +# Rust and UniFFI boundary + +## Status + +Initial boundary contract. Generated names and paths will be recorded when the +binding pipeline exists. + +## Contract + +One Rust `AppCore` instance owns canonical state. Kotlin receives immutable, +revisioned DTO snapshots, invokes explicit commands, and subscribes through a +closeable observer handle. Mutations are serialized, callbacks occur outside +locks, and stale asynchronous results cannot replace newer state. + +Normal public DTOs contain no secret. The only exception is the direct, +one-time generated-key receipt. Generated bindings belong under +`build/generated` and are not committed. + +Potentially blocking storage, credential, and relay operations must not run on +the Compose thread. Kotlin closes its observer and AppCore during application +disposal. Tests cover native loading, callback ordering, re-entry, +deregistration, cancellation, stale completion, and close races. diff --git a/docs/implementation/nostr-runtime-rcld.md b/docs/implementation/nostr-runtime-rcld.md @@ -2,7 +2,7 @@ ## Status -- Program status: approved and not started. +- Program status: in progress. - Active RCLD: none. - Active atomic checkpoint: none. - Repository: `oss/studio_app` standalone Git repository. @@ -248,7 +248,7 @@ lanes. ### RCLD-01: Authority and dependency baseline -Status: pending. +Status: completed. Scope: checkpoint 1. Record live repository state, authoritative handoff files, reference source SHAs and paths, adopted and rejected ideas, license boundaries, @@ -449,7 +449,7 @@ handoff commit sequence. ### RCLD-01 -- [ ] 01. Audit live repository and reference sources. +- [x] 01. Audit live repository and reference sources. ### RCLD-02 @@ -551,7 +551,7 @@ handoff commit sequence. ## Unfinished RCLD ledger -- [ ] RCLD-01: Authority and dependency baseline. +- [x] RCLD-01: Authority and dependency baseline. - [ ] RCLD-02: Rust workspace and domain. - [ ] RCLD-03: Application state machine. - [ ] RCLD-04: SQLite persistence. diff --git a/docs/runbooks/local-relay-development.md b/docs/runbooks/local-relay-development.md @@ -0,0 +1,13 @@ +# Local relay development + +## Status + +Initial runbook contract. Exact commands will be added with the relay fixture. + +Development and tests use local WebSocket relays only. The default development +endpoint is `ws://localhost:8080`; tests bind ephemeral loopback ports. Override +the ordered relay list with comma-separated `RADROOTS_NOSTR_RELAYS` values. + +Plain `ws://` is accepted only for `localhost`, `127.0.0.0/8`, and `::1`. +Production remote endpoints require `wss://`. Tests must fail closed rather +than contact public relays when a local fixture is unavailable. diff --git a/docs/security/key-management.md b/docs/security/key-management.md @@ -0,0 +1,25 @@ +# Nostr key management + +## Status + +Initial security contract. Implementation-specific recovery phases and +platform results will be added with their checkpoints. + +## Storage boundary + +Production Nostr secret keys are stored only through Rust `SecretStore` in the +OS credential store. The service is `org.radroots.studio.nostr`; the credential +account key is canonical public-key hex. There is no ordinary-file fallback. + +Secrets are forbidden in SQLite, profile cache, operation journals, public +snapshots, normal DTOs, logs, errors, filenames, preferences, fixtures, and +golden files. Rust application secret wrappers are non-cloneable, redacted, +non-serializable values. + +Generated nsec is returned once in a direct operation receipt, displayed in +non-saveable Kotlin state, and cleared after acknowledgement or disposal. +Explicit clipboard copy uses conditional delayed clearing. Imported key input +crosses an unavoidable JVM String boundary once and is cleared immediately. + +Keyring unavailability is a safe recoverable error. Cross-resource operations +publish no partial success and use a non-secret journal for restart recovery. diff --git a/docs/testing/nostr-accounts-test-plan.md b/docs/testing/nostr-accounts-test-plan.md @@ -0,0 +1,26 @@ +# Nostr accounts test plan + +## Status + +Initial test contract. Record exact commands and completed coverage as the +runtime is implemented. + +## Required lanes + +- Rust domain validation, canonicalization, state invariants, and transition + tables. +- SQLite migrations, restart restoration, account isolation, corruption, and + transaction failure injection. +- SecretStore contract, platform smoke tests, and no-secret guards. +- Nostr key vectors, signed-event verification, kind-0 ordering, and bounded + profile parsing. +- Local ephemeral relay, cached-first refresh, timeout, invalid data, + cancellation, and stale-result behavior. +- UniFFI generation, DTO mapping, callback ordering/re-entry/deregistration, + object disposal, and native loading. +- Kotlin store lifecycle and Compose UI coverage for generate, import, backup, + selection, activation, refresh, switch, sign out, removal, and safe errors. +- End-to-end restart, recovery, account isolation, and packaged-runtime smoke. + +No test uses a public relay. There is no numeric coverage threshold; tests must +provide strong best-effort behavioral coverage throughout RCL development.