field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

commit 47a7ee7dfe288847982c54d6468df8bd02238bd4
parent ca0f36d84bfb6db55a4de61b2cb2357dd1ac590c
Author: triesap <tyson@radroots.org>
Date:   Mon, 21 Sep 2026 22:08:17 +0000

retraction: enforce author and target authority

- Require exact signed source and shared product identity
- Recheck historical plans before signing and publication
- Preserve passive recovery, cancellation and repeated requests
- Qualify forged targets and matching native consumers

Diffstat:
MREADME.md | 8++++++++
MTeraFFI/provenance.json | 54+++++++++++++++++++++++++++---------------------------
MTeraFFI/source.lock | 4++--
MTeraFFI/source/aarch64-apple-darwin.json | 128++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
MTeraFFI/source/aarch64-apple-ios-sim.json | 128++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
MTeraFFI/source/aarch64-apple-ios.json | 128++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mcore/crates/tera_core/src/runtime/product_surface/outbox.rs | 143+++++++++----------------------------------------------------------------------
Mcore/crates/tera_core/src/runtime/product_surface/outbox/advance.rs | 29++++++++++++++++-------------
Mcore/crates/tera_core/src/runtime/product_surface/outbox/coordinate.rs | 5++++-
Acore/crates/tera_core/src/runtime/product_surface/outbox/retraction.rs | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_core/src/runtime/product_surface/outbox/retraction_authority.rs | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/tera_core/src/runtime/product_surface/outbox/revision_coordinator.rs | 3++-
Mcore/crates/tera_core/src/runtime/product_surface/outbox/revision_delivery.rs | 3++-
Mcore/crates/tera_core/src/runtime/product_surface/outbox/revision_preparation.rs | 10++++++++++
Mcore/crates/tera_core/src/runtime/product_surface/outbox/revision_status.rs | 8+++++++-
Acore/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_authority_tests.rs | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_effect_tests.rs | 257+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_source_tests.rs | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_support.rs | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_delivery_support.rs | 14+-------------
Mcore/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_delivery_tests.rs | 8+++-----
Mcore/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_preparation_tests.rs | 17+++++------------
Mcore/crates/tera_core/src/runtime/product_surface/projection.rs | 76++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mcore/crates/tera_ffi/tests/revision_status_contract.rs | 67++++++++++++++++++++++++++++++++++++++-----------------------------
Mcore/crates/tera_ffi/tests/runtime_delegation.rs | 45++++++++++++++++++++++++++++++++++++++++-----
Acore/crates/tera_ffi/tests/support/retraction.rs | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mrelease/provenance.json | 4++--
Mtest-fixtures/legacy-identifiers.v1.json | 46+++++++++++++++++++++++++++++++++-------------
28 files changed, 1482 insertions(+), 410 deletions(-)

diff --git a/README.md b/README.md @@ -299,6 +299,14 @@ policy; inspecting or acting on a revision does not replace composer editing. Meeting an Any relay policy does not imply completion at every saved destination, and even a fully accepted retraction remains a request, not proof of erasure. +Retraction requires the exact signed original in the current account's store. +Rust verifies its signature, author, event kind, address and product card identity +before capture and again before signing, local admission and delivery. A native +card or an older saved signature alone does not authorize those effects. Missing +source evidence holds publication while retained receipts and cancellation remain +available. Already retracted originals remain eligible evidence for repeated +requests; their visibility does not grant or remove authorship. + Addressable publication keeps its full kind, author and identifier coordinate. Local requests acquire durable ownership before signing or publication, and a superseded request cannot regain that ownership by retrying after restart. diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -22,9 +22,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 77626640, + "bytes": 77721544, "path": "TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "8ce3ff37fd19da343d3027e71a2a913c2563b4ff6c884b40090a9809f68cf5a2" + "sha256": "3d58282922df6f90668ceffde976e88137cc3e467cae7e2885f9644a8a16f1ec" }, { "bytes": 88720, @@ -37,9 +37,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 77693008, + "bytes": 77788528, "path": "TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "e7ec700d624c53c8c11e143e3819fd65215af4dadb2117b76c0a4e7d33ba712e" + "sha256": "192ea19311867527593d7a757e115e45f315d1475822ec6d52c80cdba946671c" }, { "bytes": 55165, @@ -77,34 +77,34 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 22625552, + "bytes": 22686736, "path": "native/aarch64-apple-darwin/libtera_ffi.dylib", - "sha256": "99c97e184256c6119518290fd859a2e8b31bbd0a78bf9eb63488e88f56eb6686" + "sha256": "a1b112a78aaa1419b4fab8dfb2624dd3529f9206eedc45e64c20430b5b7d8e89" }, { - "bytes": 77626640, + "bytes": 77721544, "path": "native/aarch64-apple-ios-sim/libtera_ffi.a", - "sha256": "8ce3ff37fd19da343d3027e71a2a913c2563b4ff6c884b40090a9809f68cf5a2" + "sha256": "3d58282922df6f90668ceffde976e88137cc3e467cae7e2885f9644a8a16f1ec" }, { - "bytes": 77693008, + "bytes": 77788528, "path": "native/aarch64-apple-ios/libtera_ffi.a", - "sha256": "e7ec700d624c53c8c11e143e3819fd65215af4dadb2117b76c0a4e7d33ba712e" + "sha256": "192ea19311867527593d7a757e115e45f315d1475822ec6d52c80cdba946671c" }, { - "bytes": 122523, + "bytes": 124608, "path": "source/aarch64-apple-darwin.json", - "sha256": "fdfa46afe9595e8bc058336938efeafc6900cd8699c0d267707f0cdfedb6a66d" + "sha256": "203767e8e334c6648618917ff2f6468dd285711cbdf1bf59ea1c7aa573d27357" }, { - "bytes": 122367, + "bytes": 124452, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "3640eb7a42aa71c5444c6d08c1313e2245b06eb443665c6de03daa5154075901" + "sha256": "06a5e96fdac823083eedb2341c46b4f84de3e66fc80da395789d0176b8340f2b" }, { - "bytes": 122363, + "bytes": 124448, "path": "source/aarch64-apple-ios.json", - "sha256": "e4d5eabf0d110c4c43328b8203df2a51394bfea1340af16a4a015a279f041c12" + "sha256": "4ec8cc644372b9716ecf75f1c16843dcb704788e4ba827df78288aa61c726e70" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "681e757519909bf8e177ed3238398988c9d0a89d" + "tree": "555ee9dfbfd65fcdb930d41fdb4f3145b2f2f431" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -139,9 +139,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 77626640, + "bytes": 77721544, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "8ce3ff37fd19da343d3027e71a2a913c2563b4ff6c884b40090a9809f68cf5a2" + "sha256": "3d58282922df6f90668ceffde976e88137cc3e467cae7e2885f9644a8a16f1ec" }, { "bytes": 88720, @@ -154,9 +154,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 77693008, + "bytes": 77788528, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "e7ec700d624c53c8c11e143e3819fd65215af4dadb2117b76c0a4e7d33ba712e" + "sha256": "192ea19311867527593d7a757e115e45f315d1475822ec6d52c80cdba946671c" }, { "bytes": 730192, @@ -174,19 +174,19 @@ "sha256": "81a943ef98405676b4f65932a4a8d4bdb4011923f65fa2e3496fcb58e1d68be8" }, { - "bytes": 122523, + "bytes": 124608, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "fdfa46afe9595e8bc058336938efeafc6900cd8699c0d267707f0cdfedb6a66d" + "sha256": "203767e8e334c6648618917ff2f6468dd285711cbdf1bf59ea1c7aa573d27357" }, { - "bytes": 122367, + "bytes": 124452, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "3640eb7a42aa71c5444c6d08c1313e2245b06eb443665c6de03daa5154075901" + "sha256": "06a5e96fdac823083eedb2341c46b4f84de3e66fc80da395789d0176b8340f2b" }, { - "bytes": 122363, + "bytes": 124448, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "e4d5eabf0d110c4c43328b8203df2a51394bfea1340af16a4a015a279f041c12" + "sha256": "4ec8cc644372b9716ecf75f1c16843dcb704788e4ba827df78288aa61c726e70" } ], "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 = "681e757519909bf8e177ed3238398988c9d0a89d" -manifest_sha256 = "c184a6b1dd38541e27f792fbd41f90690e6c63c89165951a4526c06b60cf5800" +source_tree = "555ee9dfbfd65fcdb930d41fdb4f3145b2f2f431" +manifest_sha256 = "04d36e07e071305a5a665f64b836565fb58a4a4bf8e018087881f74badb2227d" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -849,16 +849,16 @@ "sha256": "0dc99c1cc9cba832448368fe295a2a2b0ade4bc160d7f8f81c948b2460398794" }, "core/crates/tera_core/src/runtime/product_surface/outbox.rs": { - "bytes": 163071, - "git_blob": "e9c6244cf23a722dc08973a3fb69178e3787226c", + "bytes": 158747, + "git_blob": "5983f3f1c0696983948f141441beb0aa39d50cb3", "mode": "100644", - "sha256": "aced3db673ba9f356416d269020ab3b5f8346d50cfccf19e4c2d914556ecf64e" + "sha256": "87ec51e858c218ddb318db14f4bef4e591d7b4997f23431e8630ea2794992576" }, "core/crates/tera_core/src/runtime/product_surface/outbox/advance.rs": { - "bytes": 6023, - "git_blob": "17357131136396c6b25cb60b85e586e9356f84bb", + "bytes": 6042, + "git_blob": "acf620cc3a55f4848e4e6cf230fa9c66189dd358", "mode": "100644", - "sha256": "03deb13b477a7897c7f8da921fcefe35ddb3b6b2324da49c90bea1a457cdec72" + "sha256": "9414d32451da32b586cd012459255926fc245f8547436bedae4e54597fc4b10f" }, "core/crates/tera_core/src/runtime/product_surface/outbox/configuration.rs": { "bytes": 5088, @@ -867,10 +867,10 @@ "sha256": "60e185523bacac3123fabb45214bf76a1b04a45b00fe326216e88785e54059b9" }, "core/crates/tera_core/src/runtime/product_surface/outbox/coordinate.rs": { - "bytes": 3386, - "git_blob": "fbf6865903177f279d8cff868e5757ad8cf4a30a", + "bytes": 3507, + "git_blob": "d0145136b2623fc997b02e8c71759817cfd8e744", "mode": "100644", - "sha256": "e08f8ca6432b2b592aa3dc93af358c6bcee6cc9c09b341ab194088107f1b745c" + "sha256": "4adf0a0f945518cbec0e594ba15128db36a744cca4f872efd945b8b826579b33" }, "core/crates/tera_core/src/runtime/product_surface/outbox/inventory.rs": { "bytes": 9243, @@ -914,23 +914,35 @@ "mode": "100644", "sha256": "b3a063da96304c439b92504a294dde8a4938b97b01cb237fa203372b17ab17f6" }, + "core/crates/tera_core/src/runtime/product_surface/outbox/retraction.rs": { + "bytes": 4425, + "git_blob": "6c080412a147952802ef47b61a27847a75ba629e", + "mode": "100644", + "sha256": "e9d8e53bcd8ac38f26b5ac5b02ba0a8629d63895a2f9fb4c1ec4deec8217cc91" + }, + "core/crates/tera_core/src/runtime/product_surface/outbox/retraction_authority.rs": { + "bytes": 6399, + "git_blob": "20be9d988c573720d2dd3f1f561700578d24661e", + "mode": "100644", + "sha256": "bef44329536f50366142485918993f508594337b5e2eb46c786cc8c8bf5dc932" + }, "core/crates/tera_core/src/runtime/product_surface/outbox/revision_coordinator.rs": { - "bytes": 7105, - "git_blob": "8c110a37e891f941393a5a1d9b7ef55a2f97e24f", + "bytes": 7155, + "git_blob": "cd1279ebbe10fb956847c5fea5b16dc54a39e7bc", "mode": "100644", - "sha256": "7fc6e4c36d0ec184e8d4559ee89e75ef42c0a5d79b085d98575dd130b3b038dd" + "sha256": "a495abb8019d0dd59edeff5af043736c37821fc66b95ab0ca1640461d3c97bd0" }, "core/crates/tera_core/src/runtime/product_surface/outbox/revision_delivery.rs": { - "bytes": 9085, - "git_blob": "fa7fed9b805a8b5f5739cac96dbc6e5e61bc7099", + "bytes": 9112, + "git_blob": "58a844ec6e999e4b979907e8489ae279ad084141", "mode": "100644", - "sha256": "e8853d0e5413c7d1168d5e89696cff8760a5e743a35e086de1e78745f86d4b86" + "sha256": "c797fc6c828d4fd36abb20c74472526db6cc348de1734c11a0a05e2c5740ec98" }, "core/crates/tera_core/src/runtime/product_surface/outbox/revision_preparation.rs": { - "bytes": 4492, - "git_blob": "6a373bb9e273c7dc1039061783adc46472defdbe", + "bytes": 4852, + "git_blob": "6acec1ef232c8f4599330555a464b931de54cf41", "mode": "100644", - "sha256": "1ffe8f3e29aae5b876a5e0e314117c197f628a615d2f39173bd9af6bbb47e790" + "sha256": "ada51be0ee64130b3e6d524f7f26c4d8007e1823b0e462ed3f72fa8e35c81135" }, "core/crates/tera_core/src/runtime/product_surface/outbox/revision_source.rs": { "bytes": 4500, @@ -939,10 +951,10 @@ "sha256": "3ea623a3c78b70313bb5fee89ac968d671b0822dfa9c1d82a7046a6f1eb5d2ff" }, "core/crates/tera_core/src/runtime/product_surface/outbox/revision_status.rs": { - "bytes": 10127, - "git_blob": "19905196b9ec6bcb616529387eb39b312e445404", + "bytes": 10612, + "git_blob": "76a83628322645f2748f5cbf617ed30238237d44", "mode": "100644", - "sha256": "0e83616701321035cb2c83fbd6477d03d083316bb22596924ea2d69d1606b2a0" + "sha256": "5c84abdebac4ae0d0f1a56a329eb2315f7634c9f0889598b3cbede038a2dc752" }, "core/crates/tera_core/src/runtime/product_surface/outbox/tests/coordinate_clock_tests.rs": { "bytes": 2937, @@ -992,23 +1004,47 @@ "mode": "100644", "sha256": "f627f5c0461cdb233f58b24a25b2e9dc79c3f14c6418dec0e1ee05874a5284df" }, + "core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_authority_tests.rs": { + "bytes": 5350, + "git_blob": "d0d4ea484f2f35a246dcdc135b5b5841597c7ba5", + "mode": "100644", + "sha256": "da1a248b23ea657522ef55694ed7641e1d7521ed0866796792323bb579e33b58" + }, + "core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_effect_tests.rs": { + "bytes": 8277, + "git_blob": "69223f8265520be87f66a5a44752af5a5ad9214e", + "mode": "100644", + "sha256": "4d651b11b000d4b3ef7733b1728d69b3efdfb52adac1b7e1e1f48f5a1120b924" + }, + "core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_source_tests.rs": { + "bytes": 3789, + "git_blob": "90595995f7094b9225425c81822b3cfa1fe6af42", + "mode": "100644", + "sha256": "18077a2662581cf38a923cd9c64daee6e8bff01f26a23a45b7af805a65bde9eb" + }, + "core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_support.rs": { + "bytes": 1873, + "git_blob": "cf5a1ea4009a7f9a827c04a45ffe2a951b031268", + "mode": "100644", + "sha256": "945391aafdc9f2459d29c52cbe710a01b5cebeb1f08bd784dd6a6d06a203ced2" + }, "core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_delivery_support.rs": { - "bytes": 5868, - "git_blob": "071eac1958db8ec5d426d9321e8e2829eeba6009", + "bytes": 5587, + "git_blob": "e1b8f8220121bee9c86b1cac90c6b29e347240f5", "mode": "100644", - "sha256": "9aec5cf6970c10af9b89589544dd2baecbc77cbb50a37aecd640e0e2b36bd2b1" + "sha256": "a4088255c1e829844810e06ee7d552d71ad4a7ded1d78bc4d4a3ed4508be08ff" }, "core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_delivery_tests.rs": { - "bytes": 12834, - "git_blob": "79dbac65c04ae0917482ea86a73b84af5df722a4", + "bytes": 12788, + "git_blob": "b839bf9072ee163586a4fc7f5dc20fc9fbc2af7a", "mode": "100644", - "sha256": "068803b7edd8deccd7a766e1eb2c4851c54850e4f47192ff200ff8781adf928b" + "sha256": "a597cf04c1bf87e5fa4b2f42c206da19bdd1ea2ca50c0fc5d864c5cab58e936b" }, "core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_preparation_tests.rs": { - "bytes": 10823, - "git_blob": "c73b288ac2a6229928e0d4e9159fed123145c597", + "bytes": 10735, + "git_blob": "542d62ca8fb926552d8b372b003ae2c2df3fef0a", "mode": "100644", - "sha256": "707a8e087f5ec9b43f71812f0ae2ff37edabe9b75d0b5031415d0f03e2cfd1a0" + "sha256": "d323ae611668bf7f6950328d87d99563456568f79d03d1c5650f762c425ed0ef" }, "core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_status_tests.rs": { "bytes": 7049, @@ -1029,10 +1065,10 @@ "sha256": "8b0ecc3c1b75aabe971c21532e023dd7e0a3fba01a4ca9f9f079db1ed3775cee" }, "core/crates/tera_core/src/runtime/product_surface/projection.rs": { - "bytes": 23423, - "git_blob": "a0186bb29cafbaab4268af10acd5dc701cf4c228", + "bytes": 24159, + "git_blob": "c337e15c0906e7ccff52997532ca3ed2726f3bdf", "mode": "100644", - "sha256": "06923cd1020ac2609cf78f9b25b6def7a486a6d07583d25e0b80f34c7c287ec5" + "sha256": "6107c43ddcb8ed1f2ce4229a6c92dd40a3288410e1bdccd39b0838993c6eb58f" }, "core/crates/tera_core/src/runtime/product_surface/publication.rs": { "bytes": 2790, @@ -2043,16 +2079,16 @@ "sha256": "1d2e6015400b2ba03819ed01bae674817a18a2c8e626ba3dfeca24dc2fb8763c" }, "core/crates/tera_ffi/tests/revision_status_contract.rs": { - "bytes": 3304, - "git_blob": "6667bb0a0a639e3a40c8608c4c517dc682bc02b0", + "bytes": 3263, + "git_blob": "4645d038adf64ec8e254ac3a822c93629b76b626", "mode": "100644", - "sha256": "57bfe282e7836f28ee252619473b6cc857b9d88fe1e215e63f0c022cfb9a7c33" + "sha256": "db41f36cd1d8f0dc9d1f6b065b0d4dbaaab7d11b01a185a05c5b478a5c7a5816" }, "core/crates/tera_ffi/tests/runtime_delegation.rs": { - "bytes": 31638, - "git_blob": "6f522596187e553db15bb7945aa795bbc96a9354", + "bytes": 32771, + "git_blob": "87f18bfbabd578955c3bf0a0a9f79a193d77ca7c", "mode": "100644", - "sha256": "99004edb9aa4af89118c1f494fd670c5641721c41251a732bc327822f23b676c" + "sha256": "facba685b9148ca651a82c21acbd10c6b11b85ca067e23971739eb28e1b79a13" }, "core/crates/tera_ffi/tests/runtime_lifecycle.rs": { "bytes": 4293, @@ -2096,6 +2132,12 @@ "mode": "100644", "sha256": "135ed57af77bdf67979e423cd6ef3c0a1cef051ea88d109ad3f2937393e688c8" }, + "core/crates/tera_ffi/tests/support/retraction.rs": { + "bytes": 3703, + "git_blob": "b782880c72f15ba31abf6f0465b27c676cf14a12", + "mode": "100644", + "sha256": "aaa566b9d432dfdbc5fb403967de61de6ed81983826ec051f912656c915208ed" + }, "core/crates/tera_ffi/tests/today_sync_contract.rs": { "bytes": 4854, "git_blob": "d63714958e422bd39b71a6db92b567c080d07bab", @@ -2595,13 +2637,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 167808, - "git_blob": "1c21e5b73d9effa6ebcf4cad298868574b9ceee9", + "bytes": 168574, + "git_blob": "4efe38933170b055e6e6a79f8fc719243f24ab5e", "mode": "100644", - "sha256": "4362152b23a985ae966ed67b08635454a65434b5d440fd862224bb08316199a9" + "sha256": "48c0cb55ae2695e065ac47ff290c84fed0913e017363764bb3fcd9106f88d396" } }, "policy": "staged_inputs", - "tree": "681e757519909bf8e177ed3238398988c9d0a89d" + "tree": "555ee9dfbfd65fcdb930d41fdb4f3145b2f2f431" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -845,16 +845,16 @@ "sha256": "0dc99c1cc9cba832448368fe295a2a2b0ade4bc160d7f8f81c948b2460398794" }, "core/crates/tera_core/src/runtime/product_surface/outbox.rs": { - "bytes": 163071, - "git_blob": "e9c6244cf23a722dc08973a3fb69178e3787226c", + "bytes": 158747, + "git_blob": "5983f3f1c0696983948f141441beb0aa39d50cb3", "mode": "100644", - "sha256": "aced3db673ba9f356416d269020ab3b5f8346d50cfccf19e4c2d914556ecf64e" + "sha256": "87ec51e858c218ddb318db14f4bef4e591d7b4997f23431e8630ea2794992576" }, "core/crates/tera_core/src/runtime/product_surface/outbox/advance.rs": { - "bytes": 6023, - "git_blob": "17357131136396c6b25cb60b85e586e9356f84bb", + "bytes": 6042, + "git_blob": "acf620cc3a55f4848e4e6cf230fa9c66189dd358", "mode": "100644", - "sha256": "03deb13b477a7897c7f8da921fcefe35ddb3b6b2324da49c90bea1a457cdec72" + "sha256": "9414d32451da32b586cd012459255926fc245f8547436bedae4e54597fc4b10f" }, "core/crates/tera_core/src/runtime/product_surface/outbox/configuration.rs": { "bytes": 5088, @@ -863,10 +863,10 @@ "sha256": "60e185523bacac3123fabb45214bf76a1b04a45b00fe326216e88785e54059b9" }, "core/crates/tera_core/src/runtime/product_surface/outbox/coordinate.rs": { - "bytes": 3386, - "git_blob": "fbf6865903177f279d8cff868e5757ad8cf4a30a", + "bytes": 3507, + "git_blob": "d0145136b2623fc997b02e8c71759817cfd8e744", "mode": "100644", - "sha256": "e08f8ca6432b2b592aa3dc93af358c6bcee6cc9c09b341ab194088107f1b745c" + "sha256": "4adf0a0f945518cbec0e594ba15128db36a744cca4f872efd945b8b826579b33" }, "core/crates/tera_core/src/runtime/product_surface/outbox/inventory.rs": { "bytes": 9243, @@ -910,23 +910,35 @@ "mode": "100644", "sha256": "b3a063da96304c439b92504a294dde8a4938b97b01cb237fa203372b17ab17f6" }, + "core/crates/tera_core/src/runtime/product_surface/outbox/retraction.rs": { + "bytes": 4425, + "git_blob": "6c080412a147952802ef47b61a27847a75ba629e", + "mode": "100644", + "sha256": "e9d8e53bcd8ac38f26b5ac5b02ba0a8629d63895a2f9fb4c1ec4deec8217cc91" + }, + "core/crates/tera_core/src/runtime/product_surface/outbox/retraction_authority.rs": { + "bytes": 6399, + "git_blob": "20be9d988c573720d2dd3f1f561700578d24661e", + "mode": "100644", + "sha256": "bef44329536f50366142485918993f508594337b5e2eb46c786cc8c8bf5dc932" + }, "core/crates/tera_core/src/runtime/product_surface/outbox/revision_coordinator.rs": { - "bytes": 7105, - "git_blob": "8c110a37e891f941393a5a1d9b7ef55a2f97e24f", + "bytes": 7155, + "git_blob": "cd1279ebbe10fb956847c5fea5b16dc54a39e7bc", "mode": "100644", - "sha256": "7fc6e4c36d0ec184e8d4559ee89e75ef42c0a5d79b085d98575dd130b3b038dd" + "sha256": "a495abb8019d0dd59edeff5af043736c37821fc66b95ab0ca1640461d3c97bd0" }, "core/crates/tera_core/src/runtime/product_surface/outbox/revision_delivery.rs": { - "bytes": 9085, - "git_blob": "fa7fed9b805a8b5f5739cac96dbc6e5e61bc7099", + "bytes": 9112, + "git_blob": "58a844ec6e999e4b979907e8489ae279ad084141", "mode": "100644", - "sha256": "e8853d0e5413c7d1168d5e89696cff8760a5e743a35e086de1e78745f86d4b86" + "sha256": "c797fc6c828d4fd36abb20c74472526db6cc348de1734c11a0a05e2c5740ec98" }, "core/crates/tera_core/src/runtime/product_surface/outbox/revision_preparation.rs": { - "bytes": 4492, - "git_blob": "6a373bb9e273c7dc1039061783adc46472defdbe", + "bytes": 4852, + "git_blob": "6acec1ef232c8f4599330555a464b931de54cf41", "mode": "100644", - "sha256": "1ffe8f3e29aae5b876a5e0e314117c197f628a615d2f39173bd9af6bbb47e790" + "sha256": "ada51be0ee64130b3e6d524f7f26c4d8007e1823b0e462ed3f72fa8e35c81135" }, "core/crates/tera_core/src/runtime/product_surface/outbox/revision_source.rs": { "bytes": 4500, @@ -935,10 +947,10 @@ "sha256": "3ea623a3c78b70313bb5fee89ac968d671b0822dfa9c1d82a7046a6f1eb5d2ff" }, "core/crates/tera_core/src/runtime/product_surface/outbox/revision_status.rs": { - "bytes": 10127, - "git_blob": "19905196b9ec6bcb616529387eb39b312e445404", + "bytes": 10612, + "git_blob": "76a83628322645f2748f5cbf617ed30238237d44", "mode": "100644", - "sha256": "0e83616701321035cb2c83fbd6477d03d083316bb22596924ea2d69d1606b2a0" + "sha256": "5c84abdebac4ae0d0f1a56a329eb2315f7634c9f0889598b3cbede038a2dc752" }, "core/crates/tera_core/src/runtime/product_surface/outbox/tests/coordinate_clock_tests.rs": { "bytes": 2937, @@ -988,23 +1000,47 @@ "mode": "100644", "sha256": "f627f5c0461cdb233f58b24a25b2e9dc79c3f14c6418dec0e1ee05874a5284df" }, + "core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_authority_tests.rs": { + "bytes": 5350, + "git_blob": "d0d4ea484f2f35a246dcdc135b5b5841597c7ba5", + "mode": "100644", + "sha256": "da1a248b23ea657522ef55694ed7641e1d7521ed0866796792323bb579e33b58" + }, + "core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_effect_tests.rs": { + "bytes": 8277, + "git_blob": "69223f8265520be87f66a5a44752af5a5ad9214e", + "mode": "100644", + "sha256": "4d651b11b000d4b3ef7733b1728d69b3efdfb52adac1b7e1e1f48f5a1120b924" + }, + "core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_source_tests.rs": { + "bytes": 3789, + "git_blob": "90595995f7094b9225425c81822b3cfa1fe6af42", + "mode": "100644", + "sha256": "18077a2662581cf38a923cd9c64daee6e8bff01f26a23a45b7af805a65bde9eb" + }, + "core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_support.rs": { + "bytes": 1873, + "git_blob": "cf5a1ea4009a7f9a827c04a45ffe2a951b031268", + "mode": "100644", + "sha256": "945391aafdc9f2459d29c52cbe710a01b5cebeb1f08bd784dd6a6d06a203ced2" + }, "core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_delivery_support.rs": { - "bytes": 5868, - "git_blob": "071eac1958db8ec5d426d9321e8e2829eeba6009", + "bytes": 5587, + "git_blob": "e1b8f8220121bee9c86b1cac90c6b29e347240f5", "mode": "100644", - "sha256": "9aec5cf6970c10af9b89589544dd2baecbc77cbb50a37aecd640e0e2b36bd2b1" + "sha256": "a4088255c1e829844810e06ee7d552d71ad4a7ded1d78bc4d4a3ed4508be08ff" }, "core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_delivery_tests.rs": { - "bytes": 12834, - "git_blob": "79dbac65c04ae0917482ea86a73b84af5df722a4", + "bytes": 12788, + "git_blob": "b839bf9072ee163586a4fc7f5dc20fc9fbc2af7a", "mode": "100644", - "sha256": "068803b7edd8deccd7a766e1eb2c4851c54850e4f47192ff200ff8781adf928b" + "sha256": "a597cf04c1bf87e5fa4b2f42c206da19bdd1ea2ca50c0fc5d864c5cab58e936b" }, "core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_preparation_tests.rs": { - "bytes": 10823, - "git_blob": "c73b288ac2a6229928e0d4e9159fed123145c597", + "bytes": 10735, + "git_blob": "542d62ca8fb926552d8b372b003ae2c2df3fef0a", "mode": "100644", - "sha256": "707a8e087f5ec9b43f71812f0ae2ff37edabe9b75d0b5031415d0f03e2cfd1a0" + "sha256": "d323ae611668bf7f6950328d87d99563456568f79d03d1c5650f762c425ed0ef" }, "core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_status_tests.rs": { "bytes": 7049, @@ -1025,10 +1061,10 @@ "sha256": "8b0ecc3c1b75aabe971c21532e023dd7e0a3fba01a4ca9f9f079db1ed3775cee" }, "core/crates/tera_core/src/runtime/product_surface/projection.rs": { - "bytes": 23423, - "git_blob": "a0186bb29cafbaab4268af10acd5dc701cf4c228", + "bytes": 24159, + "git_blob": "c337e15c0906e7ccff52997532ca3ed2726f3bdf", "mode": "100644", - "sha256": "06923cd1020ac2609cf78f9b25b6def7a486a6d07583d25e0b80f34c7c287ec5" + "sha256": "6107c43ddcb8ed1f2ce4229a6c92dd40a3288410e1bdccd39b0838993c6eb58f" }, "core/crates/tera_core/src/runtime/product_surface/publication.rs": { "bytes": 2790, @@ -2039,16 +2075,16 @@ "sha256": "1d2e6015400b2ba03819ed01bae674817a18a2c8e626ba3dfeca24dc2fb8763c" }, "core/crates/tera_ffi/tests/revision_status_contract.rs": { - "bytes": 3304, - "git_blob": "6667bb0a0a639e3a40c8608c4c517dc682bc02b0", + "bytes": 3263, + "git_blob": "4645d038adf64ec8e254ac3a822c93629b76b626", "mode": "100644", - "sha256": "57bfe282e7836f28ee252619473b6cc857b9d88fe1e215e63f0c022cfb9a7c33" + "sha256": "db41f36cd1d8f0dc9d1f6b065b0d4dbaaab7d11b01a185a05c5b478a5c7a5816" }, "core/crates/tera_ffi/tests/runtime_delegation.rs": { - "bytes": 31638, - "git_blob": "6f522596187e553db15bb7945aa795bbc96a9354", + "bytes": 32771, + "git_blob": "87f18bfbabd578955c3bf0a0a9f79a193d77ca7c", "mode": "100644", - "sha256": "99004edb9aa4af89118c1f494fd670c5641721c41251a732bc327822f23b676c" + "sha256": "facba685b9148ca651a82c21acbd10c6b11b85ca067e23971739eb28e1b79a13" }, "core/crates/tera_ffi/tests/runtime_lifecycle.rs": { "bytes": 4293, @@ -2092,6 +2128,12 @@ "mode": "100644", "sha256": "135ed57af77bdf67979e423cd6ef3c0a1cef051ea88d109ad3f2937393e688c8" }, + "core/crates/tera_ffi/tests/support/retraction.rs": { + "bytes": 3703, + "git_blob": "b782880c72f15ba31abf6f0465b27c676cf14a12", + "mode": "100644", + "sha256": "aaa566b9d432dfdbc5fb403967de61de6ed81983826ec051f912656c915208ed" + }, "core/crates/tera_ffi/tests/today_sync_contract.rs": { "bytes": 4854, "git_blob": "d63714958e422bd39b71a6db92b567c080d07bab", @@ -2591,13 +2633,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 167808, - "git_blob": "1c21e5b73d9effa6ebcf4cad298868574b9ceee9", + "bytes": 168574, + "git_blob": "4efe38933170b055e6e6a79f8fc719243f24ab5e", "mode": "100644", - "sha256": "4362152b23a985ae966ed67b08635454a65434b5d440fd862224bb08316199a9" + "sha256": "48c0cb55ae2695e065ac47ff290c84fed0913e017363764bb3fcd9106f88d396" } }, "policy": "staged_inputs", - "tree": "681e757519909bf8e177ed3238398988c9d0a89d" + "tree": "555ee9dfbfd65fcdb930d41fdb4f3145b2f2f431" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -845,16 +845,16 @@ "sha256": "0dc99c1cc9cba832448368fe295a2a2b0ade4bc160d7f8f81c948b2460398794" }, "core/crates/tera_core/src/runtime/product_surface/outbox.rs": { - "bytes": 163071, - "git_blob": "e9c6244cf23a722dc08973a3fb69178e3787226c", + "bytes": 158747, + "git_blob": "5983f3f1c0696983948f141441beb0aa39d50cb3", "mode": "100644", - "sha256": "aced3db673ba9f356416d269020ab3b5f8346d50cfccf19e4c2d914556ecf64e" + "sha256": "87ec51e858c218ddb318db14f4bef4e591d7b4997f23431e8630ea2794992576" }, "core/crates/tera_core/src/runtime/product_surface/outbox/advance.rs": { - "bytes": 6023, - "git_blob": "17357131136396c6b25cb60b85e586e9356f84bb", + "bytes": 6042, + "git_blob": "acf620cc3a55f4848e4e6cf230fa9c66189dd358", "mode": "100644", - "sha256": "03deb13b477a7897c7f8da921fcefe35ddb3b6b2324da49c90bea1a457cdec72" + "sha256": "9414d32451da32b586cd012459255926fc245f8547436bedae4e54597fc4b10f" }, "core/crates/tera_core/src/runtime/product_surface/outbox/configuration.rs": { "bytes": 5088, @@ -863,10 +863,10 @@ "sha256": "60e185523bacac3123fabb45214bf76a1b04a45b00fe326216e88785e54059b9" }, "core/crates/tera_core/src/runtime/product_surface/outbox/coordinate.rs": { - "bytes": 3386, - "git_blob": "fbf6865903177f279d8cff868e5757ad8cf4a30a", + "bytes": 3507, + "git_blob": "d0145136b2623fc997b02e8c71759817cfd8e744", "mode": "100644", - "sha256": "e08f8ca6432b2b592aa3dc93af358c6bcee6cc9c09b341ab194088107f1b745c" + "sha256": "4adf0a0f945518cbec0e594ba15128db36a744cca4f872efd945b8b826579b33" }, "core/crates/tera_core/src/runtime/product_surface/outbox/inventory.rs": { "bytes": 9243, @@ -910,23 +910,35 @@ "mode": "100644", "sha256": "b3a063da96304c439b92504a294dde8a4938b97b01cb237fa203372b17ab17f6" }, + "core/crates/tera_core/src/runtime/product_surface/outbox/retraction.rs": { + "bytes": 4425, + "git_blob": "6c080412a147952802ef47b61a27847a75ba629e", + "mode": "100644", + "sha256": "e9d8e53bcd8ac38f26b5ac5b02ba0a8629d63895a2f9fb4c1ec4deec8217cc91" + }, + "core/crates/tera_core/src/runtime/product_surface/outbox/retraction_authority.rs": { + "bytes": 6399, + "git_blob": "20be9d988c573720d2dd3f1f561700578d24661e", + "mode": "100644", + "sha256": "bef44329536f50366142485918993f508594337b5e2eb46c786cc8c8bf5dc932" + }, "core/crates/tera_core/src/runtime/product_surface/outbox/revision_coordinator.rs": { - "bytes": 7105, - "git_blob": "8c110a37e891f941393a5a1d9b7ef55a2f97e24f", + "bytes": 7155, + "git_blob": "cd1279ebbe10fb956847c5fea5b16dc54a39e7bc", "mode": "100644", - "sha256": "7fc6e4c36d0ec184e8d4559ee89e75ef42c0a5d79b085d98575dd130b3b038dd" + "sha256": "a495abb8019d0dd59edeff5af043736c37821fc66b95ab0ca1640461d3c97bd0" }, "core/crates/tera_core/src/runtime/product_surface/outbox/revision_delivery.rs": { - "bytes": 9085, - "git_blob": "fa7fed9b805a8b5f5739cac96dbc6e5e61bc7099", + "bytes": 9112, + "git_blob": "58a844ec6e999e4b979907e8489ae279ad084141", "mode": "100644", - "sha256": "e8853d0e5413c7d1168d5e89696cff8760a5e743a35e086de1e78745f86d4b86" + "sha256": "c797fc6c828d4fd36abb20c74472526db6cc348de1734c11a0a05e2c5740ec98" }, "core/crates/tera_core/src/runtime/product_surface/outbox/revision_preparation.rs": { - "bytes": 4492, - "git_blob": "6a373bb9e273c7dc1039061783adc46472defdbe", + "bytes": 4852, + "git_blob": "6acec1ef232c8f4599330555a464b931de54cf41", "mode": "100644", - "sha256": "1ffe8f3e29aae5b876a5e0e314117c197f628a615d2f39173bd9af6bbb47e790" + "sha256": "ada51be0ee64130b3e6d524f7f26c4d8007e1823b0e462ed3f72fa8e35c81135" }, "core/crates/tera_core/src/runtime/product_surface/outbox/revision_source.rs": { "bytes": 4500, @@ -935,10 +947,10 @@ "sha256": "3ea623a3c78b70313bb5fee89ac968d671b0822dfa9c1d82a7046a6f1eb5d2ff" }, "core/crates/tera_core/src/runtime/product_surface/outbox/revision_status.rs": { - "bytes": 10127, - "git_blob": "19905196b9ec6bcb616529387eb39b312e445404", + "bytes": 10612, + "git_blob": "76a83628322645f2748f5cbf617ed30238237d44", "mode": "100644", - "sha256": "0e83616701321035cb2c83fbd6477d03d083316bb22596924ea2d69d1606b2a0" + "sha256": "5c84abdebac4ae0d0f1a56a329eb2315f7634c9f0889598b3cbede038a2dc752" }, "core/crates/tera_core/src/runtime/product_surface/outbox/tests/coordinate_clock_tests.rs": { "bytes": 2937, @@ -988,23 +1000,47 @@ "mode": "100644", "sha256": "f627f5c0461cdb233f58b24a25b2e9dc79c3f14c6418dec0e1ee05874a5284df" }, + "core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_authority_tests.rs": { + "bytes": 5350, + "git_blob": "d0d4ea484f2f35a246dcdc135b5b5841597c7ba5", + "mode": "100644", + "sha256": "da1a248b23ea657522ef55694ed7641e1d7521ed0866796792323bb579e33b58" + }, + "core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_effect_tests.rs": { + "bytes": 8277, + "git_blob": "69223f8265520be87f66a5a44752af5a5ad9214e", + "mode": "100644", + "sha256": "4d651b11b000d4b3ef7733b1728d69b3efdfb52adac1b7e1e1f48f5a1120b924" + }, + "core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_source_tests.rs": { + "bytes": 3789, + "git_blob": "90595995f7094b9225425c81822b3cfa1fe6af42", + "mode": "100644", + "sha256": "18077a2662581cf38a923cd9c64daee6e8bff01f26a23a45b7af805a65bde9eb" + }, + "core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_support.rs": { + "bytes": 1873, + "git_blob": "cf5a1ea4009a7f9a827c04a45ffe2a951b031268", + "mode": "100644", + "sha256": "945391aafdc9f2459d29c52cbe710a01b5cebeb1f08bd784dd6a6d06a203ced2" + }, "core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_delivery_support.rs": { - "bytes": 5868, - "git_blob": "071eac1958db8ec5d426d9321e8e2829eeba6009", + "bytes": 5587, + "git_blob": "e1b8f8220121bee9c86b1cac90c6b29e347240f5", "mode": "100644", - "sha256": "9aec5cf6970c10af9b89589544dd2baecbc77cbb50a37aecd640e0e2b36bd2b1" + "sha256": "a4088255c1e829844810e06ee7d552d71ad4a7ded1d78bc4d4a3ed4508be08ff" }, "core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_delivery_tests.rs": { - "bytes": 12834, - "git_blob": "79dbac65c04ae0917482ea86a73b84af5df722a4", + "bytes": 12788, + "git_blob": "b839bf9072ee163586a4fc7f5dc20fc9fbc2af7a", "mode": "100644", - "sha256": "068803b7edd8deccd7a766e1eb2c4851c54850e4f47192ff200ff8781adf928b" + "sha256": "a597cf04c1bf87e5fa4b2f42c206da19bdd1ea2ca50c0fc5d864c5cab58e936b" }, "core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_preparation_tests.rs": { - "bytes": 10823, - "git_blob": "c73b288ac2a6229928e0d4e9159fed123145c597", + "bytes": 10735, + "git_blob": "542d62ca8fb926552d8b372b003ae2c2df3fef0a", "mode": "100644", - "sha256": "707a8e087f5ec9b43f71812f0ae2ff37edabe9b75d0b5031415d0f03e2cfd1a0" + "sha256": "d323ae611668bf7f6950328d87d99563456568f79d03d1c5650f762c425ed0ef" }, "core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_status_tests.rs": { "bytes": 7049, @@ -1025,10 +1061,10 @@ "sha256": "8b0ecc3c1b75aabe971c21532e023dd7e0a3fba01a4ca9f9f079db1ed3775cee" }, "core/crates/tera_core/src/runtime/product_surface/projection.rs": { - "bytes": 23423, - "git_blob": "a0186bb29cafbaab4268af10acd5dc701cf4c228", + "bytes": 24159, + "git_blob": "c337e15c0906e7ccff52997532ca3ed2726f3bdf", "mode": "100644", - "sha256": "06923cd1020ac2609cf78f9b25b6def7a486a6d07583d25e0b80f34c7c287ec5" + "sha256": "6107c43ddcb8ed1f2ce4229a6c92dd40a3288410e1bdccd39b0838993c6eb58f" }, "core/crates/tera_core/src/runtime/product_surface/publication.rs": { "bytes": 2790, @@ -2039,16 +2075,16 @@ "sha256": "1d2e6015400b2ba03819ed01bae674817a18a2c8e626ba3dfeca24dc2fb8763c" }, "core/crates/tera_ffi/tests/revision_status_contract.rs": { - "bytes": 3304, - "git_blob": "6667bb0a0a639e3a40c8608c4c517dc682bc02b0", + "bytes": 3263, + "git_blob": "4645d038adf64ec8e254ac3a822c93629b76b626", "mode": "100644", - "sha256": "57bfe282e7836f28ee252619473b6cc857b9d88fe1e215e63f0c022cfb9a7c33" + "sha256": "db41f36cd1d8f0dc9d1f6b065b0d4dbaaab7d11b01a185a05c5b478a5c7a5816" }, "core/crates/tera_ffi/tests/runtime_delegation.rs": { - "bytes": 31638, - "git_blob": "6f522596187e553db15bb7945aa795bbc96a9354", + "bytes": 32771, + "git_blob": "87f18bfbabd578955c3bf0a0a9f79a193d77ca7c", "mode": "100644", - "sha256": "99004edb9aa4af89118c1f494fd670c5641721c41251a732bc327822f23b676c" + "sha256": "facba685b9148ca651a82c21acbd10c6b11b85ca067e23971739eb28e1b79a13" }, "core/crates/tera_ffi/tests/runtime_lifecycle.rs": { "bytes": 4293, @@ -2092,6 +2128,12 @@ "mode": "100644", "sha256": "135ed57af77bdf67979e423cd6ef3c0a1cef051ea88d109ad3f2937393e688c8" }, + "core/crates/tera_ffi/tests/support/retraction.rs": { + "bytes": 3703, + "git_blob": "b782880c72f15ba31abf6f0465b27c676cf14a12", + "mode": "100644", + "sha256": "aaa566b9d432dfdbc5fb403967de61de6ed81983826ec051f912656c915208ed" + }, "core/crates/tera_ffi/tests/today_sync_contract.rs": { "bytes": 4854, "git_blob": "d63714958e422bd39b71a6db92b567c080d07bab", @@ -2591,13 +2633,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 167808, - "git_blob": "1c21e5b73d9effa6ebcf4cad298868574b9ceee9", + "bytes": 168574, + "git_blob": "4efe38933170b055e6e6a79f8fc719243f24ab5e", "mode": "100644", - "sha256": "4362152b23a985ae966ed67b08635454a65434b5d440fd862224bb08316199a9" + "sha256": "48c0cb55ae2695e065ac47ff290c84fed0913e017363764bb3fcd9106f88d396" } }, "policy": "staged_inputs", - "tree": "681e757519909bf8e177ed3238398988c9d0a89d" + "tree": "555ee9dfbfd65fcdb930d41fdb4f3145b2f2f431" } } diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox.rs b/core/crates/tera_core/src/runtime/product_surface/outbox.rs @@ -47,6 +47,8 @@ use crate::runtime::TeraRuntime; mod advance; mod coordinate; +mod retraction; +mod retraction_authority; pub(super) use coordinate::coordinate_plan; #[path = "outbox/revision_coordinator.rs"] mod revision_coordinator; @@ -1820,107 +1822,6 @@ impl TeraRuntime { self.draft_status_from(receipt.draft().clone()).await } - /// Persists an independent strict NIP-09 retraction as a normal durable outbox item. - #[allow(clippy::too_many_arguments)] - pub async fn phase1_save_retraction_draft( - &self, - draft_id: [u8; 16], - command_type: AddCommandType, - target_card_id: CardId, - target_event_id: &str, - target_kind: u32, - target_address: Option<&str>, - reason: &str, - authored_at_unix_s: u64, - persisted_at_unix_ms: u64, - ) -> Result<Phase1DraftStatus, Phase1DraftError> { - let _command = self.lifecycle.enter()?; - let admission = self.mutations.draft(draft_id)?; - self.phase1_save_retraction_draft_admitted( - &admission, - draft_id, - command_type, - target_card_id, - target_event_id, - target_kind, - target_address, - reason, - authored_at_unix_s, - persisted_at_unix_ms, - None, - ) - .await - } - - #[allow(clippy::too_many_arguments)] - async fn phase1_save_retraction_draft_admitted( - &self, - _admission: &crate::runtime::mutation_admission::MutationPermit<'_>, - draft_id: [u8; 16], - command_type: AddCommandType, - target_card_id: CardId, - target_event_id: &str, - target_kind: u32, - target_address: Option<&str>, - reason: &str, - authored_at_unix_s: u64, - persisted_at_unix_ms: u64, - revision_parent_draft_id: Option<[u8; 16]>, - ) -> Result<Phase1DraftStatus, Phase1DraftError> { - let target_shape_valid = match command_type { - AddCommandType::CreateUpdate - | AddCommandType::CreatePhotoUpdate - | AddCommandType::CreateAsk => target_kind == 1 && target_address.is_none(), - AddCommandType::CreateEvent => { - matches!(target_kind, 31_922 | 31_923) && target_address.is_some() - } - AddCommandType::CreateFoodAvailability => { - target_kind == 30_402 && target_address.is_some() - } - }; - if !target_shape_valid || authored_at_unix_s == 0 || persisted_at_unix_ms == 0 { - return Err(Phase1DraftError::InvalidDraft); - } - let author = self.draft_author()?; - let draft_id = - AuthoredDraftId::new(draft_id).map_err(|_| Phase1DraftError::InvalidDraft)?; - let event_target = Nip09DeletionEventTarget::parse(target_event_id, target_kind) - .map_err(|_| Phase1DraftError::InvalidDraft)?; - let address_targets = target_address - .map(Nip09DeletionAddressTarget::parse) - .transpose() - .map_err(|_| Phase1DraftError::InvalidDraft)? - .into_iter() - .collect(); - let request = - AuthoredNip09DeletionRequest::new(reason, vec![event_target], address_targets) - .map_err(|_| Phase1DraftError::InvalidDraft)?; - let plan = phase1_retraction_plan(&request, authored_at_unix_s, hex::encode(author)) - .map_err(|_| Phase1DraftError::InvalidDraft)?; - let wire = PlanWireV1::from_plan(&plan) - .to_json() - .map_err(|_| Phase1DraftError::InvalidDraft)?; - let mut payload = Phase1DraftPayload::retraction(command_type, target_card_id, wire)?; - payload.revision_parent_draft_id = revision_parent_draft_id; - let bytes = payload.encode()?; - let draft = AuthoredDraft::initial( - draft_id, - author, - DRAFT_PAYLOAD_SCHEMA, - bytes, - AuthoredDraftStage::Draft, - None, - persisted_at_unix_ms, - ) - .map_err(|_| Phase1DraftError::InvalidDraft)?; - let receipt = self - .storage()? - .append_authored_draft(draft, None) - .await - .map_err(map_draft_storage_error)?; - self.draft_status_from(receipt.draft().clone()).await - } - /// Advances one media prerequisite without mutating any prior revision. pub async fn phase1_update_draft_media( &self, @@ -3220,6 +3121,10 @@ mod tests { mod coordinate_support; mod coordinate_tests; mod recovery_completion_tests; + mod retraction_authority_tests; + mod retraction_effect_tests; + mod retraction_source_tests; + mod retraction_support; mod revision_delivery_support; mod revision_delivery_tests; mod revision_preparation_tests; @@ -3697,19 +3602,8 @@ mod tests { None, ) .unwrap(); - let source_event_id = "b".repeat(64); - let source = - CardSourceIdentity::Event(radroots_event::EventId::parse(&source_event_id).unwrap()); - let target = Phase1RevisionTarget::new( - AddCommandType::CreatePhotoUpdate, - CardId::derive(TodayCardType::PhotoUpdate, &source), - source_event_id, - 1, - None, - AUTHOR, - ) - .unwrap(); let (command, media) = photo_command(); + let target = retraction_support::retain_original(&runtime, &command).await; let replacement_content = format!("Harvest photo {}", media.url()); let media_form = Phase1DraftMediaSnapshot { opaque_reference: media.local_reference().to_owned(), @@ -3787,21 +3681,13 @@ mod tests { .unwrap(); std::fs::create_dir_all(store.owner_directory()).unwrap(); let runtime = RuntimeBuilder::new(store.clone()).build().await.unwrap(); - let source_event_id = "c".repeat(64); - let target = Phase1RevisionTarget::new( - AddCommandType::CreateAsk, - CardId::derive( - TodayCardType::Ask, - &CardSourceIdentity::Event( - radroots_event::EventId::parse(&source_event_id).unwrap(), - ), + let target = retraction_support::retain_original( + &runtime, + &Phase1AddCommand::CreateAsk( + CreateAsk::new("Who has seed potatoes?", Vec::new()).unwrap(), ), - source_event_id, - 1, - None, - AUTHOR, ) - .unwrap(); + .await; let saved = runtime .phase1_save_revision_intent( Phase1ReviseIntent::new( @@ -3899,13 +3785,14 @@ mod tests { #[tokio::test] async fn retraction_is_independent_and_add_advance_attempts_delivery() { let runtime = signing_runtime(); - let target = CardId::parse(&"a".repeat(64)).unwrap(); + let original = retraction_support::original(&runtime).await; + let target = original.card_id(); let retraction = runtime .phase1_save_retraction_draft( [5; 16], AddCommandType::CreateUpdate, target, - &"b".repeat(64), + original.source_event_id(), 1, None, "Replaced by a corrected copy", diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox/advance.rs b/core/crates/tera_core/src/runtime/product_surface/outbox/advance.rs @@ -3,14 +3,7 @@ use super::*; impl TeraRuntime { - pub(in crate::runtime::product_surface) async fn advance_push_request( - &self, - request: PushRequest, - ) -> Result<(), Phase1DraftError> { - self.advance_push_request_with_clock(request, phase1_operation_now_unix_ms) - .await - } - + #[cfg(test)] pub(in crate::runtime::product_surface) async fn advance_push_request_with_clock( &self, request: PushRequest, @@ -25,9 +18,6 @@ impl TeraRuntime { request: PushRequest, head: &AuthoredDraft, ) -> Result<(), Phase1DraftError> { - if super::super::coordinate::intent_from_draft(head)?.is_none() { - return self.advance_push_request(request).await; - } self.advance_push_request_inner(request, None, Some(head), phase1_operation_now_unix_ms) .await } @@ -47,9 +37,15 @@ impl TeraRuntime { &self, request: PushRequest, selected: TargetSet, + owner: &AuthoredDraft, ) -> Result<(), Phase1DraftError> { - self.advance_push_request_inner(request, Some(selected), None, phase1_operation_now_unix_ms) - .await + self.advance_push_request_inner( + request, + Some(selected), + Some(owner), + phase1_operation_now_unix_ms, + ) + .await } async fn advance_push_request_inner( @@ -88,6 +84,9 @@ impl TeraRuntime { if !self.publication_retry_at(&status, now)?.may_start() { return Ok(()); } + let authority_plan = request.plan().clone(); + self.require_publication_source(&authority_plan, owner) + .await?; let intent = owner .map(super::super::coordinate::intent_from_draft) @@ -129,6 +128,8 @@ impl TeraRuntime { AdmissionState::Pending | AdmissionState::Retryable ) { + self.require_publication_source(&authority_plan, owner) + .await?; if let Some(intent) = &intent { self.require_coordinate_current_at(intent, clock()? / 1_000) .await?; @@ -149,6 +150,8 @@ impl TeraRuntime { AuthoredDeliveryState::Pending | AuthoredDeliveryState::Retryable ) { + self.require_publication_source(&authority_plan, owner) + .await?; if let Some(intent) = &intent { self.require_coordinate_current_at(intent, clock()? / 1_000) .await?; diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox/coordinate.rs b/core/crates/tera_core/src/runtime/product_surface/outbox/coordinate.rs @@ -74,8 +74,11 @@ impl TeraRuntime { } self.require_legacy_publication_running(sync_id_for(&head)?) .await?; + let request = push_request(&head)?; + self.require_publication_source(request.plan(), Some(&head)) + .await?; self.sync()? - .sign_prepared(push_request(&head)?) + .sign_prepared(request) .await .map_err(|_| Phase1DraftError::Operation)?; self.require_draft_coordinate_current(&head).await?; diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox/retraction.rs b/core/crates/tera_core/src/runtime/product_surface/outbox/retraction.rs @@ -0,0 +1,115 @@ +//! Independent and revision-child retraction capture; shared NIP09 construction. + +use super::*; + +impl TeraRuntime { + /// Persists an independent strict NIP-09 retraction as a normal durable outbox item. + #[allow(clippy::too_many_arguments)] + pub async fn phase1_save_retraction_draft( + &self, + draft_id: [u8; 16], + command_type: AddCommandType, + target_card_id: CardId, + target_event_id: &str, + target_kind: u32, + target_address: Option<&str>, + reason: &str, + authored_at_unix_s: u64, + persisted_at_unix_ms: u64, + ) -> Result<Phase1DraftStatus, Phase1DraftError> { + let _command = self.lifecycle.enter()?; + let admission = self.mutations.draft(draft_id)?; + let target = Phase1RevisionTarget::new( + command_type, + target_card_id, + target_event_id, + target_kind, + target_address.map(str::to_owned), + hex::encode(self.draft_author()?), + )?; + self.require_revision_source(&target).await?; + self.phase1_save_retraction_draft_admitted( + &admission, + draft_id, + command_type, + target_card_id, + target_event_id, + target_kind, + target_address, + reason, + authored_at_unix_s, + persisted_at_unix_ms, + None, + ) + .await + } + + #[allow(clippy::too_many_arguments)] + pub(super) async fn phase1_save_retraction_draft_admitted( + &self, + _admission: &crate::runtime::mutation_admission::MutationPermit<'_>, + draft_id: [u8; 16], + command_type: AddCommandType, + target_card_id: CardId, + target_event_id: &str, + target_kind: u32, + target_address: Option<&str>, + reason: &str, + authored_at_unix_s: u64, + persisted_at_unix_ms: u64, + revision_parent_draft_id: Option<[u8; 16]>, + ) -> Result<Phase1DraftStatus, Phase1DraftError> { + let target_shape_valid = match command_type { + AddCommandType::CreateUpdate + | AddCommandType::CreatePhotoUpdate + | AddCommandType::CreateAsk => target_kind == 1 && target_address.is_none(), + AddCommandType::CreateEvent => { + matches!(target_kind, 31_922 | 31_923) && target_address.is_some() + } + AddCommandType::CreateFoodAvailability => { + target_kind == 30_402 && target_address.is_some() + } + }; + if !target_shape_valid || authored_at_unix_s == 0 || persisted_at_unix_ms == 0 { + return Err(Phase1DraftError::InvalidDraft); + } + let author = self.draft_author()?; + let draft_id = + AuthoredDraftId::new(draft_id).map_err(|_| Phase1DraftError::InvalidDraft)?; + let event_target = Nip09DeletionEventTarget::parse(target_event_id, target_kind) + .map_err(|_| Phase1DraftError::InvalidDraft)?; + let address_targets = target_address + .map(Nip09DeletionAddressTarget::parse) + .transpose() + .map_err(|_| Phase1DraftError::InvalidDraft)? + .into_iter() + .collect(); + let request = + AuthoredNip09DeletionRequest::new(reason, vec![event_target], address_targets) + .map_err(|_| Phase1DraftError::InvalidDraft)?; + let plan = phase1_retraction_plan(&request, authored_at_unix_s, hex::encode(author)) + .map_err(|_| Phase1DraftError::InvalidDraft)?; + let wire = PlanWireV1::from_plan(&plan) + .to_json() + .map_err(|_| Phase1DraftError::InvalidDraft)?; + let mut payload = Phase1DraftPayload::retraction(command_type, target_card_id, wire)?; + payload.revision_parent_draft_id = revision_parent_draft_id; + let bytes = payload.encode()?; + let draft = AuthoredDraft::initial( + draft_id, + author, + DRAFT_PAYLOAD_SCHEMA, + bytes, + AuthoredDraftStage::Draft, + None, + persisted_at_unix_ms, + ) + .map_err(|_| Phase1DraftError::InvalidDraft)?; + let receipt = self + .storage()? + .append_authored_draft(draft, None) + .await + .map_err(map_draft_storage_error)?; + self.draft_status_from(receipt.draft().clone()).await + } +} diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox/retraction_authority.rs b/core/crates/tera_core/src/runtime/product_surface/outbox/retraction_authority.rs @@ -0,0 +1,159 @@ +//! Exact signed source authority is independent of native cards and visibility. + +use super::*; +use crate::runtime::product_surface::projection::{admitted_card_source, admitted_card_type}; +use radroots_event::EventId; +use radroots_event_codec::{admission::admit_verified_event, verify::verify_nip01_event}; +use radroots_storage::event::{EventQuery, EventQueryBounds}; + +impl TeraRuntime { + pub(super) async fn require_revision_source( + &self, + target: &Phase1RevisionTarget, + ) -> Result<(), Phase1DraftError> { + // Bound the frame while retaining this read in the caller's lifetime. + Box::pin(self.check_revision_source(target)).await + } + + async fn check_revision_source( + &self, + target: &Phase1RevisionTarget, + ) -> Result<(), Phase1DraftError> { + target.validate()?; + if target.author_public_key != hex::encode(self.draft_author()?) { + return Err(Phase1DraftError::InvalidRevision); + } + let id = EventId::parse(&target.source_event_id) + .map_err(|_| Phase1DraftError::InvalidRevision)?; + let query = EventQuery::for_ids( + EventQueryBounds::first(1).map_err(|_| Phase1DraftError::InvalidRevision)?, + vec![id], + ) + .map_err(|_| Phase1DraftError::InvalidRevision)?; + let page = self + .client + .storage() + .map_err(|_| Phase1DraftError::Storage)? + .query_verified(query) + .await + .map_err(|_| Phase1DraftError::Storage)?; + let [stored] = page.items() else { + return Err(Phase1DraftError::InvalidRevision); + }; + // Stored admission typestate is not a substitute for actual signature + // evidence, and suppressed sources remain eligible exact-ID evidence. + let verified = verify_nip01_event(stored.event().envelope().clone()) + .map_err(|_| Phase1DraftError::InvalidRevision)?; + let admitted = + admit_verified_event(verified).map_err(|_| Phase1DraftError::InvalidRevision)?; + let event = admitted.event(); + let kind = admitted_card_type(&admitted).map_err(|_| Phase1DraftError::InvalidRevision)?; + let source = + admitted_card_source(&admitted, kind).map_err(|_| Phase1DraftError::InvalidRevision)?; + let address = match &source { + CardSourceIdentity::Event(_) => None, + CardSourceIdentity::Address { + kind, + author_pubkey, + identifier, + } => Some(format!("{kind}:{author_pubkey}:{identifier}")), + }; + if event.id() != &id + || event.author().to_hex() != target.author_public_key + || event.kind_u32() != target.source_kind + || address != target.source_address + || CardId::derive(kind, &source) != target.card_id + { + return Err(Phase1DraftError::InvalidRevision); + } + Ok(()) + } + + pub(super) async fn require_publication_source( + &self, + plan: &AuthoredEventPlan, + owner: Option<&AuthoredDraft>, + ) -> Result<(), Phase1DraftError> { + if plan.author().as_bytes() != &self.draft_author()? { + return Err(Phase1DraftError::InvalidRevision); + } + let payload = match owner { + Some(head) if head.payload_schema() == DRAFT_PAYLOAD_SCHEMA => { + let payload = Phase1DraftPayload::decode(head)?; + let frozen = PlanWireV1::from_json(&payload.plan_wire_json) + .map_err(|_| Phase1DraftError::Corrupt)?; + if frozen.plan() != plan || head.author() != plan.author().as_bytes() { + return Err(Phase1DraftError::InvalidRevision); + } + Some(payload) + } + _ => None, + }; + if plan.body().kind() == 5 { + let payload = payload.ok_or(Phase1DraftError::InvalidRevision)?; + let target = retraction_target(&payload, plan)?; + self.require_revision_source(&target).await?; + } else if let Some(revision) = payload.and_then(|payload| payload.revision) + && revision.policy == Phase1RevisionPolicy::ReplaceThenRetract + { + self.require_revision_source(&revision.target).await?; + } + Ok(()) + } +} + +fn retraction_target( + payload: &Phase1DraftPayload, + plan: &AuthoredEventPlan, +) -> Result<Phase1RevisionTarget, Phase1DraftError> { + if payload.kind != Phase1DraftKind::Retraction { + return Err(Phase1DraftError::InvalidRevision); + } + let tags = plan.body().tags(); + let value = |name: &str| -> Result<Option<&str>, Phase1DraftError> { + let mut matching = tags + .iter() + .filter(|tag| tag.first().map(String::as_str) == Some(name)); + let first = matching.next(); + if matching.next().is_some() || first.is_some_and(|tag| tag.len() != 2) { + return Err(Phase1DraftError::InvalidRevision); + } + Ok(first.and_then(|tag| tag.get(1)).map(String::as_str)) + }; + let event = value("e")?.ok_or(Phase1DraftError::InvalidRevision)?; + let kind = value("k")? + .ok_or(Phase1DraftError::InvalidRevision)? + .parse() + .map_err(|_| Phase1DraftError::InvalidRevision)?; + let address = value("a")?; + let target = Phase1RevisionTarget::new( + payload.command_type, + payload + .target_card_id + .ok_or(Phase1DraftError::InvalidRevision)?, + event, + kind, + address.map(str::to_owned), + plan.author().to_hex(), + )?; + let request = AuthoredNip09DeletionRequest::new( + plan.body().content(), + vec![ + Nip09DeletionEventTarget::parse(event, kind) + .map_err(|_| Phase1DraftError::InvalidRevision)?, + ], + address + .map(Nip09DeletionAddressTarget::parse) + .transpose() + .map_err(|_| Phase1DraftError::InvalidRevision)? + .into_iter() + .collect(), + ) + .map_err(|_| Phase1DraftError::InvalidRevision)?; + let canonical = phase1_retraction_plan(&request, plan.created_at(), plan.author().to_hex()) + .map_err(|_| Phase1DraftError::InvalidRevision)?; + if canonical != *plan { + return Err(Phase1DraftError::InvalidRevision); + } + Ok(target) +} diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox/revision_coordinator.rs b/core/crates/tera_core/src/runtime/product_surface/outbox/revision_coordinator.rs @@ -18,7 +18,8 @@ impl TeraRuntime { if matches!( status.replacement.state(), Phase1OutboxState::Draft | Phase1OutboxState::ReadyToSign - ) { + ) && status.replacement_progress.can_resume + { let now_unix_ms = phase1_operation_now_unix_ms()?; self.phase1_queue_draft_admitted( &admission, diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox/revision_delivery.rs b/core/crates/tera_core/src/runtime/product_surface/outbox/revision_delivery.rs @@ -175,7 +175,8 @@ impl TeraRuntime { match self.revision_delivery_selection(head).await? { RevisionDelivery::Independent => self.advance_owned_push_request(request, head).await, RevisionDelivery::Selected(targets) => { - self.advance_push_request_selected(request, targets).await + self.advance_push_request_selected(request, targets, head) + .await } RevisionDelivery::Held => { // Reconciliation cannot initiate another effect in this call. diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox/revision_preparation.rs b/core/crates/tera_core/src/runtime/product_surface/outbox/revision_preparation.rs @@ -85,6 +85,16 @@ impl TeraRuntime { return Err(Phase1DraftError::RevisionConflict); } } else { + if policy == Phase1RevisionPolicy::ReplaceThenRetract { + self.require_revision_source( + &payload + .revision + .as_ref() + .ok_or(Phase1DraftError::Corrupt)? + .target, + ) + .await?; + } let receipt = storage .append_authored_draft(candidate.clone(), None) .await diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox/revision_status.rs b/core/crates/tera_core/src/runtime/product_surface/outbox/revision_status.rs @@ -76,15 +76,21 @@ impl TeraRuntime { }; let mut replacement_progress = Box::pin(self.revision_branch_status(&replacement)).await?; replacement_progress.can_resume &= self.coordinate_may_resume(replacement.draft()).await?; + // Missing source proof holds new effects, without hiding old receipts or stop/reconciliation. + let source_authorized = revision.policy != Phase1RevisionPolicy::ReplaceThenRetract + || self.require_revision_source(&revision.target).await.is_ok(); + replacement_progress.can_resume &= source_authorized || replacement_progress.can_reconcile; let retraction_progress = match retraction.as_ref() { Some(child) => { let mut branch = Box::pin(self.revision_branch_status(child)).await?; branch.can_resume &= child.revision_parent_draft_id() == Some(replacement_draft_id); + branch.can_resume &= source_authorized || branch.can_reconcile; Some(branch) } None => None, }; - let child_eligible = !replacement_progress.stopped + let child_eligible = source_authorized + && !replacement_progress.stopped && replacement_progress .targets .as_ref() diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_authority_tests.rs b/core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_authority_tests.rs @@ -0,0 +1,172 @@ +use super::{retraction_support::*, *}; + +#[tokio::test] +async fn retraction_authority_rejects_forged_or_missing_source_without_creating_work() { + let runtime = signing_runtime(); + let target = original(&runtime).await; + let missing = "b".repeat(64); + let cases = [ + (target.card_id, missing.clone(), 1, None), + ( + CardId::derive( + TodayCardType::Update, + &CardSourceIdentity::Event(radroots_event::EventId::parse(&missing).unwrap()), + ), + target.source_event_id.clone(), + 1, + None, + ), + (target.card_id, target.source_event_id.clone(), 5, None), + ( + target.card_id, + target.source_event_id.clone(), + 1, + Some(format!("30402:{AUTHOR}:foreign")), + ), + ]; + for (index, (card, id, kind, address)) in cases.into_iter().enumerate() { + let result = runtime + .phase1_save_retraction_draft( + [index as u8 + 1; 16], + AddCommandType::CreateUpdate, + card, + &id, + kind, + address.as_deref(), + "Remove", + 1_700_000_001, + 1_700_000_001_000, + ) + .await; + assert!(matches!(result, Err(Phase1DraftError::InvalidRevision))); + } + assert!(runtime.phase1_draft_heads(20).await.unwrap().is_empty()); + runtime.require_revision_source(&target).await.unwrap(); +} + +#[tokio::test] +async fn retraction_authority_rejects_other_authors_even_with_matching_native_card_metadata() { + let runtime = signing_runtime(); + let secret = format!("{:064x}", 2); + let author = nostr::Keys::parse(&secret) + .unwrap() + .public_key() + .to_string(); + let command = Phase1AddCommand::CreateUpdate(CreateUpdate::new("Other author").unwrap()); + let plan = command + .authored_plan(1_700_000_000, author.clone()) + .unwrap(); + let event = signed_plan(&plan, &secret); + super::coordinate_support::retain(&runtime, event.clone()).await; + let card = card_id(AddCommandType::CreateUpdate, &plan).unwrap(); + let forged = Phase1RevisionTarget::new( + AddCommandType::CreateUpdate, + card, + event.id().to_hex(), + 1, + None, + AUTHOR, + ) + .unwrap(); + assert!(matches!( + runtime.require_revision_source(&forged).await, + Err(Phase1DraftError::InvalidRevision) + )); + let honest = Phase1RevisionTarget::new( + AddCommandType::CreateUpdate, + card, + event.id().to_hex(), + 1, + None, + author, + ) + .unwrap(); + assert!(matches!( + runtime.require_revision_source(&honest).await, + Err(Phase1DraftError::InvalidRevision) + )); + assert!(runtime.phase1_draft_heads(20).await.unwrap().is_empty()); +} + +#[tokio::test] +async fn retraction_authority_preserves_repeated_request_and_signed_plan_identity() { + let runtime = signing_runtime(); + let target = original(&runtime).await; + let saved = runtime + .phase1_save_retraction_draft( + [201; 16], + target.command_type, + target.card_id, + &target.source_event_id, + 1, + None, + "Remove", + 1_700_000_001, + 1_700_000_001_000, + ) + .await + .unwrap(); + let repeated = runtime + .phase1_save_retraction_draft( + [201; 16], + target.command_type, + target.card_id, + &target.source_event_id, + 1, + None, + "Remove", + 1_700_000_001, + 1_700_000_001_000, + ) + .await + .unwrap(); + assert_eq!(saved.draft(), repeated.draft()); + let plan = PlanWireV1::from_json( + &Phase1DraftPayload::decode(saved.draft()) + .unwrap() + .plan_wire_json, + ) + .unwrap(); + runtime + .require_publication_source(plan.plan(), Some(saved.draft())) + .await + .unwrap(); + assert!(matches!( + runtime.require_publication_source(plan.plan(), None).await, + Err(Phase1DraftError::InvalidRevision) + )); + // An old payload must not smuggle an additional target behind an honest card. + let request = AuthoredNip09DeletionRequest::new( + "Remove", + vec![ + Nip09DeletionEventTarget::parse(&target.source_event_id, 1).unwrap(), + Nip09DeletionEventTarget::parse("b".repeat(64), 1).unwrap(), + ], + vec![], + ) + .unwrap(); + let extra = phase1_retraction_plan(&request, 1_700_000_001, AUTHOR).unwrap(); + let payload = Phase1DraftPayload::retraction( + target.command_type, + target.card_id, + PlanWireV1::from_plan(&extra).to_json().unwrap(), + ) + .unwrap(); + let head = AuthoredDraft::initial( + AuthoredDraftId::new([205; 16]).unwrap(), + *extra.author().as_bytes(), + DRAFT_PAYLOAD_SCHEMA, + payload.encode().unwrap(), + AuthoredDraftStage::Draft, + None, + 1_700_000_001_000, + ) + .unwrap(); + assert_eq!( + runtime + .require_publication_source(&extra, Some(&head)) + .await + .unwrap_err(), + Phase1DraftError::InvalidRevision + ); +} diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_effect_tests.rs b/core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_effect_tests.rs @@ -0,0 +1,257 @@ +use super::{coordinate_effect_support as effects, retraction_support::*, *}; +use radroots_storage::EventStore; +use std::{sync::atomic::Ordering, time::Duration}; + +#[tokio::test] +async fn retained_revision_without_source_is_readable_and_stoppable_but_cannot_resume() { + let first = signing_runtime(); + let saved = super::revision_delivery_support::saved_revision(&first).await; + let recovered = signing_runtime(); + recovered + .storage() + .unwrap() + .append_authored_draft(saved.replacement().draft().clone(), None) + .await + .unwrap(); + let id = *saved.replacement().draft().draft_id().as_bytes(); + let status = recovered.phase1_revision_status(id).await.unwrap(); + assert_eq!(status.replacement().draft(), saved.replacement().draft()); + assert!(!status.can_resume()); + assert!(status.can_cancel()); + let held = recovered.phase1_advance_revision(id).await.unwrap(); + assert_eq!(held.replacement().draft(), status.replacement().draft()); + assert!(!held.can_resume()); + assert!( + recovered + .phase1_draft_status(id) + .await + .unwrap() + .push() + .is_none() + ); + let stopped = recovered.phase1_cancel_revision(id).await.unwrap(); + assert_eq!(stopped.phase(), Phase1RevisionPhase::Cancelled); +} + +#[tokio::test] +async fn retained_unproven_retractions_cannot_sign_admit_suppress_or_deliver() { + for already_signed in [false, true] { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let relay = format!("ws://{}", listener.local_addr().unwrap()); + let signer = effects::PausedSigner::new(); + signer.pause.store(false, Ordering::SeqCst); + let runtime = effects::runtime(signer.clone(), &relay); + let target = original_target(&Phase1AddCommand::CreateUpdate( + CreateUpdate::new("Original harvest").unwrap(), + )); + let saved = { + // Reproduce a historical capture that predated source authorization. + let permit = runtime.mutations.draft([202; 16]).unwrap(); + runtime + .phase1_save_retraction_draft_admitted( + &permit, + [202; 16], + target.command_type, + target.card_id, + &target.source_event_id, + 1, + None, + "Remove", + 1_700_000_001, + 1_700_000_001_000, + None, + ) + .await + .unwrap() + }; + let policy = Phase1QueuePolicy::new( + vec![relay], + Phase1RelaySatisfaction::AllAccepted, + 2_000_000_000_000, + Phase1CancellationPolicy::LocalCooperative, + ) + .unwrap(); + let queued = runtime + .phase1_queue_draft( + [202; 16], + saved.draft().revision().get(), + policy, + 1_700_000_001_001, + ) + .await + .unwrap(); + let before = queued.draft().clone(); + if already_signed { + // Reproduce an old signed artifact without granting it new admission. + runtime + .sync() + .unwrap() + .sign_prepared(push_request(&before).unwrap()) + .await + .unwrap(); + } + assert_eq!( + runtime + .phase1_sign_queued_draft([202; 16], before.revision().get()) + .await + .unwrap_err(), + Phase1DraftError::InvalidRevision + ); + assert_eq!( + runtime + .phase1_advance_draft([202; 16], before.revision().get()) + .await + .unwrap_err(), + Phase1DraftError::InvalidRevision + ); + let after = runtime.phase1_draft_status([202; 16]).await.unwrap(); + assert_eq!(after.draft(), &before); + let push = after.push().unwrap(); + assert_eq!(push.artifact().signed().is_some(), already_signed); + assert!(!push.artifact().admission_state().is_admitted()); + assert!(push.delivery_plan().attempts().is_empty()); + assert_eq!( + signer.calls.load(Ordering::SeqCst), + usize::from(already_signed) + ); + assert_eq!( + EventStore::status(runtime.client.storage().unwrap()) + .await + .unwrap() + .raw_events(), + 0 + ); + assert!( + tokio::time::timeout(Duration::from_millis(50), listener.accept()) + .await + .is_err() + ); + runtime + .phase1_cancel_draft([202; 16], before.revision().get(), 1_700_000_001_002) + .await + .unwrap(); + } +} + +#[tokio::test] +async fn locally_retracted_source_remains_proof_for_a_repeated_request() { + let runtime = signing_runtime(); + let target = original_target(&Phase1AddCommand::CreateUpdate( + CreateUpdate::new("Original harvest").unwrap(), + )); + let source = runtime + .phase1_save_draft( + [200; 16], + Phase1AddCommand::CreateUpdate(CreateUpdate::new("Original harvest").unwrap()), + 1_700_000_000, + vec![], + None, + 1_700_000_000_000, + ) + .await + .unwrap(); + let source = runtime + .phase1_queue_draft( + [200; 16], + source.draft().revision().get(), + policy(), + 1_700_000_000_001, + ) + .await + .unwrap(); + let source = runtime + .phase1_sign_queued_draft([200; 16], source.draft().revision().get()) + .await + .unwrap(); + runtime + .sync() + .unwrap() + .admit_signed(sync_id_for(source.draft()).unwrap()) + .await + .unwrap(); + let visible = EventStore::rebuild_visibility(runtime.client.storage().unwrap()) + .await + .unwrap(); + assert!( + visible + .visible_event_ids() + .iter() + .any(|id| id.to_hex() == target.source_event_id) + ); + let saved = runtime + .phase1_save_retraction_draft( + [203; 16], + target.command_type, + target.card_id, + &target.source_event_id, + 1, + None, + "Remove", + 1_700_000_001, + 1_700_000_001_000, + ) + .await + .unwrap(); + let queued = runtime + .phase1_queue_draft( + [203; 16], + saved.draft().revision().get(), + policy(), + 1_700_000_001_001, + ) + .await + .unwrap(); + let signed = runtime + .phase1_sign_queued_draft([203; 16], queued.draft().revision().get()) + .await + .unwrap(); + runtime + .sync() + .unwrap() + .admit_signed(sync_id_for(signed.draft()).unwrap()) + .await + .unwrap(); + let suppressed = EventStore::rebuild_visibility(runtime.client.storage().unwrap()) + .await + .unwrap(); + assert!( + suppressed + .suppressed_event_ids() + .iter() + .any(|id| id.to_hex() == target.source_event_id) + ); + assert!( + !suppressed + .visible_event_ids() + .iter() + .any(|id| id.to_hex() == target.source_event_id) + ); + runtime.require_revision_source(&target).await.unwrap(); + let repeated = runtime + .phase1_save_retraction_draft( + [204; 16], + target.command_type, + target.card_id, + &target.source_event_id, + 1, + None, + "Remove", + 1_700_000_001, + 1_700_000_001_000, + ) + .await + .unwrap(); + let first = PlanWireV1::from_json( + &Phase1DraftPayload::decode(saved.draft()) + .unwrap() + .plan_wire_json, + ) + .unwrap(); + let second = PlanWireV1::from_json( + &Phase1DraftPayload::decode(repeated.draft()) + .unwrap() + .plan_wire_json, + ) + .unwrap(); + assert_eq!(first.plan(), second.plan()); +} diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_source_tests.rs b/core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_source_tests.rs @@ -0,0 +1,111 @@ +use super::{coordinate_support, retraction_support::*, *}; +use radroots_event::{ + SignedEvent, + admission::{RawEvent, SignatureVerifier}, +}; +use radroots_storage::event::{EventAdmission, EventStore}; +use radroots_transport::{ + TransportId, + source::{EventProvenance, ObservedEvent}, + target::Target, +}; + +struct Permissive; +impl SignatureVerifier for Permissive { + fn verify_signature( + &self, + _: &radroots_event::envelope::EventEnvelope, + ) -> Result<(), radroots_event::admission::Error> { + Ok(()) + } +} + +#[tokio::test] +async fn source_authority_reverifies_stored_signature_and_actual_product_family() { + let runtime = signing_runtime(); + let command = Phase1AddCommand::CreateUpdate(CreateUpdate::new("Original harvest").unwrap()); + let plan = command.authored_plan(1_700_000_000, AUTHOR).unwrap(); + let mut wire = signed_plan(&plan, SECRET).wire().clone(); + wire.sig = "0".repeat(128); + let event = + SignedEvent::from_wire_verified_id(wire.clone(), serde_json::to_string(&wire).unwrap()) + .unwrap(); + let verified = RawEvent::new(event.envelope().clone()) + .verify_id() + .unwrap() + .verify_signature(&Permissive) + .unwrap(); + let relay = Target::new(TransportId::NOSTR, "wss://relay.example").unwrap(); + let observed = ObservedEvent::new( + event, + EventProvenance::new( + TransportId::NOSTR, + relay.fingerprint().clone(), + 1_700_000_000_000, + ) + .unwrap(), + ); + EventStore::admit( + runtime.client.storage().unwrap(), + EventAdmission::verified(observed, verified).unwrap(), + ) + .await + .unwrap(); + assert_eq!( + runtime + .require_revision_source(&original_target(&command)) + .await + .unwrap_err(), + Phase1DraftError::InvalidRevision + ); + + // The native family and derived card may agree while the signed source is a different family. + let valid = Phase1AddCommand::CreateUpdate(CreateUpdate::new("Another original").unwrap()); + let target = retain_original(&runtime, &valid).await; + let source = + CardSourceIdentity::Event(radroots_event::EventId::parse(&target.source_event_id).unwrap()); + let forged = Phase1RevisionTarget::new( + AddCommandType::CreateAsk, + CardId::derive(TodayCardType::Ask, &source), + target.source_event_id, + 1, + None, + AUTHOR, + ) + .unwrap(); + assert_eq!( + runtime.require_revision_source(&forged).await.unwrap_err(), + Phase1DraftError::InvalidRevision + ); +} + +#[tokio::test] +async fn address_source_requires_exact_signed_kind_and_coordinate_even_when_metadata_is_consistent() +{ + let runtime = signing_runtime(); + let (command, _) = coordinate_support::calendar("original", "Market"); + let plan = command.authored_plan(1_700_000_000, AUTHOR).unwrap(); + let event = signed_plan(&plan, SECRET); + coordinate_support::retain(&runtime, event.clone()).await; + for (kind, identifier, accepted) in [ + (31_923, "original", true), + (31_923, "forged", false), + (31_922, "original", false), + ] { + let source = CardSourceIdentity::address(kind, AUTHOR, identifier).unwrap(); + let target = Phase1RevisionTarget::new( + AddCommandType::CreateEvent, + CardId::derive(TodayCardType::Event, &source), + event.id().to_hex(), + kind, + Some(format!("{kind}:{AUTHOR}:{identifier}")), + AUTHOR, + ) + .unwrap(); + assert_eq!( + runtime.require_revision_source(&target).await.is_ok(), + accepted + ); + } + assert!(runtime.phase1_draft_heads(20).await.unwrap().is_empty()); +} diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_support.rs b/core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_support.rs @@ -0,0 +1,51 @@ +use super::*; +use nostr::secp256k1::{Keypair, Message, SECP256K1}; +use radroots_event::{SignedEvent, wire::v1::Nip01EventWire}; + +pub(super) fn signed_plan(plan: &AuthoredEventPlan, secret: &str) -> SignedEvent { + let keys = nostr::Keys::parse(secret).unwrap(); + let sig = SECP256K1.sign_schnorr_no_aux_rand( + &Message::from_digest(*plan.expected_event_id().as_bytes()), + &Keypair::from_secret_key(SECP256K1, keys.secret_key()), + ); + let wire = Nip01EventWire { + id: plan.expected_event_id().to_hex(), + pubkey: plan.author().to_hex(), + created_at: plan.created_at(), + kind: plan.body().kind(), + tags: plan.body().tags().to_vec(), + content: plan.body().content().into(), + sig: sig.to_string(), + extra: Default::default(), + }; + SignedEvent::from_wire_verified_id(wire.clone(), serde_json::to_string(&wire).unwrap()).unwrap() +} + +pub(super) async fn original(runtime: &TeraRuntime) -> Phase1RevisionTarget { + let command = Phase1AddCommand::CreateUpdate(CreateUpdate::new("Original harvest").unwrap()); + retain_original(runtime, &command).await +} + +pub(super) fn original_target(command: &Phase1AddCommand) -> Phase1RevisionTarget { + let plan = command.authored_plan(1_700_000_000, AUTHOR).unwrap(); + let kind = command.command_type(); + Phase1RevisionTarget::from_source( + kind, + card_id(kind, &plan).unwrap(), + plan.expected_event_id().to_hex(), + None, + AUTHOR, + ) + .unwrap() +} + +pub(super) async fn retain_original( + runtime: &TeraRuntime, + command: &Phase1AddCommand, +) -> Phase1RevisionTarget { + let plan = command.authored_plan(1_700_000_000, AUTHOR).unwrap(); + let event = signed_plan(&plan, SECRET); + let target = original_target(command); + super::coordinate_support::retain(runtime, event).await; + target +} diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_delivery_support.rs b/core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_delivery_support.rs @@ -118,19 +118,7 @@ pub(super) async fn persistent_runtime(root: &std::path::Path, urls: &[String]) } pub(super) async fn saved_revision(runtime: &TeraRuntime) -> Phase1RevisionStatus { - let event = "b".repeat(64); - let target = Phase1RevisionTarget::new( - AddCommandType::CreateUpdate, - CardId::derive( - TodayCardType::Update, - &CardSourceIdentity::Event(radroots_event::EventId::parse(&event).unwrap()), - ), - event, - 1, - None, - AUTHOR, - ) - .unwrap(); + let target = super::retraction_support::original(runtime).await; runtime .prepare_revision_intent( [102; 16], diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_delivery_tests.rs b/core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_delivery_tests.rs @@ -206,11 +206,9 @@ async fn cancellation_stops_both_partial_branches_and_direct_child_after_reopen( #[tokio::test] async fn historical_unlinked_revision_reason_is_held_without_rewriting_old_payload() { let runtime = signing_runtime(); - let source = "b".repeat(64); - let card = CardId::derive( - TodayCardType::Update, - &CardSourceIdentity::Event(radroots_event::EventId::parse(&source).unwrap()), - ); + let target = super::retraction_support::original(&runtime).await; + let source = target.source_event_id; + let card = target.card_id; let old = runtime .phase1_save_retraction_draft( [103; 16], diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_preparation_tests.rs b/core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_preparation_tests.rs @@ -2,18 +2,9 @@ use super::*; use std::io::{BufRead, Read, Write}; fn intent(content: &str) -> Phase1ReviseIntent { - let event = "ab".repeat(32); - let target = Phase1RevisionTarget::from_source( - AddCommandType::CreateUpdate, - CardId::derive( - TodayCardType::Update, - &CardSourceIdentity::Event(radroots_event::EventId::parse(&event).unwrap()), - ), - event, - None, - AUTHOR.to_owned(), - ) - .unwrap(); + let target = super::retraction_support::original_target(&Phase1AddCommand::CreateUpdate( + CreateUpdate::new("Original harvest").unwrap(), + )); Phase1ReviseIntent::new( target, Phase1AddCommand::CreateUpdate(CreateUpdate::new(content).unwrap()), @@ -29,6 +20,7 @@ fn intent(content: &str) -> Phase1ReviseIntent { #[tokio::test] async fn repeat_preparation_is_one_graph_and_changed_capture_conflicts() { let runtime = runtime(); + super::retraction_support::original(&runtime).await; let captured = intent("Original replacement"); let first = runtime .prepare_revision_intent([101; 16], captured.clone()) @@ -253,6 +245,7 @@ async fn revision_child() { let store = config(std::path::Path::new(&root)); std::fs::create_dir_all(store.owner_directory()).unwrap(); let runtime = RuntimeBuilder::new(store).build().await.unwrap(); + super::retraction_support::original(&runtime).await; let captured = intent("Crash-safe replacement"); let saved = runtime .prepare_revision_intent([101; 16], captured.clone()) diff --git a/core/crates/tera_core/src/runtime/product_surface/projection.rs b/core/crates/tera_core/src/runtime/product_surface/projection.rs @@ -50,20 +50,9 @@ pub fn classify_admitted_event( RadrootsAdmittedEvent::Comment(_) => supporting(SupportingProfile::Comment), RadrootsAdmittedEvent::DeletionRequest(_) => supporting(SupportingProfile::Deletion), RadrootsAdmittedEvent::RootPost(event) => { - let card_type = match event.projection().classification() { - RadrootsPostClassification::Update => TodayCardType::Update, - RadrootsPostClassification::PhotoUpdate => TodayCardType::PhotoUpdate, - RadrootsPostClassification::Ask => TodayCardType::Ask, - RadrootsPostClassification::ThreadExcluded => { - return ProductEventClassification::Excluded( - ProductEventExclusion::UnsupportedProfile, - ); - } - _ => { - return ProductEventClassification::Excluded( - ProductEventExclusion::UnsupportedProfile, - ); - } + let card_type = match admitted_card_type(admitted) { + Ok(value) => value, + Err(reason) => return ProductEventClassification::Excluded(reason), }; card( admitted, @@ -100,19 +89,36 @@ pub fn classify_admitted_event( } } -const fn supporting(profile: SupportingProfile) -> ProductEventClassification { - ProductEventClassification::Supporting(profile) +/// Product identity without locality, visibility, page or native-card authority. +pub(super) fn admitted_card_type( + admitted: &RadrootsAdmittedEvent, +) -> Result<TodayCardType, ProductEventExclusion> { + Ok(match admitted { + RadrootsAdmittedEvent::RootPost(event) => match event.projection().classification() { + RadrootsPostClassification::Update => TodayCardType::Update, + RadrootsPostClassification::PhotoUpdate => TodayCardType::PhotoUpdate, + RadrootsPostClassification::Ask => TodayCardType::Ask, + _ => return Err(ProductEventExclusion::UnsupportedProfile), + }, + RadrootsAdmittedEvent::FoodAvailability(_) => TodayCardType::FoodAvailability, + RadrootsAdmittedEvent::ContractValidated(event) + if matches!( + event.contract_id(), + "radroots.calendar.date_event.v1" | "radroots.calendar.time_event.v1" + ) => + { + TodayCardType::Event + } + _ => return Err(ProductEventExclusion::UnsupportedProfile), + }) } -fn card( +pub(super) fn admitted_card_source( admitted: &RadrootsAdmittedEvent, card_type: TodayCardType, - context: ContextAdmission, - media: Vec<MediaReference>, - lifecycle: CardLifecycleState, -) -> ProductEventClassification { +) -> Result<CardSourceIdentity, ProductEventExclusion> { let event = admitted.event(); - let source = match card_type { + Ok(match card_type { TodayCardType::Update | TodayCardType::PhotoUpdate | TodayCardType::Ask => { CardSourceIdentity::Event(*event.id()) } @@ -123,19 +129,33 @@ fn card( .find(|tag| tag.first().map(String::as_str) == Some("d")) .and_then(|tag| tag.get(1).cloned()); let Some(identifier) = identifier else { - return ProductEventClassification::Excluded( - ProductEventExclusion::InvalidSourceIdentity, - ); + return Err(ProductEventExclusion::InvalidSourceIdentity); }; let Ok(source) = CardSourceIdentity::address(event.kind_u32(), event.author().to_hex(), identifier) else { - return ProductEventClassification::Excluded( - ProductEventExclusion::InvalidSourceIdentity, - ); + return Err(ProductEventExclusion::InvalidSourceIdentity); }; source } + }) +} + +const fn supporting(profile: SupportingProfile) -> ProductEventClassification { + ProductEventClassification::Supporting(profile) +} + +fn card( + admitted: &RadrootsAdmittedEvent, + card_type: TodayCardType, + context: ContextAdmission, + media: Vec<MediaReference>, + lifecycle: CardLifecycleState, +) -> ProductEventClassification { + let event = admitted.event(); + let source = match admitted_card_source(admitted, card_type) { + Ok(value) => value, + Err(reason) => return ProductEventClassification::Excluded(reason), }; let source_address = match &source { CardSourceIdentity::Event(_) => None, diff --git a/core/crates/tera_ffi/tests/revision_status_contract.rs b/core/crates/tera_ffi/tests/revision_status_contract.rs @@ -1,17 +1,22 @@ use tera_ffi::*; +#[path = "support/retraction.rs"] +mod retraction; mod support; #[tokio::test] async fn revision_status_maps_original_relation_actions_and_stopped_facts_across_reopen() { let (root, runtime) = support::runtime().await; - let source_event_id = "ab".repeat(32); - let card_id = tera_core::runtime::product_surface::CardId::derive( - tera_core::runtime::product_surface::TodayCardType::Update, - &tera_core::runtime::product_surface::CardSourceIdentity::Event( - radroots_event::EventId::parse(&source_event_id).unwrap(), - ), + runtime.shutdown().await.unwrap(); + let (card_id, source_event_id) = retraction::seed(root.path(), input()).await; + let runtime = TeraRuntime::new( + root.path().to_string_lossy().into_owned(), + support::PUBLIC_KEY.into(), + support::GENERATION.into(), + 1_800_000_000_000, + ProtectedDataAvailability::Available, ) - .to_hex(); + .await + .unwrap(); let saved = runtime .phase1_save_revision_intent(FfiRevisionInputRecord { request_id: "ac".repeat(16), @@ -20,28 +25,7 @@ async fn revision_status_maps_original_relation_actions_and_stopped_facts_across source_event_id: source_event_id.clone(), source_address: None, author_public_key: support::PUBLIC_KEY.into(), - replacement: FfiAddDraftInput { - schema_version: 1, - command_type: FfiAddCommandType::CreateUpdate, - content: "Corrected harvest".into(), - identifier: None, - title: None, - summary: None, - location: None, - event_timing: None, - event_start_date: None, - event_end_date: None, - event_start_unix_s: None, - event_end_unix_s: None, - event_timezone: None, - price_amount: None, - currency: None, - unit: None, - quantity: None, - food_published_at_unix_s: None, - food_status: None, - media: vec![], - }, + replacement: input(), }) .await .unwrap(); @@ -87,3 +71,28 @@ async fn revision_status_maps_original_relation_actions_and_stopped_facts_across ); runtime.shutdown().await.unwrap(); } + +fn input() -> FfiAddDraftInput { + FfiAddDraftInput { + schema_version: 1, + command_type: FfiAddCommandType::CreateUpdate, + content: "Corrected harvest".into(), + identifier: None, + title: None, + summary: None, + location: None, + event_timing: None, + event_start_date: None, + event_end_date: None, + event_start_unix_s: None, + event_end_unix_s: None, + event_timezone: None, + price_amount: None, + currency: None, + unit: None, + quantity: None, + food_published_at_unix_s: None, + food_status: None, + media: vec![], + } +} diff --git a/core/crates/tera_ffi/tests/runtime_delegation.rs b/core/crates/tera_ffi/tests/runtime_delegation.rs @@ -15,6 +15,8 @@ use tera_ffi::{ FfiTodayProjectionUpdate, MOBILE_FFI_SCHEMA_VERSION, TeraAppError, }; +#[path = "support/retraction.rs"] +mod retraction; mod support; #[tokio::test] @@ -395,7 +397,7 @@ async fn native_boundary_delegates_the_complete_core_surface() { .phase1_validate_add_draft(add.clone(), 1_800_000_001) .expect("valid draft"); let saved = runtime - .phase1_save_add_intent(add, None, None) + .phase1_save_add_intent(add.clone(), None, None) .await .expect("saved draft"); let draft_id = saved.draft_id.clone(); @@ -475,12 +477,36 @@ async fn native_boundary_delegates_the_complete_core_surface() { .is_err() ); + runtime.shutdown().await.unwrap(); + let (source_card, source_event) = retraction::seed(_root.path(), add).await; + let runtime = tera_ffi::TeraRuntime::new( + _root.path().to_string_lossy().into_owned(), + support::PUBLIC_KEY.into(), + support::GENERATION.into(), + 1_800_000_000_000, + tera_ffi::ProtectedDataAvailability::Available, + ) + .await + .unwrap(); + runtime + .configure_device_relays(vec!["wss://10.0.0.5:7447".to_owned()]) + .await + .unwrap(); + runtime + .configure_blossom( + FfiBlossomHostKind::PhysicalDevice, + FfiBlossomEndpointAuthority::PrivateNetworkDevelopment, + "https://10.0.0.5:3100".to_owned(), + vec![], + ) + .await + .unwrap(); let retraction_id = "0a".repeat(16); let retraction_input = FfiRetractionDraftInput { schema_version: MOBILE_FFI_SCHEMA_VERSION, command_type: FfiAddCommandType::CreateUpdate, - target_card_id: "c".repeat(64), - target_event_id: "a".repeat(64), + target_card_id: source_card.clone(), + target_event_id: source_event.clone(), target_kind: 1, target_address: None, reason: "Replaced with a corrected copy".to_owned(), @@ -511,7 +537,7 @@ async fn native_boundary_delegates_the_complete_core_surface() { .await .expect("saved retraction"); assert_eq!(retraction.kind, FfiDraftKind::Retraction); - assert_eq!(retraction.card_id, "c".repeat(64)); + assert_eq!(retraction.card_id, source_card); assert!(retraction.form.is_none()); let queued_retraction = runtime .phase1_queue_draft( @@ -705,7 +731,7 @@ async fn native_boundary_delegates_the_complete_core_surface() { ); assert_eq!(unlocked.settings.revision, completed.settings.revision); - let source_event_id = "ab".repeat(32); + let source_event_id = source_event; let source = tera_core::runtime::product_surface::CardSourceIdentity::Event( radroots_event::EventId::parse(&source_event_id).expect("source event id"), ); @@ -820,6 +846,15 @@ async fn native_boundary_delegates_the_complete_core_surface() { FfiOutboxState::Cancelled ); + // The seeded source changed history after the earlier empty projection. + runtime + .phase1_refresh_today( + local_network.clone(), + 1_800_000_010, + FfiTodayProjectionUpdate::Incremental, + ) + .await + .unwrap(); let cache = runtime .phase1_media_cache_status(local_network.clone()) .await diff --git a/core/crates/tera_ffi/tests/support/retraction.rs b/core/crates/tera_ffi/tests/support/retraction.rs @@ -0,0 +1,99 @@ +//! Real locally admitted source for the native delegation test; no remote I/O. +use secp256k1::{Keypair, Message, Secp256k1, SecretKey}; +use tera_ffi::*; + +struct FixtureSigner; + +#[async_trait::async_trait] +impl TeraHostSigner for FixtureSigner { + async fn signer_status(&self) -> SignerStatusRecord { + SignerStatusRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + availability: SignerAvailabilityRecord::Ready, + } + } + + async fn sign(&self, request: HostSigningRequest) -> HostSigningResult { + let mut bytes = [0; 32]; + bytes[31] = 1; + let keypair = + Keypair::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&bytes).unwrap()); + assert_eq!( + request.public_key, + keypair.x_only_public_key().0.to_string() + ); + let digest = request.event_id_digest.try_into().unwrap(); + let signature = + Secp256k1::new().sign_schnorr_no_aux_rand(&Message::from_digest(digest), &keypair); + HostSigningResult { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + outcome: HostSigningOutcome::Signed, + operation_id: request.operation_id, + signer_request_id: request.signer_request_id, + public_key: request.public_key, + purpose: request.purpose, + signature_hex: Some(signature.to_string()), + completed_at_unix_ms: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() + .try_into() + .unwrap(), + } + } +} + +pub async fn seed(root: &std::path::Path, input: FfiAddDraftInput) -> (String, String) { + use tera_core::runtime::product_surface::{CreateUpdate, Phase1AddCommand}; + let at = 1_700_000_000; + let event = Phase1AddCommand::CreateUpdate(CreateUpdate::new(&input.content).unwrap()) + .authored_plan(at, super::support::PUBLIC_KEY) + .unwrap() + .expected_event_id() + .to_hex(); + let runtime = TeraRuntime::with_host_signer( + root.to_string_lossy().into_owned(), + super::support::PUBLIC_KEY.into(), + super::support::GENERATION.into(), + 1_800_000_000_000, + ProtectedDataAvailability::Available, + Box::new(FixtureSigner), + ) + .await + .unwrap(); + // A deliberately closed ephemeral loopback port bounds delivery; admission is local. + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let relay = format!("ws://{}", listener.local_addr().unwrap()); + drop(listener); + runtime + .configure_simulator_relays(vec![relay.clone()]) + .await + .unwrap(); + let id = "ee".repeat(16); + let saved = runtime + .phase1_save_draft(id.clone(), input, at, None, at * 1000) + .await + .unwrap(); + let queued = runtime + .phase1_queue_draft( + id.clone(), + saved.revision, + FfiQueuePolicyRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + relay_urls: vec![relay], + satisfaction: FfiRelaySatisfaction::AllAccepted, + delivery_deadline_unix_ms: 2_000_000_000_000, + cancellation: FfiCancellationPolicy::LocalCooperative, + }, + at * 1000 + 1, + ) + .await + .unwrap(); + let _delivery = runtime + .phase1_advance_draft(id.clone(), queued.revision) + .await; + let source = runtime.phase1_draft_status(id).await.unwrap(); + assert_eq!(source.settlement.unwrap().admitted, 1); + runtime.shutdown().await.unwrap(); + (source.card_id, event) +} diff --git a/release/provenance.json b/release/provenance.json @@ -2,7 +2,7 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "81a943ef98405676b4f65932a4a8d4bdb4011923f65fa2e3496fcb58e1d68be8", - "ffi_provenance_sha256": "c184a6b1dd38541e27f792fbd41f90690e6c63c89165951a4526c06b60cf5800", + "ffi_provenance_sha256": "04d36e07e071305a5a665f64b836565fb58a4a4bf8e018087881f74badb2227d", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "bfc57ed1f259dad8dcff7ec061b0889942a27bfdc9f9f01ebb631e25b715267b", @@ -22,7 +22,7 @@ "lib_revision": "e6f296604363e5293d6abc6e18bf5eaea2177b9f", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "a7e31d77d31ecbc1e0e5a78f2681e3fb0c6f367af0edf8b4ceef0af973a78681", - "tera_ffi_source_tree": "681e757519909bf8e177ed3238398988c9d0a89d", + "tera_ffi_source_tree": "555ee9dfbfd65fcdb930d41fdb4f3145b2f2f431", "xcode_package_lock_sha256": "d98a614a38dce7b55c2925dee51c2d4bf8dcfc4e31779d0c5fe8561c45d6e3ba" }, "version": "0.1.0-alpha" diff --git a/test-fixtures/legacy-identifiers.v1.json b/test-fixtures/legacy-identifiers.v1.json @@ -144,7 +144,7 @@ }, { "path": "core/crates/tera_core/src/runtime/product_surface/projection.rs", - "count": 14 + "count": 19 }, { "path": "core/crates/tera_core/src/runtime/product_surface/today.rs", @@ -1918,7 +1918,7 @@ "occurrences": [ { "path": "core/crates/tera_core/src/runtime/product_surface/projection.rs", - "count": 5 + "count": 4 } ] }, @@ -3264,7 +3264,7 @@ }, { "path": "core/crates/tera_core/src/runtime/product_surface/projection.rs", - "count": 1 + "count": 2 }, { "path": "core/crates/tera_core/src/runtime/product_surface/today_calendar_migration_tests.rs", @@ -3282,7 +3282,7 @@ }, { "path": "core/crates/tera_core/src/runtime/product_surface/projection.rs", - "count": 1 + "count": 2 } ] }, @@ -4884,13 +4884,17 @@ }, { "path": "core/crates/tera_core/src/runtime/product_surface/outbox.rs", - "count": 5 + "count": 3 }, { "path": "core/crates/tera_core/src/runtime/product_surface/outbox/coordinate.rs", "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/outbox/retraction_authority.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/outbox/tests/coordinate_ordering_tests.rs", "count": 1 }, @@ -4899,15 +4903,15 @@ "count": 2 }, { - "path": "core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_delivery_support.rs", + "path": "core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_authority_tests.rs", "count": 1 }, { - "path": "core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_delivery_tests.rs", - "count": 1 + "path": "core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_source_tests.rs", + "count": 4 }, { - "path": "core/crates/tera_core/src/runtime/product_surface/outbox/tests/revision_preparation_tests.rs", + "path": "core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_support.rs", "count": 1 }, { @@ -5007,10 +5011,6 @@ "count": 1 }, { - "path": "core/crates/tera_ffi/tests/revision_status_contract.rs", - "count": 1 - }, - { "path": "core/crates/tera_ffi/tests/runtime_delegation.rs", "count": 1 } @@ -5049,6 +5049,10 @@ "count": 2 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/outbox/retraction_authority.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/outbox/tests/coordinate_support.rs", "count": 1 }, @@ -5835,6 +5839,10 @@ "count": 2 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/outbox/retraction_authority.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/outbox/tests/coordinate_effect_tests.rs", "count": 1 }, @@ -5851,6 +5859,14 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_effect_tests.rs", + "count": 1 + }, + { + "path": "core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_source_tests.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/publication.rs", "count": 1 }, @@ -6163,6 +6179,10 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/outbox/tests/retraction_source_tests.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/publication.rs", "count": 1 },