commit 16a9a67380e23a3ffad45a813db767092df2899e parent 2554ed9c1c37372c05badfc75bd00c28a0de986f Author: triesap <tyson@radroots.org> Date: Wed, 26 Aug 2026 01:48:51 +0000 storage: freeze HarvestCircle state contract - bind state paths to the typed runtime context - freeze schema v1 and its empty migration catalog - govern platform and resource coordinates - record exact API and source-lock evidence Diffstat:
18 files changed, 1526 insertions(+), 82 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md @@ -76,6 +76,16 @@ substitute. ## Application and security boundaries +- Product state is bound only through `radroots_runtime_paths::RuntimeContext` + for service `harvestcircle` and instance `desktop`; the canonical database + and lock names are `state.sqlite` and `state.lock`. The exact schema starts + at v1, future migrations start at v2, and `radroots_service_sqlite` owns the + governed SQLite mechanics. +- `harvestcircle.sqlite3` is legacy evidence only. Never delete, rename, + import, dual-read, dual-write, or otherwise treat it as current state. +- Native production qualification is limited to macOS aarch64 and Linux + x86_64. Do not add or claim another target without an explicit contract + change and its complete platform evidence. - Keep Compose screens and stores as client presentation and orchestration. Do not make them a source of truth for accounts, identities, approvals, domain objects, synchronization, reconciliation, or publication state. diff --git a/build-logic/contracts/src/main/kotlin/org/harvestcircle/buildlogic/contracts/ProductCoordinates.kt b/build-logic/contracts/src/main/kotlin/org/harvestcircle/buildlogic/contracts/ProductCoordinates.kt @@ -25,10 +25,32 @@ public class ProductCoordinates private constructor( "desktop.main_class", "ffi.kotlin_package", "ffi.cdylib_name", - "database.qualifier", - "database.organization", - "database.application", - "database.filename", + "storage.service_id", + "storage.instance_id", + "storage.database_filename", + "storage.lock_filename", + "storage.application_id", + "storage.application_id_text", + "storage.initial_schema_version", + "legacy.database.qualifier", + "legacy.database.organization", + "legacy.database.application", + "legacy.database.filename", + "legacy.database.disposition", + "platform.macos.architecture", + "platform.linux.architecture", + "limit.identities", + "limit.unfinished_durable_operations", + "limit.preference_value_utf8_bytes", + "limit.relay_endpoints", + "limit.relay_url_bytes", + "limit.events_per_relay", + "limit.events_total", + "limit.observers", + "limit.actor_mailbox", + "limit.command_deadline_min_ms", + "limit.command_deadline_max_ms", + "backup.member_limit", "keyring.service", "environment.prefix", "vendor.name", @@ -67,15 +89,37 @@ public class ProductCoordinates private constructor( when (key) { "schema" -> value == SCHEMA "product.name", "vendor.name", "copyright.notice" -> value.length <= 160 - "product.slug", "ffi.cdylib_name", "database.qualifier", "database.organization", - "database.application", + "product.slug", "ffi.cdylib_name", -> value.isLowerIdentifier() "kotlin.root_namespace", "desktop.application_id", "desktop.bundle_id", "desktop.main_class", "ffi.kotlin_package", "keyring.service", -> value.isDottedIdentifier() - "database.filename" -> - value.endsWith(".sqlite3") && ".." !in value && - value.all { it.isAsciiLetterOrDigit() || it in "._-" } + "storage.service_id" -> value == "harvestcircle" + "storage.instance_id" -> value == "desktop" + "storage.database_filename" -> value == "state.sqlite" + "storage.lock_filename" -> value == "state.lock" + "storage.application_id" -> value == "1212371505" + "storage.application_id_text" -> value == "HCR1" + "storage.initial_schema_version" -> value == "1" + "legacy.database.qualifier" -> value == "org" + "legacy.database.organization" -> value == "harvestcircle" + "legacy.database.application" -> value == "desktop" + "legacy.database.filename" -> value == "harvestcircle.sqlite3" + "legacy.database.disposition" -> value == "untouched_and_unsupported" + "platform.macos.architecture" -> value == "aarch64" + "platform.linux.architecture" -> value == "x86_64" + "limit.identities" -> value == "256" + "limit.unfinished_durable_operations" -> value == "1024" + "limit.preference_value_utf8_bytes" -> value == "4096" + "limit.relay_endpoints" -> value == "16" + "limit.relay_url_bytes" -> value == "2048" + "limit.events_per_relay" -> value == "64" + "limit.events_total" -> value == "1024" + "limit.observers" -> value == "32" + "limit.actor_mailbox" -> value == "64" + "limit.command_deadline_min_ms" -> value == "1" + "limit.command_deadline_max_ms" -> value == "30000" + "backup.member_limit" -> value == "caller_supplied_positive" "environment.prefix" -> value.firstOrNull() in 'A'..'Z' && value.endsWith('_') && value.all { it in 'A'..'Z' || it.isDigit() || it == '_' } diff --git a/build-logic/contracts/src/test/kotlin/org/harvestcircle/buildlogic/contracts/BuildContractsTest.kt b/build-logic/contracts/src/test/kotlin/org/harvestcircle/buildlogic/contracts/BuildContractsTest.kt @@ -13,7 +13,7 @@ class BuildContractsTest { assertEquals("HarvestCircle", coordinates["product.name"]) assertEquals( - "93bf10e334e989b20ba5fb8ed05e5d55b83f4502efba5f893aef4dc1a66c8223", + "bf50f9ea6c2537406de255f025463e670eb6263c295f992f7e4c4db36d957064", coordinates.digest, ) assertEquals(coordinates.digest, ProductCoordinates.parse(productCoordinates.replace("\n", "\r\n")).digest) @@ -33,7 +33,10 @@ class BuildContractsTest { assertFails { ProductCoordinates.parse(productCoordinates.substringAfter('\n')) } assertFails { ProductCoordinates.parse(productCoordinates.replace("product.name=HarvestCircle", "product.name")) } assertFails { ProductCoordinates.parse(productCoordinates.replaceCoordinate("product.slug", "INVALID")) } - assertFails { ProductCoordinates.parse(productCoordinates.replace("harvestcircle.sqlite3", "../other.sqlite3")) } + assertFails { ProductCoordinates.parse(productCoordinates.replaceCoordinate("storage.database_filename", "other.sqlite")) } + assertFails { ProductCoordinates.parse(productCoordinates.replaceCoordinate("legacy.database.filename", "../other.sqlite3")) } + assertFails { ProductCoordinates.parse(productCoordinates.replaceCoordinate("limit.identities", "257")) } + assertFails { ProductCoordinates.parse(productCoordinates.replaceCoordinate("platform.linux.architecture", "aarch64")) } val validMutations = linkedMapOf( @@ -45,16 +48,12 @@ class BuildContractsTest { "desktop.main_class" to "org.example.MainKt", "ffi.kotlin_package" to "org.example.ffi", "ffi.cdylib_name" to "example_ffi", - "database.qualifier" to "com", - "database.organization" to "example", - "database.application" to "test", - "database.filename" to "example.sqlite3", "keyring.service" to "org.example.desktop.nostr", "environment.prefix" to "EXAMPLE_", "vendor.name" to "Example Cooperative", "copyright.notice" to "Copyright Example contributors", ) - assertEquals(ProductCoordinates.requiredKeys.size - 1, validMutations.size) + assertTrue(ProductCoordinates.requiredKeys.size > validMutations.size) validMutations.forEach { (key, replacement) -> val mutated = ProductCoordinates.parse(productCoordinates.replaceCoordinate(key, replacement)) assertEquals(replacement, mutated[key]) @@ -172,10 +171,35 @@ class BuildContractsTest { ffi.kotlin_package=org.harvestcircle.ffi ffi.cdylib_name=harvestcircle_ffi - database.qualifier=org - database.organization=harvestcircle - database.application=desktop - database.filename=harvestcircle.sqlite3 + storage.service_id=harvestcircle + storage.instance_id=desktop + storage.database_filename=state.sqlite + storage.lock_filename=state.lock + storage.application_id=1212371505 + storage.application_id_text=HCR1 + storage.initial_schema_version=1 + + legacy.database.qualifier=org + legacy.database.organization=harvestcircle + legacy.database.application=desktop + legacy.database.filename=harvestcircle.sqlite3 + legacy.database.disposition=untouched_and_unsupported + + platform.macos.architecture=aarch64 + platform.linux.architecture=x86_64 + + limit.identities=256 + limit.unfinished_durable_operations=1024 + limit.preference_value_utf8_bytes=4096 + limit.relay_endpoints=16 + limit.relay_url_bytes=2048 + limit.events_per_relay=64 + limit.events_total=1024 + limit.observers=32 + limit.actor_mailbox=64 + limit.command_deadline_min_ms=1 + limit.command_deadline_max_ms=30000 + backup.member_limit=caller_supplied_positive keyring.service=org.harvestcircle.desktop.nostr environment.prefix=HARVESTCIRCLE_ diff --git a/build-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt b/build-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt @@ -561,10 +561,32 @@ class ConventionPluginSmokeTest { desktop.main_class=org.harvestcircle.desktop.MainKt ffi.kotlin_package=org.harvestcircle.ffi ffi.cdylib_name=harvestcircle_ffi - database.qualifier=org - database.organization=harvestcircle - database.application=desktop - database.filename=harvestcircle.sqlite3 + storage.service_id=harvestcircle + storage.instance_id=desktop + storage.database_filename=state.sqlite + storage.lock_filename=state.lock + storage.application_id=1212371505 + storage.application_id_text=HCR1 + storage.initial_schema_version=1 + legacy.database.qualifier=org + legacy.database.organization=harvestcircle + legacy.database.application=desktop + legacy.database.filename=harvestcircle.sqlite3 + legacy.database.disposition=untouched_and_unsupported + platform.macos.architecture=aarch64 + platform.linux.architecture=x86_64 + limit.identities=256 + limit.unfinished_durable_operations=1024 + limit.preference_value_utf8_bytes=4096 + limit.relay_endpoints=16 + limit.relay_url_bytes=2048 + limit.events_per_relay=64 + limit.events_total=1024 + limit.observers=32 + limit.actor_mailbox=64 + limit.command_deadline_min_ms=1 + limit.command_deadline_max_ms=30000 + backup.member_limit=caller_supplied_positive keyring.service=org.harvestcircle.desktop.nostr environment.prefix=HARVESTCIRCLE_ vendor.name=Radroots Labs @@ -578,7 +600,7 @@ class ConventionPluginSmokeTest { contract.major=4 contract.minor=3 contract.hash=b32b9a47d12e445e93866ae0ab668b18de503ba6c999e3a053f26dc9509ddaf9 - product.coordinate_digest=93bf10e334e989b20ba5fb8ed05e5d55b83f4502efba5f893aef4dc1a66c8223 + product.coordinate_digest=bf50f9ea6c2537406de255f025463e670eb6263c295f992f7e4c4db36d957064 snapshot.schema=1 storage.schema.minimum=5 storage.schema.current=10 diff --git a/config/product/harvestcircle-v1.properties b/config/product/harvestcircle-v1.properties @@ -11,10 +11,35 @@ desktop.main_class=org.harvestcircle.desktop.MainKt ffi.kotlin_package=org.harvestcircle.ffi ffi.cdylib_name=harvestcircle_ffi -database.qualifier=org -database.organization=harvestcircle -database.application=desktop -database.filename=harvestcircle.sqlite3 +storage.service_id=harvestcircle +storage.instance_id=desktop +storage.database_filename=state.sqlite +storage.lock_filename=state.lock +storage.application_id=1212371505 +storage.application_id_text=HCR1 +storage.initial_schema_version=1 + +legacy.database.qualifier=org +legacy.database.organization=harvestcircle +legacy.database.application=desktop +legacy.database.filename=harvestcircle.sqlite3 +legacy.database.disposition=untouched_and_unsupported + +platform.macos.architecture=aarch64 +platform.linux.architecture=x86_64 + +limit.identities=256 +limit.unfinished_durable_operations=1024 +limit.preference_value_utf8_bytes=4096 +limit.relay_endpoints=16 +limit.relay_url_bytes=2048 +limit.events_per_relay=64 +limit.events_total=1024 +limit.observers=32 +limit.actor_mailbox=64 +limit.command_deadline_min_ms=1 +limit.command_deadline_max_ms=30000 +backup.member_limit=caller_supplied_positive keyring.service=org.harvestcircle.desktop.nostr environment.prefix=HARVESTCIRCLE_ diff --git a/core/Cargo.lock b/core/Cargo.lock @@ -48,6 +48,12 @@ dependencies = [ ] [[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] name = "anstyle" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -292,6 +298,15 @@ dependencies = [ ] [[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] name = "atomic-destructor" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -633,6 +648,30 @@ dependencies = [ ] [[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crossbeam-queue" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +dependencies = [ + "crossbeam-utils", +] + +[[package]] name = "crossbeam-utils" version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -727,10 +766,19 @@ dependencies = [ ] [[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] name = "either" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +dependencies = [ + "serde", +] [[package]] name = "elliptic-curve" @@ -847,6 +895,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" [[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] name = "foldhash" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -888,6 +947,7 @@ checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -911,6 +971,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] name = "futures-io" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1153,6 +1235,8 @@ dependencies = [ "harvestcircle_product", "hmac", "keyring", + "radroots_runtime_paths", + "radroots_service_sqlite", "refinery", "rusqlite", "rustix", @@ -1189,6 +1273,8 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -1432,6 +1518,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] name = "js-sys" version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1512,6 +1604,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] name = "log" version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1524,6 +1625,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" [[package]] +name = "mediatype" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120fa187be19d9962f0926633453784691731018a2bf936ddb4e29101b79c4a7" + +[[package]] name = "memchr" version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1790,6 +1897,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] name = "password-hash" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1934,15 +2064,140 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] +name = "radroots_blossom" +version = "0.1.0-alpha" +source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +dependencies = [ + "mediatype", + "sha2", + "unicode-general-category", + "url", +] + +[[package]] +name = "radroots_core" +version = "0.1.0-alpha" +source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +dependencies = [ + "rust_decimal", + "serde", +] + +[[package]] +name = "radroots_event" +version = "0.1.0-alpha" +source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +dependencies = [ + "hex", + "jiff-tzdb", + "radroots_blossom", + "radroots_core", + "radroots_identity", + "radroots_protocol", + "serde", + "serde_json", + "sha2", + "unicode-general-category", + "url", +] + +[[package]] +name = "radroots_event_codec" +version = "0.1.0-alpha" +source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +dependencies = [ + "hex", + "radroots_blossom", + "radroots_core", + "radroots_event", + "radroots_identity", + "radroots_protocol", + "secp256k1", + "serde", + "serde_json", + "sha2", +] + +[[package]] name = "radroots_identity" version = "0.1.0-alpha" source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" dependencies = [ "k256", + "serde", "thiserror 2.0.20", ] [[package]] +name = "radroots_protocol" +version = "0.1.0-alpha" +source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +dependencies = [ + "serde", +] + +[[package]] +name = "radroots_runtime_paths" +version = "0.1.0-alpha" +source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +dependencies = [ + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "radroots_service_sqlite" +version = "0.1.0-alpha" +source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +dependencies = [ + "fs2", + "futures", + "libsqlite3-sys", + "radroots_runtime_paths", + "radroots_storage", + "rustix", + "serde", + "serde_json", + "sha2", + "sqlx", + "tokio", +] + +[[package]] +name = "radroots_storage" +version = "0.1.0-alpha" +source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +dependencies = [ + "radroots_event", + "radroots_event_codec", + "radroots_protocol", + "radroots_trade", + "radroots_transport", + "sha2", +] + +[[package]] +name = "radroots_trade" +version = "0.1.0-alpha" +source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +dependencies = [ + "radroots_core", + "radroots_event", + "radroots_identity", +] + +[[package]] +name = "radroots_transport" +version = "0.1.0-alpha" +source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +dependencies = [ + "radroots_event", + "radroots_identity", + "radroots_protocol", + "sha2", +] + +[[package]] name = "rand" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2002,6 +2257,15 @@ dependencies = [ ] [[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] name = "redox_users" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2122,6 +2386,18 @@ dependencies = [ ] [[package]] +name = "rust_decimal" +version = "1.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2a24f50780bc85f09cc6ac299bdf1424302742d77221106859c9d8b102126a" +dependencies = [ + "arrayvec", + "num-traits", + "serde", + "wasm-bindgen", +] + +[[package]] name = "rustc-demangle" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2205,6 +2481,12 @@ dependencies = [ ] [[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] name = "scroll" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2466,6 +2748,15 @@ dependencies = [ ] [[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] name = "sqlite-wasm-rs" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2478,6 +2769,110 @@ dependencies = [ ] [[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64", + "bytes", + "cfg-if", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink", + "indexmap", + "log", + "memchr", + "percent-encoding", + "serde", + "sha2", + "smallvec", + "thiserror 2.0.20", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.119", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-sqlite", + "syn 2.0.119", + "tokio", + "url", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +dependencies = [ + "atoi", + "flume", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "thiserror 2.0.20", + "tracing", + "url", +] + +[[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2703,6 +3098,17 @@ dependencies = [ ] [[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] name = "tokio-tungstenite" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2775,6 +3181,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2837,6 +3244,12 @@ dependencies = [ ] [[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + +[[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3071,6 +3484,7 @@ dependencies = [ "cfg-if", "once_cell", "rustversion", + "serde", "wasm-bindgen-macro", "wasm-bindgen-shared", ] diff --git a/core/Cargo.toml b/core/Cargo.toml @@ -44,6 +44,8 @@ harvestcircle_storage = { path = "crates/harvestcircle_storage", version = "=0.1 harvestcircle_test_bridge = { path = "crates/harvestcircle_test_bridge", version = "=0.1.0-alpha" } harvestcircle_uniffi_bindgen = { path = "crates/harvestcircle_uniffi_bindgen", version = "=0.1.0-alpha" } radroots_identity = { git = "https://github.com/radrootslabs/lib", rev = "be9db78e060ebc0000fa7827ac32efa3f6504f53", version = "=0.1.0-alpha", default-features = false } +radroots_runtime_paths = { git = "https://github.com/radrootslabs/lib", rev = "be9db78e060ebc0000fa7827ac32efa3f6504f53", version = "=0.1.0-alpha", default-features = false } +radroots_service_sqlite = { git = "https://github.com/radrootslabs/lib", rev = "be9db78e060ebc0000fa7827ac32efa3f6504f53", version = "=0.1.0-alpha", default-features = false } getrandom = { version = "0.2", default-features = false } hmac = { version = "0.12", default-features = false } quote = { version = "1" } diff --git a/core/compatibility/harvestcircle-ffi-v4.properties b/core/compatibility/harvestcircle-ffi-v4.properties @@ -3,7 +3,7 @@ contract.id=harvestcircle-desktop-ffi-v4 contract.major=4 contract.minor=3 contract.hash=45e62243f3ce91b400fe7a3735ad6ad0e3f92b7a93673555fb4e2c18ba99f635 -product.coordinate_digest=93bf10e334e989b20ba5fb8ed05e5d55b83f4502efba5f893aef4dc1a66c8223 +product.coordinate_digest=bf50f9ea6c2537406de255f025463e670eb6263c295f992f7e4c4db36d957064 snapshot.schema=1 storage.schema.minimum=5 storage.schema.current=10 diff --git a/core/compatibility/harvestcircle-storage-api-v1.txt b/core/compatibility/harvestcircle-storage-api-v1.txt @@ -0,0 +1,175 @@ +pub mod harvestcircle_storage +pub mod harvestcircle_storage::db +pub struct harvestcircle_storage::db::Database +impl harvestcircle_storage::db::Database +pub fn harvestcircle_storage::db::Database::authenticate_repair_candidate(&std::path::Path, &harvestcircle_storage::RepairAuthorization) -> core::result::Result<harvestcircle_storage::RepairCandidate, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::export_quarantined(&std::path::Path, &std::path::Path, &harvestcircle_storage::RepairAuthorization) -> core::result::Result<harvestcircle_storage::QuarantineExportReceipt, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::in_memory() -> core::result::Result<Self, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::install_repair_candidate(&std::path::Path, &harvestcircle_storage::RepairCandidate, &harvestcircle_storage::RepairAuthorization) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::open(&std::path::Path) -> core::result::Result<Self, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::preflight(&std::path::Path) -> core::result::Result<harvestcircle_storage::DatabasePreflight, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::restore_migration_backup(&std::path::Path, u32) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::schema_version(&self) -> core::result::Result<u32, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::verify_migration_backup(&std::path::Path, u32) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +impl harvestcircle_storage::db::Database +pub fn harvestcircle_storage::db::Database::initialize_installation_id(&self, &str) -> core::result::Result<alloc::string::String, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::load_installation_id(&self) -> core::result::Result<core::option::Option<alloc::string::String>, harvestcircle_domain::error::SafeError> +impl harvestcircle_application::ports::AppStateRepository for harvestcircle_storage::db::Database +pub fn harvestcircle_storage::db::Database::load_selected_identity(&self) -> core::result::Result<core::option::Option<harvestcircle_domain::key::PublicKey>, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::save_selected_identity(&self, core::option::Option<harvestcircle_domain::key::PublicKey>) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +impl harvestcircle_application::ports::DurableOperationRepository for harvestcircle_storage::db::Database +pub fn harvestcircle_storage::db::Database::advance_durable_operation(&self, &harvestcircle_application::ports::DurableRequestId, harvestcircle_application::ports::DurableOperationPhase, harvestcircle_application::ports::DurableOperationPhase, harvestcircle_domain::time::UnixTimestamp, core::option::Option<harvestcircle_application::ports::OperationDiagnostic>) -> core::result::Result<harvestcircle_application::ports::DurableIdentityOperation, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::begin_durable_operation(&self, &harvestcircle_application::ports::DurableRequestId, harvestcircle_application::ports::DurableOperationKind, harvestcircle_domain::key::PublicKey, core::option::Option<u64>, harvestcircle_application::ports::OperationPriorState, harvestcircle_domain::time::UnixTimestamp) -> core::result::Result<harvestcircle_application::ports::DurableOperationStart, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::finalize_durable_operation(&self, &harvestcircle_application::ports::DurableRequestId, harvestcircle_application::ports::DurableOperationPhase, harvestcircle_application::ports::DurableTerminalOutcome, core::option::Option<u64>, harvestcircle_domain::time::UnixTimestamp) -> core::result::Result<harvestcircle_application::ports::DurableOperationReceipt, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::list_unfinished_durable_operations(&self) -> core::result::Result<alloc::vec::Vec<harvestcircle_application::ports::DurableIdentityOperation>, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::load_durable_operation(&self, &harvestcircle_application::ports::DurableRequestId) -> core::result::Result<core::option::Option<harvestcircle_application::ports::DurableIdentityOperation>, harvestcircle_domain::error::SafeError> +impl harvestcircle_application::ports::IdentityNamespaceRepository for harvestcircle_storage::db::Database +pub fn harvestcircle_storage::db::Database::clear_owner(&self, harvestcircle_domain::key::PublicKey) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::get_value(&self, harvestcircle_domain::key::PublicKey, harvestcircle_application::ports::IdentityPreferenceKey) -> core::result::Result<core::option::Option<alloc::string::String>, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::set_value(&self, harvestcircle_domain::key::PublicKey, harvestcircle_application::ports::IdentityPreferenceKey, &str) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +impl harvestcircle_application::ports::IdentityRepository for harvestcircle_storage::db::Database +pub fn harvestcircle_storage::db::Database::find_identity(&self, harvestcircle_domain::key::PublicKey) -> core::result::Result<core::option::Option<harvestcircle_domain::identity::NostrIdentity>, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::insert_identity(&self, &harvestcircle_domain::identity::NostrIdentity) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::list_identities(&self) -> core::result::Result<alloc::vec::Vec<harvestcircle_domain::identity::NostrIdentity>, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::remove_identity(&self, harvestcircle_domain::key::PublicKey) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::update_identity(&self, &harvestcircle_domain::identity::NostrIdentity) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +impl harvestcircle_application::ports::OperationJournal for harvestcircle_storage::db::Database +pub fn harvestcircle_storage::db::Database::begin_operation(&self, harvestcircle_application::ports::IdentityOperationKind, harvestcircle_domain::key::PublicKey, harvestcircle_domain::time::UnixTimestamp) -> core::result::Result<harvestcircle_application::ports::OperationId, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::finalize_operation(&self, harvestcircle_application::ports::OperationId) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::list_pending_operations(&self) -> core::result::Result<alloc::vec::Vec<harvestcircle_application::ports::PendingIdentityOperation>, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::update_operation(&self, harvestcircle_application::ports::OperationId, harvestcircle_application::ports::IdentityOperationPhase, harvestcircle_domain::time::UnixTimestamp, core::option::Option<harvestcircle_application::ports::OperationDiagnostic>) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +impl harvestcircle_application::ports::ProfileRepository for harvestcircle_storage::db::Database +pub fn harvestcircle_storage::db::Database::load_profile(&self, harvestcircle_domain::key::PublicKey) -> core::result::Result<core::option::Option<harvestcircle_application::ports::CachedProfile>, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::record_refresh_status(&self, harvestcircle_domain::key::PublicKey, harvestcircle_domain::time::UnixTimestamp, harvestcircle_application::ports::ProfileRefreshStatus) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::remove_profile(&self, harvestcircle_domain::key::PublicKey) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::save_profile(&self, &harvestcircle_application::ports::CachedProfile) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub const harvestcircle_storage::db::CURRENT_SCHEMA_VERSION: u32 +pub mod harvestcircle_storage::identities +pub mod harvestcircle_storage::identity_namespace +pub mod harvestcircle_storage::journal +pub mod harvestcircle_storage::os_keyring +pub struct harvestcircle_storage::os_keyring::OsKeyringSecretStore +impl harvestcircle_application::secrets::SecretStore for harvestcircle_storage::os_keyring::OsKeyringSecretStore +pub fn harvestcircle_storage::os_keyring::OsKeyringSecretStore::contains(&self, harvestcircle_domain::key::PublicKey) -> core::result::Result<bool, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::os_keyring::OsKeyringSecretStore::delete(&self, harvestcircle_domain::key::PublicKey) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::os_keyring::OsKeyringSecretStore::load(&self, harvestcircle_domain::key::PublicKey) -> core::result::Result<harvestcircle_domain::key::SecretKeyInput, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::os_keyring::OsKeyringSecretStore::put(&self, harvestcircle_domain::key::PublicKey, harvestcircle_domain::key::SecretKeyInput) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub const harvestcircle_storage::os_keyring::CREDENTIAL_SERVICE: &str +pub mod harvestcircle_storage::profiles +pub enum harvestcircle_storage::DatabasePreflight +pub harvestcircle_storage::DatabasePreflight::Fresh +pub harvestcircle_storage::DatabasePreflight::Quarantined +pub harvestcircle_storage::DatabasePreflight::Quarantined::issues: alloc::vec::Vec<harvestcircle_storage::PersistedIdentityIssue> +pub harvestcircle_storage::DatabasePreflight::Quarantined::schema_version: u32 +pub harvestcircle_storage::DatabasePreflight::Ready +pub harvestcircle_storage::DatabasePreflight::Ready::schema_version: u32 +pub enum harvestcircle_storage::HarvestCircleStorageContractError +pub harvestcircle_storage::HarvestCircleStorageContractError::CanonicalPaths +pub harvestcircle_storage::HarvestCircleStorageContractError::ContextIdentity +pub harvestcircle_storage::HarvestCircleStorageContractError::MigrationCatalog +pub harvestcircle_storage::HarvestCircleStorageContractError::SchemaCatalog +impl core::error::Error for harvestcircle_storage::HarvestCircleStorageContractError +impl core::fmt::Display for harvestcircle_storage::HarvestCircleStorageContractError +pub fn harvestcircle_storage::HarvestCircleStorageContractError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub enum harvestcircle_storage::PersistedIdentityIssueKind +pub harvestcircle_storage::PersistedIdentityIssueKind::DisplayIdentityMismatch +pub harvestcircle_storage::PersistedIdentityIssueKind::InvalidCurvePoint +pub harvestcircle_storage::PersistedIdentityIssueKind::MalformedEncoding +pub harvestcircle_storage::PersistedIdentityIssueKind::NonCanonicalEncoding +pub struct harvestcircle_storage::Database +impl harvestcircle_storage::db::Database +pub fn harvestcircle_storage::db::Database::authenticate_repair_candidate(&std::path::Path, &harvestcircle_storage::RepairAuthorization) -> core::result::Result<harvestcircle_storage::RepairCandidate, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::export_quarantined(&std::path::Path, &std::path::Path, &harvestcircle_storage::RepairAuthorization) -> core::result::Result<harvestcircle_storage::QuarantineExportReceipt, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::in_memory() -> core::result::Result<Self, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::install_repair_candidate(&std::path::Path, &harvestcircle_storage::RepairCandidate, &harvestcircle_storage::RepairAuthorization) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::open(&std::path::Path) -> core::result::Result<Self, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::preflight(&std::path::Path) -> core::result::Result<harvestcircle_storage::DatabasePreflight, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::restore_migration_backup(&std::path::Path, u32) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::schema_version(&self) -> core::result::Result<u32, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::verify_migration_backup(&std::path::Path, u32) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +impl harvestcircle_storage::db::Database +pub fn harvestcircle_storage::db::Database::initialize_installation_id(&self, &str) -> core::result::Result<alloc::string::String, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::load_installation_id(&self) -> core::result::Result<core::option::Option<alloc::string::String>, harvestcircle_domain::error::SafeError> +impl harvestcircle_application::ports::AppStateRepository for harvestcircle_storage::db::Database +pub fn harvestcircle_storage::db::Database::load_selected_identity(&self) -> core::result::Result<core::option::Option<harvestcircle_domain::key::PublicKey>, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::save_selected_identity(&self, core::option::Option<harvestcircle_domain::key::PublicKey>) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +impl harvestcircle_application::ports::DurableOperationRepository for harvestcircle_storage::db::Database +pub fn harvestcircle_storage::db::Database::advance_durable_operation(&self, &harvestcircle_application::ports::DurableRequestId, harvestcircle_application::ports::DurableOperationPhase, harvestcircle_application::ports::DurableOperationPhase, harvestcircle_domain::time::UnixTimestamp, core::option::Option<harvestcircle_application::ports::OperationDiagnostic>) -> core::result::Result<harvestcircle_application::ports::DurableIdentityOperation, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::begin_durable_operation(&self, &harvestcircle_application::ports::DurableRequestId, harvestcircle_application::ports::DurableOperationKind, harvestcircle_domain::key::PublicKey, core::option::Option<u64>, harvestcircle_application::ports::OperationPriorState, harvestcircle_domain::time::UnixTimestamp) -> core::result::Result<harvestcircle_application::ports::DurableOperationStart, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::finalize_durable_operation(&self, &harvestcircle_application::ports::DurableRequestId, harvestcircle_application::ports::DurableOperationPhase, harvestcircle_application::ports::DurableTerminalOutcome, core::option::Option<u64>, harvestcircle_domain::time::UnixTimestamp) -> core::result::Result<harvestcircle_application::ports::DurableOperationReceipt, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::list_unfinished_durable_operations(&self) -> core::result::Result<alloc::vec::Vec<harvestcircle_application::ports::DurableIdentityOperation>, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::load_durable_operation(&self, &harvestcircle_application::ports::DurableRequestId) -> core::result::Result<core::option::Option<harvestcircle_application::ports::DurableIdentityOperation>, harvestcircle_domain::error::SafeError> +impl harvestcircle_application::ports::IdentityNamespaceRepository for harvestcircle_storage::db::Database +pub fn harvestcircle_storage::db::Database::clear_owner(&self, harvestcircle_domain::key::PublicKey) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::get_value(&self, harvestcircle_domain::key::PublicKey, harvestcircle_application::ports::IdentityPreferenceKey) -> core::result::Result<core::option::Option<alloc::string::String>, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::set_value(&self, harvestcircle_domain::key::PublicKey, harvestcircle_application::ports::IdentityPreferenceKey, &str) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +impl harvestcircle_application::ports::IdentityRepository for harvestcircle_storage::db::Database +pub fn harvestcircle_storage::db::Database::find_identity(&self, harvestcircle_domain::key::PublicKey) -> core::result::Result<core::option::Option<harvestcircle_domain::identity::NostrIdentity>, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::insert_identity(&self, &harvestcircle_domain::identity::NostrIdentity) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::list_identities(&self) -> core::result::Result<alloc::vec::Vec<harvestcircle_domain::identity::NostrIdentity>, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::remove_identity(&self, harvestcircle_domain::key::PublicKey) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::update_identity(&self, &harvestcircle_domain::identity::NostrIdentity) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +impl harvestcircle_application::ports::OperationJournal for harvestcircle_storage::db::Database +pub fn harvestcircle_storage::db::Database::begin_operation(&self, harvestcircle_application::ports::IdentityOperationKind, harvestcircle_domain::key::PublicKey, harvestcircle_domain::time::UnixTimestamp) -> core::result::Result<harvestcircle_application::ports::OperationId, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::finalize_operation(&self, harvestcircle_application::ports::OperationId) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::list_pending_operations(&self) -> core::result::Result<alloc::vec::Vec<harvestcircle_application::ports::PendingIdentityOperation>, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::update_operation(&self, harvestcircle_application::ports::OperationId, harvestcircle_application::ports::IdentityOperationPhase, harvestcircle_domain::time::UnixTimestamp, core::option::Option<harvestcircle_application::ports::OperationDiagnostic>) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +impl harvestcircle_application::ports::ProfileRepository for harvestcircle_storage::db::Database +pub fn harvestcircle_storage::db::Database::load_profile(&self, harvestcircle_domain::key::PublicKey) -> core::result::Result<core::option::Option<harvestcircle_application::ports::CachedProfile>, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::record_refresh_status(&self, harvestcircle_domain::key::PublicKey, harvestcircle_domain::time::UnixTimestamp, harvestcircle_application::ports::ProfileRefreshStatus) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::remove_profile(&self, harvestcircle_domain::key::PublicKey) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::db::Database::save_profile(&self, &harvestcircle_application::ports::CachedProfile) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub struct harvestcircle_storage::HarvestCircleStorageContract +impl harvestcircle_storage::HarvestCircleStorageContract +pub fn harvestcircle_storage::HarvestCircleStorageContract::application_id(&self) -> radroots_service_sqlite::metadata::ServiceSqliteApplicationId +pub fn harvestcircle_storage::HarvestCircleStorageContract::from_runtime_context(&radroots_runtime_paths::context::RuntimeContext) -> core::result::Result<Self, harvestcircle_storage::HarvestCircleStorageContractError> +pub const fn harvestcircle_storage::HarvestCircleStorageContract::migrations(&self) -> &radroots_service_sqlite::migration::MigrationCatalog +pub const fn harvestcircle_storage::HarvestCircleStorageContract::paths(&self) -> &radroots_service_sqlite::open::ServiceSqlitePaths +pub const fn harvestcircle_storage::HarvestCircleStorageContract::schema(&self) -> &radroots_service_sqlite::integrity::catalog::SchemaCatalog +pub const fn harvestcircle_storage::HarvestCircleStorageContract::state_schema_version(&self) -> core::num::nonzero::NonZeroU32 +impl core::fmt::Debug for harvestcircle_storage::HarvestCircleStorageContract +pub fn harvestcircle_storage::HarvestCircleStorageContract::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct harvestcircle_storage::OsKeyringSecretStore +impl harvestcircle_application::secrets::SecretStore for harvestcircle_storage::os_keyring::OsKeyringSecretStore +pub fn harvestcircle_storage::os_keyring::OsKeyringSecretStore::contains(&self, harvestcircle_domain::key::PublicKey) -> core::result::Result<bool, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::os_keyring::OsKeyringSecretStore::delete(&self, harvestcircle_domain::key::PublicKey) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::os_keyring::OsKeyringSecretStore::load(&self, harvestcircle_domain::key::PublicKey) -> core::result::Result<harvestcircle_domain::key::SecretKeyInput, harvestcircle_domain::error::SafeError> +pub fn harvestcircle_storage::os_keyring::OsKeyringSecretStore::put(&self, harvestcircle_domain::key::PublicKey, harvestcircle_domain::key::SecretKeyInput) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub struct harvestcircle_storage::PersistedIdentityIssue +impl harvestcircle_storage::PersistedIdentityIssue +pub const fn harvestcircle_storage::PersistedIdentityIssue::column(&self) -> &'static str +pub const fn harvestcircle_storage::PersistedIdentityIssue::fingerprint(&self) -> &[u8; 32] +pub const fn harvestcircle_storage::PersistedIdentityIssue::kind(&self) -> harvestcircle_storage::PersistedIdentityIssueKind +pub const fn harvestcircle_storage::PersistedIdentityIssue::row_id(&self) -> i64 +pub const fn harvestcircle_storage::PersistedIdentityIssue::table(&self) -> &'static str +pub struct harvestcircle_storage::QuarantineExportReceipt +impl harvestcircle_storage::QuarantineExportReceipt +pub fn harvestcircle_storage::QuarantineExportReceipt::authentication_tag(&self) -> &str +pub fn harvestcircle_storage::QuarantineExportReceipt::path(&self) -> &std::path::Path +pub fn harvestcircle_storage::QuarantineExportReceipt::sha256(&self) -> &str +pub struct harvestcircle_storage::RepairAuthorization(_) +impl harvestcircle_storage::RepairAuthorization +pub fn harvestcircle_storage::RepairAuthorization::from_bytes(alloc::vec::Vec<u8>) -> core::result::Result<Self, harvestcircle_domain::error::SafeError> +pub struct harvestcircle_storage::RepairCandidate +impl harvestcircle_storage::RepairCandidate +pub fn harvestcircle_storage::RepairCandidate::path(&self) -> &std::path::Path +pub const harvestcircle_storage::CREDENTIAL_SERVICE: &str +pub const harvestcircle_storage::CURRENT_SCHEMA_VERSION: u32 +pub const harvestcircle_storage::HARVESTCIRCLE_ACTOR_MAILBOX_CAPACITY: usize +pub const harvestcircle_storage::HARVESTCIRCLE_APPLICATION_ID: u32 +pub const harvestcircle_storage::HARVESTCIRCLE_COMMAND_DEADLINE_MAX_MS: u64 +pub const harvestcircle_storage::HARVESTCIRCLE_COMMAND_DEADLINE_MIN_MS: u64 +pub const harvestcircle_storage::HARVESTCIRCLE_EVENTS_PER_RELAY_CAPACITY: usize +pub const harvestcircle_storage::HARVESTCIRCLE_EVENTS_TOTAL_CAPACITY: usize +pub const harvestcircle_storage::HARVESTCIRCLE_IDENTITY_CAPACITY: usize +pub const harvestcircle_storage::HARVESTCIRCLE_INSTANCE_ID: &str +pub const harvestcircle_storage::HARVESTCIRCLE_OBSERVER_CAPACITY: usize +pub const harvestcircle_storage::HARVESTCIRCLE_PREFERENCE_VALUE_UTF8_BYTES: usize +pub const harvestcircle_storage::HARVESTCIRCLE_RELAY_ENDPOINT_CAPACITY: usize +pub const harvestcircle_storage::HARVESTCIRCLE_RELAY_URL_UTF8_BYTES: usize +pub const harvestcircle_storage::HARVESTCIRCLE_SERVICE_ID: &str +pub const harvestcircle_storage::HARVESTCIRCLE_STATE_SCHEMA_VERSION: u32 +pub const harvestcircle_storage::HARVESTCIRCLE_UNFINISHED_DURABLE_OPERATION_CAPACITY: usize +pub const fn harvestcircle_storage::harvestcircle_initial_schema_sql() -> &'static [&'static str] +pub fn harvestcircle_storage::harvestcircle_migration_catalog() -> core::result::Result<radroots_service_sqlite::migration::MigrationCatalog, harvestcircle_storage::HarvestCircleStorageContractError> +pub fn harvestcircle_storage::harvestcircle_schema_catalog() -> core::result::Result<radroots_service_sqlite::integrity::catalog::SchemaCatalog, harvestcircle_storage::HarvestCircleStorageContractError> diff --git a/core/crates/harvestcircle_ffi/src/commands.rs b/core/crates/harvestcircle_ffi/src/commands.rs @@ -16,7 +16,8 @@ use harvestcircle_domain::{ }; use harvestcircle_nostr::SdkNostrClient; use harvestcircle_product::{ - DATABASE_APPLICATION, DATABASE_FILENAME, DATABASE_ORGANIZATION, DATABASE_QUALIFIER, + LEGACY_DATABASE_APPLICATION, LEGACY_DATABASE_FILENAME, LEGACY_DATABASE_ORGANIZATION, + LEGACY_DATABASE_QUALIFIER, }; use harvestcircle_runtime::{ RuntimeActorHandle, RuntimeDependencies, UuidInstallationIdentitySource, @@ -697,14 +698,14 @@ fn application_database_path(input: &RuntimeOpenInputDto) -> Result<PathBuf, Har if canonical != directory { return Err(path_unavailable()); } - return Ok(canonical.join(DATABASE_FILENAME)); + return Ok(canonical.join(LEGACY_DATABASE_FILENAME)); } ProjectDirs::from( - DATABASE_QUALIFIER, - DATABASE_ORGANIZATION, - DATABASE_APPLICATION, + LEGACY_DATABASE_QUALIFIER, + LEGACY_DATABASE_ORGANIZATION, + LEGACY_DATABASE_APPLICATION, ) - .map(|project| project.data_dir().join(DATABASE_FILENAME)) + .map(|project| project.data_dir().join(LEGACY_DATABASE_FILENAME)) .ok_or_else(path_unavailable) } @@ -832,15 +833,15 @@ mod tests { use harvestcircle_storage::{CREDENTIAL_SERVICE, CURRENT_SCHEMA_VERSION}; use super::{ - ACTOR_MAILBOX_CAPACITY, CompatibilityExpectation, DATABASE_APPLICATION, DATABASE_FILENAME, - DATABASE_ORGANIZATION, DATABASE_QUALIFIER, FFI_CONTRACT_HASH, FFI_CONTRACT_ID, + ACTOR_MAILBOX_CAPACITY, CompatibilityExpectation, FFI_CONTRACT_HASH, FFI_CONTRACT_ID, FFI_CONTRACT_MAJOR, FFI_CONTRACT_MINOR, HarvestCircleAppCore, HarvestCircleError, - PRODUCT_COORDINATE_DIGEST, ProjectDirs, RelayBootstrapInputDto, RequestContextDto, - RuntimeCore, RuntimeOpenInputDto, SNAPSHOT_SCHEMA_VERSION, SystemClock, WireErrorCategory, - WireErrorCode, WireRecoveryAction, actor_mailbox_capacity, application_database_path, - compatibility_descriptor, confirmation_expired, generated_commit_failed, - local_first_relay_configuration, path_unavailable, runtime, runtime_unavailable, - verify_compatibility, + LEGACY_DATABASE_APPLICATION, LEGACY_DATABASE_FILENAME, LEGACY_DATABASE_ORGANIZATION, + LEGACY_DATABASE_QUALIFIER, PRODUCT_COORDINATE_DIGEST, ProjectDirs, RelayBootstrapInputDto, + RequestContextDto, RuntimeCore, RuntimeOpenInputDto, SNAPSHOT_SCHEMA_VERSION, SystemClock, + WireErrorCategory, WireErrorCode, WireRecoveryAction, actor_mailbox_capacity, + application_database_path, compatibility_descriptor, confirmation_expired, + generated_commit_failed, local_first_relay_configuration, path_unavailable, runtime, + runtime_unavailable, verify_compatibility, }; async fn in_memory_core() -> Arc<HarvestCircleAppCore> { @@ -1197,16 +1198,16 @@ mod tests { #[test] fn final_product_coordinates_do_not_adopt_the_temporary_namespace() { - assert_eq!(DATABASE_QUALIFIER, "org"); - assert_eq!(DATABASE_ORGANIZATION, "harvestcircle"); - assert_eq!(DATABASE_APPLICATION, "desktop"); - assert_eq!(DATABASE_FILENAME, "harvestcircle.sqlite3"); + assert_eq!(LEGACY_DATABASE_QUALIFIER, "org"); + assert_eq!(LEGACY_DATABASE_ORGANIZATION, "harvestcircle"); + assert_eq!(LEGACY_DATABASE_APPLICATION, "desktop"); + assert_eq!(LEGACY_DATABASE_FILENAME, "harvestcircle.sqlite3"); assert_eq!(CREDENTIAL_SERVICE, "org.harvestcircle.desktop.nostr"); let current = ProjectDirs::from( - DATABASE_QUALIFIER, - DATABASE_ORGANIZATION, - DATABASE_APPLICATION, + LEGACY_DATABASE_QUALIFIER, + LEGACY_DATABASE_ORGANIZATION, + LEGACY_DATABASE_APPLICATION, ) .expect("current product coordinates"); let temporary = @@ -1232,7 +1233,7 @@ mod tests { }; assert_eq!( application_database_path(&explicit).expect("explicit path"), - canonical.join(DATABASE_FILENAME), + canonical.join(LEGACY_DATABASE_FILENAME), ); for rejected in [ diff --git a/core/crates/harvestcircle_product/src/lib.rs b/core/crates/harvestcircle_product/src/lib.rs @@ -44,8 +44,23 @@ mod tests { ); assert!( parse(&source.replace( - "database.filename=harvestcircle.sqlite3", - "database.filename=../other.sqlite3" + "storage.database_filename=state.sqlite", + "storage.database_filename=other.sqlite" + )) + .is_err() + ); + assert!( + parse(&source.replace( + "legacy.database.filename=harvestcircle.sqlite3", + "legacy.database.filename=../other.sqlite3" + )) + .is_err() + ); + assert!(parse(&source.replace("limit.identities=256", "limit.identities=257")).is_err()); + assert!( + parse(&source.replace( + "platform.linux.architecture=x86_64", + "platform.linux.architecture=aarch64" )) .is_err() ); @@ -53,7 +68,7 @@ mod tests { } #[test] - fn every_coordinate_value_is_manifest_owned_and_generated() { + fn mutable_presentation_coordinates_are_manifest_owned_and_generated() { let source = include_str!("../../../../config/product/harvestcircle-v1.properties"); let mutations = [ ("product.name", "Harvest Circle Test"), @@ -64,16 +79,11 @@ mod tests { ("desktop.main_class", "org.example.MainKt"), ("ffi.kotlin_package", "org.example.ffi"), ("ffi.cdylib_name", "example_ffi"), - ("database.qualifier", "com"), - ("database.organization", "example"), - ("database.application", "test"), - ("database.filename", "example.sqlite3"), ("keyring.service", "org.example.desktop.nostr"), ("environment.prefix", "EXAMPLE_"), ("vendor.name", "Example Cooperative"), ("copyright.notice", "Copyright Example contributors"), ]; - assert_eq!(mutations.len() + 1, REQUIRED_KEYS.len()); for (key, replacement) in mutations { let original = parse(source).unwrap().remove(key).unwrap(); let mutated = source.replace( @@ -89,6 +99,7 @@ mod tests { .contains(&format!("= {replacement:?};")) ); } + assert!(REQUIRED_KEYS.len() > mutations.len()); } #[test] @@ -97,7 +108,7 @@ mod tests { let expected = digest(source).expect("canonical product digest"); assert_eq!( expected, - "93bf10e334e989b20ba5fb8ed05e5d55b83f4502efba5f893aef4dc1a66c8223" + "bf50f9ea6c2537406de255f025463e670eb6263c295f992f7e4c4db36d957064" ); assert_eq!(digest(&source.replace('\n', "\r\n")).unwrap(), expected); assert_eq!( diff --git a/core/crates/harvestcircle_product/src/parser.rs b/core/crates/harvestcircle_product/src/parser.rs @@ -13,10 +13,32 @@ pub const REQUIRED_KEYS: &[&str] = &[ "desktop.main_class", "ffi.kotlin_package", "ffi.cdylib_name", - "database.qualifier", - "database.organization", - "database.application", - "database.filename", + "storage.service_id", + "storage.instance_id", + "storage.database_filename", + "storage.lock_filename", + "storage.application_id", + "storage.application_id_text", + "storage.initial_schema_version", + "legacy.database.qualifier", + "legacy.database.organization", + "legacy.database.application", + "legacy.database.filename", + "legacy.database.disposition", + "platform.macos.architecture", + "platform.linux.architecture", + "limit.identities", + "limit.unfinished_durable_operations", + "limit.preference_value_utf8_bytes", + "limit.relay_endpoints", + "limit.relay_url_bytes", + "limit.events_per_relay", + "limit.events_total", + "limit.observers", + "limit.actor_mailbox", + "limit.command_deadline_min_ms", + "limit.command_deadline_max_ms", + "backup.member_limit", "keyring.service", "environment.prefix", "vendor.name", @@ -111,24 +133,39 @@ fn validate_coordinate(key: &str, value: &str) -> Result<(), String> { let valid = match key { "schema" => value == SCHEMA, "product.name" | "vendor.name" | "copyright.notice" => value.len() <= 160, - "product.slug" - | "ffi.cdylib_name" - | "database.qualifier" - | "database.organization" - | "database.application" => is_lower_identifier(value), + "product.slug" | "ffi.cdylib_name" => is_lower_identifier(value), "kotlin.root_namespace" | "desktop.application_id" | "desktop.bundle_id" | "desktop.main_class" | "ffi.kotlin_package" | "keyring.service" => is_dotted_identifier(value), - "database.filename" => { - value.ends_with(".sqlite3") - && !value.contains("..") - && value - .bytes() - .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-')) - } + "storage.service_id" => value == "harvestcircle", + "storage.instance_id" => value == "desktop", + "storage.database_filename" => value == "state.sqlite", + "storage.lock_filename" => value == "state.lock", + "storage.application_id" => value == "1212371505", + "storage.application_id_text" => value == "HCR1", + "storage.initial_schema_version" => value == "1", + "legacy.database.qualifier" => value == "org", + "legacy.database.organization" => value == "harvestcircle", + "legacy.database.application" => value == "desktop", + "legacy.database.filename" => value == "harvestcircle.sqlite3", + "legacy.database.disposition" => value == "untouched_and_unsupported", + "platform.macos.architecture" => value == "aarch64", + "platform.linux.architecture" => value == "x86_64", + "limit.identities" => value == "256", + "limit.unfinished_durable_operations" => value == "1024", + "limit.preference_value_utf8_bytes" => value == "4096", + "limit.relay_endpoints" => value == "16", + "limit.relay_url_bytes" => value == "2048", + "limit.events_per_relay" => value == "64", + "limit.events_total" => value == "1024", + "limit.observers" => value == "32", + "limit.actor_mailbox" => value == "64", + "limit.command_deadline_min_ms" => value == "1", + "limit.command_deadline_max_ms" => value == "30000", + "backup.member_limit" => value == "caller_supplied_positive", "environment.prefix" => { value .bytes() diff --git a/core/crates/harvestcircle_storage/Cargo.toml b/core/crates/harvestcircle_storage/Cargo.toml @@ -17,6 +17,8 @@ keyring = "=4.1.6" harvestcircle_application.workspace = true harvestcircle_domain.workspace = true harvestcircle_product.workspace = true +radroots_runtime_paths.workspace = true +radroots_service_sqlite.workspace = true refinery = { version = "=0.9.2", default-features = false, features = ["rusqlite"] } getrandom.workspace = true hmac.workspace = true diff --git a/core/crates/harvestcircle_storage/src/contract.rs b/core/crates/harvestcircle_storage/src/contract.rs @@ -0,0 +1,609 @@ +//! Sealed HarvestCircle service-state identity and schema contract. + +use core::{fmt, num::NonZeroU32}; +use std::error::Error; + +use radroots_runtime_paths::RuntimeContext; +use radroots_service_sqlite::{ + MigrationCatalog, MigrationDescriptor, SchemaCatalog, SchemaCatalogContractError, SchemaDigest, + SchemaObject, SchemaObjectKind, SchemaVersionCatalog, ServiceSqliteApplicationId, + ServiceSqlitePaths, +}; + +pub const HARVESTCIRCLE_SERVICE_ID: &str = "harvestcircle"; +pub const HARVESTCIRCLE_INSTANCE_ID: &str = "desktop"; +pub const HARVESTCIRCLE_APPLICATION_ID: u32 = 0x4843_5231; +pub const HARVESTCIRCLE_STATE_SCHEMA_VERSION: u32 = 1; +pub const HARVESTCIRCLE_IDENTITY_CAPACITY: usize = 256; +pub const HARVESTCIRCLE_UNFINISHED_DURABLE_OPERATION_CAPACITY: usize = 1_024; +pub const HARVESTCIRCLE_PREFERENCE_VALUE_UTF8_BYTES: usize = 4_096; +pub const HARVESTCIRCLE_RELAY_ENDPOINT_CAPACITY: usize = 16; +pub const HARVESTCIRCLE_RELAY_URL_UTF8_BYTES: usize = 2_048; +pub const HARVESTCIRCLE_EVENTS_PER_RELAY_CAPACITY: usize = 64; +pub const HARVESTCIRCLE_EVENTS_TOTAL_CAPACITY: usize = 1_024; +pub const HARVESTCIRCLE_OBSERVER_CAPACITY: usize = 32; +pub const HARVESTCIRCLE_ACTOR_MAILBOX_CAPACITY: usize = 64; +pub const HARVESTCIRCLE_COMMAND_DEADLINE_MIN_MS: u64 = 1; +pub const HARVESTCIRCLE_COMMAND_DEADLINE_MAX_MS: u64 = 30_000; + +pub(crate) const CREATE_ACCOUNT_IDENTITIES_SQL: &str = r#"CREATE TABLE account_identities ( + public_key BLOB NOT NULL PRIMARY KEY CHECK (length(public_key) = 32), + npub TEXT NOT NULL UNIQUE CHECK (length(CAST(npub AS BLOB)) = 63), + label TEXT CHECK (label IS NULL OR length(CAST(label AS BLOB)) BETWEEN 1 AND 80), + created_at_unix_s INTEGER NOT NULL CHECK (created_at_unix_s >= 0), + last_used_at_unix_s INTEGER CHECK (last_used_at_unix_s IS NULL OR last_used_at_unix_s >= 0) +) STRICT"#; + +pub(crate) const CREATE_LOCAL_SIGNER_BINDINGS_SQL: &str = r#"CREATE TABLE local_signer_bindings ( + account_public_key BLOB NOT NULL, + binding_public_key BLOB NOT NULL, + binding_kind TEXT NOT NULL CHECK (binding_kind = 'local_secret'), + availability TEXT NOT NULL CHECK ( + availability IN ('available', 'credential_missing', 'store_unavailable') + ), + PRIMARY KEY (account_public_key, binding_public_key), + UNIQUE (account_public_key, binding_kind), + FOREIGN KEY (account_public_key) REFERENCES account_identities(public_key) ON DELETE CASCADE, + CHECK (length(account_public_key) = 32), + CHECK (length(binding_public_key) = 32), + CHECK (account_public_key = binding_public_key) +) STRICT"#; + +pub(crate) const CREATE_RUNTIME_STATE_SQL: &str = r#"CREATE TABLE runtime_state ( + singleton INTEGER NOT NULL PRIMARY KEY CHECK (singleton = 1), + selected_public_key BLOB REFERENCES account_identities(public_key) ON DELETE SET NULL, + active_account_public_key BLOB, + active_binding_public_key BLOB, + session_generation INTEGER NOT NULL DEFAULT 0 CHECK (session_generation >= 0), + FOREIGN KEY (active_account_public_key, active_binding_public_key) + REFERENCES local_signer_bindings(account_public_key, binding_public_key) + ON DELETE SET NULL, + CHECK (selected_public_key IS NULL OR length(selected_public_key) = 32), + CHECK ( + (active_account_public_key IS NULL AND active_binding_public_key IS NULL) + OR + (length(active_account_public_key) = 32 AND length(active_binding_public_key) = 32) + ) +) STRICT"#; + +pub(crate) const CREATE_PROFILE_CACHE_SQL: &str = r#"CREATE TABLE profile_cache ( + subject_public_key BLOB NOT NULL PRIMARY KEY + REFERENCES account_identities(public_key) ON DELETE CASCADE, + event_id BLOB NOT NULL CHECK (length(event_id) = 32), + event_created_at_unix_s INTEGER NOT NULL CHECK (event_created_at_unix_s >= 0), + name TEXT CHECK (name IS NULL OR length(CAST(name AS BLOB)) BETWEEN 1 AND 128), + display_name TEXT CHECK (display_name IS NULL OR length(CAST(display_name AS BLOB)) BETWEEN 1 AND 128), + nip05 TEXT CHECK (nip05 IS NULL OR length(CAST(nip05 AS BLOB)) BETWEEN 1 AND 320), + about TEXT CHECK (about IS NULL OR length(CAST(about AS BLOB)) BETWEEN 1 AND 4096), + picture TEXT CHECK (picture IS NULL OR length(CAST(picture AS BLOB)) BETWEEN 1 AND 2048), + refreshed_at_unix_s INTEGER NOT NULL CHECK (refreshed_at_unix_s >= 0), + refresh_status TEXT NOT NULL CHECK (refresh_status IN ('success', 'offline', 'invalid_data')) +) STRICT"#; + +pub(crate) const CREATE_ACCOUNT_PREFERENCES_SQL: &str = r#"CREATE TABLE account_preferences ( + owner_public_key BLOB NOT NULL REFERENCES account_identities(public_key) ON DELETE CASCADE, + preference_key TEXT NOT NULL CHECK (preference_key = 'namespace_probe'), + preference_value TEXT NOT NULL CHECK ( + length(CAST(preference_value AS BLOB)) BETWEEN 1 AND 4096 + ), + PRIMARY KEY (owner_public_key, preference_key), + CHECK (length(owner_public_key) = 32) +) STRICT"#; + +pub(crate) const CREATE_DURABLE_OPERATIONS_SQL: &str = r#"CREATE TABLE durable_operations ( + request_id TEXT NOT NULL PRIMARY KEY CHECK ( + length(CAST(request_id AS BLOB)) = 36 + AND request_id = lower(request_id) + AND request_id NOT GLOB '*[^0-9a-f-]*' + AND substr(request_id, 9, 1) = '-' + AND substr(request_id, 14, 1) = '-' + AND substr(request_id, 15, 1) = '7' + AND substr(request_id, 19, 1) = '-' + AND substr(request_id, 20, 1) IN ('8', '9', 'a', 'b') + AND substr(request_id, 24, 1) = '-' + ), + operation_kind TEXT NOT NULL CHECK (operation_kind IN ('create', 'import', 'repair', 'remove')), + account_public_key BLOB NOT NULL CHECK (length(account_public_key) = 32), + binding_public_key BLOB NOT NULL CHECK (length(binding_public_key) = 32), + expected_revision INTEGER CHECK (expected_revision IS NULL OR expected_revision >= 0), + phase TEXT NOT NULL CHECK (phase IN ( + 'intent_recorded', 'credential_written', 'metadata_committed', 'selection_committed', + 'compensation_pending', 'credential_deleted', 'metadata_deleted', 'finalized' + )), + terminal_outcome TEXT CHECK ( + terminal_outcome IS NULL OR terminal_outcome IN ('completed', 'cancelled', 'failed') + ), + prior_selected_public_key BLOB CHECK ( + prior_selected_public_key IS NULL OR length(prior_selected_public_key) = 32 + ), + prior_binding_availability TEXT CHECK ( + prior_binding_availability IS NULL OR prior_binding_availability IN ( + 'available', 'credential_missing', 'store_unavailable' + ) + ), + resulting_revision INTEGER CHECK (resulting_revision IS NULL OR resulting_revision >= 0), + updated_at_unix_s INTEGER NOT NULL CHECK (updated_at_unix_s >= 0), + diagnostic_code TEXT CHECK (diagnostic_code IS NULL OR diagnostic_code IN ( + 'storage_unavailable', 'keyring_unavailable', 'credential_missing', + 'compensation_failed', 'conflict', 'expired' + )), + CHECK (account_public_key = binding_public_key), + CHECK ( + (phase = 'finalized' AND terminal_outcome IS NOT NULL) + OR + (phase <> 'finalized' AND terminal_outcome IS NULL AND resulting_revision IS NULL) + ) +) STRICT"#; + +pub(crate) const CREATE_INSTALLATION_IDENTITY_SQL: &str = r#"CREATE TABLE installation_identity ( + singleton INTEGER NOT NULL PRIMARY KEY CHECK (singleton = 1), + installation_id BLOB NOT NULL CHECK (length(installation_id) = 16) +) STRICT"#; + +pub(crate) const CREATE_INSTALLATION_IDENTITY_NO_UPDATE_SQL: &str = r#"CREATE TRIGGER installation_identity_no_update +BEFORE UPDATE ON installation_identity +BEGIN + SELECT RAISE(ABORT, 'installation identity is immutable'); +END"#; + +pub(crate) const CREATE_INSTALLATION_IDENTITY_NO_DELETE_SQL: &str = r#"CREATE TRIGGER installation_identity_no_delete +BEFORE DELETE ON installation_identity +BEGIN + SELECT RAISE(ABORT, 'installation identity is immutable'); +END"#; + +const INITIAL_SCHEMA_SQL: [&str; 9] = [ + CREATE_ACCOUNT_IDENTITIES_SQL, + CREATE_LOCAL_SIGNER_BINDINGS_SQL, + CREATE_RUNTIME_STATE_SQL, + CREATE_PROFILE_CACHE_SQL, + CREATE_ACCOUNT_PREFERENCES_SQL, + CREATE_DURABLE_OPERATIONS_SQL, + CREATE_INSTALLATION_IDENTITY_SQL, + CREATE_INSTALLATION_IDENTITY_NO_UPDATE_SQL, + CREATE_INSTALLATION_IDENTITY_NO_DELETE_SQL, +]; + +const OBJECT_DIGESTS: [[u8; 32]; 9] = [ + [ + 203, 193, 254, 189, 121, 130, 165, 156, 1, 155, 21, 35, 130, 72, 131, 44, 34, 217, 168, + 187, 96, 155, 40, 226, 113, 78, 22, 255, 8, 65, 23, 38, + ], + [ + 204, 249, 174, 98, 165, 65, 184, 31, 254, 31, 101, 17, 139, 176, 170, 131, 88, 225, 158, 6, + 174, 77, 192, 131, 84, 153, 142, 200, 213, 235, 141, 82, + ], + [ + 9, 60, 207, 79, 94, 50, 51, 84, 228, 163, 119, 152, 227, 137, 166, 31, 166, 235, 70, 79, + 228, 160, 229, 246, 4, 87, 11, 172, 36, 151, 102, 234, + ], + [ + 17, 90, 168, 107, 5, 179, 134, 52, 25, 51, 228, 255, 236, 36, 157, 152, 26, 66, 108, 147, + 239, 116, 3, 99, 82, 220, 182, 236, 209, 136, 126, 97, + ], + [ + 180, 232, 35, 143, 72, 174, 82, 223, 52, 122, 142, 211, 5, 167, 155, 75, 69, 223, 34, 117, + 131, 5, 132, 107, 175, 198, 215, 16, 71, 114, 4, 127, + ], + [ + 135, 198, 48, 230, 122, 86, 86, 153, 66, 95, 22, 123, 24, 164, 49, 229, 246, 218, 210, 233, + 61, 182, 81, 194, 251, 121, 165, 203, 8, 29, 63, 27, + ], + [ + 132, 111, 227, 84, 42, 121, 244, 99, 22, 255, 131, 104, 48, 33, 7, 146, 174, 120, 176, 103, + 37, 23, 171, 90, 90, 215, 142, 212, 32, 9, 250, 188, + ], + [ + 142, 248, 11, 168, 116, 173, 238, 101, 167, 191, 95, 63, 180, 126, 229, 156, 164, 217, 108, + 71, 221, 145, 70, 169, 91, 117, 33, 93, 34, 250, 120, 151, + ], + [ + 127, 153, 155, 84, 191, 170, 38, 18, 239, 225, 90, 123, 208, 172, 218, 99, 2, 212, 181, + 212, 194, 19, 99, 242, 225, 249, 202, 134, 204, 219, 200, 23, + ], +]; +const VERSION_ONE_DIGEST: [u8; 32] = [ + 61, 122, 56, 39, 178, 126, 179, 157, 145, 167, 19, 2, 172, 134, 213, 107, 151, 196, 212, 57, + 17, 112, 163, 67, 240, 140, 61, 62, 5, 101, 14, 71, +]; + +/// A sealed binding between one HarvestCircle runtime context and the v1 state catalogs. +/// +/// External callers cannot forge alternate paths or catalogs: +/// +/// ```compile_fail +/// use harvestcircle_storage::HarvestCircleStorageContract; +/// +/// let _ = HarvestCircleStorageContract { +/// paths: todo!(), +/// migrations: todo!(), +/// schema: todo!(), +/// }; +/// ``` +#[derive(Clone, PartialEq, Eq)] +pub struct HarvestCircleStorageContract { + paths: ServiceSqlitePaths, + migrations: MigrationCatalog, + schema: SchemaCatalog, +} + +impl HarvestCircleStorageContract { + /// Binds the exact HarvestCircle service and desktop instance to canonical paths. + pub fn from_runtime_context( + context: &RuntimeContext, + ) -> Result<Self, HarvestCircleStorageContractError> { + if context.service().as_str() != HARVESTCIRCLE_SERVICE_ID + || context.instance().as_str() != HARVESTCIRCLE_INSTANCE_ID + { + return Err(HarvestCircleStorageContractError::ContextIdentity); + } + let paths = ServiceSqlitePaths::from_runtime_context(context) + .map_err(|_| HarvestCircleStorageContractError::CanonicalPaths)?; + let migrations = harvestcircle_migration_catalog()?; + let schema = schema_catalog_for(&migrations)?; + Ok(Self { + paths, + migrations, + schema, + }) + } + + #[must_use] + pub const fn paths(&self) -> &ServiceSqlitePaths { + &self.paths + } + + #[must_use] + pub const fn migrations(&self) -> &MigrationCatalog { + &self.migrations + } + + #[must_use] + pub const fn schema(&self) -> &SchemaCatalog { + &self.schema + } + + #[must_use] + pub fn application_id(&self) -> ServiceSqliteApplicationId { + ServiceSqliteApplicationId::new(HARVESTCIRCLE_APPLICATION_ID) + .expect("HCR1 is a valid SQLite application ID") + } + + #[must_use] + pub const fn state_schema_version(&self) -> NonZeroU32 { + NonZeroU32::new(HARVESTCIRCLE_STATE_SCHEMA_VERSION) + .expect("HarvestCircle schema v1 is nonzero") + } +} + +impl fmt::Debug for HarvestCircleStorageContract { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("HarvestCircleStorageContract") + .field("service", &HARVESTCIRCLE_SERVICE_ID) + .field("instance", &HARVESTCIRCLE_INSTANCE_ID) + .field("paths", &"[redacted]") + .field("schema_version", &HARVESTCIRCLE_STATE_SCHEMA_VERSION) + .finish() + } +} + +/// Stable, path-free storage-contract construction failure. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum HarvestCircleStorageContractError { + ContextIdentity, + CanonicalPaths, + MigrationCatalog, + SchemaCatalog, +} + +impl fmt::Display for HarvestCircleStorageContractError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::ContextIdentity => "HarvestCircle storage context identity is invalid", + Self::CanonicalPaths => "HarvestCircle storage paths are invalid", + Self::MigrationCatalog => "HarvestCircle migration catalog is invalid", + Self::SchemaCatalog => "HarvestCircle schema catalog is invalid", + }) + } +} + +impl Error for HarvestCircleStorageContractError {} + +#[must_use] +pub const fn harvestcircle_initial_schema_sql() -> &'static [&'static str] { + &INITIAL_SCHEMA_SQL +} + +pub fn harvestcircle_migration_catalog() +-> Result<MigrationCatalog, HarvestCircleStorageContractError> { + MigrationCatalog::new(std::iter::empty::<MigrationDescriptor>()) + .map_err(|_| HarvestCircleStorageContractError::MigrationCatalog) +} + +pub fn harvestcircle_schema_catalog() -> Result<SchemaCatalog, HarvestCircleStorageContractError> { + let migrations = harvestcircle_migration_catalog()?; + schema_catalog_for(&migrations) +} + +fn schema_catalog_for( + migrations: &MigrationCatalog, +) -> Result<SchemaCatalog, HarvestCircleStorageContractError> { + let version = SchemaVersionCatalog::new( + HARVESTCIRCLE_STATE_SCHEMA_VERSION, + schema_objects()?, + SchemaDigest::from_bytes(VERSION_ONE_DIGEST), + ) + .map_err(schema_error)?; + SchemaCatalog::new(migrations, [version]).map_err(schema_error) +} + +fn schema_objects() -> Result<Vec<SchemaObject>, HarvestCircleStorageContractError> { + let identities = [ + ( + SchemaObjectKind::Table, + "account_identities", + "account_identities", + ), + ( + SchemaObjectKind::Table, + "local_signer_bindings", + "local_signer_bindings", + ), + (SchemaObjectKind::Table, "runtime_state", "runtime_state"), + (SchemaObjectKind::Table, "profile_cache", "profile_cache"), + ( + SchemaObjectKind::Table, + "account_preferences", + "account_preferences", + ), + ( + SchemaObjectKind::Table, + "durable_operations", + "durable_operations", + ), + ( + SchemaObjectKind::Table, + "installation_identity", + "installation_identity", + ), + ( + SchemaObjectKind::Trigger, + "installation_identity_no_update", + "installation_identity", + ), + ( + SchemaObjectKind::Trigger, + "installation_identity_no_delete", + "installation_identity", + ), + ]; + identities + .into_iter() + .zip(INITIAL_SCHEMA_SQL) + .zip(OBJECT_DIGESTS) + .map(|(((kind, name, table), sql), digest)| { + SchemaObject::new(kind, name, table, sql, SchemaDigest::from_bytes(digest)) + .map_err(schema_error) + }) + .collect() +} + +const fn schema_error(_: SchemaCatalogContractError) -> HarvestCircleStorageContractError { + HarvestCircleStorageContractError::SchemaCatalog +} + +#[cfg(test)] +mod tests { + use super::*; + use radroots_runtime_paths::{ + InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, + RadrootsPlatform, RuntimeContextBootstrap, RuntimeContextSource, ServiceId, + }; + + fn context(service: &str, instance: &str) -> RuntimeContext { + RuntimeContext::resolve( + &RadrootsPathResolver::new(RadrootsPlatform::Macos, RadrootsHostEnvironment::default()), + RuntimeContextBootstrap::new( + RadrootsPathProfile::RepoLocal, + Some(std::path::PathBuf::from("/tmp/harvestcircle-contract")), + RuntimeContextSource::BootstrapCli, + RuntimeContextSource::BootstrapCli, + ) + .expect("bootstrap"), + ServiceId::new(service).expect("service"), + InstanceId::new(instance).expect("instance"), + ) + .expect("context") + } + + #[test] + fn exact_context_paths_and_catalogs_are_sealed() { + let contract = HarvestCircleStorageContract::from_runtime_context(&context( + HARVESTCIRCLE_SERVICE_ID, + HARVESTCIRCLE_INSTANCE_ID, + )) + .expect("contract"); + assert!(contract.paths().state_database().ends_with("state.sqlite")); + assert!(contract.paths().state_lock().ends_with("state.lock")); + assert_eq!( + contract.application_id().get(), + HARVESTCIRCLE_APPLICATION_ID + ); + assert_eq!(contract.state_schema_version().get(), 1); + assert_eq!(contract.migrations().current_version(), 1); + assert!(contract.migrations().descriptors().is_empty()); + assert_eq!(contract.schema().versions().len(), 1); + assert_eq!(harvestcircle_initial_schema_sql().len(), 9); + } + + #[test] + fn context_identity_and_public_diagnostics_fail_closed() { + for invalid in [ + context("myc", "desktop"), + context("harvestcircle", "primary"), + ] { + assert_eq!( + HarvestCircleStorageContract::from_runtime_context(&invalid), + Err(HarvestCircleStorageContractError::ContextIdentity) + ); + } + for error in [ + HarvestCircleStorageContractError::ContextIdentity, + HarvestCircleStorageContractError::CanonicalPaths, + HarvestCircleStorageContractError::MigrationCatalog, + HarvestCircleStorageContractError::SchemaCatalog, + ] { + assert!(error.source().is_none()); + assert!(!error.to_string().contains("/tmp")); + } + let debug = format!( + "{:?}", + HarvestCircleStorageContract::from_runtime_context(&context( + HARVESTCIRCLE_SERVICE_ID, + HARVESTCIRCLE_INSTANCE_ID, + )) + .unwrap() + ); + assert!(debug.contains("[redacted]")); + assert!(!debug.contains("/tmp")); + } + + #[test] + fn machine_coordinates_match_the_typed_contract() { + assert_eq!( + harvestcircle_product::STORAGE_SERVICE_ID, + HARVESTCIRCLE_SERVICE_ID + ); + assert_eq!( + harvestcircle_product::STORAGE_INSTANCE_ID, + HARVESTCIRCLE_INSTANCE_ID + ); + assert_eq!( + harvestcircle_product::STORAGE_DATABASE_FILENAME, + "state.sqlite" + ); + assert_eq!(harvestcircle_product::STORAGE_LOCK_FILENAME, "state.lock"); + assert_eq!( + harvestcircle_product::STORAGE_APPLICATION_ID, + HARVESTCIRCLE_APPLICATION_ID.to_string() + ); + assert_eq!(harvestcircle_product::STORAGE_APPLICATION_ID_TEXT, "HCR1"); + assert_eq!( + harvestcircle_product::STORAGE_INITIAL_SCHEMA_VERSION, + HARVESTCIRCLE_STATE_SCHEMA_VERSION.to_string() + ); + assert_eq!( + harvestcircle_product::LEGACY_DATABASE_FILENAME, + "harvestcircle.sqlite3" + ); + assert_eq!( + harvestcircle_product::LEGACY_DATABASE_DISPOSITION, + "untouched_and_unsupported" + ); + assert_eq!( + harvestcircle_product::PLATFORM_MACOS_ARCHITECTURE, + "aarch64" + ); + assert_eq!(harvestcircle_product::PLATFORM_LINUX_ARCHITECTURE, "x86_64"); + assert_eq!( + harvestcircle_product::LIMIT_IDENTITIES, + HARVESTCIRCLE_IDENTITY_CAPACITY.to_string() + ); + assert_eq!( + harvestcircle_product::LIMIT_UNFINISHED_DURABLE_OPERATIONS, + HARVESTCIRCLE_UNFINISHED_DURABLE_OPERATION_CAPACITY.to_string() + ); + assert_eq!( + harvestcircle_product::LIMIT_PREFERENCE_VALUE_UTF8_BYTES, + HARVESTCIRCLE_PREFERENCE_VALUE_UTF8_BYTES.to_string() + ); + assert_eq!( + harvestcircle_product::LIMIT_RELAY_ENDPOINTS, + HARVESTCIRCLE_RELAY_ENDPOINT_CAPACITY.to_string() + ); + assert_eq!( + harvestcircle_product::LIMIT_RELAY_URL_BYTES, + HARVESTCIRCLE_RELAY_URL_UTF8_BYTES.to_string() + ); + assert_eq!( + harvestcircle_product::LIMIT_EVENTS_PER_RELAY, + HARVESTCIRCLE_EVENTS_PER_RELAY_CAPACITY.to_string() + ); + assert_eq!( + harvestcircle_product::LIMIT_EVENTS_TOTAL, + HARVESTCIRCLE_EVENTS_TOTAL_CAPACITY.to_string() + ); + assert_eq!( + harvestcircle_product::LIMIT_OBSERVERS, + HARVESTCIRCLE_OBSERVER_CAPACITY.to_string() + ); + assert_eq!( + harvestcircle_product::LIMIT_ACTOR_MAILBOX, + HARVESTCIRCLE_ACTOR_MAILBOX_CAPACITY.to_string() + ); + assert_eq!( + harvestcircle_product::LIMIT_COMMAND_DEADLINE_MIN_MS, + HARVESTCIRCLE_COMMAND_DEADLINE_MIN_MS.to_string() + ); + assert_eq!( + harvestcircle_product::LIMIT_COMMAND_DEADLINE_MAX_MS, + HARVESTCIRCLE_COMMAND_DEADLINE_MAX_MS.to_string() + ); + assert_eq!( + harvestcircle_product::BACKUP_MEMBER_LIMIT, + "caller_supplied_positive" + ); + } + + #[test] + fn schema_sql_executes_as_one_fresh_strict_v1_inventory() { + let connection = rusqlite::Connection::open_in_memory().expect("memory database"); + connection + .execute_batch("PRAGMA foreign_keys = ON;") + .expect("foreign keys"); + for statement in harvestcircle_initial_schema_sql() { + connection + .execute_batch(statement) + .expect("schema statement"); + } + let mut statement = connection + .prepare( + "SELECT type, name, tbl_name FROM sqlite_schema \ + WHERE name NOT LIKE 'sqlite_%' ORDER BY type, name LIMIT 10", + ) + .expect("inventory statement"); + let inventory = statement + .query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + )) + }) + .expect("inventory") + .collect::<Result<Vec<_>, _>>() + .expect("inventory rows"); + assert_eq!(inventory.len(), 9); + assert_eq!( + inventory + .iter() + .filter(|(kind, _, _)| kind == "table") + .count(), + 7 + ); + assert_eq!( + inventory + .iter() + .filter(|(kind, _, _)| kind == "trigger") + .count(), + 2 + ); + assert!(inventory.iter().all(|(_, name, _)| { + !matches!( + name.as_str(), + "application_schema" | "operation_journal" | "refinery_schema_history" + ) + })); + } +} diff --git a/core/crates/harvestcircle_storage/src/lib.rs b/core/crates/harvestcircle_storage/src/lib.rs @@ -2,6 +2,7 @@ #![cfg_attr(coverage_nightly, feature(coverage_attribute))] mod compatibility; +mod contract; pub mod db; pub mod identities; pub mod identity_namespace; @@ -15,6 +16,18 @@ mod recovery; mod repair; pub use compatibility::{DatabasePreflight, PersistedIdentityIssue, PersistedIdentityIssueKind}; +pub use contract::{ + HARVESTCIRCLE_ACTOR_MAILBOX_CAPACITY, HARVESTCIRCLE_APPLICATION_ID, + HARVESTCIRCLE_COMMAND_DEADLINE_MAX_MS, HARVESTCIRCLE_COMMAND_DEADLINE_MIN_MS, + HARVESTCIRCLE_EVENTS_PER_RELAY_CAPACITY, HARVESTCIRCLE_EVENTS_TOTAL_CAPACITY, + HARVESTCIRCLE_IDENTITY_CAPACITY, HARVESTCIRCLE_INSTANCE_ID, HARVESTCIRCLE_OBSERVER_CAPACITY, + HARVESTCIRCLE_PREFERENCE_VALUE_UTF8_BYTES, HARVESTCIRCLE_RELAY_ENDPOINT_CAPACITY, + HARVESTCIRCLE_RELAY_URL_UTF8_BYTES, HARVESTCIRCLE_SERVICE_ID, + HARVESTCIRCLE_STATE_SCHEMA_VERSION, HARVESTCIRCLE_UNFINISHED_DURABLE_OPERATION_CAPACITY, + HarvestCircleStorageContract, HarvestCircleStorageContractError, + harvestcircle_initial_schema_sql, harvestcircle_migration_catalog, + harvestcircle_schema_catalog, +}; pub use db::{CURRENT_SCHEMA_VERSION, Database}; pub use os_keyring::{CREDENTIAL_SERVICE, OsKeyringSecretStore}; pub use repair::{QuarantineExportReceipt, RepairAuthorization, RepairCandidate}; diff --git a/core/crates/harvestcircle_storage/src/repair.rs b/core/crates/harvestcircle_storage/src/repair.rs @@ -3,7 +3,7 @@ use std::io::Read; use std::path::{Path, PathBuf}; use harvestcircle_domain::{SafeError, SafeErrorCode, SafeMessage}; -use harvestcircle_product::DATABASE_FILENAME; +use harvestcircle_product::LEGACY_DATABASE_FILENAME; use hmac::{Hmac, Mac}; use rusqlite::{Connection, MAIN_DB, OpenFlags}; use sha2::{Digest, Sha256}; @@ -143,7 +143,7 @@ pub(crate) fn install_candidate( return Err(storage_error()); } copy_secure(&candidate.path, &replacement)?; - let retained = parent.join(format!("{DATABASE_FILENAME}.quarantined-evidence")); + let retained = parent.join(format!("{LEGACY_DATABASE_FILENAME}.quarantined-evidence")); if retained.try_exists().map_err(|_| storage_error())? { let _ = fs::remove_file(&replacement); return Err(storage_error()); diff --git a/radroots.lib.source-lock.v1.toml b/radroots.lib.source-lock.v1.toml @@ -5,4 +5,4 @@ architecture = "radroots.crates.release.v2" workspace_catalog_sha256 = "deca0c080deae187ff8186c0708903e42f41ea57f77c5f91581e23aa561164a4" version = "0.1.0-alpha" source_archive_sha256 = "aec2fe198b200f40af81424fbec70a9a8f22b0b38455bc6c81b7eb3be4241748" -lockfile_sha256 = "a2cb8a0f1d252434acba5e417f93ab9cd3be945af554452b3b40e4a8dcea3c80" +lockfile_sha256 = "1bbaae4bd586b936120bb8b2cfab8a5463e7e15aa3d6de0ccdf85f831835467f" diff --git a/tools/xtask/src/lib.rs b/tools/xtask/src/lib.rs @@ -751,6 +751,12 @@ fn provenance_check(root: &Path, inventory: &Inventory, findings: &mut Vec<Strin format!( "radroots_identity = {{ git = \"https://github.com/radrootslabs/lib\", rev = \"{LIB_REVISION}\", version = \"=0.1.0-alpha\", default-features = false }}" ), + format!( + "radroots_runtime_paths = {{ git = \"https://github.com/radrootslabs/lib\", rev = \"{LIB_REVISION}\", version = \"=0.1.0-alpha\", default-features = false }}" + ), + format!( + "radroots_service_sqlite = {{ git = \"https://github.com/radrootslabs/lib\", rev = \"{LIB_REVISION}\", version = \"=0.1.0-alpha\", default-features = false }}" + ), ] { if cargo .lines() @@ -783,7 +789,7 @@ fn provenance_check(root: &Path, inventory: &Inventory, findings: &mut Vec<Strin "workspace_catalog_sha256 = \"deca0c080deae187ff8186c0708903e42f41ea57f77c5f91581e23aa561164a4\"\n", "version = \"0.1.0-alpha\"\n", "source_archive_sha256 = \"aec2fe198b200f40af81424fbec70a9a8f22b0b38455bc6c81b7eb3be4241748\"\n", - "lockfile_sha256 = \"a2cb8a0f1d252434acba5e417f93ab9cd3be945af554452b3b40e4a8dcea3c80\"\n", + "lockfile_sha256 = \"1bbaae4bd586b936120bb8b2cfab8a5463e7e15aa3d6de0ccdf85f831835467f\"\n", ); if read_text(root, SOURCE_LOCK_PATH) != expected_source_lock { findings.push(format!("{SOURCE_LOCK_PATH}: exact Lib source lock changed")); @@ -798,6 +804,37 @@ fn provenance_check(root: &Path, inventory: &Inventory, findings: &mut Vec<Strin root, "config/product/harvestcircle-v1.properties", )); + for (key, expected) in [ + ("storage.service_id", "harvestcircle"), + ("storage.instance_id", "desktop"), + ("storage.database_filename", "state.sqlite"), + ("storage.lock_filename", "state.lock"), + ("storage.application_id", "1212371505"), + ("storage.application_id_text", "HCR1"), + ("storage.initial_schema_version", "1"), + ("legacy.database.filename", "harvestcircle.sqlite3"), + ("legacy.database.disposition", "untouched_and_unsupported"), + ("platform.macos.architecture", "aarch64"), + ("platform.linux.architecture", "x86_64"), + ("limit.identities", "256"), + ("limit.unfinished_durable_operations", "1024"), + ("limit.preference_value_utf8_bytes", "4096"), + ("limit.relay_endpoints", "16"), + ("limit.relay_url_bytes", "2048"), + ("limit.events_per_relay", "64"), + ("limit.events_total", "1024"), + ("limit.observers", "32"), + ("limit.actor_mailbox", "64"), + ("limit.command_deadline_min_ms", "1"), + ("limit.command_deadline_max_ms", "30000"), + ("backup.member_limit", "caller_supplied_positive"), + ] { + if coordinates.get(key).map(String::as_str) != Some(expected) { + findings.push(format!( + "config/product/harvestcircle-v1.properties: {key} must remain {expected}" + )); + } + } let uniffi = read_text(root, "core/crates/harvestcircle_ffi/uniffi.toml"); let ffi_package = coordinates .get("ffi.kotlin_package") @@ -832,7 +869,25 @@ fn provenance_check(root: &Path, inventory: &Inventory, findings: &mut Vec<Strin { findings.push("app/shared/build.gradle.kts: shared KMP target boundary changed".to_owned()); } - for required in [PROVENANCE_PATH, SOURCE_LOCK_PATH] { + const STORAGE_API_BASELINE: &str = "core/compatibility/harvestcircle-storage-api-v1.txt"; + let storage_api = read_text(root, STORAGE_API_BASELINE); + for required in [ + "pub struct harvestcircle_storage::HarvestCircleStorageContract", + "pub const harvestcircle_storage::HARVESTCIRCLE_APPLICATION_ID: u32", + "pub fn harvestcircle_storage::harvestcircle_schema_catalog()", + ] { + if !storage_api.contains(required) { + findings.push(format!("{STORAGE_API_BASELINE}: missing {required}")); + } + } + for forbidden in ["rusqlite::", "refinery::", "sqlx::"] { + if storage_api.contains(forbidden) { + findings.push(format!( + "{STORAGE_API_BASELINE}: dependency-owned API leaked: {forbidden}" + )); + } + } + for required in [PROVENANCE_PATH, SOURCE_LOCK_PATH, STORAGE_API_BASELINE] { if !inventory.paths.iter().any(|path| path == required) { findings.push(format!("{required}: governed source evidence is missing")); }