app

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

commit a877b5ccc713e474424ff94a66193e7dae6b11b5
parent cba63fa6f57648f54300b3f2d5d4b854b851b01c
Author: triesap <tyson@radroots.org>
Date:   Sun,  2 Aug 2026 20:12:11 +0000

docs: complete Nostr account runtime documentation

- describe implemented ownership state persistence and relay flows
- record the UniFFI lifecycle native artifact and platform boundaries
- document credential exposure recovery and deletion semantics
- align local-relay and verification runbooks with executable tests

Diffstat:
Mdocs/adr/0001-rust-core-and-uniffi.md | 2+-
Mdocs/adr/0003-account-metadata-and-secret-storage.md | 2+-
Mdocs/architecture/nostr-accounts.md | 43++++++++++++++++++++++++++++++++++++-------
Mdocs/architecture/rust-ffi-boundary.md | 23+++++++++++++++++++----
Mdocs/implementation/nostr-runtime-rcld.md | 2+-
Mdocs/runbooks/local-relay-development.md | 15+++++++++++++++
Mdocs/security/key-management.md | 25+++++++++++++++++++++----
Mdocs/testing/nostr-accounts-test-plan.md | 34+++++++++++++++++++++++++++-------
8 files changed, 121 insertions(+), 25 deletions(-)

diff --git a/docs/adr/0001-rust-core-and-uniffi.md b/docs/adr/0001-rust-core-and-uniffi.md @@ -2,7 +2,7 @@ ## Status -Accepted. +Accepted and implemented. ## Decision diff --git a/docs/adr/0003-account-metadata-and-secret-storage.md b/docs/adr/0003-account-metadata-and-secret-storage.md @@ -2,7 +2,7 @@ ## Status -Accepted. +Accepted and implemented. ## Decision diff --git a/docs/architecture/nostr-accounts.md b/docs/architecture/nostr-accounts.md @@ -8,14 +8,17 @@ public availability state to `Available`, and retains the original metadata. ## Status -Initial architecture contract. Update this document as each implemented -checkpoint makes paths and behavior concrete. +Implemented MVP architecture. ## 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. +`PersistentAppCore` composes the application ports with one SQLite `Database`; +the FFI `StudioAppCore` adds the OS credential adapter, clock, SDK relay client, +and observer ownership. `RadrootsApplication` creates one native core and one +`StudioAppStore` for the application composition and closes both on disposal. ## Identity and state @@ -33,14 +36,40 @@ 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. +Generate and import durably create and select a signed-out account. Import +accepts nsec or canonical secret-key hex and rejects duplicates without +overwriting credentials. A saved account explicitly marked +`CredentialMissing` is the only repair exception. Removal requires a +revision-bound, single-use confirmation token so stale UI confirmation cannot +delete a changed target. + +Public state revisions increase only when public state changes. Observers get +the current snapshot at subscription and later revisions after locks are +released. A relay completion is bound to its initiating active public key; a +sign-out or replacement makes the completion stale and prevents publication. + ## 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. +SQLite stores public account metadata, selected public key, verified kind-zero +profile cache, account-owned typed preferences, migration state, and the +non-secret recovery journal. Public Nostr events are keyed by event ID and +author. Account-private values are keyed by owner public key and cascade on +removal. Secret keys are never stored in SQLite. + +The production database is exactly +`ProjectDirs::from("org", "radroots", "studio").data_dir()/studio.sqlite3`. +The `directories` crate maps that base to the platform application-data area; +tests inject temporary or in-memory databases instead. ## 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. +loads cached profile state. Refresh then queries kind-zero events using the +production SDK adapter, verifies event ID, signature, author, kind, bounds, and +metadata, and selects newest timestamp with lowest event ID as the tie-breaker. +Offline, timeout, or invalid data retains cache and produces a nonfatal state. + +Remote relays require `wss://`. Plain `ws://` is restricted to localhost, +`127.0.0.0/8`, and `::1`. Development mode falls back to +`ws://localhost:8080`; packaged mode fails safely if no valid relay is +configured. Tests use only ephemeral loopback relays or controlled fakes. diff --git a/docs/architecture/rust-ffi-boundary.md b/docs/architecture/rust-ffi-boundary.md @@ -16,16 +16,26 @@ 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. +Potentially blocking storage and credential operations use Rust blocking tasks; +relay operations use the supervised Tokio runtime. UniFFI exports them as +suspending Kotlin calls. `StudioAppStore` accepts at most one command at a +time, keeps Compose state read-only to consumers, rejects stale observer +revisions, and closes its removal ticket, subscription, gateway, and native +core during application disposal. The public snapshot DTO carries revisioned lifecycle, account, session, relay, profile, and safe-error values. It contains no credential field. A generated account's nsec is confined to the explicit one-time receipt added with the command boundary. +The exported command surface is bootstrap, snapshot, generate, import, select, +activate, sign out, profile refresh, removal request/confirmation, subscribe, +unsubscribe, and shutdown. `ObserverSubscription.unsubscribe` is idempotent; +`StudioAppCore.shutdown` deregisters remaining observers and signs out before +the generated object handle is closed. Callback tests re-enter `snapshot`, +observe asynchronous profile refresh, and prove no callback arrives after +unsubscribe. + ## Native artifact The FFI crate builds as both an `rlib` for Rust tests and a `cdylib` for the JVM @@ -56,3 +66,8 @@ current-host library under JNA's platform resource prefix inside the desktop resources, allowing the packaged JVM runtime to extract and load it without a machine-specific absolute path. `NativeLoaderTest` crosses the generated ABI and verifies the native crate version without opening storage or credentials. + +Current platform resource prefixes are `darwin-aarch64`, `darwin-x86-64`, +`linux-aarch64`, `linux-x86-64`, `win32-aarch64`, and `win32-x86-64`. The build +stages only the current host artifact; each release platform must build and +smoke its own package rather than reusing another platform's dynamic library. diff --git a/docs/implementation/nostr-runtime-rcld.md b/docs/implementation/nostr-runtime-rcld.md @@ -550,7 +550,7 @@ handoff commit sequence. ### RCLD-12 - [x] 60. Add dependency and license documentation. -- [ ] 61. Complete architecture, security, testing, and runbook documentation. +- [x] 61. Complete architecture, security, testing, and runbook documentation. - [ ] 62. Add Makefile-governed final validation tasks and platform ledger. Do not add `.github/**` or `scripts/**`. - [ ] 63. Perform final source audit and acceptance reconciliation. diff --git a/docs/runbooks/local-relay-development.md b/docs/runbooks/local-relay-development.md @@ -14,6 +14,8 @@ without any public-network dependency: ```sh cargo test --manifest-path core/Cargo.toml -p radroots-studio-application sdk_client +cargo test --manifest-path core/Cargo.toml -p radroots-studio-storage local_relay_e2e +cargo test --manifest-path core/Cargo.toml -p radroots-studio-ffi ffi_callback_receives_async_profile_refresh ``` Development and tests use local WebSocket relays only. The default development @@ -23,3 +25,16 @@ 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. + +For interactive development, either start a relay at +`ws://localhost:8080` or set `RADROOTS_NOSTR_RELAYS` before `make dev` or +`make run`. Use a comma-separated ordered list when testing more than one +relay. Do not put credentials in this variable. The development launcher sets +the native core to development mode; packaged applications use packaged mode +and therefore require explicit valid relay configuration. + +If refresh fails, inspect the safe relay/profile state in the active home +screen. Cached metadata remains visible. Invalid configuration is corrected by +restarting with a valid environment value; there is no in-app relay editor in +this MVP. Local test failures should be reproduced with the focused commands +above before broad workspace validation. diff --git a/docs/security/key-management.md b/docs/security/key-management.md @@ -2,8 +2,7 @@ ## Status -Initial security contract. Implementation-specific recovery phases and -platform results will be added with their checkpoints. +Implemented MVP security contract. ## Storage boundary @@ -25,8 +24,12 @@ 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. +Explicit copy places the nsec in the operating-system clipboard, which is an +additional user-authorized exposure that the application cannot reliably +revoke. Imported key input crosses an unavoidable JVM `String` boundary once; +the masked Compose draft is cleared immediately when the command is accepted, +before the native coroutine executes. The in-flight JVM argument cannot be +guaranteed zeroized and is never logged or added to public state. Keyring unavailability is a safe recoverable error. Cross-resource operations publish no partial success and use a non-secret journal for restart recovery. @@ -46,3 +49,17 @@ An empty journal performs no keyring operation. Removal recovery retries an intent, continues honestly from `CredentialDeleted`, completes metadata and account-namespace cleanup, persists deterministic fallback selection, and then finalizes the entry. Keyring failure leaves the phase unchanged for retry. + +Add/import records intent before credential creation. After credential write it +records `CredentialWritten`, writes public metadata and selection, records +`MetadataWritten`, then finalizes. A metadata failure attempts credential +compensation; a failed compensation leaves enough non-secret journal state for +startup recovery. Removal records intent, signs out if necessary, deletes the +credential, records `CredentialDeleted`, deletes public and account-owned data, +records `MetadataDeleted`, persists fallback selection, and finalizes. + +Corrupt or inaccessible SQLite fails safely and is not silently recreated. +Locked or unavailable credentials do not become watch-only accounts. Platform +credential behavior must be checked on macOS, Windows, and Linux; the real +keyring smoke remains ignored by default because it mutates user credential +state. diff --git a/docs/testing/nostr-accounts-test-plan.md b/docs/testing/nostr-accounts-test-plan.md @@ -2,8 +2,9 @@ ## Status -Initial test contract. Record exact commands and completed coverage as the -runtime is implemented. +Implemented test contract. There is no numeric coverage threshold; the suite +uses behavior-focused best-effort coverage and fails if no Kotlin tests are +discovered. ## Required lanes @@ -35,13 +36,32 @@ cargo clippy --manifest-path core/Cargo.toml --workspace --all-targets -- -D war cargo test --manifest-path core/Cargo.toml --workspace ``` -Run the existing desktop regression lane separately until the Makefile combines -the Rust and desktop lifecycles: +Run the desktop, generated binding, native loader, store, and Compose lanes: ```sh ./gradlew --no-daemon :app:desktop:test ``` -The capsule intentionally has no `.github/**` workflow and no validation -script. The Makefile will become the combined human-facing command surface -without changing these repository-owned Cargo and Gradle gates. +Focused integration paths are: + +```sh +cargo test --manifest-path core/Cargo.toml -p radroots-studio-storage local_relay_e2e +cargo test --manifest-path core/Cargo.toml -p radroots-studio-storage restart_restores_selection +cargo test --manifest-path core/Cargo.toml -p radroots-studio-ffi ffi_callback_receives_async_profile_refresh +./gradlew --no-daemon :app:desktop:test --tests org.radroots.studio.ffi.NativeLoaderTest +``` + +`local_relay_e2e.rs` imports and activates a deterministic signer, reads signed +kind-zero metadata through an ephemeral loopback relay, observes loading/fresh +revisions, verifies SQLite cache, and checks public redaction. +`restart_isolation.rs` reopens the database, restores selected accounts, proves +owner-scoped values remain isolated, and scans bytes for known secrets. FFI +tests cover callback re-entry, asynchronous refresh, unsubscribe, and shutdown. +Compose tests cover the complete minimal UI surface and use fake actions rather +than credentials or public relays. + +The real OS keyring smoke test is ignored by default and must be explicitly run +on each supported target in an isolated test account. Packaging and native +loader smoke are current-host checks; cross-platform results belong in the +final validation ledger. The capsule intentionally has no `.github/**` workflow +and no `scripts/**` command surface.