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:
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.