commit ed076f0dea6334260ed82bf83cb38edc98d39c4c parent 4f28e5ccc9d72c4385b26a9e07ca198f156dcc79 Author: triesap <tyson@radroots.org> Date: Thu, 10 Sep 2026 13:44:41 +0000 compose: Install governed draft persistence - Bind scoped composer payloads to a versioned schema checksum - Reject unsupported or corrupt records without changing original bytes - Verify SQLite reopen preserves drafts and authored operation state - Regenerate exact native artifacts and qualify standalone consumers Diffstat:
13 files changed, 990 insertions(+), 61 deletions(-)
diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -22,9 +22,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 74158312, + "bytes": 74225840, "path": "TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "bd6bf366fb6a9aa1d16f248863152bca91987e6f86d0a8d4263e35b1080eafa4" + "sha256": "0d90f00e64d20a779c3b1708a9fb945193a3fe42a13a95042eb33657b6f2cafc" }, { "bytes": 71509, @@ -37,9 +37,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 74223136, + "bytes": 74296656, "path": "TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "47e6b3fc1a8a538709300a3a0f6dcb006c0de13493a611616c76a542836aac55" + "sha256": "5ae7484efae0b205ab449db35ee8e2c75c2a64792efd526b1dcae5f5fcc51743" }, { "bytes": 42544, @@ -77,34 +77,34 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 20614064, + "bytes": 20612096, "path": "native/aarch64-apple-darwin/libtera_ffi.dylib", - "sha256": "86167d0c906cb9fa1804834de4f96bd1966d302d2c80cdb3bb5ab765471c6edb" + "sha256": "1f6f20b535b3789c409258cd1c1cb73c5ab1a50929abad00118b1be9cf24b58e" }, { - "bytes": 74158312, + "bytes": 74225840, "path": "native/aarch64-apple-ios-sim/libtera_ffi.a", - "sha256": "bd6bf366fb6a9aa1d16f248863152bca91987e6f86d0a8d4263e35b1080eafa4" + "sha256": "0d90f00e64d20a779c3b1708a9fb945193a3fe42a13a95042eb33657b6f2cafc" }, { - "bytes": 74223136, + "bytes": 74296656, "path": "native/aarch64-apple-ios/libtera_ffi.a", - "sha256": "47e6b3fc1a8a538709300a3a0f6dcb006c0de13493a611616c76a542836aac55" + "sha256": "5ae7484efae0b205ab449db35ee8e2c75c2a64792efd526b1dcae5f5fcc51743" }, { - "bytes": 73537, + "bytes": 74677, "path": "source/aarch64-apple-darwin.json", - "sha256": "0298803e4efc991b0bba6f68b5297b6d299c99d5e8935d5ca261b4f6648b7e05" + "sha256": "0a45a688f1e1faacce0a38e71990f13ea2061258ec23d858a7f7481ade4285a3" }, { - "bytes": 73381, + "bytes": 74521, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "f425a738ea4a720c6665aaa08ed6f128fcf9df0abd6cda871f760f8ae1cf8bf5" + "sha256": "6336b22885385b8ad0a4fefe58b5fc2c97fe054c08b65ed247462ad8f7d78387" }, { - "bytes": 73377, + "bytes": 74517, "path": "source/aarch64-apple-ios.json", - "sha256": "9d05336e0586128305b4a4cd5183acdc1ed84805ea8e5603e21b567e6cb87e7f" + "sha256": "9a24669e914ecce38ca5a151fe66b0970ef6e01f8d8fa080ec04debb504dc076" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "628534dfa9763f18b3ebba50dc25b1adb71801ba" + "tree": "e0ba029e9f889064f46953f7033508d9ea2e48cf" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -139,9 +139,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 74158312, + "bytes": 74225840, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "bd6bf366fb6a9aa1d16f248863152bca91987e6f86d0a8d4263e35b1080eafa4" + "sha256": "0d90f00e64d20a779c3b1708a9fb945193a3fe42a13a95042eb33657b6f2cafc" }, { "bytes": 71509, @@ -154,9 +154,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 74223136, + "bytes": 74296656, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "47e6b3fc1a8a538709300a3a0f6dcb006c0de13493a611616c76a542836aac55" + "sha256": "5ae7484efae0b205ab449db35ee8e2c75c2a64792efd526b1dcae5f5fcc51743" }, { "bytes": 521598, @@ -174,19 +174,19 @@ "sha256": "e2e025ee9a29182970f52f53593796afd154cdfb09aba197474577103dcb4ad6" }, { - "bytes": 73537, + "bytes": 74677, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "0298803e4efc991b0bba6f68b5297b6d299c99d5e8935d5ca261b4f6648b7e05" + "sha256": "0a45a688f1e1faacce0a38e71990f13ea2061258ec23d858a7f7481ade4285a3" }, { - "bytes": 73381, + "bytes": 74521, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "f425a738ea4a720c6665aaa08ed6f128fcf9df0abd6cda871f760f8ae1cf8bf5" + "sha256": "6336b22885385b8ad0a4fefe58b5fc2c97fe054c08b65ed247462ad8f7d78387" }, { - "bytes": 73377, + "bytes": 74517, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "9d05336e0586128305b4a4cd5183acdc1ed84805ea8e5603e21b567e6cb87e7f" + "sha256": "9a24669e914ecce38ca5a151fe66b0970ef6e01f8d8fa080ec04debb504dc076" } ], "schema": "tera.installed-native-artifacts.v1" diff --git a/TeraFFI/source.lock b/TeraFFI/source.lock @@ -1,7 +1,7 @@ schema = "tera.installed-source.v1" repository = "https://github.com/radrootslabs/tera" -source_tree = "628534dfa9763f18b3ebba50dc25b1adb71801ba" -manifest_sha256 = "47abd7034be7df2e7f58cf6404dbcff14ce75599b38b999f24064f64b05965d5" +source_tree = "e0ba029e9f889064f46953f7033508d9ea2e48cf" +manifest_sha256 = "2cc3c61abb397437ace14ca9d9aec9da57eee00064152a8c6bd4faac7b7536ec" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -645,10 +645,10 @@ "sha256": "dff55e46521c26f5f0ece024453b55c15f132fa94f78e2cba3d141f123d4eca0" }, "core/crates/tera_core/src/runtime/product_surface.rs": { - "bytes": 6792, - "git_blob": "346a7cd12368feae85595c80719caf52ec6bc417", + "bytes": 6919, + "git_blob": "c6b9a5fa1e0f1ba53b90630fb49e0383ad740a92", "mode": "100644", - "sha256": "e386e570092c204602b26cfd7a515179c32502e3855be91547c1471b5eed43ac" + "sha256": "d49d1d7e3ecbda3239183045e4171d2aa736398c099cac9038353a64e8137352" }, "core/crates/tera_core/src/runtime/product_surface/authoring.rs": { "bytes": 11282, @@ -681,10 +681,10 @@ "sha256": "3ec65d1cb2d94740396137d03920b212afb74692dba11729e4ce5bb3c5a2a789" }, "core/crates/tera_core/src/runtime/product_surface/composer.rs": { - "bytes": 1925, - "git_blob": "18d40988e6a1045444e7ff3a205aa045d8b113d0", + "bytes": 2087, + "git_blob": "84ae67d8e260d987d51298a357c735da7294c959", "mode": "100644", - "sha256": "f8fc65558d1b3c7a20edff9a5bdecf47d6bdca64eab8a7a6faa9075e7b7b8cdd" + "sha256": "d8955b5998e3471b8a4e55b934138ea76fe96d6803355c69b977f06a81893e4e" }, "core/crates/tera_core/src/runtime/product_surface/composer/form.rs": { "bytes": 6734, @@ -698,6 +698,24 @@ "mode": "100644", "sha256": "3dc9c2fc93fa3f368de6717dbe85406c240a71f05e7d94784fc4f2553b53897b" }, + "core/crates/tera_core/src/runtime/product_surface/composer/storage.rs": { + "bytes": 7272, + "git_blob": "0a3417af19a294f9cb2bc120be59186de8f95cb7", + "mode": "100644", + "sha256": "271f6a8dd894ff10a371173f2b098ad4cd9413ab0d9c28c95fe34bb4045e1b69" + }, + "core/crates/tera_core/src/runtime/product_surface/composer/storage_sqlite_tests.rs": { + "bytes": 6768, + "git_blob": "39cd2a36593b1d2ddf15085ccc9802a3eb9b83a9", + "mode": "100644", + "sha256": "0424a1c944c4325c6040db31ccd5d2a4d37d39333f7244d80e5e9bb650ee9b32" + }, + "core/crates/tera_core/src/runtime/product_surface/composer/storage_tests.rs": { + "bytes": 12429, + "git_blob": "c92c6f6e948f5e849b4aed52e5c3cce484cfe431", + "mode": "100644", + "sha256": "a2f8f4466fd488655edf1e2422a1f25396a584c37b0f70baf95b73640ef7719a" + }, "core/crates/tera_core/src/runtime/product_surface/composer/tests.rs": { "bytes": 11869, "git_blob": "fdb5caa3b8172923577ea860eb42fdf913754cc9", @@ -1172,6 +1190,12 @@ "mode": "100644", "sha256": "7377d9ebcbb29633e3edc14b1a09b2c92ca8f1f8c21e5ecdb0299d6719bcf1ab" }, + "core/fixtures/composer_schema_v1.json": { + "bytes": 2078, + "git_blob": "964920ce32cd7cbc8dbe09ca08192118b0189818", + "mode": "100644", + "sha256": "48ac3978641bb1485e9a4c7037533a5eefb1ad176e0eb43ead800d7e12c1f084" + }, "core/provenance/tera_bindgen/LICENSE-APACHE": { "bytes": 10831, "git_blob": "9f90a7c2df28eba05d53e5c6f9ac246c018c6bee", @@ -1581,13 +1605,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 121846, - "git_blob": "6d5c4b58152735f4655422275bcb8873af1efb05", + "bytes": 122410, + "git_blob": "f0e450953eab508af6345f09c4ae7cc73ff446e2", "mode": "100644", - "sha256": "5b3c4b76223c1e56cce542a7fc15f91ba97547f2f1912646e5c323989861a844" + "sha256": "1da17843897d19a73571f9675276909d114d6730c5ae6ff5b44ae6af19a81494" } }, "policy": "staged_inputs", - "tree": "628534dfa9763f18b3ebba50dc25b1adb71801ba" + "tree": "e0ba029e9f889064f46953f7033508d9ea2e48cf" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -641,10 +641,10 @@ "sha256": "dff55e46521c26f5f0ece024453b55c15f132fa94f78e2cba3d141f123d4eca0" }, "core/crates/tera_core/src/runtime/product_surface.rs": { - "bytes": 6792, - "git_blob": "346a7cd12368feae85595c80719caf52ec6bc417", + "bytes": 6919, + "git_blob": "c6b9a5fa1e0f1ba53b90630fb49e0383ad740a92", "mode": "100644", - "sha256": "e386e570092c204602b26cfd7a515179c32502e3855be91547c1471b5eed43ac" + "sha256": "d49d1d7e3ecbda3239183045e4171d2aa736398c099cac9038353a64e8137352" }, "core/crates/tera_core/src/runtime/product_surface/authoring.rs": { "bytes": 11282, @@ -677,10 +677,10 @@ "sha256": "3ec65d1cb2d94740396137d03920b212afb74692dba11729e4ce5bb3c5a2a789" }, "core/crates/tera_core/src/runtime/product_surface/composer.rs": { - "bytes": 1925, - "git_blob": "18d40988e6a1045444e7ff3a205aa045d8b113d0", + "bytes": 2087, + "git_blob": "84ae67d8e260d987d51298a357c735da7294c959", "mode": "100644", - "sha256": "f8fc65558d1b3c7a20edff9a5bdecf47d6bdca64eab8a7a6faa9075e7b7b8cdd" + "sha256": "d8955b5998e3471b8a4e55b934138ea76fe96d6803355c69b977f06a81893e4e" }, "core/crates/tera_core/src/runtime/product_surface/composer/form.rs": { "bytes": 6734, @@ -694,6 +694,24 @@ "mode": "100644", "sha256": "3dc9c2fc93fa3f368de6717dbe85406c240a71f05e7d94784fc4f2553b53897b" }, + "core/crates/tera_core/src/runtime/product_surface/composer/storage.rs": { + "bytes": 7272, + "git_blob": "0a3417af19a294f9cb2bc120be59186de8f95cb7", + "mode": "100644", + "sha256": "271f6a8dd894ff10a371173f2b098ad4cd9413ab0d9c28c95fe34bb4045e1b69" + }, + "core/crates/tera_core/src/runtime/product_surface/composer/storage_sqlite_tests.rs": { + "bytes": 6768, + "git_blob": "39cd2a36593b1d2ddf15085ccc9802a3eb9b83a9", + "mode": "100644", + "sha256": "0424a1c944c4325c6040db31ccd5d2a4d37d39333f7244d80e5e9bb650ee9b32" + }, + "core/crates/tera_core/src/runtime/product_surface/composer/storage_tests.rs": { + "bytes": 12429, + "git_blob": "c92c6f6e948f5e849b4aed52e5c3cce484cfe431", + "mode": "100644", + "sha256": "a2f8f4466fd488655edf1e2422a1f25396a584c37b0f70baf95b73640ef7719a" + }, "core/crates/tera_core/src/runtime/product_surface/composer/tests.rs": { "bytes": 11869, "git_blob": "fdb5caa3b8172923577ea860eb42fdf913754cc9", @@ -1168,6 +1186,12 @@ "mode": "100644", "sha256": "7377d9ebcbb29633e3edc14b1a09b2c92ca8f1f8c21e5ecdb0299d6719bcf1ab" }, + "core/fixtures/composer_schema_v1.json": { + "bytes": 2078, + "git_blob": "964920ce32cd7cbc8dbe09ca08192118b0189818", + "mode": "100644", + "sha256": "48ac3978641bb1485e9a4c7037533a5eefb1ad176e0eb43ead800d7e12c1f084" + }, "core/provenance/tera_bindgen/LICENSE-APACHE": { "bytes": 10831, "git_blob": "9f90a7c2df28eba05d53e5c6f9ac246c018c6bee", @@ -1577,13 +1601,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 121846, - "git_blob": "6d5c4b58152735f4655422275bcb8873af1efb05", + "bytes": 122410, + "git_blob": "f0e450953eab508af6345f09c4ae7cc73ff446e2", "mode": "100644", - "sha256": "5b3c4b76223c1e56cce542a7fc15f91ba97547f2f1912646e5c323989861a844" + "sha256": "1da17843897d19a73571f9675276909d114d6730c5ae6ff5b44ae6af19a81494" } }, "policy": "staged_inputs", - "tree": "628534dfa9763f18b3ebba50dc25b1adb71801ba" + "tree": "e0ba029e9f889064f46953f7033508d9ea2e48cf" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -641,10 +641,10 @@ "sha256": "dff55e46521c26f5f0ece024453b55c15f132fa94f78e2cba3d141f123d4eca0" }, "core/crates/tera_core/src/runtime/product_surface.rs": { - "bytes": 6792, - "git_blob": "346a7cd12368feae85595c80719caf52ec6bc417", + "bytes": 6919, + "git_blob": "c6b9a5fa1e0f1ba53b90630fb49e0383ad740a92", "mode": "100644", - "sha256": "e386e570092c204602b26cfd7a515179c32502e3855be91547c1471b5eed43ac" + "sha256": "d49d1d7e3ecbda3239183045e4171d2aa736398c099cac9038353a64e8137352" }, "core/crates/tera_core/src/runtime/product_surface/authoring.rs": { "bytes": 11282, @@ -677,10 +677,10 @@ "sha256": "3ec65d1cb2d94740396137d03920b212afb74692dba11729e4ce5bb3c5a2a789" }, "core/crates/tera_core/src/runtime/product_surface/composer.rs": { - "bytes": 1925, - "git_blob": "18d40988e6a1045444e7ff3a205aa045d8b113d0", + "bytes": 2087, + "git_blob": "84ae67d8e260d987d51298a357c735da7294c959", "mode": "100644", - "sha256": "f8fc65558d1b3c7a20edff9a5bdecf47d6bdca64eab8a7a6faa9075e7b7b8cdd" + "sha256": "d8955b5998e3471b8a4e55b934138ea76fe96d6803355c69b977f06a81893e4e" }, "core/crates/tera_core/src/runtime/product_surface/composer/form.rs": { "bytes": 6734, @@ -694,6 +694,24 @@ "mode": "100644", "sha256": "3dc9c2fc93fa3f368de6717dbe85406c240a71f05e7d94784fc4f2553b53897b" }, + "core/crates/tera_core/src/runtime/product_surface/composer/storage.rs": { + "bytes": 7272, + "git_blob": "0a3417af19a294f9cb2bc120be59186de8f95cb7", + "mode": "100644", + "sha256": "271f6a8dd894ff10a371173f2b098ad4cd9413ab0d9c28c95fe34bb4045e1b69" + }, + "core/crates/tera_core/src/runtime/product_surface/composer/storage_sqlite_tests.rs": { + "bytes": 6768, + "git_blob": "39cd2a36593b1d2ddf15085ccc9802a3eb9b83a9", + "mode": "100644", + "sha256": "0424a1c944c4325c6040db31ccd5d2a4d37d39333f7244d80e5e9bb650ee9b32" + }, + "core/crates/tera_core/src/runtime/product_surface/composer/storage_tests.rs": { + "bytes": 12429, + "git_blob": "c92c6f6e948f5e849b4aed52e5c3cce484cfe431", + "mode": "100644", + "sha256": "a2f8f4466fd488655edf1e2422a1f25396a584c37b0f70baf95b73640ef7719a" + }, "core/crates/tera_core/src/runtime/product_surface/composer/tests.rs": { "bytes": 11869, "git_blob": "fdb5caa3b8172923577ea860eb42fdf913754cc9", @@ -1168,6 +1186,12 @@ "mode": "100644", "sha256": "7377d9ebcbb29633e3edc14b1a09b2c92ca8f1f8c21e5ecdb0299d6719bcf1ab" }, + "core/fixtures/composer_schema_v1.json": { + "bytes": 2078, + "git_blob": "964920ce32cd7cbc8dbe09ca08192118b0189818", + "mode": "100644", + "sha256": "48ac3978641bb1485e9a4c7037533a5eefb1ad176e0eb43ead800d7e12c1f084" + }, "core/provenance/tera_bindgen/LICENSE-APACHE": { "bytes": 10831, "git_blob": "9f90a7c2df28eba05d53e5c6f9ac246c018c6bee", @@ -1577,13 +1601,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 121846, - "git_blob": "6d5c4b58152735f4655422275bcb8873af1efb05", + "bytes": 122410, + "git_blob": "f0e450953eab508af6345f09c4ae7cc73ff446e2", "mode": "100644", - "sha256": "5b3c4b76223c1e56cce542a7fc15f91ba97547f2f1912646e5c323989861a844" + "sha256": "1da17843897d19a73571f9675276909d114d6730c5ae6ff5b44ae6af19a81494" } }, "policy": "staged_inputs", - "tree": "628534dfa9763f18b3ebba50dc25b1adb71801ba" + "tree": "e0ba029e9f889064f46953f7033508d9ea2e48cf" } } diff --git a/core/crates/tera_core/src/runtime/product_surface.rs b/core/crates/tera_core/src/runtime/product_surface.rs @@ -34,8 +34,10 @@ pub use calendar_timing::{CalendarTiming, CalendarTimingError, DateBasedTiming, #[cfg(feature = "mobile-social")] pub use composer::{ COMPOSER_CONTENT_MAX_BYTES, COMPOSER_FORM_MAX_BYTES, COMPOSER_MEDIA_MAX, + COMPOSER_PAYLOAD_SCHEMA, COMPOSER_SCHEMA_SHA256, COMPOSER_SCHEMA_VERSION, COMPOSER_TEXT_MAX_BYTES, ComposerDraft, ComposerEditSequence, ComposerError, ComposerFormInput, ComposerId, ComposerMediaInput, ComposerPartialForm, ComposerRevision, ComposerScope, + ComposerStorageError, ComposerStorageRecord, }; pub use context::{ ContextAdmission, ContextRank, LocalNetwork, LocalNetworkAdmission, LocalNetworkError, diff --git a/core/crates/tera_core/src/runtime/product_surface/composer.rs b/core/crates/tera_core/src/runtime/product_surface/composer.rs @@ -2,12 +2,17 @@ mod form; mod identity; +mod storage; pub use form::{ COMPOSER_CONTENT_MAX_BYTES, COMPOSER_FORM_MAX_BYTES, COMPOSER_MEDIA_MAX, COMPOSER_TEXT_MAX_BYTES, ComposerFormInput, ComposerMediaInput, ComposerPartialForm, }; pub use identity::{ComposerEditSequence, ComposerId, ComposerRevision, ComposerScope}; +pub use storage::{ + COMPOSER_PAYLOAD_SCHEMA, COMPOSER_SCHEMA_SHA256, COMPOSER_SCHEMA_VERSION, ComposerStorageError, + ComposerStorageRecord, +}; #[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)] pub enum ComposerError { diff --git a/core/crates/tera_core/src/runtime/product_surface/composer/storage.rs b/core/crates/tera_core/src/runtime/product_surface/composer/storage.rs @@ -0,0 +1,204 @@ +//! Versioned application payloads carried by the existing shared storage owner. + +use radroots_storage::{ + authored_draft::{AuthoredDraft, AuthoredDraftId, AuthoredDraftStage}, + authored_draft_query::AuthoredDraftScope, +}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use super::{ + COMPOSER_FORM_MAX_BYTES, ComposerDraft, ComposerEditSequence, ComposerId, ComposerPartialForm, + ComposerRevision, ComposerScope, +}; + +pub const COMPOSER_PAYLOAD_SCHEMA: &str = "tera.composer.v1"; +pub const COMPOSER_SCHEMA_VERSION: u64 = 1; +/// Pins the exact standalone schema descriptor, including field and resource bounds. +pub const COMPOSER_SCHEMA_SHA256: &str = + "48ac3978641bb1485e9a4c7037533a5eefb1ad176e0eb43ead800d7e12c1f084"; +const SCOPE_DOMAIN: &[u8] = b"tera.composer-scope.v1\0"; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)] +pub enum ComposerStorageError { + #[error("record is not a composer")] + WrongSchema, + #[error("composer belongs to a different account or context")] + ScopeMismatch, + #[error("composer schema is unsupported")] + UnsupportedSchema, + #[error("composer record requires repair")] + CorruptRecord, + #[error("composer timestamp is not representable")] + InvalidTimestamp, + #[error("composer storage representation is invalid")] + InvalidRecord, +} + +/// Probe identity before interpreting a future body. Complete current decoding below +/// still rejects unknown and duplicate fields, including nested form/media fields. +#[derive(Deserialize)] +struct SchemaHeader { + schema_version: u64, + schema_sha256: String, +} + +#[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct StoredComposerV1 { + schema_version: u64, + schema_sha256: String, + scope: ComposerScope, + edit_sequence: ComposerEditSequence, + form: ComposerPartialForm, +} + +/// A validated application/storage mapping. Construction is not a durable save receipt. +#[derive(Clone)] +pub struct ComposerStorageRecord { + stored: AuthoredDraft, + draft: ComposerDraft, +} + +impl std::fmt::Debug for ComposerStorageRecord { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("ComposerStorageRecord") + .field("draft", &self.draft) + .finish_non_exhaustive() + } +} + +impl ComposerStorageRecord { + /// Creates an initial scoped envelope for the existing owner to commit. + pub fn initial( + id: ComposerId, + scope: ComposerScope, + edit_sequence: ComposerEditSequence, + form: ComposerPartialForm, + created_at_unix_ms: u64, + ) -> Result<Self, ComposerStorageError> { + validate_time(created_at_unix_ms)?; + let stored_scope = Self::scope_digest(&scope)?; + let wire = StoredComposerV1 { + schema_version: COMPOSER_SCHEMA_VERSION, + schema_sha256: COMPOSER_SCHEMA_SHA256.to_owned(), + scope: scope.clone(), + edit_sequence, + form, + }; + let payload = serde_json::to_vec(&wire).map_err(|_| ComposerStorageError::InvalidRecord)?; + if payload.len() > COMPOSER_FORM_MAX_BYTES { + return Err(ComposerStorageError::InvalidRecord); + } + let stored = AuthoredDraft::initial( + AuthoredDraftId::new(*id.as_bytes()) + .map_err(|_| ComposerStorageError::InvalidRecord)?, + scope.author().into_bytes(), + COMPOSER_PAYLOAD_SCHEMA, + payload, + AuthoredDraftStage::Draft, + None, + created_at_unix_ms, + ) + .and_then(|draft| draft.with_scope(stored_scope)) + .map_err(|_| ComposerStorageError::InvalidRecord)?; + Ok(Self { + stored, + draft: ComposerDraft::new( + id, + ComposerRevision::INITIAL, + scope, + edit_sequence, + wire.form, + ), + }) + } + + /// Validates existing bytes for an independently supplied stable scope without writes. + pub fn decode( + stored: AuthoredDraft, + scope: &ComposerScope, + ) -> Result<Self, ComposerStorageError> { + if stored.payload_schema() != COMPOSER_PAYLOAD_SCHEMA { + return Err(ComposerStorageError::WrongSchema); + } + if stored.author() != scope.author().as_bytes() + || stored.scope() != Some(Self::scope_digest(scope)?) + { + return Err(ComposerStorageError::ScopeMismatch); + } + stored + .validate() + .map_err(|_| ComposerStorageError::CorruptRecord)?; + if stored.stage() != AuthoredDraftStage::Draft + || stored.operation_id().is_some() + || stored.payload().len() > COMPOSER_FORM_MAX_BYTES + { + return Err(ComposerStorageError::CorruptRecord); + } + validate_time(stored.created_at_unix_ms())?; + validate_time(stored.updated_at_unix_ms())?; + let header: SchemaHeader = serde_json::from_slice(stored.payload()) + .map_err(|_| ComposerStorageError::CorruptRecord)?; + if header.schema_version != COMPOSER_SCHEMA_VERSION + || header.schema_sha256 != COMPOSER_SCHEMA_SHA256 + { + return Err(ComposerStorageError::UnsupportedSchema); + } + let wire: StoredComposerV1 = serde_json::from_slice(stored.payload()) + .map_err(|_| ComposerStorageError::CorruptRecord)?; + if &wire.scope != scope { + return Err(ComposerStorageError::CorruptRecord); + } + let draft = ComposerDraft::new( + ComposerId::new(*stored.draft_id().as_bytes()) + .map_err(|_| ComposerStorageError::CorruptRecord)?, + ComposerRevision::new(stored.revision().get()) + .map_err(|_| ComposerStorageError::CorruptRecord)?, + wire.scope, + wire.edit_sequence, + wire.form, + ); + Ok(Self { stored, draft }) + } + + pub fn draft(&self) -> &ComposerDraft { + &self.draft + } + pub fn stored(&self) -> &AuthoredDraft { + &self.stored + } + pub fn into_stored(self) -> AuthoredDraft { + self.stored + } + + /// Stable namespace, independent of process/session generation and relay configuration. + pub fn scope_digest(scope: &ComposerScope) -> Result<AuthoredDraftScope, ComposerStorageError> { + let context = scope.local_network().as_bytes(); + let length = + u32::try_from(context.len()).map_err(|_| ComposerStorageError::InvalidRecord)?; + let mut digest = Sha256::new(); + digest.update(SCOPE_DOMAIN); + digest.update(scope.author().as_bytes()); + digest.update(length.to_be_bytes()); + digest.update(context); + AuthoredDraftScope::new(digest.finalize().into()) + .map_err(|_| ComposerStorageError::InvalidRecord) + } +} + +fn validate_time(value: u64) -> Result<(), ComposerStorageError> { + if value == 0 || value > i64::MAX as u64 { + return Err(ComposerStorageError::InvalidTimestamp); + } + Ok(()) +} + +#[cfg(test)] +#[path = "storage_tests.rs"] +mod tests; + +#[cfg(all(test, not(target_arch = "wasm32")))] +#[path = "storage_sqlite_tests.rs"] +mod sqlite_tests; diff --git a/core/crates/tera_core/src/runtime/product_surface/composer/storage_sqlite_tests.rs b/core/crates/tera_core/src/runtime/product_surface/composer/storage_sqlite_tests.rs @@ -0,0 +1,208 @@ +use super::{tests::*, *}; +use crate::runtime::{ + builder::RuntimeBuilder, + product_surface::{ + CreateUpdate, Phase1AddCommand, Phase1CancellationPolicy, Phase1QueuePolicy, + Phase1RelaySatisfaction, ProfileMetadataCommand, + }, + store::{MobileUserStoreConfig, ProtectedDataAvailability}, +}; +use radroots_storage::{ + Error, + authored_draft::AuthoredDraftStore, + authored_draft_query::{AuthoredDraftQuery, AuthoredDraftQueryRecord}, +}; + +#[tokio::test] +async fn existing_sdk_owner_reopens_mixed_records_without_rewriting_drafts_or_operations() { + let root = tempfile::tempdir().unwrap(); + let config = MobileUserStoreConfig::from_encoded( + root.path(), + AUTHOR, + &"02".repeat(32), + NOW, + ProtectedDataAvailability::Available, + ) + .unwrap(); + assert_eq!( + config.owner_directory(), + root.path().join("radroots/users").join(AUTHOR) + ); + std::fs::create_dir_all(config.owner_directory()).unwrap(); + let runtime = RuntimeBuilder::new(config.clone()).build().await.unwrap(); + assert_eq!( + runtime.sdk_storage_status().await.unwrap().backend, + "sqlite" + ); + let query = AuthoredDraftQuery::new( + scope().author().into_bytes(), + COMPOSER_PAYLOAD_SCHEMA, + Some(ComposerStorageRecord::scope_digest(&scope()).unwrap()), + 20, + ) + .unwrap(); + assert!( + AuthoredDraftStore::query_authored_drafts(runtime.client.storage().unwrap(), query.clone()) + .await + .unwrap() + .records() + .is_empty() + ); + let mut originals = Vec::new(); + for id in [1, 2] { + let saved = runtime + .phase1_save_draft( + [id; 16], + Phase1AddCommand::CreateUpdate(CreateUpdate::new("legacy draft fixture").unwrap()), + NOW / 1000, + Vec::new(), + None, + NOW, + ) + .await + .unwrap(); + originals.push(saved.draft().clone()); + } + let queued = runtime + .phase1_queue_draft( + [2; 16], + 1, + Phase1QueuePolicy::new( + vec!["wss://relay.example".into()], + Phase1RelaySatisfaction::AllAccepted, + NOW + 60_000, + Phase1CancellationPolicy::LocalCooperative, + ) + .unwrap(), + NOW + 1, + ) + .await + .unwrap(); + assert!(queued.draft().operation_id().is_some()); + let operation = queued.push().unwrap().clone(); + originals.push(queued.draft().clone()); + let profile = runtime + .phase1_save_profile_metadata( + ProfileMetadataCommand::new( + "grower".into(), + Some("Local Grower".into()), + None, + None, + None, + None, + None, + ) + .unwrap(), + ) + .await + .unwrap(); + originals.push(profile.draft().clone()); + let good = record(10); + AuthoredDraftStore::append_authored_draft( + runtime.client.storage().unwrap(), + good.stored().clone(), + None, + ) + .await + .unwrap(); + runtime.shutdown().await.unwrap(); + drop(runtime); + + // Opening existing mixed state uses the same owner, schema and namespace. + let runtime = RuntimeBuilder::new(config.clone()).build().await.unwrap(); + let store = runtime.client.storage().unwrap(); + let page = AuthoredDraftStore::query_authored_drafts(store, query.clone()) + .await + .unwrap(); + assert_eq!( + page.records(), + [AuthoredDraftQueryRecord::Draft(good.stored().clone())] + ); + let restored = ComposerStorageRecord::decode(good.stored().clone(), &scope()).unwrap(); + assert_eq!(restored.draft(), good.draft()); + + // An ID collision must not replace the active strict journal head. + assert_eq!( + AuthoredDraftStore::append_authored_draft(store, record(1).into_stored(), None).await, + Err(Error::DraftRevisionConflict) + ); + let mut future: serde_json::Value = serde_json::from_slice(good.stored().payload()).unwrap(); + future["schema_sha256"] = serde_json::json!("injected incompatible schema checksum"); + let unknown = with_payload(11, serde_json::to_vec(&future).unwrap()); + let corrupt = with_payload(12, b"{injected incomplete conversion".to_vec()); + for (draft, expected) in [ + (&unknown, ComposerStorageError::UnsupportedSchema), + (&corrupt, ComposerStorageError::CorruptRecord), + ] { + AuthoredDraftStore::append_authored_draft(store, draft.clone(), None) + .await + .unwrap(); + let persisted = AuthoredDraftStore::authored_draft_head(store, draft.draft_id()) + .await + .unwrap() + .unwrap(); + assert_eq!( + ComposerStorageRecord::decode(persisted, &scope()).unwrap_err(), + expected + ); + assert_eq!( + AuthoredDraftStore::authored_draft_head(store, draft.draft_id()) + .await + .unwrap() + .as_ref(), + Some(draft) + ); + } + runtime.shutdown().await.unwrap(); + drop(runtime); + + let runtime = RuntimeBuilder::new(config).build().await.unwrap(); + let store = runtime.client.storage().unwrap(); + for original in &originals { + assert_eq!( + AuthoredDraftStore::authored_draft_revision( + store, + original.draft_id(), + original.revision() + ) + .await + .unwrap() + .as_ref(), + Some(original) + ); + } + for original in [&originals[0], &originals[2], &originals[3]] { + assert_eq!( + AuthoredDraftStore::authored_draft_head(store, original.draft_id()) + .await + .unwrap() + .as_ref(), + Some(original) + ); + } + assert_eq!( + runtime.phase1_draft_status([2; 16]).await.unwrap().push(), + Some(&operation) + ); + assert_eq!( + runtime.phase1_draft_status([2; 16]).await.unwrap().draft(), + queued.draft() + ); + assert_eq!( + runtime + .phase1_profile_status(*profile.draft().draft_id().as_bytes()) + .await + .unwrap() + .draft(), + profile.draft() + ); + let page = AuthoredDraftStore::query_authored_drafts(store, query) + .await + .unwrap(); + assert_eq!( + page.records(), + [good.into_stored(), unknown, corrupt].map(AuthoredDraftQueryRecord::Draft) + ); + assert!(page.next_cursor().is_none()); + runtime.shutdown().await.unwrap(); +} diff --git a/core/crates/tera_core/src/runtime/product_surface/composer/storage_tests.rs b/core/crates/tera_core/src/runtime/product_surface/composer/storage_tests.rs @@ -0,0 +1,367 @@ +use super::*; +use crate::runtime::product_surface::{ + AddCommandType, CANONICAL_ADD_COMMAND_TYPES, COMPOSER_CONTENT_MAX_BYTES, COMPOSER_MEDIA_MAX, + COMPOSER_TEXT_MAX_BYTES, ComposerFormInput, ComposerMediaInput, LocalNetworkId, + Phase1DraftEventTiming, +}; +use radroots_identity::PublicKey; +use serde_json::{Value, json}; +use std::collections::BTreeSet; + +pub(super) const AUTHOR: &str = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"; +pub(super) const NOW: u64 = 1_800_000_000_000; + +pub(super) fn scope() -> ComposerScope { + ComposerScope::new( + PublicKey::from_hex(AUTHOR).unwrap(), + LocalNetworkId::new("nearby".into()).unwrap(), + ) +} + +fn media() -> ComposerMediaInput { + ComposerMediaInput { + opaque_reference: "media:private-fixture".into(), + sha256: "a".repeat(64), + media_type: "image/png".into(), + byte_size: 512, + width: 4, + height: 4, + alt: "PRIVATE_ALT".into(), + prepared_at_unix_s: NOW / 1000, + } +} + +pub(super) fn record(id: u8) -> ComposerStorageRecord { + let mut form = ComposerFormInput::empty(AddCommandType::CreateFoodAvailability); + form.content = "PRIVATE_EDIT\n\0 café ".into(); + form.price_amount = Some("-".into()); + form.event_start_date = Some("2026-09-".into()); + form.media.push(media()); + ComposerStorageRecord::initial( + ComposerId::new([id; 16]).unwrap(), + scope(), + ComposerEditSequence::new(7).unwrap(), + ComposerPartialForm::new(form).unwrap(), + NOW, + ) + .unwrap() +} + +pub(super) fn with_payload(id: u8, payload: Vec<u8>) -> AuthoredDraft { + AuthoredDraft::initial( + AuthoredDraftId::new([id; 16]).unwrap(), + scope().author().into_bytes(), + COMPOSER_PAYLOAD_SCHEMA, + payload, + AuthoredDraftStage::Draft, + None, + NOW, + ) + .unwrap() + .with_scope(ComposerStorageRecord::scope_digest(&scope()).unwrap()) + .unwrap() +} + +fn wire() -> Value { + serde_json::from_slice(record(1).stored().payload()).unwrap() +} + +fn assert_error(stored: AuthoredDraft, error: ComposerStorageError) { + assert_eq!( + ComposerStorageRecord::decode(stored, &scope()).unwrap_err(), + error + ); +} + +#[test] +fn descriptor_pins_exact_wire_fields_bounds_families_and_scope_hash() { + let bytes = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../fixtures/composer_schema_v1.json" + )); + assert_eq!(hex::encode(Sha256::digest(bytes)), COMPOSER_SCHEMA_SHA256); + let descriptor: Value = serde_json::from_slice(bytes).unwrap(); + assert_eq!(descriptor["payload_schema"], COMPOSER_PAYLOAD_SCHEMA); + assert_eq!(descriptor["schema_version"], COMPOSER_SCHEMA_VERSION); + let encoded = wire(); + for (field, value) in [ + ("payload_fields", &encoded), + ("scope_fields", &encoded["scope"]), + ("form_fields", &encoded["form"]), + ("media_fields", &encoded["form"]["media"][0]), + ] { + let expected: BTreeSet<_> = descriptor[field] + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap()) + .collect(); + let actual: BTreeSet<_> = value + .as_object() + .unwrap() + .keys() + .map(String::as_str) + .collect(); + assert_eq!(actual, expected, "{field}"); + } + assert_eq!( + descriptor["creation_families"], + json!(CANONICAL_ADD_COMMAND_TYPES) + ); + assert_eq!( + descriptor["event_timing"], + json!([ + Phase1DraftEventTiming::AllDay, + Phase1DraftEventTiming::Timed + ]) + ); + assert_eq!( + descriptor["bounds"], + json!({ + "payload_bytes": COMPOSER_FORM_MAX_BYTES, "content_utf8_bytes": COMPOSER_CONTENT_MAX_BYTES, + "partial_text_utf8_bytes": COMPOSER_TEXT_MAX_BYTES, "media_count": COMPOSER_MEDIA_MAX, + "media_reference_bytes": 256, "media_type_utf8_bytes": 128, + "media_alt_utf8_bytes": COMPOSER_TEXT_MAX_BYTES, "media_file_bytes": 10 * 1024 * 1024, + "local_network_utf8_bytes": 256, "revision_max": ComposerRevision::MAX, + "edit_sequence_max": u64::MAX, "timestamp_ms_max": i64::MAX as u64, + }) + ); + assert_eq!( + hex::encode( + ComposerStorageRecord::scope_digest(&scope()) + .unwrap() + .as_bytes() + ), + "40da79f35835c1cd90c8d8ddb9712da70a9727a8d3d07f44f40235a5ed300800" + ); +} + +#[test] +fn exact_partial_payload_identity_and_history_round_trip_without_signing_authority() { + let original = record(1); + let restored = ComposerStorageRecord::decode(original.stored().clone(), &scope()).unwrap(); + assert_eq!(restored.draft(), original.draft()); + assert_eq!(restored.stored(), original.stored()); + assert_eq!(restored.stored().stage(), AuthoredDraftStage::Draft); + assert!(restored.stored().operation_id().is_none()); + let mut payload = wire(); + payload["edit_sequence"] = json!(19); + payload["form"]["price_amount"] = json!("1."); + let next = original + .stored() + .successor( + serde_json::to_vec(&payload).unwrap(), + AuthoredDraftStage::Draft, + None, + NOW + 1, + ) + .unwrap(); + let restored = ComposerStorageRecord::decode(next, &scope()).unwrap(); + assert_eq!(restored.draft().revision().get(), 2); + assert_eq!(restored.draft().edit_sequence().get(), 19); + assert_eq!( + restored.draft().form().input().price_amount.as_deref(), + Some("1.") + ); + assert_eq!( + original.draft().form().input().price_amount.as_deref(), + Some("-") + ); + assert_eq!(restored.stored().created_at_unix_ms(), NOW); + assert_eq!(restored.stored().updated_at_unix_ms(), NOW + 1); +} + +#[test] +fn future_version_or_checksum_is_rejected_before_interpreting_incompatible_body() { + for (version, checksum) in [(2, COMPOSER_SCHEMA_SHA256), (1, "different-schema")] { + let payload = json!({"schema_version": version, "schema_sha256": checksum, + "scope": ["future"], "edit_sequence": "future", "form": ["future"]}); + assert_error( + with_payload(1, serde_json::to_vec(&payload).unwrap()), + ComposerStorageError::UnsupportedSchema, + ); + } +} + +#[test] +fn malformed_current_records_unknown_fields_and_duplicate_keys_fail_closed() { + for bytes in [ + b"{".to_vec(), + b"{}".to_vec(), + vec![b' '; COMPOSER_FORM_MAX_BYTES + 1], + ] { + assert_error(with_payload(1, bytes), ComposerStorageError::CorruptRecord); + } + for pointer in ["", "/scope", "/form", "/form/media/0"] { + let mut payload = wire(); + payload.pointer_mut(pointer).unwrap()["unexpected"] = json!(true); + assert_error( + with_payload(1, serde_json::to_vec(&payload).unwrap()), + ComposerStorageError::CorruptRecord, + ); + } + let bytes = String::from_utf8(record(1).stored().payload().to_vec()).unwrap(); + for (find, replacement) in [ + ( + "\"schema_version\":1", + "\"schema_version\":1,\"schema_version\":1", + ), + ( + "\"edit_sequence\":7", + "\"edit_sequence\":7,\"edit_sequence\":7", + ), + ("\"width\":4", "\"width\":4,\"width\":4"), + ] { + assert!(bytes.contains(find)); + assert_error( + with_payload(1, bytes.replace(find, replacement).into_bytes()), + ComposerStorageError::CorruptRecord, + ); + } + for (pointer, value) in [ + ("/form", json!(null)), + ("/edit_sequence", json!(0)), + ( + "/form/content", + json!("x".repeat(COMPOSER_CONTENT_MAX_BYTES + 1)), + ), + ("/scope/local_network", json!("other")), + ] { + let mut payload = wire(); + *payload.pointer_mut(pointer).unwrap() = value; + assert_error( + with_payload(1, serde_json::to_vec(&payload).unwrap()), + ComposerStorageError::CorruptRecord, + ); + } +} + +#[test] +fn foreign_scope_and_schema_are_rejected_before_payload_inspection() { + let draft = with_payload(1, b"not JSON".to_vec()); + let other = ComposerScope::new( + scope().author(), + LocalNetworkId::new("other".into()).unwrap(), + ); + assert_eq!( + ComposerStorageRecord::decode(draft.clone(), &other).unwrap_err(), + ComposerStorageError::ScopeMismatch + ); + for (field, value, error) in [ + ( + "author", + json!([2; 32].to_vec()), + ComposerStorageError::ScopeMismatch, + ), + ("scope", json!(null), ComposerStorageError::ScopeMismatch), + ( + "scope", + json!([3; 32].to_vec()), + ComposerStorageError::ScopeMismatch, + ), + ( + "payload_schema", + json!("fixture.noncomposer.v1"), + ComposerStorageError::WrongSchema, + ), + ] { + let mut envelope = serde_json::to_value(&draft).unwrap(); + envelope[field] = value; + assert_error(serde_json::from_value(envelope).unwrap(), error); + } +} + +#[test] +fn storage_timestamps_revisions_and_nonediting_stages_are_checked() { + let original = record(1); + for time in [0, i64::MAX as u64 + 1, u64::MAX] { + assert_eq!( + ComposerStorageRecord::initial( + original.draft().id(), + scope(), + ComposerEditSequence::INITIAL, + original.draft().form().clone(), + time + ) + .unwrap_err(), + ComposerStorageError::InvalidTimestamp + ); + } + for time in [1, i64::MAX as u64] { + let value = ComposerStorageRecord::initial( + original.draft().id(), + scope(), + ComposerEditSequence::INITIAL, + original.draft().form().clone(), + time, + ) + .unwrap(); + assert!(ComposerStorageRecord::decode(value.into_stored(), &scope()).is_ok()); + } + let preparing = original + .stored() + .successor( + original.stored().payload().to_vec(), + AuthoredDraftStage::MediaPreparing, + None, + NOW + 1, + ) + .unwrap(); + assert_error(preparing, ComposerStorageError::CorruptRecord); + for (field, value, error) in [ + ( + "revision", + json!(i64::MAX as u64 + 1), + ComposerStorageError::CorruptRecord, + ), + ( + "updated_at_unix_ms", + json!(u64::MAX), + ComposerStorageError::InvalidTimestamp, + ), + ] { + let mut envelope = serde_json::to_value(original.stored()).unwrap(); + envelope[field] = value; + assert_error(serde_json::from_value(envelope).unwrap(), error); + } +} + +#[test] +fn maximum_escaped_form_and_scope_fit_the_envelope_bound_and_debug_is_redacted() { + let mut payload = wire(); + for (key, value) in payload["form"].as_object_mut().unwrap() { + if key == "content" { + *value = json!("\0".repeat(COMPOSER_CONTENT_MAX_BYTES)); + } else if !matches!(key.as_str(), "command_type" | "event_timing" | "media") + && !key.ends_with("unix_s") + { + *value = json!("\0".repeat(COMPOSER_TEXT_MAX_BYTES)); + } + } + let mut reference = media(); + reference.alt = "\0".repeat(COMPOSER_TEXT_MAX_BYTES); + reference.opaque_reference = "media:".to_owned() + &"a".repeat(250); + payload["form"]["media"] = json!(vec![reference; COMPOSER_MEDIA_MAX]); + let selected = ComposerScope::new( + scope().author(), + LocalNetworkId::new("é".repeat(128)).unwrap(), + ); + let value = ComposerStorageRecord::initial( + ComposerId::new([1; 16]).unwrap(), + selected.clone(), + ComposerEditSequence::new(u64::MAX).unwrap(), + serde_json::from_value(payload["form"].clone()).unwrap(), + NOW, + ) + .unwrap(); + assert!(value.stored().payload().len() <= COMPOSER_FORM_MAX_BYTES); + assert_eq!( + ComposerStorageRecord::decode(value.stored().clone(), &selected) + .unwrap() + .draft(), + value.draft() + ); + let debug = format!("{:?}", record(1)); + assert!(!debug.contains("PRIVATE_")); + assert!(!debug.contains("media:private-fixture")); +} diff --git a/core/fixtures/composer_schema_v1.json b/core/fixtures/composer_schema_v1.json @@ -0,0 +1,55 @@ +{ + "schema": "tera.composer.schema-descriptor.v1", + "payload_schema": "tera.composer.v1", + "schema_version": 1, + "encoding": "utf8_json", + "unknown_fields": "reject", + "payload_fields": ["schema_version", "schema_sha256", "scope", "edit_sequence", "form"], + "scope_fields": ["author", "local_network"], + "form_fields": [ + "command_type", "content", "identifier", "title", "summary", "location", + "event_timing", "event_start_date", "event_end_date", "event_start_unix_s", + "event_end_unix_s", "event_timezone", "price_amount", "currency", "unit", + "quantity", "food_published_at_unix_s", "food_status", "media" + ], + "media_fields": [ + "opaque_reference", "sha256", "media_type", "byte_size", "width", "height", + "alt", "prepared_at_unix_s" + ], + "creation_families": [ + "CreateUpdate", "CreatePhotoUpdate", "CreateAsk", "CreateEvent", "CreateFoodAvailability" + ], + "event_timing": ["all_day", "timed"], + "bounds": { + "payload_bytes": 1048576, + "content_utf8_bytes": 65535, + "partial_text_utf8_bytes": 1024, + "media_count": 20, + "media_reference_bytes": 256, + "media_type_utf8_bytes": 128, + "media_alt_utf8_bytes": 1024, + "media_file_bytes": 10485760, + "local_network_utf8_bytes": 256, + "revision_max": 9223372036854775807, + "edit_sequence_max": 18446744073709551615, + "timestamp_ms_max": 9223372036854775807 + }, + "envelope": { + "owner": "AuthoredDraft", + "draft_id": "nonzero_16_bytes", + "author": "validated_x_only_public_key", + "scope": "required_stable_scope_digest", + "stage": "draft", + "operation_id": null, + "revision": "positive_checked_shared_revision", + "timestamps": "positive_created_and_nondecreasing_updated" + }, + "scope_hash": { + "algorithm": "sha256", + "domain": "tera.composer-scope.v1", + "zero_terminator": true, + "parts": ["author_32_bytes", "local_network_length_u32_be", "local_network_utf8"] + }, + "partial_fields": "preserve_exact_utf8_without_publication_validation", + "media_metadata": "not_evidence_of_byte_ownership_or_upload_readiness" +} diff --git a/release/provenance.json b/release/provenance.json @@ -2,7 +2,7 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "e2e025ee9a29182970f52f53593796afd154cdfb09aba197474577103dcb4ad6", - "ffi_provenance_sha256": "47abd7034be7df2e7f58cf6404dbcff14ce75599b38b999f24064f64b05965d5", + "ffi_provenance_sha256": "2cc3c61abb397437ace14ca9d9aec9da57eee00064152a8c6bd4faac7b7536ec", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "61e3d99a2c7f051a91d6a38b3b1f7974fb4c56da3a1c9758f5ad06fc66f1d856", @@ -22,7 +22,7 @@ "lib_revision": "363b9be18831be2fd6ef505d6154fb878ed8efd7", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "94ae067a374726cdaf6b4ca0a5e44663c57fcdc5334060c5ffef5e79cfbf04c0", - "tera_ffi_source_tree": "628534dfa9763f18b3ebba50dc25b1adb71801ba", + "tera_ffi_source_tree": "e0ba029e9f889064f46953f7033508d9ea2e48cf", "xcode_package_lock_sha256": "c7f41934ea25f7a287bdc4f3a6ecabbf09a3a0bdd0f5a3e58183f355ca814096" }, "version": "0.1.0-alpha" diff --git a/test-fixtures/legacy-identifiers.v1.json b/test-fixtures/legacy-identifiers.v1.json @@ -1807,6 +1807,10 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/composer/storage_sqlite_tests.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/projection.rs", "count": 7 }, @@ -4239,6 +4243,10 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/composer/storage_tests.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/composer/tests.rs", "count": 1 }, @@ -4581,6 +4589,14 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/composer/storage.rs", + "count": 1 + }, + { + "path": "core/crates/tera_core/src/runtime/product_surface/composer/storage_sqlite_tests.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/outbox.rs", "count": 5 },