app

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

commit 9677468e591b741583323a4065f0f35a62f29f32
parent 4a37ef1feefda4f0e6d95145dbf65dfadb8b15f2
Author: triesap <tyson@radroots.org>
Date:   Mon,  3 Aug 2026 20:52:18 +0000

docs: remove ad-hoc documentation

Diffstat:
Ddocs/adr/0001-rust-core-and-uniffi.md | 18------------------
Ddocs/adr/0002-nostr-library-selection.md | 24------------------------
Ddocs/adr/0003-account-metadata-and-secret-storage.md | 27---------------------------
Ddocs/architecture/nostr-accounts.md | 75---------------------------------------------------------------------------
Ddocs/architecture/radroots-crates-release-v1-reconciliation.md | 63---------------------------------------------------------------
Ddocs/architecture/reference-research.md | 144-------------------------------------------------------------------------------
Ddocs/architecture/rust-ffi-boundary.md | 73-------------------------------------------------------------------------
Ddocs/implementation/nostr-runtime-rcld.md | 571-------------------------------------------------------------------------------
Ddocs/runbooks/local-relay-development.md | 40----------------------------------------
Ddocs/security/key-management.md | 67-------------------------------------------------------------------
Ddocs/testing/final-validation-ledger.md | 87-------------------------------------------------------------------------------
Ddocs/testing/nostr-accounts-test-plan.md | 67-------------------------------------------------------------------
Ddocs/testing/platform-validation.md | 62--------------------------------------------------------------
13 files changed, 0 insertions(+), 1318 deletions(-)

diff --git a/docs/adr/0001-rust-core-and-uniffi.md b/docs/adr/0001-rust-core-and-uniffi.md @@ -1,18 +0,0 @@ -# ADR 0001: Rust core and UniFFI - -## Status - -Accepted and implemented. - -## 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 @@ -1,24 +0,0 @@ -# ADR 0002: Nostr library selection - -## Status - -Accepted and implemented. - -## Decision - -Use `nostr-sdk` `0.44.1` and its compatible `nostr` `0.44.1` source for key -generation, NIP-19, event/signature verification, and relay client behavior. -The registry release of `nostr` `0.44.1` is yanked, so Cargo pins the upstream -workspace commit `5bba5163eb77107f82c4a8262cf29d7f33a73219`. 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 use the matching `nostr-relay-builder` `0.44.1` fixture and -never require public network access. The direct dependency and license ledger -is maintained in `docs/architecture/reference-research.md`; lockfile or version -catalog changes require renewed compatibility and license review. diff --git a/docs/adr/0003-account-metadata-and-secret-storage.md b/docs/adr/0003-account-metadata-and-secret-storage.md @@ -1,27 +0,0 @@ -# ADR 0003: Account metadata and secret storage - -## Status - -Accepted and implemented. - -## 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. - -Use `refinery 0.9.2` with bundled `rusqlite 0.39.0`. Refinery's supported -Rusqlite range ends at 0.39, so this compatibility pin replaces the initially -reviewed 0.40.1 candidate. - -## 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 @@ -1,75 +0,0 @@ -# Nostr accounts architecture - -An import that resolves to an account with an existing credential returns -`AccountAlreadyExists` and does not overwrite either resource. The sole repair -path is an existing account explicitly marked `CredentialMissing` with no -credential present; matching import restores that credential, changes the -public availability state to `Available`, and retains the original metadata. - -## Status - -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 - -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. - -Npub and nsec domain values enforce their expected NIP-19 shape. The Nostr -adapter performs checksum and key conversion validation. Nsec remains redacted -and is exposed only in the import adapter or one-time generated-key receipt. - -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. - -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 - -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 -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/radroots-crates-release-v1-reconciliation.md b/docs/architecture/radroots-crates-release-v1-reconciliation.md @@ -1,63 +0,0 @@ -# Radroots crates release v1 reconciliation - -## Decision - -The Studio cutover described by Radroots crates release v1 Steps 274–278 is an -evidence-based obsolete-source deviation for the current `studio_app` capsule. -It must not be implemented by restoring the retired desktop runtime or by -adding an unused `radroots_sdk` dependency. - -The crates-release review inspected Studio commit -`8d849a204b0b67865603f5c877ad7409fc922d30`. That revision contained the GPUI -workspace, `crates/runtime/src/sdk.rs`, `studio.sqlite`, direct Radroots lower -crate dependencies, and sibling paths into `../lib` and `../sdk`. Commit -`ae0179669c59261fe273a58d781763f69ad2b198` deleted that runtime. The current -head descends from the reviewed revision and implements the later approved -Rust/UniFFI/Compose Nostr-account architecture completed by the capsule's -63-checkpoint runtime sequence. - -The current product does not implement the farms, listings, trades, generic -storage, or generic synchronization semantics owned by `radroots_sdk`. Its -canonical Rust `AppCore` owns only Studio account/session state and bounded -kind-zero profile refresh. Adding the SDK without consuming those semantics -would create a false dependency and two lifecycle owners. - -## Step reconciliation - -- Step 274 is satisfied by removal of all production sibling paths to the - Radroots crate repositories. Workspace `path` dependencies are capsule-local - composition between Studio-owned crates. There are no direct dependencies on - a lower `radroots_*` package. Direct `nostr` and `nostr-sdk` dependencies are - deliberate upstream protocol adapters for the current product contract. -- Step 275's reviewed supervisor no longer exists. The replacement retains one - host-owned `AppCore`, a supervised Tokio runtime, explicit async UniFFI - commands, revision-bound results, closeable observers, and explicit shutdown. -- Step 276 is satisfied by the Studio-owned migration-managed SQLite database - under `core/crates/storage`. SDK backup or status code cannot own, mutate, or - report this database because the current graph contains no SDK edge. -- Step 277's generic signing, transport, and sync effects were deleted with the - reviewed runtime. Current key handling stays behind `SecretStore`; profile - fetch is bounded, local-relay tested, account/revision bound, and presentation - state remains host-owned. -- Step 278 is satisfied by the capsule's locked Rust and Gradle checks, native - loader smoke, complete application tests, and current-host distribution - package. A local Radroots registry canary is not applicable because the - resolved graph contains no Radroots registry package. - -## Boundary guard - -The Studio manifest graph must continue to satisfy all of these conditions: - -1. no dependency path escapes the `studio_app` capsule; -2. no `radroots_*` dependency is added merely to claim SDK migration; -3. Studio account, preference, profile-cache, and presentation state remains - host-owned; -4. secrets remain in the operating-system credential adapter and never enter - SQLite, DTOs, logs, or generated bindings; -5. any future farms, listings, trades, or generic sync product surface must be - introduced through the then-current `radroots_sdk` API and its real package - artifacts, not by restoring the deleted runtime. - -This reconciliation preserves the crates-release architecture requirement: -consumers use the correct layer for the semantics they actually consume, and a -new SDK edge is required when Studio first consumes SDK-owned semantics. diff --git a/docs/architecture/reference-research.md b/docs/architecture/reference-research.md @@ -1,144 +0,0 @@ -# Nostr account runtime reference research - -## Status - -Baseline recorded on 2026-08-02 and reconciled against the implemented runtime -on the same date. - -## Local baseline - -- Repository: standalone `oss/studio_app` capsule. -- Branch: `master`. -- Baseline commit: `9cd07ced24d62eca4cc89f09c82a3539c7fe9dcf`. -- Runtime: Kotlin JVM 21 and Compose Desktop. -- Current runtime: Rust-owned account state and command handling, SQLite public - persistence, OS credential storage, rust-nostr relay access, and UniFFI DTOs. -- Kotlin is a thin Compose shell with presentation-only input, backup, chooser, - busy, and safe-problem state. -- 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: `nostr-sdk` remains at `0.44.1`. Because crates.io - yanked `nostr` `0.44.1`, the matching `nostr` workspace package is pinned to - upstream commit `5bba5163eb77107f82c4a8262cf29d7f33a73219`; 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. - -## Implemented dependency and license ledger - -The following direct dependencies are pinned by `core/Cargo.toml`, -`core/Cargo.lock`, and `gradle/libs.versions.toml`. License identifiers were -reconciled from the checked-out upstream Cargo manifests or resolved Maven POMs. - -| Dependency | Pin | License | Runtime role | -| --- | --- | --- | --- | -| `nostr` | git `5bba5163eb77107f82c4a8262cf29d7f33a73219` (`0.44.1`) | MIT | keys, NIP-19, events | -| `nostr-sdk` | `0.44.1` | MIT | relay client | -| `nostr-relay-builder` | `0.44.1` | MIT | test-only local relay | -| `uniffi` | `0.32.0` | MPL-2.0 | Rust/Kotlin bindings | -| `keyring` | `4.1.6` | MIT OR Apache-2.0 | OS credential adapter | -| `rusqlite` | `0.39.0` | MIT | bundled SQLite adapter | -| `refinery` | `0.9.2` | MIT | SQLite migrations | -| `secrecy` | `0.10.3` | Apache-2.0 OR MIT | redacted secret values | -| `zeroize` | `1.9.0` | Apache-2.0 OR MIT | secret-memory cleanup support | -| `directories` | `6.0.0` | MIT OR Apache-2.0 | canonical data location | -| `url` | `2.5.8` | MIT OR Apache-2.0 | relay URL parsing | -| `tokio` | `1.47.1` | MIT | async runtime | -| `tempfile` | `3.23.0` | MIT OR Apache-2.0 | test-only isolated storage | -| Kotlin/JVM | `2.4.10` | Apache-2.0 | JVM language and tooling | -| Compose Multiplatform | `1.11.1` | Apache-2.0 | desktop UI | -| kotlinx.coroutines | `1.9.0` | Apache-2.0 | thin-store command dispatch | -| JNA | `5.17.0` | LGPL-2.1-or-later OR Apache-2.0 | native library loading | - -The workspace itself is GPL-3.0-only. SQLite's bundled C implementation is -public domain; `rusqlite` remains MIT. Exact Rust transitive resolution is -committed in `core/Cargo.lock`; Gradle dependency verification uses the pinned -version catalog and wrapper distribution. A dependency upgrade requires -re-running the license and compatibility review, including the UniFFI loader -and current-platform package smoke. - -## Final selection rationale - -rust-nostr provides maintained protocol primitives and relay behavior while -allowing Radroots to keep domain, persistence, secret, and lifecycle contracts -behind its own ports. Notedeck supplied useful architectural comparison, but -adopting its lower-level account/storage stack would duplicate the selected -SQLite and state-machine ownership and would complicate clean-room provenance. -No Notedeck or nostrdb source was copied. diff --git a/docs/architecture/rust-ffi-boundary.md b/docs/architecture/rust-ffi-boundary.md @@ -1,73 +0,0 @@ -# Rust and UniFFI boundary - -## Status - -The proc-macro UniFFI namespace is `radroots_studio_ffi`. Kotlin bindings use -the package `org.radroots.studio.ffi`; generated sources remain build output. - -## 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 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 -binding. Build the current-host development artifact from the capsule root: - -```sh -cargo build --manifest-path core/Cargo.toml -p radroots-studio-ffi -``` - -On macOS this produces -`core/target/debug/libradroots_studio_ffi.dylib`. Other desktop hosts use the -platform-equivalent `radroots_studio_ffi` dynamic-library filename under the -same Cargo profile directory. - -Gradle exposes the same operation as `:app:desktop:buildRustCore`. The task -tracks Rust manifests, lockfile, sources, and migrations as inputs and the -current-host debug dynamic library as its output. It does not redirect Cargo's -target directory. - -`:app:desktop:generateUniFfiKotlin` runs the repository-pinned UniFFI 0.32 -generator against that dynamic library and writes Kotlin to -`app/desktop/build/generated/uniffi/kotlin`. The main Kotlin source set reads -that generated directory, and `compileKotlin` depends on generation. Generated -bindings are ignored build output and are never committed. - -Development and tests point JNA at `core/target/debug`. Packaging stages the -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 @@ -1,571 +0,0 @@ -# Nostr runtime multi-RCLD - -## Status - -- Program status: completed. -- Active RCLD: none. -- Active atomic checkpoint: none. -- Repository: `oss/studio_app` standalone Git repository. -- Baseline branch: `master`. -- Baseline commit: `9cd07ced24d62eca4cc89f09c82a3539c7fe9dcf`. -- Implementation commits require a separate execution directive. - -## Objective - -Replace the Kotlin-only generic account-server proof with a Nostr-only desktop -application whose canonical account and application runtime is implemented in -Rust under `core/**`. Compose Desktop remains a thin native JVM 21 shell over a -UniFFI boundary. The UI proves the complete local Nostr accounts model with -minimal primitives; it does not introduce the final product design system. - -## Authority and repository boundary - -The sole documentary authority for this program is the handoff package at: - -`../../docs/handoff/radroots_studio_app_v1_kotlin_basis_handoff/**` - -The relative path above is from the `oss/studio_app` capsule root. This plan is -a derived execution ledger and is subordinate to that package. Other monorepo -documents and the legacy `/studio_app` tree are not implementation authority. - -All source changes belong inside this standalone `oss/studio_app` repository. -Do not stage or commit its gitlink from the parent repository as part of these -RCLDs. Do not modify the parent repository or the legacy `/studio_app` tree. - -## Execution contract - -1. Execute the RCLDs in numeric order. -2. Keep only one RCLD and one atomic checkpoint active at a time. -3. Preserve the 63 checkpoint order defined below. Do not merge, skip, or - reorder checkpoints merely to reduce commit count. -4. Before each checkpoint, inspect its authority and the nested repository - status. Reconcile the remaining ledger after each green checkpoint. -5. Implement the smallest complete change for the active checkpoint. -6. Run its narrow verify lane plus every relevant inherited regression lane. -7. Repair or split a red checkpoint. Never commit a red checkpoint. -8. Commit only when an execution directive explicitly authorizes commits. -9. Never push, publish, deploy, or update the parent gitlink without separate - authorization. -10. Record any necessary deviation in this document before implementation. A - deviation may clarify an unsafe instruction but may not broaden product - scope. - -## Approved constraints and normalized decisions - -### Build and repository policy - -- Do not use extbuild in this OSS capsule. -- Do not add `.github/**`, `scripts/**`, empty directories, or placeholder - future modules. -- Keep the Makefile as the human-facing lifecycle surface. It delegates to - repository-owned Cargo and Gradle tasks. -- Keep generated UniFFI Kotlin, staged native libraries, Cargo output, Gradle - output, and packaged artifacts under ignored build directories. -- Preserve package `org.radroots.studio`, Gradle module `:app:desktop`, JVM 21, - the pinned Kotlin and Compose versions, the application icon, native package - identity, transparent macOS title bar, 1284 by 795 initial window size, 1080 - by 720 minimum size, and fail-closed Kotlin test discovery. -- Normalize application and package version metadata around `0.1.0-alpha`; use - an installer-compatible numeric form only where a native packaging tool - rejects prerelease text, and document that mapping. - -### Runtime ownership - -- Rust `AppCore` is the sole canonical owner of accounts, selection, active - session, persistence, recovery, relay/profile work, snapshot revisions, and - safe errors. -- Kotlin owns native-window lifecycle, generated-binding lifecycle, - presentation-only state, user input, command dispatch, and immutable DTO - rendering. -- Kotlin must not reproduce the Rust reducer, session state machine, storage - policy, or relay policy. -- Mutating AppCore commands are serialized. Public snapshots are immutable and - revisioned monotonically. -- Observers are never called while an internal lock is held. Notifications are - serialized in revision order and stop after deregistration. - -### Account and session semantics - -- Canonical account identity is a lowercase 64-character Nostr public-key hex - value. `npub` is a human-facing encoding, not a storage key. -- A local label is optional and is distinct from Nostr `name` and - `display_name` profile fields. -- Generate and import persist and select an account but do not activate it. -- Startup restores saved accounts and persisted selection but starts signed - out. It does not access a credential or relay until explicit activation. -- Selection and active session are independent. Selecting a candidate does not - destroy the current active session. -- Activation prepares and validates the candidate credential, signer, cached - profile, and relay session before atomically replacing a working session. - Failure preserves the previous session. -- Sign out cancels account tasks and drops signer/session state while retaining - account metadata, selection, and credential. -- Removal uses a single-use confirmation token bound to the target pubkey and - snapshot revision. Active removal signs out first and never auto-activates a - fallback. -- Removing a selected account chooses the next persisted account, then the - preceding account. Empty registries have no selection. -- An existing account and credential returns `AccountAlreadyExists` without - overwrite. Matching import into an explicit `CredentialMissing` account is - the supported repair path. - -### Secret boundary - -- Production secret keys exist only in the OS credential store through a Rust - `SecretStore`; there is no file, preference, encrypted-file, or plaintext - fallback. -- Use credential service `org.radroots.studio.nostr` and canonical pubkey hex - as its account key. -- Application secret wrappers are non-`Clone`, non-`Debug`, non-`Display`, and - non-serializable. Cloneable or debug-capable upstream SDK secret types remain - inside the narrow signer adapter and never cross domain or FFI boundaries. -- Secrets are forbidden in SQLite, profile cache, operation journals, - snapshots, normal public DTOs, logs, errors, filenames, preferences, - analytics, fixtures, and golden files. -- `GenerateAccountReceipt.generated_nsec` is the sole transient public DTO - exception. It never enters `AppSnapshot` or persistence. -- Kotlin holds generated nsec only in non-saveable presentation state. It is - cleared on acknowledgement, replacement, or application disposal. -- Copying generated nsec is explicit. Clear the clipboard after 60 seconds only - when it still contains the copied value. -- Imported key text is masked, submitted once, and cleared as soon as the - command is accepted, before coroutine or native execution. The JVM - limitation is documented and tested as far as the platform permits. - -### Persistence and recovery - -- Use migration-managed bundled SQLite at an injectable OS application-data - location. -- Persist accounts, selected account, bounded profile cache, typed non-secret - account preferences, and a non-secret cross-resource operation journal. -- Publish success only after durable state commits. Never update the public - snapshot first and merely log a persistence failure. -- Add/import recovery covers credential write, metadata commit, compensation, - compensation failure, and restart. -- Removal recovery records intent, credential deletion, metadata deletion, and - finalization as distinct phases. A deleted credential is never represented - as restorable. -- Missing credentials produce an explicit `CredentialMissing` repair state, - not an implicit read-only signer. -- Corrupt or unsupported databases fail safely and are not destructively - recreated without an explicit future recovery feature. -- Do not expose arbitrary `set_account_scoped_value` or - `get_account_scoped_value` methods through shipping UniFFI. Prove account - partitioning through a typed Rust repository interface and integration tests - until a real typed preference is required. - -### Relay and profile behavior - -- Read the ordered relay list from comma-separated - `RADROOTS_NOSTR_RELAYS`. Trim and deduplicate while preserving order. -- Accept `wss://`. Accept `ws://` only for `localhost`, `127.0.0.0/8`, and - `::1`. Reject user information, fragments, and non-WebSocket schemes. -- Use `ws://localhost:8080` only as a development/test fallback. A packaged - runtime without valid configuration reports a safe configuration state. -- Tests use local ephemeral relays or controlled fakes and never public - internet relays. -- Verify kind-0 event ID, signature, author, and kind before parsing. Select the - newest `created_at`; break equal timestamps with the lexicographically lowest - event ID. -- Bound relay messages, raw event content, parsed profile fields, and stored - profile data. -- Emit cached profile state before asynchronous refresh. Timeout, offline, or - invalid relay data preserves the cache and produces a nonfatal state. -- Display `nip05` and `picture` as bounded metadata. NIP-05 verification and - remote-image fetching are out of scope. - -### UniFFI and Kotlin lifecycle - -- Use UniFFI async operations for work that could block the Compose thread. -- Supervise Rust relay tasks and cancel them on replacement, sign out, and - AppCore close. -- Bind asynchronous results to their initiating account and revision so stale - results cannot overwrite newer state. -- Ensure callback delivery is safe for JVM thread attachment and callback - re-entry. Close observer handles and AppCore explicitly from Kotlin. -- Generated bindings remain under `build/generated` and are never hand-edited. -- Keep one AppCore instance at the application root. The Kotlin store subscribes - once, exposes read-only Compose state, forwards commands, and closes cleanly. - -### Minimal product UI - -- Provide an inactive accounts screen with generate, masked import, saved - accounts, selection, activation, and confirmed removal. -- Provide a one-time generated-key backup panel with npub, nsec, warning, copy, - and acknowledgement. -- Provide a minimal active-account screen with pubkey, npub, cached profile, - relay/profile state, configured relays, refresh, switch-account, and sign-out - controls. -- Use basic Compose primitives, existing limited styling, stable test tags, and - accessibility descriptions. Do not add a design system. -- Remove UUID account identity, obsolete remote-endpoint fields, fake session - flags, the canonical Kotlin reducer/store, and all generic remote-onboarding - language from active source and tests. - -### Dependency baseline - -Pin exact compatible versions in the dependency checkpoint and record their -licenses and primary-source provenance: - -- stable `nostr` and `nostr-sdk` `0.44.1`; -- UniFFI `0.32.0`; -- `keyring` `4.1.6`, behind the Radroots `SecretStore`; -- `rusqlite` `0.39.0` with bundled SQLite, constrained by Refinery `0.9.2`; -- `refinery` `0.9.2`; -- `secrecy` `0.10.3`; -- `zeroize` `1.9.0`; -- `directories` `6.0.0`. - -Dependency-source deviation: crates.io has yanked `nostr` 0.44.1 and refuses a -new exact registry resolution. The `nostr` crate is therefore pinned to the -upstream rust-nostr commit `5bba5163eb77107f82c4a8262cf29d7f33a73219`, -whose crate manifest declares version 0.44.1. This preserves the approved -version and uses the upstream source rather than substituting a newer release. - -Notedeck and nostrdb are GPL clean-room references only. Do not copy their -code. Reject optional disk-secret storage, nondeterministic account fallback, -cache-before-persist publication, and delete-without-recovery patterns. -Reject Amethyst's encrypted-file fallback, mutation-before-cleanup transition, -swallowed deletion failures, and memory-cache-before-disk publication. - -## Global definition of green - -Every checkpoint must satisfy all applicable items below before it can be -committed: - -- formatting is clean; -- warnings are denied in first-party Rust code; -- the narrow Rust or Kotlin unit tests pass; -- inherited tests for completed behavior remain green; -- no public-internet relay is contacted by tests; -- no generated or build output is staged; -- no secret or generic server-account concept has crossed a forbidden - boundary; -- the diff contains only the active checkpoint; -- the nested repository is the only repository being changed; -- this ledger is updated if checkpoint status or remaining scope changed. - -There is no numeric coverage threshold. The required standard is strong, -best-effort behavioral coverage across Rust unit, storage, recovery, FFI, -Kotlin store, Compose UI, local relay, restart, native-loader, and end-to-end -lanes. - -## RCLD sequence - -### RCLD-01: Authority and dependency baseline - -Status: completed. - -Scope: checkpoint 1. Record live repository state, authoritative handoff files, -reference source SHAs and paths, adopted and rejected ideas, license boundaries, -dependency versions, native platform assumptions, and the approved normalized -decisions in capsule-local research and ADR material. - -Definition of green: the recorded facts match the live checkout and primary -sources; GPL references are clean-room only; no runtime source has changed. - -Verify lane: Git boundary/status inspection, primary-source version and license -checks, documentation link/path validation, and diff review. - -### RCLD-02: Rust workspace and domain - -Status: completed. - -Scope: checkpoints 2 through 11. Establish the Rust workspace and implement -safe errors, public keys, secret input/redaction, NIP-19 types, relay URLs, -account metadata, kind-0 profile rules, and immutable application snapshots. - -Definition of green: all domain values are validated types; secret wrappers are -redacted and non-cloneable; snapshots contain no secret fields; domain tests -cover valid, invalid, canonicalization, ordering, and invariant cases. - -Verify lane: Cargo formatting, clippy with warnings denied, workspace check, -and focused domain tests. - -### RCLD-03: Application state machine - -Status: completed. - -Scope: checkpoints 12 through 14. Define repository, secret, clock, and Nostr -ports; implement in-memory AppCore bootstrap, command serialization, monotonic -revisions, transition helpers, and observer registration/deregistration. - -Definition of green: selected and active state are separate; transitions are -deterministic; callbacks occur outside locks; observer lifecycle and command -traces are tested. - -Verify lane: application crate formatting, clippy, check, unit tests, callback -re-entry tests, and workspace regression tests. - -### RCLD-04: SQLite persistence - -Status: completed. - -Scope: checkpoints 15 through 20. Add bundled SQLite and migrations, account and -selection persistence, profile cache, typed account-scoped partitioning, -operation journal storage, and persisted AppCore bootstrap. - -Definition of green: fresh, migrated, restarted, isolated, corrupt, and -idempotent database cases are covered; no secret appears in schema or data; -snapshots publish only after successful commits. - -Verify lane: storage formatting, clippy, migration tests, restart tests, -failure-injection tests, database-byte secret guards, and workspace tests. - -### RCLD-05: Credential boundary - -Status: completed. - -Scope: checkpoints 21 through 24. Add SecretStore, in-memory and -failure-injection fakes, OS keyring adapter, safe platform errors, and global -no-secret guards. - -Definition of green: production has no fallback; adapter service/key naming is -stable; unavailable, missing, duplicate, read, write, and delete outcomes map -to safe errors; known secrets are absent from snapshots, persistence, errors, -and captured logs. - -Verify lane: secret-store unit and contract tests, platform adapter smoke where -available, redaction tests, Cargo checks, and inherited storage tests. - -### RCLD-06: Account generation and import - -Status: completed. - -Scope: checkpoints 25 through 30. Pin and adapt the selected Nostr library; -implement generation, nsec and secret-hex import, duplicate and repair -semantics, cross-resource rollback, journaling, and persisted commands. - -Definition of green: derived pubkeys and NIP-19 values match known vectors; -generated nsec appears only in its receipt; duplicate imports do not overwrite; -every keyring/database failure boundary is tested; partial success is never -published. - -Verify lane: Nostr vector tests, application command tests, transaction and -restart failure-injection matrix, no-secret guards, and workspace regression. - -### RCLD-07: Account lifecycle and recovery - -Status: completed. - -Scope: checkpoints 31 through 35. Implement selection, signed-out startup, -safe replacement activation, sign out, revision-bound removal confirmation, -deterministic fallback, and removal journal recovery. - -Definition of green: failed replacement preserves the active session; sign out -retains saved state; stale confirmation is rejected; removal is deterministic; -irreversible deletion phases are represented honestly and recover on restart. - -Verify lane: state-machine tables, concurrent/stale command tests, activation -failure tests, removal failure-injection matrix, restart recovery, cancellation, -and workspace tests. - -### RCLD-08: Relay and profile runtime - -Status: completed. - -Scope: checkpoints 36 through 40. Add environment relay configuration, exact -WebSocket policy, verified kind-0 parsing, deterministic local relay fixture, -cache-first asynchronous refresh, and manual refresh. - -Definition of green: relay parsing follows the approved policy; invalid events -are rejected; equal-time tie-breaking is deterministic; cached state arrives -before refresh; stale, offline, timeout, cancellation, and repeated refresh -outcomes are safe; tests use no public relay. - -Verify lane: relay parser tests, NIP event vectors, local ephemeral relay tests, -async stale-result/cancellation tests, profile cache tests, and workspace tests. - -### RCLD-09: UniFFI and native build integration - -Status: completed. - -Scope: checkpoints 41 through 47. Add UniFFI definitions and safe DTO mapping, -expose supported AppCore commands, implement observer handles, build the native -library, generate Kotlin bindings, and stage native artifacts for development, -tests, and Compose packaging. - -Definition of green: all shipping commands are callable without the generic -namespace API; generated DTOs contain no ordinary secret field; async calls do -not block Compose; callbacks are ordered and close safely; generated source is -not tracked; development and packaged native loading work on the current host. - -Verify lane: Cargo FFI tests, bindgen freshness/generation, ABI and loader smoke, -Gradle compile/test, observer cancellation/re-entry tests, package staging -inspection, and secret-field source guards. - -### RCLD-10: Thin Kotlin shell and minimal UI - -Status: completed. - -Scope: checkpoints 48 through 57. Implement the thin StudioAppStore, create and -close one AppCore at the application root, map public DTOs, implement inactive, -backup, saved-account, active-home, switch, sign-out, refresh, and removal UI, -then remove the canonical Kotlin proof and generic server-account remnants. - -Definition of green: the complete MVP is operable through minimal Compose -controls; generated secrets remain ephemeral; all important controls have -stable tags and accessibility descriptions; no UUID, remote endpoint, fake login, -or duplicated Kotlin state machine remains. - -Verify lane: Kotlin unit tests, Compose UI tests for success and failure flows, -clipboard fake/timer tests, AppCore lifecycle tests, full Gradle check, source -guards, and native-loader smoke. - -### RCLD-11: End-to-end integration hardening - -Status: completed. - -Scope: checkpoints 58 and 59. Add full local-relay end-to-end coverage plus -restart, account isolation, FFI callback, cancellation, deregistration, stale -completion, and no-secret integration tests. - -Definition of green: generate/import, activate, cached profile, relay refresh, -sign out, switch, remove, restart, repair, and callback lifecycle are proven -without public network access; account A cannot observe account B's private -namespace; secret guards pass across persisted and serialized artifacts. - -Verify lane: full Cargo workspace test, focused end-to-end suites, Gradle test, -Compose tests, native-loader smoke, and repeated runs for async determinism. - -### RCLD-12: Documentation and acceptance reconciliation - -Status: completed. - -Scope: checkpoints 60 through 63. Complete dependency/license, architecture, -security, testing, local-relay, FFI lifecycle, recovery, and platform validation -documentation; finish the Makefile lifecycle; perform the final source audit -and reconcile every acceptance criterion. - -Definition of green: required capsule documentation matches implemented paths -and behavior; Makefile exposes doctor, format, lint, test, check, build, dev, -run, package, and clean without extbuild or scripts; current-host package smoke -passes; portable loader concerns for macOS, Linux, and Windows are documented; -the final ledger records every command and residual platform risk honestly. - -Verify lane: all Makefile lifecycle targets applicable without destructive -cleanup, Cargo workspace format/clippy/check/test, Gradle check/test/build, -UniFFI generation, loader smoke, local-relay end-to-end, current-OS packaging, -forbidden-path and forbidden-term guards, Git status, and full diff audit. - -## Atomic checkpoint ledger - -All checkpoints are complete. Their order and titles are inherited from the -handoff commit sequence. - -### RCLD-01 - -- [x] 01. Audit live repository and reference sources. - -### RCLD-02 - -- [x] 02. Establish root Rust workspace skeleton. -- [x] 03. Add Rust formatting, lint, and governed check hooks. Use Cargo, - Gradle, and Makefile; do not add CI workflows or scripts. -- [x] 04. Define domain module layout and safe error shell. -- [x] 05. Implement Nostr public key value object. -- [x] 06. Implement secret input boundary and redacted secret wrapper. -- [x] 07. Add NIP-19 public/secret display contract types. -- [x] 08. Implement relay URL parser and policy. -- [x] 09. Add account public metadata value types. -- [x] 10. Add profile metadata model and kind-0 selection rules. -- [x] 11. Define immutable AppSnapshot and state enums. - -### RCLD-03 - -- [x] 12. Add application ports for repositories, secrets, clock, and Nostr - client. -- [x] 13. Implement in-memory AppCore bootstrap and observer registry. -- [x] 14. Add state transition helpers and command trace tests. - -### RCLD-04 - -- [x] 15. Create SQLite storage crate and migration runner. -- [x] 16. Implement account and selected-account persistence. -- [x] 17. Implement profile cache persistence. -- [x] 18. Implement typed account-scoped namespace persistence. -- [x] 19. Add operation journal persistence. -- [x] 20. Implement storage adapter wiring for AppCore bootstrap. - -### RCLD-05 - -- [x] 21. Add SecretStore trait and in-memory fake. -- [x] 22. Add failure-injection SecretStore fake. -- [x] 23. Implement OS keyring secret adapter. -- [x] 24. Add global no-secret snapshot and storage assertions. - -### RCLD-06 - -- [x] 25. Pin Nostr dependency and implement key generation/derivation adapter. -- [x] 26. Implement generate account command with in-memory storage. -- [x] 27. Implement import secret key command. -- [x] 28. Define and test duplicate import and credential-repair handling. -- [x] 29. Implement add/import transaction rollback across keyring and DB. -- [x] 30. Implement persisted generate/import using SQLite adapter. - -### RCLD-07 - -- [x] 31. Implement select account command. -- [x] 32. Implement activate account with safe replacement ordering. -- [x] 33. Implement sign out command. -- [x] 34. Implement revision-bound removal request/confirmation flow. -- [x] 35. Implement removal journal recovery. - -### RCLD-08 - -- [x] 36. Add relay configuration source and environment parser. -- [x] 37. Implement Nostr event verification and kind-0 parsing adapter. -- [x] 38. Add Nostr client port implementation and local relay fixture. -- [x] 39. Implement cache-first active profile refresh orchestration. -- [x] 40. Expose manual refreshActiveProfile command. - -### RCLD-09 - -- [x] 41. Create UniFFI scaffolding and DTO mapping. -- [x] 42. Expose approved AppCore commands through UniFFI. -- [x] 43. Expose observer callback and deregistration through UniFFI. -- [x] 44. Build native library artifacts from Cargo. -- [x] 45. Add Gradle task for Cargo build. -- [x] 46. Add Gradle UniFFI binding generation and generated source set. -- [x] 47. Stage native library for development, tests, and packaged app. - -### RCLD-10 - -- [x] 48. Add Kotlin StudioAppStore thin adapter. -- [x] 49. Bootstrap AppCore once at application root. -- [x] 50. Create UI model mapping helpers without server fields. -- [x] 51. Implement inactive account screen generate/import controls. -- [x] 52. Implement generated-key backup panel. -- [x] 53. Implement saved-account list, activation, and removal controls. -- [x] 54. Implement active account home screen. -- [x] 55. Implement switch account flow in UI. -- [x] 56. Remove canonical Kotlin account reducer/store usage. -- [x] 57. Remove generic remote-account remnants. - -### RCLD-11 - -- [x] 58. Add full local-relay end-to-end integration test. -- [x] 59. Add restart, account isolation, and FFI callback integration tests. - -### RCLD-12 - -- [x] 60. Add dependency and license documentation. -- [x] 61. Complete architecture, security, testing, and runbook documentation. -- [x] 62. Add Makefile-governed final validation tasks and platform ledger. Do - not add `.github/**` or `scripts/**`. -- [x] 63. Perform final source audit and acceptance reconciliation. - -## RCLD completion ledger - -- [x] RCLD-01: Authority and dependency baseline. -- [x] RCLD-02: Rust workspace and domain. -- [x] RCLD-03: Application state machine. -- [x] RCLD-04: SQLite persistence. -- [x] RCLD-05: Credential boundary. -- [x] RCLD-06: Account generation and import. -- [x] RCLD-07: Account lifecycle and recovery. -- [x] RCLD-08: Relay and profile runtime. -- [x] RCLD-09: UniFFI and native build integration. -- [x] RCLD-10: Thin Kotlin shell and minimal UI. -- [x] RCLD-11: End-to-end integration hardening. -- [x] RCLD-12: Documentation and acceptance reconciliation. diff --git a/docs/runbooks/local-relay-development.md b/docs/runbooks/local-relay-development.md @@ -1,40 +0,0 @@ -# Local relay development - -Rust reads the ordered comma-separated relay list from -`RADROOTS_NOSTR_RELAYS`, trims entries, validates the WebSocket policy, and -deduplicates while preserving first-seen order. Development and tests may use -`ws://localhost:8080` when the variable is absent or empty. Packaged builds -have no fallback and report `InvalidRelayConfiguration` until at least one -valid relay is configured. - -The Rust integration lane starts an in-process relay on an ephemeral loopback -port, publishes a signed kind-0 event, fetches it through the production SDK -adapter, and shuts both clients and the relay down before returning. Run it -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 -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. - -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 @@ -1,67 +0,0 @@ -# Nostr key management - -## Status - -Implemented MVP security contract. - -## 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. - -The production adapter uses keyring 4.1.6 with the native macOS Keychain, -Windows Credential Manager, or freedesktop Secret Service backend selected by -the crate. Unsupported, locked, inaccessible, malformed, and platform-failure -outcomes become a stable `KeyringUnavailable` error. No alternative storage is -attempted. The real keyring smoke test is ignored by default because it mutates -the invoking user's credential store and must be run explicitly on each target. - -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 copy places the nsec in the operating-system clipboard, which is an -additional user-authorized exposure. A lifecycle-owned timer clears it after -60 seconds only if the clipboard still contains the copied value; user-replaced -content is preserved. Application disposal performs the same conditional clear. -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. - -The journal contains only an operation identifier, operation kind, canonical -public key, phase, safe timestamp, and optional safe diagnostic code. It cannot -store credential text or arbitrary payloads, and it survives account metadata -removal until cleanup has been finalized. - -Workspace redaction tests scan public snapshot and safe-error debug output plus -the SQLite schema and representative durable records for known secret-hex, -nsec, and secret-prefix fixtures. These guards run before account commands are -allowed to carry production credentials. - -Startup reads the non-secret operation journal before restoring public state. -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/final-validation-ledger.md b/docs/testing/final-validation-ledger.md @@ -1,87 +0,0 @@ -# Final validation ledger - -## Result - -Validation completed on 2026-08-02 for macOS 26.5 arm64. All 30 acceptance -criteria in the authoritative handoff are satisfied by implemented source, -tests, or an explicit documented platform-validation contract. No test uses a -public relay. - -The Radroots crates release v1 reconciliation was rerun on 2026-08-03. Locked -Rust formatting, workspace all-target checks, workspace all-target tests, -Gradle desktop checks, native-loader coverage, and the current-host DMG package -were green through the governed extbuild output router. The reviewed legacy SDK -runtime is absent; the evidence-based Step 274–278 deviation is recorded in -`docs/architecture/radroots-crates-release-v1-reconciliation.md`. - -## Acceptance reconciliation - -| # | Result | Evidence | -| --- | --- | --- | -| 1 | Pass | `core/Cargo.toml` defines the Rust workspace and all five runtime crates build together. | -| 2 | Pass | `radroots-studio-application::AppCore` owns canonical snapshots, transitions, commands, observers, sessions, recovery, and refresh orchestration. | -| 3 | Pass | `StudioAppStore` maps generated UniFFI DTOs into presentation models and forwards commands without duplicating the reducer or persistence policy. | -| 4 | Pass | Domain, persistence, FFI, and Kotlin models use canonical lowercase 64-character Nostr public-key hex; forbidden UUID guards are clean. | -| 5 | Pass | The generate command, UniFFI method, Compose control, and behavior tests create a persisted local Nostr key. | -| 6 | Pass | Masked nsec/hex import is implemented through the Rust key boundary and covered by valid, invalid, duplicate, and repair tests. | -| 7 | Pass | Storage and Compose tests cover multiple saved accounts; the chooser remains scrollable within the available window. | -| 8 | Pass | Activation is available for each saved account and Rust tests prove safe session replacement. | -| 9 | Pass | Sign-out drops the active signer/session while retaining metadata, selection, and credential. | -| 10 | Pass | Removal uses a single-use target-and-revision-bound confirmation token, deterministic fallback, and recovery journal. | -| 11 | Pass | Production wiring uses `OsKeyringSecretStore` with service `org.radroots.studio.nostr` and no file fallback. | -| 12 | Pass | Redaction, schema-byte, DTO, snapshot, error, journal, and source guards keep secrets out of forbidden surfaces. | -| 13 | Pass | Generated nsec exists only in `GenerateAccountReceipt` and the transient backup UI; acknowledgement, replacement, timeout, and disposal clear it. | -| 14 | Pass | SQLite restart tests restore account metadata and selection while startup remains signed out. | -| 15 | Pass | Typed repository and restart-isolation tests prove account-local values are partitioned by owner pubkey. | -| 16 | Pass | Production relay configuration is read from `RADROOTS_NOSTR_RELAYS`. | -| 17 | Pass | Development mode alone defaults to `ws://localhost:8080`. | -| 18 | Pass | Relay parsing accepts governed WebSocket URLs only; generic account-server terms and fields are absent. | -| 19 | Pass | Activation emits cached profile state before verified kind-0 refresh; failure and stale completion preserve safe state. | -| 20 | Pass | The active screen renders `radroots`, pubkey, npub, bounded profile metadata, relay list, and profile/relay status. | -| 21 | Pass | Source guards find no generic server URL field or onboarding language. | -| 22 | Pass | The previous Kotlin `AccountsReducer` and `AccountsStore` are absent; `StudioAppStore` is an FFI adapter only. | -| 23 | Pass | `make check` passed Rust, FFI, storage, security, restart, local-relay, Kotlin-store, native-loader, and Compose UI lanes. | -| 24 | Pass | The RCLD history contains ordered, independently verified commit-sized checkpoints plus separately committed audit fixes. | -| 25 | Pass | Source, tests, generated-boundary policy, package contents, forbidden terms, and all handoff criteria were audited at checkpoint 63. | -| 26 | Pass | The command ledger below distinguishes executed checks from intentionally skipped interactive, destructive, or foreign-platform checks. | -| 27 | Pass | `docs/architecture/reference-research.md` records reviewed revisions, paths, adopted/rejected patterns, license boundaries, and dependency decisions. | -| 28 | Pass | Required ADR, architecture, security, testing, and local-relay runbook documentation exists and matches the runtime. | -| 29 | Pass | `docs/testing/platform-validation.md` defines the current-host evidence and the required Linux/Windows loader, keyring, and packaging matrix. | -| 30 | Pass | Network tests use an ephemeral loopback relay; configuration tests do not contact the network. | - -## Commands executed - -| Command | Result | -| --- | --- | -| `make check` | Passed Rust formatting, all-target Clippy with denied warnings, all workspace tests, desktop tests, and Gradle check. | -| `make build` | Passed the Rust workspace and Compose Desktop build. | -| `make bindings` | Passed UniFFI Kotlin generation and current-host native-library staging. | -| `make package` | Produced `app/desktop/build/compose/binaries/main/dmg/Radroots-1.0.0.dmg`. | -| `codesign --verify --deep --strict .../Radroots.app` | Passed. | -| `PlistBuddy` bundle inspection | Confirmed bundle ID `org.radroots.studio` and installer version `1.0.0`. | -| Packaged application JAR inspection | Confirmed `darwin-aarch64/libradroots_studio_ffi.dylib`, `icons/radroots.icns`, and `icons/radroots.png`. | -| Tracked-path guards | Found no build output, generated UniFFI source, native binary, `.github/**`, or `scripts/**` path. | -| Forbidden-term guards | Found no old package/version, UUID account, Kotlin reducer/store, generic account-server, or server-URL term. | -| Nested-repository status and history inspection | Confirmed the standalone capsule boundary and local commit sequence. | - -`make check` executed 35 application tests plus its redaction test, 21 domain -tests, 6 FFI tests, 6 Nostr tests, 19 passing storage unit tests, three storage -integration tests, the bindgen test, all documentation tests, and the complete -Kotlin/Compose test task. The one ignored storage test is the intentionally -opt-in real operating-system keyring smoke. - -## Intentionally not executed - -- The real OS keyring smoke was not enabled because it mutates the current - user's credential store. Its adapter contract tests passed. -- Linux and Windows loader, keyring, and packaging checks cannot run on this - macOS host. Their required native-host matrix is recorded in - `platform-validation.md`. -- `make dev` and `make run` are interactive launchers and were not held open. -- `make clean` was not run because it only destroys recoverable build output - and is not an acceptance behavior. - -The application/runtime artifacts retain version `0.1.0-alpha`. The macOS -installer uses `1.0.0` because `jpackage` rejects the prerelease form; this -mapping is deliberate and documented. No source-level acceptance blocker -remains. diff --git a/docs/testing/nostr-accounts-test-plan.md b/docs/testing/nostr-accounts-test-plan.md @@ -1,67 +0,0 @@ -# Nostr accounts test plan - -## Status - -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 - -- 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. - -## Rust validation gates - -Run from the capsule root: - -```sh -cargo fmt --manifest-path core/Cargo.toml --all --check -cargo clippy --manifest-path core/Cargo.toml --workspace --all-targets -- -D warnings -cargo test --manifest-path core/Cargo.toml --workspace -``` - -Run the desktop, generated binding, native loader, store, and Compose lanes: - -```sh -./gradlew --no-daemon :app:desktop:test -``` - -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. diff --git a/docs/testing/platform-validation.md b/docs/testing/platform-validation.md @@ -1,62 +0,0 @@ -# Platform validation ledger - -## Command authority - -The capsule root `Makefile` is the complete human-facing lifecycle. It invokes -Cargo with `core/Cargo.toml` explicitly and invokes the checked-in Gradle -wrapper. This standalone `oss/**` repository does not use extbuild, validation -scripts, or `.github/**` workflows. - -| Target | Contract | -| --- | --- | -| `make doctor` | report Java, Cargo, and Gradle toolchains | -| `make format` | check Rust formatting | -| `make lint` | deny Rust workspace and all-target Clippy warnings | -| `make test` | run the full Rust and Kotlin/Compose/native-loader suites | -| `make check` | run format, lint, test, and Gradle check | -| `make build` | build the Rust workspace and desktop application | -| `make bindings` | generate Kotlin bindings and stage the host native library | -| `make dev` | launch Compose hot reload in development relay mode | -| `make run` | launch Compose Desktop in development relay mode | -| `make package` | build the current-host desktop distribution | -| `make clean` | remove Gradle and Cargo build outputs | - -## Current-host result - -Validation date: 2026-08-02. - -- Host: macOS 26.5 build 25F71, arm64. -- Java: Eclipse Temurin 21.0.11 LTS. -- Cargo: 1.97.1 with the workspace Rust 1.97 toolchain contract. -- Gradle: wrapper 9.5.0. -- Native resource: `darwin-aarch64/libradroots_studio_ffi.dylib`. -- Package format: macOS DMG with application name `Radroots`, bundle ID - `org.radroots.studio`, installer-compatible version `1.0.0`, and the - generated squircle application icon. macOS `jpackage` rejects a leading-zero - app version, so this field cannot encode `0.1.0`; application and runtime - artifacts keep the full `0.1.0-alpha` prerelease version. - -The final validation checkpoint records the exact green command results and -artifact inspection. Interactive `make dev` and `make run` are launchers and -are reviewed but not held open during automated validation. `make clean` is -destructive to recoverable build outputs and is not required for acceptance. - -Checkpoint 62 results: - -| Command | Result | -| --- | --- | -| `make check` | passed Rust format, all-target Clippy, Rust workspace tests, Kotlin/Compose/native-loader tests, and Gradle check | -| `make build` | passed Rust workspace and desktop builds | -| `make bindings` | passed UniFFI generation and current-host native staging | -| `make package` | produced `Radroots-1.0.0.dmg` successfully | -| packaged app inspection | bundle ID and installer version matched; code signature verified; application jar contained the arm64 dylib and both icon resources | - -## Remaining platform matrix - -Linux x86-64/aarch64 and Windows x86-64/aarch64 resource-prefix selection is -implemented but not validated on this macOS host. Each platform must run -`make check`, `make build`, `make bindings`, the real keyring smoke in an -isolated credential account, and its native packaging smoke. A package must -contain only its matching JNA resource prefix and native filename. Cross-built -artifacts are not accepted as substitutes for host loader and credential-store -tests.