Skip to main content

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;