app

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

commit c04c5401c1bdc916afc4eda1b106d003ea64163f
parent e0ac766b313379aa8d7160d517643d1207b9646b
Author: triesap <tyson@radroots.org>
Date:   Sun,  2 Aug 2026 18:24:15 +0000

core(storage): add SQLite migration foundation

- open bundled SQLite with safe connection policy
- apply an embedded idempotent Refinery schema migration
- attempt owner-only permissions for file-backed databases
- document the compatible Rusqlite 0.39 dependency pin

Diffstat:
Mcore/Cargo.lock | 515+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/Cargo.toml | 2++
Mcore/crates/storage/Cargo.toml | 8++++++++
Acore/crates/storage/migrations/V1__initialize.sql | 6++++++
Acore/crates/storage/src/db.rs | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/storage/src/lib.rs | 10+++-------
Mdocs/adr/0003-account-metadata-and-secret-storage.md | 4++++
Mdocs/architecture/reference-research.md | 4+++-
Mdocs/implementation/nostr-runtime-rcld.md | 6+++---
9 files changed, 705 insertions(+), 11 deletions(-)

diff --git a/core/Cargo.lock b/core/Cargo.lock @@ -3,6 +3,60 @@ version = 4 [[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] name = "displaydoc" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -14,6 +68,46 @@ dependencies = [ ] [[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] name = "form_urlencoded" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -23,6 +117,36 @@ dependencies = [ ] [[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown", +] + +[[package]] name = "icu_collections" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -126,18 +250,81 @@ dependencies = [ ] [[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] name = "litemap" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] name = "potential_utf" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -147,6 +334,12 @@ dependencies = [ ] [[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] name = "proc-macro2" version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -165,6 +358,12 @@ dependencies = [ ] [[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] name = "radroots-studio-application" version = "0.1.0-alpha.0" dependencies = [ @@ -190,12 +389,141 @@ version = "0.1.0-alpha.0" [[package]] name = "radroots-studio-storage" version = "0.1.0-alpha.0" +dependencies = [ + "radroots-studio-domain", + "refinery", + "rusqlite", + "tempfile", +] [[package]] name = "radroots-studio-uniffi-bindgen" version = "0.1.0-alpha.0" [[package]] +name = "refinery" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2a344cdb48871e27addeafbbaffab8828cc12cec2b9041119e9bea0c0f551a" +dependencies = [ + "refinery-core", + "refinery-macros", +] + +[[package]] +name = "refinery-core" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eeafd893124f29183dd6afa9137a27e7bef59250223b8b660005279c60aea4" +dependencies = [ + "async-trait", + "cfg-if", + "log", + "regex", + "rusqlite", + "siphasher", + "thiserror", + "time", + "url", + "walkdir", +] + +[[package]] +name = "refinery-macros" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90cea6d11a9a4e8a85a884b6305461004101b28ca65dd35ec028e009a898e16" +dependencies = [ + "proc-macro2", + "quote", + "refinery-core", + "regex", + "syn 2.0.119", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown", + "thiserror", +] + +[[package]] +name = "rusqlite" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] name = "secrecy" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -234,12 +562,36 @@ dependencies = [ ] [[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] name = "smallvec" version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -279,6 +631,69 @@ dependencies = [ ] [[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] name = "tinystr" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -313,6 +728,106 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] name = "writeable" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/core/Cargo.toml b/core/Cargo.toml @@ -24,6 +24,8 @@ all = "deny" pedantic = "deny" [workspace.dependencies] +refinery = { version = "=0.9.2", default-features = false, features = ["rusqlite"] } +rusqlite = { version = "=0.39.0", features = ["bundled"] } secrecy = "=0.10.3" url = "=2.5.8" zeroize = "=1.9.0" diff --git a/core/crates/storage/Cargo.toml b/core/crates/storage/Cargo.toml @@ -6,5 +6,13 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[dependencies] +radroots-studio-domain = { path = "../domain" } +refinery.workspace = true +rusqlite.workspace = true + +[dev-dependencies] +tempfile = "=3.23.0" + [lints] workspace = true diff --git a/core/crates/storage/migrations/V1__initialize.sql b/core/crates/storage/migrations/V1__initialize.sql @@ -0,0 +1,6 @@ +CREATE TABLE application_schema ( + singleton INTEGER PRIMARY KEY CHECK (singleton = 1), + schema_version INTEGER NOT NULL CHECK (schema_version >= 1) +); + +INSERT INTO application_schema (singleton, schema_version) VALUES (1, 1); diff --git a/core/crates/storage/src/db.rs b/core/crates/storage/src/db.rs @@ -0,0 +1,161 @@ +use std::path::Path; +use std::sync::{Mutex, MutexGuard}; +use std::time::Duration; + +use radroots_studio_domain::{SafeError, SafeErrorCode, SafeMessage}; +use refinery::embed_migrations; +use rusqlite::{Connection, OpenFlags}; + +mod migrations { + use super::embed_migrations; + + embed_migrations!("migrations"); +} + +pub struct Database { + connection: Mutex<Connection>, +} + +impl Database { + /// Opens, configures, and migrates a file-backed `SQLite` database. + /// + /// # Errors + /// + /// Returns a safe storage error when the file, connection configuration, + /// permission update, or migration cannot complete. + pub fn open(path: &Path) -> Result<Self, SafeError> { + let flags = OpenFlags::SQLITE_OPEN_READ_WRITE + | OpenFlags::SQLITE_OPEN_CREATE + | OpenFlags::SQLITE_OPEN_NO_MUTEX; + let mut connection = + Connection::open_with_flags(path, flags).map_err(|_| storage_error())?; + configure(&connection)?; + migrations::migrations::runner() + .run(&mut connection) + .map_err(|_| corrupt_storage_error())?; + restrict_file_permissions(path)?; + Ok(Self { + connection: Mutex::new(connection), + }) + } + + /// Opens and migrates an isolated in-memory `SQLite` database. + /// + /// # Errors + /// + /// Returns a safe storage error when configuration or migration fails. + pub fn in_memory() -> Result<Self, SafeError> { + let mut connection = Connection::open_in_memory().map_err(|_| storage_error())?; + configure(&connection)?; + migrations::migrations::runner() + .run(&mut connection) + .map_err(|_| corrupt_storage_error())?; + Ok(Self { + connection: Mutex::new(connection), + }) + } + + /// Returns the highest successfully applied migration version. + /// + /// # Errors + /// + /// Returns a safe storage error when migration history cannot be read. + pub fn schema_version(&self) -> Result<u32, SafeError> { + self.connection() + .query_row( + "SELECT COALESCE(MAX(version), 0) FROM refinery_schema_history", + [], + |row| row.get(0), + ) + .map_err(|_| corrupt_storage_error()) + } + + pub(crate) fn connection(&self) -> MutexGuard<'_, Connection> { + self.connection + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + } +} + +fn configure(connection: &Connection) -> Result<(), SafeError> { + connection + .pragma_update(None, "foreign_keys", "ON") + .and_then(|()| connection.pragma_update(None, "journal_mode", "WAL")) + .and_then(|()| connection.busy_timeout(Duration::from_secs(5))) + .map_err(|_| storage_error()) +} + +#[cfg(unix)] +fn restrict_file_permissions(path: &Path) -> Result<(), SafeError> { + use std::fs; + use std::os::unix::fs::PermissionsExt; + + fs::set_permissions(path, fs::Permissions::from_mode(0o600)).map_err(|_| storage_error()) +} + +#[cfg(not(unix))] +fn restrict_file_permissions(_path: &Path) -> Result<(), SafeError> { + Ok(()) +} + +const fn storage_error() -> SafeError { + SafeError::new( + SafeErrorCode::StorageUnavailable, + SafeMessage::new("The application database is unavailable."), + ) +} + +const fn corrupt_storage_error() -> SafeError { + SafeError::new( + SafeErrorCode::StorageCorrupt, + SafeMessage::new("The application database could not be read."), + ) +} + +#[cfg(test)] +mod tests { + use std::fs; + + use tempfile::tempdir; + + use super::Database; + + #[test] + fn migration_opens_fresh_memory_database_once() { + let database = Database::in_memory().expect("open memory database"); + + assert_eq!(database.schema_version().expect("schema version"), 1); + assert_eq!(database.schema_version().expect("repeat schema version"), 1); + } + + #[test] + fn migration_persists_schema_version_across_file_reopen() { + let directory = tempdir().expect("temporary directory"); + let path = directory.path().join("studio.sqlite3"); + + { + let database = Database::open(&path).expect("open file database"); + assert_eq!(database.schema_version().expect("schema version"), 1); + } + let reopened = Database::open(&path).expect("reopen file database"); + assert_eq!(reopened.schema_version().expect("schema version"), 1); + assert!(fs::metadata(path).expect("database metadata").len() > 0); + } + + #[cfg(unix)] + #[test] + fn migration_attempts_owner_only_database_permissions() { + use std::os::unix::fs::PermissionsExt; + + let directory = tempdir().expect("temporary directory"); + let path = directory.path().join("studio.sqlite3"); + let _database = Database::open(&path).expect("open file database"); + let mode = fs::metadata(path) + .expect("database metadata") + .permissions() + .mode() + & 0o777; + + assert_eq!(mode, 0o600); + } +} diff --git a/core/crates/storage/src/lib.rs b/core/crates/storage/src/lib.rs @@ -1,9 +1,5 @@ #![doc = "Radroots Studio persistence adapters."] -#[cfg(test)] -mod tests { - #[test] - fn crate_is_available_to_the_workspace() { - assert_eq!(env!("CARGO_PKG_NAME"), "radroots-studio-storage"); - } -} +pub mod db; + +pub use db::Database; diff --git a/docs/adr/0003-account-metadata-and-secret-storage.md b/docs/adr/0003-account-metadata-and-secret-storage.md @@ -16,6 +16,10 @@ 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 diff --git a/docs/architecture/reference-research.md b/docs/architecture/reference-research.md @@ -103,7 +103,9 @@ and source audit will be reconciled again before final acceptance. - `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. +- `rusqlite` `0.39.0` with bundled SQLite. Refinery `0.9.2` constrains its + supported `rusqlite` range to `<=0.39`, so the originally reviewed `0.40.1` + cannot be used in the same migration adapter. - `refinery` `0.9.2`. - `secrecy` `0.10.3`. - `zeroize` `1.9.0`. diff --git a/docs/implementation/nostr-runtime-rcld.md b/docs/implementation/nostr-runtime-rcld.md @@ -210,7 +210,7 @@ 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.40.1` with bundled SQLite; +- `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`; @@ -293,7 +293,7 @@ re-entry tests, and workspace regression tests. ### RCLD-04: SQLite persistence -Status: pending. +Status: in progress. Scope: checkpoints 15 through 20. Add bundled SQLite and migrations, account and selection persistence, profile cache, typed account-scoped partitioning, @@ -474,7 +474,7 @@ handoff commit sequence. ### RCLD-04 -- [ ] 15. Create SQLite storage crate and migration runner. +- [x] 15. Create SQLite storage crate and migration runner. - [ ] 16. Implement account and selected-account persistence. - [ ] 17. Implement profile cache persistence. - [ ] 18. Implement typed account-scoped namespace persistence.