diff --git a/docs/development/OpenNexus生产化实施进度-2026-09-08.md b/docs/development/OpenNexus生产化实施进度-2026-09-08.md index 4151a39..c137658 100644 --- a/docs/development/OpenNexus生产化实施进度-2026-09-08.md +++ b/docs/development/OpenNexus生产化实施进度-2026-09-08.md @@ -217,3 +217,12 @@ Core 的独立数据目录目前不等于已授权 Vault。Python 旧笔记写 - 这仍是安装事务的存储层:配置只要求对象,完整类型 schema/秘密剥离、依赖计划和用户确认复核、在线撤回、停旧实例、沙箱内迁移、真实健康探测及 UI 尚待编排。不能把注入错误重开测试当成 D-03 断电/磁盘满完整矩阵,也不能开启 extensions capability。 - 本轮 Rust desktop 全量回归 78 项通过,2 个既有特殊入口 ignored;实际 Sync 子进程中断恢复由父测试执行通过。全量日志 `.build/extension-transaction-rust-full.log`。 + + +## 增量:切换前的签名配置 schema 校验 + +- switch_prepared 从重新验证的签名清单读取 configuration_schema,配置不能自带替代 schema。未声明时只接受空对象;声明使用 Draft 2020-12、离线 jsonschema 0.55.0(关闭默认网络/文件解析功能),启用格式检查和线性正则引擎。 +- schema 与配置各限 64 KiB、4096 节点、24 层;当前拒绝所有引用,包括本地引用,避免 Host 执行递归 schema。高级正则、其他 schema draft 不接受;此限制应在安装 UI 中明确展示,尚无完整配置编辑 UI。 +- 配置递归拒绝常用秘密字段;writeOnly / x-opennexus-secret 声明禁止持久化对应值,包括条件和组合分支,不能通过 anyOf 的另一个分支绕过。此规则对条件分支采取保守拒绝;秘密应由后续凭据 broker 提供,任意普通字符串不能被自动识别为秘密。 +- 测试覆盖嵌套类型、范围、未知属性、错误 schema、引用、深度/体积、秘密声明和组合绕过;安装入口拒绝秘密配置后事务表保持为空。26 项扩展回归通过,日志 `.build/extension-config-tests.log`。 +- 这不替代各扩展类型的完整运行配置契约、用户确认、在线信任与沙箱,也没有完成配置迁移/导入秘密的完整流程。整体生产化继续未完成。 diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 1a73da3..751d758 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -56,7 +56,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "getrandom 0.3.4", "once_cell", + "serde", "version_check", "zerocopy", ] @@ -497,6 +499,12 @@ dependencies = [ "piper", ] +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + [[package]] name = "brotli" version = "8.0.4" @@ -533,6 +541,12 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + [[package]] name = "bytemuck" version = "1.25.2" @@ -1068,6 +1082,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b1e3a325bc115f096c8b77bbf027a7c2592230e70be2d985be950d3d5e60ebe" +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + [[package]] name = "dbus" version = "0.9.12" @@ -1422,6 +1442,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + [[package]] name = "embed-resource" version = "3.0.11" @@ -1552,6 +1581,17 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fancy-regex" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e0387578e845beb7a1acff126228499f26cb18edf12919cc513bb863266464" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fastrand" version = "2.5.0" @@ -1637,6 +1677,17 @@ dependencies = [ "zlib-rs", ] +[[package]] +name = "fluent-uri" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1685,6 +1736,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e246562084dde8ebbcc943b261c406ce4f68e5032ec28029a251a47d6a295500" +dependencies = [ + "num", + "num-bigint", +] + [[package]] name = "fs-set-times" version = "0.20.3" @@ -1920,9 +1981,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -2149,6 +2212,11 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "hashlink" @@ -2740,6 +2808,59 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonschema" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e89c7d35509ab5248f4160addc47098d0f2b09221851bf6234a948f28a70c4" +dependencies = [ + "ahash", + "bytecount", + "data-encoding", + "email_address", + "fancy-regex", + "fraction", + "getrandom 0.3.4", + "itoa", + "jsonschema-regex", + "jsonschema-value", + "num-cmp", + "num-traits", + "percent-encoding", + "referencing", + "regex", + "serde", + "serde_json", + "strum", + "unicode-general-category", + "uuid-simd", +] + +[[package]] +name = "jsonschema-regex" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1fb77178bdde7326e2e414526342ad4e26b602ba5372e53440129dc061bc50b" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "jsonschema-value" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dadb285b774085f3f3e67cb48ecce59e5d92028f19a01fe014083a4fbaf7fea" +dependencies = [ + "ahash", + "bytecount", + "fraction", + "getrandom 0.3.4", + "num-cmp", + "num-traits", + "serde_json", + "zmij", +] + [[package]] name = "k256" version = "0.13.4" @@ -2958,6 +3079,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "micromap" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" + [[package]] name = "mime" version = "0.3.17" @@ -3127,6 +3254,7 @@ dependencies = [ "fs2", "hmac", "iota_stronghold", + "jsonschema", "rand 0.8.8", "reqwest 0.12.28", "rfd", @@ -3148,12 +3276,81 @@ dependencies = [ "zip", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3408,6 +3605,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "pango" version = "0.18.3" @@ -4007,6 +4210,23 @@ dependencies = [ "syn 3.0.5", ] +[[package]] +name = "referencing" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cc7249156d90d60480d54850c861c37c92f20c6dbb9ae85d98ecca19d81c0b2" +dependencies = [ + "ahash", + "fluent-uri", + "getrandom 0.3.4", + "hashbrown 0.17.1", + "itoa", + "micromap", + "parking_lot", + "percent-encoding", + "serde_json", +] + [[package]] name = "regex" version = "1.13.1" @@ -4822,6 +5042,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "subtle" version = "2.6.1" @@ -5667,6 +5908,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f66b1c8f8caa2ab31dc6d3f35386f16efdab89668f93411e565ac368908e8f" +[[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -5790,6 +6037,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -5808,6 +6065,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "vswhom" version = "0.1.0" diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 0e06b62..ae60b1a 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -45,6 +45,7 @@ serde-saphyr = { version = "1.2", default-features = false, features = ["deseria semver = { version = "1", features = ["serde"] } cap-std = "4.0.2" cap-fs-ext = "4.0.2" +jsonschema = { version = "0.55", default-features = false } [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_RemoteDesktop", "Win32_UI_WindowsAndMessaging", "Win32_Graphics_Gdi", "Win32_System_LibraryLoader"] } diff --git a/frontend/src-tauri/src/extension_config.rs b/frontend/src-tauri/src/extension_config.rs new file mode 100644 index 0000000..322c39e --- /dev/null +++ b/frontend/src-tauri/src/extension_config.rs @@ -0,0 +1,225 @@ +//! Signed, offline configuration schemas. Credentials belong to the vault broker. +use crate::workspace::{HostError, Result}; +use serde_json::{json, Value}; + +fn walk(value: &Value, depth: usize, nodes: &mut usize, schema: bool) -> Result<()> { + if depth > 24 || *nodes >= 4096 { + return Err(HostError::new("EXTENSION_CONFIG_LIMIT")); + } + *nodes += 1; + match value { + Value::Object(map) => { + for (key, value) in map { + if schema && matches!(key.as_str(), "$ref" | "$dynamicRef" | "$recursiveRef") { + // Recursive/unbounded schema execution is not allowed in the Host. + return Err(HostError::new("EXTENSION_CONFIG_REFERENCE")); + } + if !schema + && matches!( + key.to_ascii_lowercase().as_str(), + "api_key" + | "apikey" + | "password" + | "token" + | "secret" + | "access_token" + | "refresh_token" + | "authorization" + | "client_secret" + | "private_key" + ) + { + return Err(HostError::new("EXTENSION_CONFIG_SECRET")); + } + walk(value, depth + 1, nodes, schema)?; + } + } + Value::Array(items) => { + for item in items { + walk(item, depth + 1, nodes, schema)?; + } + } + _ => {} + } + Ok(()) +} + +// Examine every applicable schema branch; an alternative branch must not +// turn a secret declaration back into persistable plaintext. +fn reject_secrets(schema: &Value, instance: &Value) -> Result<()> { + let Some(map) = schema.as_object() else { + return Ok(()); + }; + if map.get("writeOnly") == Some(&Value::Bool(true)) + || map.get("x-opennexus-secret") == Some(&Value::Bool(true)) + { + return Err(HostError::new("EXTENSION_CONFIG_SECRET")); + } + for key in ["allOf", "anyOf", "oneOf"] { + if let Some(items) = map.get(key).and_then(Value::as_array) { + for item in items { + reject_secrets(item, instance)?; + } + } + } + for key in ["if", "then", "else", "not"] { + if let Some(child) = map.get(key) { + reject_secrets(child, instance)?; + } + } + if let Some(properties) = instance.as_object() { + for (name, value) in properties { + if let Some(child) = map.get("unevaluatedProperties") { + reject_secrets(child, value)?; + } + let declared = map.get("properties").and_then(|p| p.get(name)); + if let Some(child) = declared { + reject_secrets(child, value)?; + } + let mut matched = false; + if let Some(patterns) = map.get("patternProperties").and_then(Value::as_object) { + for (pattern, child) in patterns { + let matcher = jsonschema::options() + .offline() + .with_pattern_options(jsonschema::PatternOptions::regex()) + .build(&json!({"pattern":pattern})) + .map_err(|_| HostError::new("EXTENSION_CONFIG_SCHEMA"))?; + if matcher.is_valid(&Value::String(name.clone())) { + matched = true; + reject_secrets(child, value)?; + } + } + } + if declared.is_none() && !matched { + if let Some(child) = map.get("additionalProperties") { + reject_secrets(child, value)?; + } + } + } + if let Some(dependent) = map.get("dependentSchemas").and_then(Value::as_object) { + for (name, child) in dependent { + if properties.contains_key(name) { + reject_secrets(child, instance)?; + } + } + } + } + if let Some(items) = instance.as_array() { + let prefix = map.get("prefixItems").and_then(Value::as_array); + for (index, item) in items.iter().enumerate() { + if let Some(child) = map.get("unevaluatedItems") { + reject_secrets(child, item)?; + } + if let Some(child) = prefix + .and_then(|p| p.get(index)) + .or_else(|| map.get("items")) + { + reject_secrets(child, item)?; + } + if let Some(child) = map.get("contains") { + reject_secrets(child, item)?; + } + } + } + Ok(()) +} + +/// Schema is read from the verified manifest, never from the proposed configuration. +pub fn validate(manifest: &Value, configuration: &Value) -> Result<()> { + if !configuration.is_object() { + return Err(HostError::new("EXTENSION_CONFIG_INVALID")); + } + walk(configuration, 0, &mut 0, false)?; + let schema = manifest + .get("configuration_schema") + .cloned() + .unwrap_or_else(|| json!({"type":"object","additionalProperties":false})); + walk(&schema, 0, &mut 0, true)?; + if schema + .get("$schema") + .is_some_and(|v| v.as_str() != Some("https://json-schema.org/draft/2020-12/schema")) + { + return Err(HostError::new("EXTENSION_CONFIG_SCHEMA")); + } + for value in [&schema, configuration] { + if serde_json::to_vec(value) + .map_err(|_| HostError::new("EXTENSION_CONFIG_INVALID"))? + .len() + > 64 * 1024 + { + return Err(HostError::new("EXTENSION_CONFIG_LIMIT")); + } + } + let validator = jsonschema::options() + .offline() + .with_draft(jsonschema::Draft::Draft202012) + .with_pattern_options(jsonschema::PatternOptions::regex()) + .should_validate_formats(true) + .should_ignore_unknown_formats(false) + .build(&schema) + .map_err(|_| HostError::new("EXTENSION_CONFIG_SCHEMA"))?; + if !validator.is_valid(configuration) { + return Err(HostError::new("EXTENSION_CONFIG_INVALID")); + } + reject_secrets(&schema, configuration)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn signed_schema_checks_nested_types_bounds_and_unknown_properties() { + let manifest = json!({"configuration_schema":{"type":"object","required":["count"],"additionalProperties":false, + "properties":{"count":{"type":"integer","minimum":1,"maximum":5},"labels":{"type":"array","maxItems":2,"items":{"type":"string","pattern":"^[a-z]+$"}}}}}); + validate(&manifest, &json!({"count":2,"labels":["valid"]})).unwrap(); + for config in [ + json!({}), + json!({"count":0}), + json!({"count":"2"}), + json!({"count":2,"extra":true}), + json!({"count":2,"labels":["INVALID"]}), + ] { + assert!(validate(&manifest, &config).is_err()); + } + validate(&json!({}), &json!({})).unwrap(); + assert!(validate(&json!({}), &json!({"undeclared":true})).is_err()); + } + #[test] + fn secrets_references_and_excessive_work_are_refused_without_values_in_errors() { + let secret = "do-not-print-this-value"; + for field in ["password", "TOKEN", "api_key", "client_secret"] { + let err = validate( + &json!({"configuration_schema":true}), + &json!({"nested":[{field:secret}]}), + ) + .unwrap_err(); + assert_eq!(err.code, "EXTENSION_CONFIG_SECRET"); + assert!(!format!("{err:?}").contains(secret)); + } + for mark in ["writeOnly", "x-opennexus-secret"] { + let schema = json!({"configuration_schema":{"properties":{"custom":{"type":"string",mark:true}}}}); + assert!(validate(&schema, &json!({"custom":secret})).is_err()); + validate(&schema, &json!({})).unwrap(); + } + assert!(validate(&json!({"configuration_schema":{"anyOf":[{"properties":{"custom":{"writeOnly":true}}},true]}}),&json!({"custom":secret})).is_err()); + for reference in ["file:///etc/passwd", "https://example.com/schema", "#"] { + assert!(validate( + &json!({"configuration_schema":{"$ref":reference}}), + &json!({}) + ) + .is_err()); + } + assert!(validate(&json!({"configuration_schema":{"type":42}}), &json!({})).is_err()); + let mut deep = json!(true); + for _ in 0..25 { + deep = json!({"nested":deep}); + } + assert!(validate(&json!({"configuration_schema":true}), &deep).is_err()); + assert!(validate( + &json!({"configuration_schema":true}), + &json!({"value":"a".repeat(65536)}) + ) + .is_err()); + } +} diff --git a/frontend/src-tauri/src/extension_store.rs b/frontend/src-tauri/src/extension_store.rs index fb58658..654b028 100644 --- a/frontend/src-tauri/src/extension_store.rs +++ b/frontend/src-tauri/src/extension_store.rs @@ -290,7 +290,7 @@ impl ExtensionStore { let public: [u8; 32] = key .try_into() .map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; - let (inventory, _) = release.verify_package( + let (inventory, manifest) = release.verify_package( &public, &release.key_id, &release.namespace, @@ -298,6 +298,7 @@ impl ExtensionStore { false, &self.archive(&change.target.package_key)?, )?; + crate::extension_config::validate(&manifest, &change.target.configuration)?; let slot = hash( &serde_json::to_vec(&(&vault, &source, &release.namespace, &release.package_id)) .unwrap(), @@ -663,7 +664,7 @@ mod tests { package_key: staged.package_key, directory: prepared.directory.clone(), tree_sha256: prepared.tree_sha256, - configuration: serde_json::json!({"review":true}), + configuration: serde_json::json!({}), }, expected_revision: None, }]; @@ -674,6 +675,19 @@ mod tests { &changes ) .is_err()); + let mut invalid = changes.clone(); + invalid[0].target.configuration = serde_json::json!({"password":"never-persist-this"}); + assert!(store + .switch_prepared(&Uuid::new_v4().to_string(), &vault, &invalid) + .is_err()); + assert_eq!( + store + .db + .query_row("SELECT COUNT(*) FROM extension_transactions", [], |r| r + .get::<_, i64>(0)) + .unwrap(), + 0 + ); let operation = Uuid::new_v4().to_string(); store.switch_prepared(&operation, &vault, &changes).unwrap(); assert!(store diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index d5622b2..85f8e9d 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -45,3 +45,6 @@ pub mod extension_permit; #[cfg(feature = "desktop")] pub mod extension_transaction; + +#[cfg(feature = "desktop")] +pub mod extension_config;