myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

services_hardening_state_catalog.rs (20705B)


      1 #![forbid(unsafe_code)]
      2 
      3 use std::error::Error;
      4 
      5 use myc::{
      6     MYC_MIGRATION_CATALOG_SHA256, MYC_STATE_BASE_SCHEMA_VERSION, MYC_STATE_SCHEMA_CATALOG_SHA256,
      7     MYC_STATE_SCHEMA_VERSION, MYC_STATE_SCHEMA_VERSION_1_OBJECT_COUNT,
      8     MYC_STATE_SCHEMA_VERSION_1_SHA256, MYC_STATE_SCHEMA_VERSION_2_MIGRATION_SHA256,
      9     MYC_STATE_SCHEMA_VERSION_2_OBJECT_COUNT, MYC_STATE_SCHEMA_VERSION_2_SHA256,
     10     MYC_STATE_SCHEMA_VERSION_3_MIGRATION_SHA256, MYC_STATE_SCHEMA_VERSION_3_OBJECT_COUNT,
     11     MYC_STATE_SCHEMA_VERSION_3_SHA256, MYC_STATE_SCHEMA_VERSION_4_MIGRATION_SHA256,
     12     MYC_STATE_SCHEMA_VERSION_4_OBJECT_COUNT, MYC_STATE_SCHEMA_VERSION_4_SHA256,
     13     MYC_STATE_SCHEMA_VERSION_5_MIGRATION_SHA256, MYC_STATE_SCHEMA_VERSION_5_OBJECT_COUNT,
     14     MYC_STATE_SCHEMA_VERSION_5_SHA256, MYC_STATE_SCHEMA_VERSION_6_MIGRATION_SHA256,
     15     MYC_STATE_SCHEMA_VERSION_6_OBJECT_COUNT, MYC_STATE_SCHEMA_VERSION_6_SHA256,
     16     MYC_STATE_SCHEMA_VERSION_7_MIGRATION_SHA256, MYC_STATE_SCHEMA_VERSION_7_OBJECT_COUNT,
     17     MYC_STATE_SCHEMA_VERSION_7_SHA256, MYC_STATE_SCHEMA_VERSION_8_MIGRATION_SHA256,
     18     MYC_STATE_SCHEMA_VERSION_8_OBJECT_COUNT, MYC_STATE_SCHEMA_VERSION_8_SHA256,
     19     MYC_STATE_SCHEMA_VERSION_9_MIGRATION_SHA256, MYC_STATE_SCHEMA_VERSION_9_OBJECT_COUNT,
     20     MYC_STATE_SCHEMA_VERSION_9_SHA256, MYC_STATE_SCHEMA_VERSION_10_MIGRATION_SHA256,
     21     MYC_STATE_SCHEMA_VERSION_10_OBJECT_COUNT, MYC_STATE_SCHEMA_VERSION_10_SHA256,
     22     MYC_STATE_SCHEMA_VERSION_11_MIGRATION_SHA256, MYC_STATE_SCHEMA_VERSION_11_OBJECT_COUNT,
     23     MYC_STATE_SCHEMA_VERSION_11_SHA256, MYC_STATE_SCHEMA_VERSION_12_MIGRATION_SHA256,
     24     MYC_STATE_SCHEMA_VERSION_12_OBJECT_COUNT, MYC_STATE_SCHEMA_VERSION_12_SHA256,
     25     MycStateCatalogErrorKind, myc_migration_catalog, myc_schema_catalog,
     26     validate_myc_state_catalogs,
     27 };
     28 use radroots_service_sqlite::{
     29     MigrationCatalog, MigrationChecksum, MigrationDescriptor, SchemaCatalog, SchemaDigest,
     30     SchemaObject, SchemaObjectKind, SchemaVersionCatalog,
     31 };
     32 
     33 const CATALOG_SOURCE: &str = include_str!("../src/state_catalog.rs");
     34 const LIB_SOURCE: &str = include_str!("../src/lib.rs");
     35 const MANIFEST: &str = include_str!("../Cargo.toml");
     36 
     37 #[test]
     38 fn schema_v1_through_v12_and_all_migrations_have_exact_literal_identities() {
     39     let migrations = myc_migration_catalog().expect("Myc migration catalog");
     40     let schema = myc_schema_catalog().expect("Myc schema catalog");
     41 
     42     assert_eq!(MYC_STATE_BASE_SCHEMA_VERSION, 1);
     43     assert_eq!(MYC_STATE_SCHEMA_VERSION, 12);
     44     assert_eq!(migrations.descriptors().len(), 11);
     45     let metadata = &migrations.descriptors()[0];
     46     assert_eq!(metadata.target_version(), 2);
     47     assert_eq!(metadata.name().as_str(), "create_myc_state_metadata");
     48     assert_eq!(
     49         metadata.checksum().as_bytes(),
     50         &MYC_STATE_SCHEMA_VERSION_2_MIGRATION_SHA256
     51     );
     52     let request = &migrations.descriptors()[1];
     53     assert_eq!(request.target_version(), 3);
     54     assert_eq!(request.name().as_str(), "create_nip46_request_admission");
     55     assert_eq!(
     56         request.checksum().as_bytes(),
     57         &MYC_STATE_SCHEMA_VERSION_3_MIGRATION_SHA256
     58     );
     59     let connection = &migrations.descriptors()[2];
     60     assert_eq!(connection.target_version(), 4);
     61     assert_eq!(
     62         connection.name().as_str(),
     63         "create_connection_authorization_state"
     64     );
     65     assert_eq!(
     66         connection.checksum().as_bytes(),
     67         &MYC_STATE_SCHEMA_VERSION_4_MIGRATION_SHA256
     68     );
     69     let governance = &migrations.descriptors()[3];
     70     assert_eq!(governance.target_version(), 5);
     71     assert_eq!(
     72         governance.name().as_str(),
     73         "create_bounded_governance_state"
     74     );
     75     assert_eq!(
     76         governance.checksum().as_bytes(),
     77         &MYC_STATE_SCHEMA_VERSION_5_MIGRATION_SHA256
     78     );
     79     let delivery = &migrations.descriptors()[4];
     80     assert_eq!(delivery.target_version(), 6);
     81     assert_eq!(delivery.name().as_str(), "create_delivery_evidence_state");
     82     assert_eq!(
     83         delivery.checksum().as_bytes(),
     84         &MYC_STATE_SCHEMA_VERSION_6_MIGRATION_SHA256
     85     );
     86     let discovery = &migrations.descriptors()[5];
     87     assert_eq!(discovery.target_version(), 7);
     88     assert_eq!(discovery.name().as_str(), "create_discovery_desired_state");
     89     assert_eq!(
     90         discovery.checksum().as_bytes(),
     91         &MYC_STATE_SCHEMA_VERSION_7_MIGRATION_SHA256
     92     );
     93     let completion = &migrations.descriptors()[6];
     94     assert_eq!(completion.target_version(), 8);
     95     assert_eq!(
     96         completion.name().as_str(),
     97         "create_nip46_operation_completion"
     98     );
     99     assert_eq!(
    100         completion.checksum().as_bytes(),
    101         &MYC_STATE_SCHEMA_VERSION_8_MIGRATION_SHA256
    102     );
    103     let response = &migrations.descriptors()[7];
    104     assert_eq!(response.target_version(), 9);
    105     assert_eq!(response.name().as_str(), "create_nip46_atomic_response");
    106     assert_eq!(
    107         response.checksum().as_bytes(),
    108         &MYC_STATE_SCHEMA_VERSION_9_MIGRATION_SHA256
    109     );
    110     let configuration = &migrations.descriptors()[8];
    111     assert_eq!(configuration.target_version(), 10);
    112     assert_eq!(
    113         configuration.name().as_str(),
    114         "create_configuration_binding_history"
    115     );
    116     assert_eq!(
    117         configuration.checksum().as_bytes(),
    118         &MYC_STATE_SCHEMA_VERSION_10_MIGRATION_SHA256
    119     );
    120     let admin_operations = &migrations.descriptors()[9];
    121     assert_eq!(admin_operations.target_version(), 11);
    122     assert_eq!(
    123         admin_operations.name().as_str(),
    124         "create_admin_operation_journal"
    125     );
    126     assert_eq!(
    127         admin_operations.checksum().as_bytes(),
    128         &MYC_STATE_SCHEMA_VERSION_11_MIGRATION_SHA256
    129     );
    130     let pending_responses = &migrations.descriptors()[10];
    131     assert_eq!(pending_responses.target_version(), 12);
    132     assert_eq!(
    133         pending_responses.name().as_str(),
    134         "create_nip46_pending_response_authority"
    135     );
    136     assert_eq!(
    137         pending_responses.checksum().as_bytes(),
    138         &MYC_STATE_SCHEMA_VERSION_12_MIGRATION_SHA256
    139     );
    140     assert_eq!(migrations.current_version(), 12);
    141     assert_eq!(
    142         migrations.digest().as_bytes(),
    143         &MYC_MIGRATION_CATALOG_SHA256
    144     );
    145 
    146     assert_eq!(schema.versions().len(), 12);
    147     assert_eq!(schema.versions()[0].version(), 1);
    148     assert_eq!(
    149         schema.versions()[0].object_count(),
    150         MYC_STATE_SCHEMA_VERSION_1_OBJECT_COUNT
    151     );
    152     assert_eq!(
    153         schema.versions()[0].digest().as_bytes(),
    154         &MYC_STATE_SCHEMA_VERSION_1_SHA256
    155     );
    156     assert_eq!(schema.versions()[1].version(), 2);
    157     assert_eq!(
    158         schema.versions()[1].object_count(),
    159         MYC_STATE_SCHEMA_VERSION_2_OBJECT_COUNT
    160     );
    161     assert_eq!(schema.versions()[1].object_count(), 9);
    162     assert_eq!(
    163         schema.versions()[1].digest().as_bytes(),
    164         &MYC_STATE_SCHEMA_VERSION_2_SHA256
    165     );
    166     assert_eq!(schema.versions()[2].version(), 3);
    167     assert_eq!(
    168         schema.versions()[2].object_count(),
    169         MYC_STATE_SCHEMA_VERSION_3_OBJECT_COUNT
    170     );
    171     assert_eq!(schema.versions()[2].object_count(), 13);
    172     assert_eq!(
    173         schema.versions()[2].digest().as_bytes(),
    174         &MYC_STATE_SCHEMA_VERSION_3_SHA256
    175     );
    176     assert_eq!(schema.versions()[3].version(), 4);
    177     assert_eq!(
    178         schema.versions()[3].object_count(),
    179         MYC_STATE_SCHEMA_VERSION_4_OBJECT_COUNT
    180     );
    181     assert_eq!(schema.versions()[3].object_count(), 25);
    182     assert_eq!(
    183         schema.versions()[3].digest().as_bytes(),
    184         &MYC_STATE_SCHEMA_VERSION_4_SHA256
    185     );
    186     assert_eq!(schema.versions()[4].version(), 5);
    187     assert_eq!(
    188         schema.versions()[4].object_count(),
    189         MYC_STATE_SCHEMA_VERSION_5_OBJECT_COUNT
    190     );
    191     assert_eq!(schema.versions()[4].object_count(), 34);
    192     assert_eq!(
    193         schema.versions()[4].digest().as_bytes(),
    194         &MYC_STATE_SCHEMA_VERSION_5_SHA256
    195     );
    196     assert_eq!(schema.versions()[5].version(), 6);
    197     assert_eq!(
    198         schema.versions()[5].object_count(),
    199         MYC_STATE_SCHEMA_VERSION_6_OBJECT_COUNT
    200     );
    201     assert_eq!(schema.versions()[5].object_count(), 43);
    202     assert_eq!(
    203         schema.versions()[5].digest().as_bytes(),
    204         &MYC_STATE_SCHEMA_VERSION_6_SHA256
    205     );
    206     assert_eq!(schema.versions()[6].version(), 7);
    207     assert_eq!(
    208         schema.versions()[6].object_count(),
    209         MYC_STATE_SCHEMA_VERSION_7_OBJECT_COUNT
    210     );
    211     assert_eq!(schema.versions()[6].object_count(), 53);
    212     assert_eq!(
    213         schema.versions()[6].digest().as_bytes(),
    214         &MYC_STATE_SCHEMA_VERSION_7_SHA256
    215     );
    216     assert_eq!(schema.versions()[7].version(), 8);
    217     assert_eq!(
    218         schema.versions()[7].object_count(),
    219         MYC_STATE_SCHEMA_VERSION_8_OBJECT_COUNT
    220     );
    221     assert_eq!(schema.versions()[7].object_count(), 56);
    222     assert_eq!(
    223         schema.versions()[7].digest().as_bytes(),
    224         &MYC_STATE_SCHEMA_VERSION_8_SHA256
    225     );
    226     assert_eq!(schema.versions()[8].version(), 9);
    227     assert_eq!(
    228         schema.versions()[8].object_count(),
    229         MYC_STATE_SCHEMA_VERSION_9_OBJECT_COUNT
    230     );
    231     assert_eq!(schema.versions()[8].object_count(), 59);
    232     assert_eq!(
    233         schema.versions()[8].digest().as_bytes(),
    234         &MYC_STATE_SCHEMA_VERSION_9_SHA256
    235     );
    236     assert_eq!(schema.versions()[9].version(), 10);
    237     assert_eq!(
    238         schema.versions()[9].object_count(),
    239         MYC_STATE_SCHEMA_VERSION_10_OBJECT_COUNT
    240     );
    241     assert_eq!(schema.versions()[9].object_count(), 63);
    242     assert_eq!(
    243         schema.versions()[9].digest().as_bytes(),
    244         &MYC_STATE_SCHEMA_VERSION_10_SHA256
    245     );
    246     assert_eq!(schema.versions()[10].version(), 11);
    247     assert_eq!(
    248         schema.versions()[10].object_count(),
    249         MYC_STATE_SCHEMA_VERSION_11_OBJECT_COUNT
    250     );
    251     assert_eq!(schema.versions()[10].object_count(), 65);
    252     assert_eq!(
    253         schema.versions()[10].digest().as_bytes(),
    254         &MYC_STATE_SCHEMA_VERSION_11_SHA256
    255     );
    256     assert_eq!(schema.versions()[11].version(), 12);
    257     assert_eq!(
    258         schema.versions()[11].object_count(),
    259         MYC_STATE_SCHEMA_VERSION_12_OBJECT_COUNT
    260     );
    261     assert_eq!(schema.versions()[11].object_count(), 70);
    262     assert_eq!(
    263         schema.versions()[11].digest().as_bytes(),
    264         &MYC_STATE_SCHEMA_VERSION_12_SHA256
    265     );
    266     assert_eq!(schema.digest().as_bytes(), &MYC_STATE_SCHEMA_CATALOG_SHA256);
    267     assert_eq!(schema.migration_catalog_digest(), migrations.digest());
    268     validate_myc_state_catalogs(&migrations, &schema).expect("exact catalogs");
    269 
    270     assert_eq!(
    271         hex::encode(MYC_STATE_SCHEMA_VERSION_2_MIGRATION_SHA256),
    272         "c5eb978bda1bc70c70bd9bd44d8cba70cb2857d26a9dce74961f4b781e710037"
    273     );
    274     assert_eq!(
    275         hex::encode(MYC_MIGRATION_CATALOG_SHA256),
    276         "b1d6458245e6dfc4661aa6146b8ce8ab55f7c29bf36099d261c07f5f5167561d"
    277     );
    278     assert_eq!(
    279         hex::encode(MYC_STATE_SCHEMA_VERSION_1_SHA256),
    280         "94dc66fbca601679615c055229dc0db6119f5bd92b04390c67f698a036fa78ae"
    281     );
    282     assert_eq!(
    283         hex::encode(MYC_STATE_SCHEMA_VERSION_2_SHA256),
    284         "94739b5a34ca8ed130b946d092731b59bf1548fe541d9a9269a33d0aed1ea09e"
    285     );
    286     assert_eq!(
    287         hex::encode(MYC_STATE_SCHEMA_CATALOG_SHA256),
    288         "b52721d85c1eb8cddc28256c21179a10d5f32bcb33e9d2a6bb8fe49ecbc49a68"
    289     );
    290     assert_eq!(
    291         hex::encode(MYC_STATE_SCHEMA_VERSION_3_MIGRATION_SHA256),
    292         "753165136b3dace0091d782f33f6b10ca1a2823314158d80a4775e0af628edf9"
    293     );
    294     assert_eq!(
    295         hex::encode(MYC_STATE_SCHEMA_VERSION_3_SHA256),
    296         "572fe6a4d36c0476ec40536f48028e1558488fb8abeba0a34ba69b4b7080ba08"
    297     );
    298     assert_eq!(
    299         hex::encode(MYC_STATE_SCHEMA_VERSION_4_MIGRATION_SHA256),
    300         "939c0ed07cd15cc0c794bf6c2af7192261409305c2876f3be363e8e4fe4a1abb"
    301     );
    302     assert_eq!(
    303         hex::encode(MYC_STATE_SCHEMA_VERSION_4_SHA256),
    304         "479863d37d91e6c269fa3573db6c2e767cdd3b24a93ab482d774bcec0218c174"
    305     );
    306     assert_eq!(
    307         hex::encode(MYC_STATE_SCHEMA_VERSION_5_MIGRATION_SHA256),
    308         "0e004fcb5d0bc7c951b16f4334533d10efcb18a826f624de0922d86dc8504b33"
    309     );
    310     assert_eq!(
    311         hex::encode(MYC_STATE_SCHEMA_VERSION_5_SHA256),
    312         "fe89d4af7de4eded78a3f062dc9d300f06cf0f4cfca5fa5cffc93af1146f21c5"
    313     );
    314     assert_eq!(
    315         hex::encode(MYC_STATE_SCHEMA_VERSION_6_MIGRATION_SHA256),
    316         "4649b9afd03fc07f89fe184f027925675a0265951ea7cf75e06a4312c55a82bd"
    317     );
    318     assert_eq!(
    319         hex::encode(MYC_STATE_SCHEMA_VERSION_6_SHA256),
    320         "557273306e68e9306dc1d7c7009e1cb7c9d15b8d52e07db7d0bd51e83f054492"
    321     );
    322     assert_eq!(
    323         hex::encode(MYC_STATE_SCHEMA_VERSION_7_MIGRATION_SHA256),
    324         "6097c40776a57dd4bddc04e652987217d6f5991f2d5e94d322d1208619254e6c"
    325     );
    326     assert_eq!(
    327         hex::encode(MYC_STATE_SCHEMA_VERSION_7_SHA256),
    328         "3b3528911a293499d9721da71b6ad07a5e723e97b6ebf5b40a19b9058958bf79"
    329     );
    330     assert_eq!(
    331         hex::encode(MYC_STATE_SCHEMA_VERSION_8_MIGRATION_SHA256),
    332         "b81f980c91acd98b91ecd5cb248e4027c9f47c1dc8d0134a61af5c38238315bc"
    333     );
    334     assert_eq!(
    335         hex::encode(MYC_STATE_SCHEMA_VERSION_8_SHA256),
    336         "50158cb093ed70b3d5783762b18d21b7809665c89fde925d872365909d28f06e"
    337     );
    338     assert_eq!(
    339         hex::encode(MYC_STATE_SCHEMA_VERSION_9_MIGRATION_SHA256),
    340         "fdb039d472cd62e7da46c40a9786c3a9a7ec55f2ae7db689d0f855fbbf8a9566"
    341     );
    342     assert_eq!(
    343         hex::encode(MYC_STATE_SCHEMA_VERSION_9_SHA256),
    344         "eee76f4ff0bd2dc2c061ae384e00de16c5f5efc4b6edd0ac7f73cad83a991ff7"
    345     );
    346     assert_eq!(
    347         hex::encode(MYC_STATE_SCHEMA_VERSION_10_MIGRATION_SHA256),
    348         "28423ebb59f4b26223307b74a7e12905ca4818c01a655203ee8d63c911f44489"
    349     );
    350     assert_eq!(
    351         hex::encode(MYC_STATE_SCHEMA_VERSION_10_SHA256),
    352         "b77ed23afa39ff45dda250faa1ebfc0587c34462658fc49fb7b2fbc8909ba303"
    353     );
    354     assert_eq!(
    355         hex::encode(MYC_STATE_SCHEMA_VERSION_11_MIGRATION_SHA256),
    356         "16385368aa4ee40ea7002a0ab42645bc68b546a4ba6afdfede565fe026576c96"
    357     );
    358     assert_eq!(
    359         hex::encode(MYC_STATE_SCHEMA_VERSION_11_SHA256),
    360         "0de7fe176ea7da6030424addc29b9a91363e88b04dc78ddab480fd0f0af94e5a"
    361     );
    362     assert_eq!(
    363         hex::encode(MYC_STATE_SCHEMA_VERSION_12_MIGRATION_SHA256),
    364         "388ee51de599f37b7bb1956cebd518461f3eb193535ffe6bb9eac2d82bbe3e37"
    365     );
    366     assert_eq!(
    367         hex::encode(MYC_STATE_SCHEMA_VERSION_12_SHA256),
    368         "d893a23ba68e463489ad7ef1e6a80ea4e88089381c73decc3243a4ce6f64da8f"
    369     );
    370 }
    371 
    372 #[test]
    373 fn independent_validator_rejects_migration_or_schema_drift() {
    374     const SQL: &str = "CREATE TABLE unexpected (value INTEGER NOT NULL) STRICT";
    375     let migration =
    376         MigrationDescriptor::sql(2, "unexpected_schema", SQL, MigrationChecksum::for_sql(SQL))
    377             .expect("valid drift fixture");
    378     let migrations = MigrationCatalog::new([migration]).expect("drift migration catalog");
    379     let expected_schema = myc_schema_catalog().expect("expected schema");
    380     assert_eq!(
    381         validate_myc_state_catalogs(&migrations, &expected_schema)
    382             .expect_err("migration drift")
    383             .kind(),
    384         MycStateCatalogErrorKind::CatalogMismatch
    385     );
    386 
    387     let expected_migrations = myc_migration_catalog().expect("expected migrations");
    388     let v1 = SchemaVersionCatalog::new(
    389         1,
    390         [],
    391         SchemaDigest::from_bytes(MYC_STATE_SCHEMA_VERSION_1_SHA256),
    392     )
    393     .expect("schema v1");
    394     let object_digest =
    395         SchemaObject::computed_digest(SchemaObjectKind::Table, "unexpected", "unexpected", SQL)
    396             .expect("object digest");
    397     let object = SchemaObject::new(
    398         SchemaObjectKind::Table,
    399         "unexpected",
    400         "unexpected",
    401         SQL,
    402         object_digest,
    403     )
    404     .expect("schema object");
    405     let snapshot_digest =
    406         SchemaVersionCatalog::computed_digest(2, [object.clone()]).expect("snapshot digest");
    407     let v2 = SchemaVersionCatalog::new(2, [object.clone()], snapshot_digest).expect("schema v2");
    408     let v3_digest =
    409         SchemaVersionCatalog::computed_digest(3, [object.clone()]).expect("schema-v3 digest");
    410     let v3 = SchemaVersionCatalog::new(3, [object.clone()], v3_digest).expect("schema v3");
    411     let v4_digest =
    412         SchemaVersionCatalog::computed_digest(4, [object.clone()]).expect("schema-v4 digest");
    413     let v4 = SchemaVersionCatalog::new(4, [object.clone()], v4_digest).expect("schema v4");
    414     let v5_digest =
    415         SchemaVersionCatalog::computed_digest(5, [object.clone()]).expect("schema-v5 digest");
    416     let v5 = SchemaVersionCatalog::new(5, [object.clone()], v5_digest).expect("schema v5");
    417     let v6_digest =
    418         SchemaVersionCatalog::computed_digest(6, [object.clone()]).expect("schema-v6 digest");
    419     let v6 = SchemaVersionCatalog::new(6, [object.clone()], v6_digest).expect("schema v6");
    420     let v7_digest =
    421         SchemaVersionCatalog::computed_digest(7, [object.clone()]).expect("schema-v7 digest");
    422     let v7 = SchemaVersionCatalog::new(7, [object.clone()], v7_digest).expect("schema v7");
    423     let v8_digest =
    424         SchemaVersionCatalog::computed_digest(8, [object.clone()]).expect("schema-v8 digest");
    425     let v8 = SchemaVersionCatalog::new(8, [object.clone()], v8_digest).expect("schema v8");
    426     let v9_digest =
    427         SchemaVersionCatalog::computed_digest(9, [object.clone()]).expect("schema-v9 digest");
    428     let v9 = SchemaVersionCatalog::new(9, [object.clone()], v9_digest).expect("schema v9");
    429     let v10_digest =
    430         SchemaVersionCatalog::computed_digest(10, [object.clone()]).expect("schema-v10 digest");
    431     let v10 = SchemaVersionCatalog::new(10, [object.clone()], v10_digest).expect("schema v10");
    432     let v11_digest =
    433         SchemaVersionCatalog::computed_digest(11, [object.clone()]).expect("schema-v11 digest");
    434     let v11 = SchemaVersionCatalog::new(11, [object.clone()], v11_digest).expect("schema v11");
    435     let v12_digest =
    436         SchemaVersionCatalog::computed_digest(12, [object.clone()]).expect("schema-v12 digest");
    437     let v12 = SchemaVersionCatalog::new(12, [object], v12_digest).expect("schema v12");
    438     let schema = SchemaCatalog::new(
    439         &expected_migrations,
    440         [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12],
    441     )
    442     .expect("drift schema catalog");
    443     assert_eq!(
    444         validate_myc_state_catalogs(&expected_migrations, &schema)
    445             .expect_err("schema drift")
    446             .kind(),
    447         MycStateCatalogErrorKind::CatalogMismatch
    448     );
    449 }
    450 
    451 #[test]
    452 fn catalog_errors_are_stable_source_free_and_redacted() {
    453     const SQL: &str = "CREATE TABLE secret_table (value INTEGER NOT NULL) STRICT";
    454     let migration =
    455         MigrationDescriptor::sql(2, "secret_schema", SQL, MigrationChecksum::for_sql(SQL))
    456             .expect("migration");
    457     let migrations = MigrationCatalog::new([migration]).expect("migration catalog");
    458     let expected_schema = myc_schema_catalog().expect("expected schema");
    459     let error = validate_myc_state_catalogs(&migrations, &expected_schema).expect_err("mismatch");
    460 
    461     assert_eq!(error.kind(), MycStateCatalogErrorKind::CatalogMismatch);
    462     assert_eq!(error.code(), "state_catalog_mismatch");
    463     assert!(Error::source(&error).is_none());
    464     let rendered = format!("{error} {error:?}");
    465     assert!(!rendered.contains("secret"));
    466     assert!(!rendered.contains(&hex::encode(MigrationChecksum::for_sql(SQL).as_bytes())));
    467 }
    468 
    469 #[test]
    470 fn catalog_source_is_pure_pinned_and_uses_only_the_shared_authority() {
    471     assert!(MANIFEST.contains(
    472         "radroots_service_sqlite = { git = \"https://github.com/radrootslabs/lib\", rev = \"055096853fca95e15d0f813d33a14aca13be3881\", version = \"=0.1.0-alpha\" }"
    473     ));
    474     assert!(LIB_SOURCE.contains("mod state_catalog;"));
    475     assert!(!LIB_SOURCE.contains("pub mod state_catalog;"));
    476     assert!(CATALOG_SOURCE.contains("MigrationDescriptor::sql("));
    477     assert!(CATALOG_SOURCE.contains("MigrationChecksum::from_bytes("));
    478     assert!(CATALOG_SOURCE.contains("SchemaDigest::from_bytes("));
    479     assert!(CATALOG_SOURCE.contains("FROM publication_outbox;"));
    480     assert!(
    481         CATALOG_SOURCE.contains("INSERT INTO delivery_targets SELECT * FROM publication_targets;")
    482     );
    483     assert!(
    484         CATALOG_SOURCE
    485             .contains("INSERT INTO delivery_attempts SELECT * FROM publication_attempts;")
    486     );
    487     assert!(CATALOG_SOURCE.contains("delivery_jobs_guard_insert"));
    488     assert!(!CATALOG_SOURCE.contains("computed_digest"));
    489     assert!(!CATALOG_SOURCE.contains("ALTER TABLE"));
    490     for forbidden in [
    491         "sqlx::",
    492         "rusqlite",
    493         "libsqlite3_sys",
    494         "raw_sql",
    495         "std::fs",
    496         "std::path",
    497         "SqliteConnection",
    498         "Transaction",
    499     ] {
    500         assert!(
    501             !CATALOG_SOURCE.contains(forbidden),
    502             "found forbidden catalog authority `{forbidden}`"
    503         );
    504     }
    505 }