tauri_plugin_hot_update/config.rs
1//! Plugin configuration: the `plugins.hot-update` object in
2//! `tauri.conf.json`, validated at plugin init so a bad config is a startup
3//! error, never a runtime surprise. The update source is deliberately
4//! config-only — JS cannot pass a manifest URL or keys, so compromised
5//! webview content can never redirect updates.
6//!
7//! ```json
8//! {
9//! "plugins": {
10//! "hot-update": {
11//! "manifestUrl": "https://updates.example.com/manifest.json",
12//! "pubkeys": ["RWT..."],
13//! "enabled": true
14//! }
15//! }
16//! }
17//! ```
18
19use serde::Deserialize;
20
21use crate::update::UpdateConfig;
22use crate::{Error, Result};
23
24/// `plugins.hot-update` in `tauri.conf.json`.
25#[derive(Debug, Clone, Deserialize)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27pub struct Config {
28 /// URL of `manifest.json`. The detached signature is fetched from
29 /// `{manifestUrl}.minisig`, so this must be a plain file URL with no
30 /// query string. Required when `enabled` is true.
31 #[serde(default)]
32 pub manifest_url: Option<String>,
33 /// Trusted minisign public keys — raw base64 (`RW…`) or full
34 /// `minisign.pub` file contents. A manifest verifying under ANY key is
35 /// trusted (ship old + new keys during a rotation). At least one is
36 /// required when `enabled` is true.
37 #[serde(default)]
38 pub pubkeys: Vec<String>,
39 /// Dark-ship switch: register the plugin but keep it inert. When false,
40 /// no update is ever checked or served — the app runs on its embedded
41 /// assets exactly as if the plugin were absent.
42 #[serde(default = "default_true")]
43 pub enabled: bool,
44}
45
46fn default_true() -> bool {
47 true
48}
49
50impl Config {
51 /// Validate into the pipeline's [`UpdateConfig`], or `None` when
52 /// disabled. Every failure here aborts app startup by design: the config
53 /// ships inside the store binary, so a malformed trust anchor or URL is
54 /// a build/config bug the developer must see on first run.
55 ///
56 /// A disabled config is not validated at all — dark-shipping with
57 /// placeholder values must never brick the app.
58 pub(crate) fn validate(&self) -> Result<Option<UpdateConfig>> {
59 if !self.enabled {
60 return Ok(None);
61 }
62 let url = self
63 .manifest_url
64 .as_deref()
65 .map(str::trim)
66 .unwrap_or_default();
67 if url.is_empty() {
68 return Err(Error::Config(
69 "`manifestUrl` is required when the plugin is enabled".into(),
70 ));
71 }
72 if !url.starts_with("https://") && !url.starts_with("http://") {
73 return Err(Error::Config(format!(
74 "`manifestUrl` must be an http(s) URL, got {url:?}"
75 )));
76 }
77 if url.contains('?') {
78 return Err(Error::Config(
79 "`manifestUrl` must be a plain file URL without a query string \
80 (the detached signature is fetched from `<manifestUrl>.minisig`)"
81 .into(),
82 ));
83 }
84 if self.pubkeys.is_empty() {
85 return Err(Error::Config(
86 "at least one trusted minisign public key is required in `pubkeys`".into(),
87 ));
88 }
89 crate::manifest::validate_pubkeys(&self.pubkeys)?;
90 Ok(Some(UpdateConfig {
91 manifest_url: url.to_string(),
92 pubkeys: self.pubkeys.clone(),
93 }))
94 }
95}
96
97#[cfg(test)]
98mod tests;