release(main): 同步 dev 最新候选改动
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled

This commit is contained in:
Cheng Zhou
2026-07-16 17:15:50 +08:00
parent 32167fba02
commit d5279b124f
393 changed files with 51630 additions and 9711 deletions
+4 -1
View File
@@ -9,6 +9,7 @@
"dialog:allow-save",
"fs:allow-read-file",
"fs:allow-write-file",
"fs:allow-mkdir",
"fs:allow-remove",
{
"identifier": "fs:scope",
@@ -16,7 +17,9 @@
{ "path": "$TEMP/ctms-desktop/**" }
]
},
"notification:default",
"notification:allow-is-permission-granted",
"notification:allow-request-permission",
"notification:allow-notify",
{
"identifier": "opener:allow-open-path",
"allow": [
Binary file not shown.

Before

Width:  |  Height:  |  Size: 293 B

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 B

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 B

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 B

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 B

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 B

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 756 B

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 B

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 846 B

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 B

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 B

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 B

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 B

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 374 KiB

+91 -7
View File
@@ -1,7 +1,8 @@
use sha2::{Digest, Sha256};
use url::Url;
const CREDENTIAL_SERVICE: &str = "cn.huapont.ctms.desktop.session";
const SESSION_CREDENTIAL_SERVICE: &str = "cn.huapont.ctms.desktop.session";
const LOGIN_CREDENTIAL_SERVICE: &str = "cn.huapont.ctms.desktop.login";
fn credential_account(server_origin: &str) -> Result<String, String> {
let parsed = Url::parse(server_origin).map_err(|_| "服务器地址格式不正确".to_string())?;
@@ -23,9 +24,9 @@ fn credential_account(server_origin: &str) -> Result<String, String> {
}
#[cfg(any(target_os = "macos", windows))]
fn get_entry(server_origin: &str) -> Result<keyring::Entry, String> {
fn get_entry(service: &str, server_origin: &str) -> Result<keyring::Entry, String> {
let account = credential_account(server_origin)?;
keyring::Entry::new(CREDENTIAL_SERVICE, &account)
keyring::Entry::new(service, &account)
.map_err(|error| format!("无法访问系统凭据库:{error}"))
}
@@ -34,7 +35,7 @@ pub async fn credential_get(server_origin: String) -> Result<Option<String>, Str
tauri::async_runtime::spawn_blocking(move || {
#[cfg(any(target_os = "macos", windows))]
{
let entry = get_entry(&server_origin)?;
let entry = get_entry(SESSION_CREDENTIAL_SERVICE, &server_origin)?;
return match entry.get_password() {
Ok(token) => Ok(Some(token)),
Err(keyring::Error::NoEntry) => Ok(None),
@@ -59,7 +60,7 @@ pub async fn credential_set(server_origin: String, token: String) -> Result<(),
tauri::async_runtime::spawn_blocking(move || {
#[cfg(any(target_os = "macos", windows))]
{
return get_entry(&server_origin)?
return get_entry(SESSION_CREDENTIAL_SERVICE, &server_origin)?
.set_password(&token)
.map_err(|error| format!("保存系统凭据失败:{error}"));
}
@@ -78,7 +79,7 @@ pub async fn credential_delete(server_origin: String) -> Result<(), String> {
tauri::async_runtime::spawn_blocking(move || {
#[cfg(any(target_os = "macos", windows))]
{
let entry = get_entry(&server_origin)?;
let entry = get_entry(SESSION_CREDENTIAL_SERVICE, &server_origin)?;
return match entry.delete_credential() {
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
Err(error) => Err(format!("删除系统凭据失败:{error}")),
@@ -94,9 +95,74 @@ pub async fn credential_delete(server_origin: String) -> Result<(), String> {
.map_err(|error| format!("删除系统凭据任务失败:{error}"))?
}
#[tauri::command]
pub async fn login_credential_get(server_origin: String) -> Result<Option<String>, String> {
tauri::async_runtime::spawn_blocking(move || {
#[cfg(any(target_os = "macos", windows))]
{
let entry = get_entry(LOGIN_CREDENTIAL_SERVICE, &server_origin)?;
return match entry.get_password() {
Ok(credential) => Ok(Some(credential)),
Err(keyring::Error::NoEntry) => Ok(None),
Err(error) => Err(format!("读取登录凭据失败:{error}")),
};
}
#[cfg(not(any(target_os = "macos", windows)))]
{
let _ = credential_account(&server_origin)?;
Err("当前平台不支持系统凭据存储".to_string())
}
})
.await
.map_err(|error| format!("读取登录凭据任务失败:{error}"))?
}
#[tauri::command]
pub async fn login_credential_set(server_origin: String, credential: String) -> Result<(), String> {
if credential.trim().is_empty() {
return Err("拒绝保存空登录凭据".to_string());
}
tauri::async_runtime::spawn_blocking(move || {
#[cfg(any(target_os = "macos", windows))]
{
return get_entry(LOGIN_CREDENTIAL_SERVICE, &server_origin)?
.set_password(&credential)
.map_err(|error| format!("保存登录凭据失败:{error}"));
}
#[cfg(not(any(target_os = "macos", windows)))]
{
let _ = credential_account(&server_origin)?;
Err("当前平台不支持系统凭据存储".to_string())
}
})
.await
.map_err(|error| format!("保存登录凭据任务失败:{error}"))?
}
#[tauri::command]
pub async fn login_credential_delete(server_origin: String) -> Result<(), String> {
tauri::async_runtime::spawn_blocking(move || {
#[cfg(any(target_os = "macos", windows))]
{
let entry = get_entry(LOGIN_CREDENTIAL_SERVICE, &server_origin)?;
return match entry.delete_credential() {
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
Err(error) => Err(format!("删除登录凭据失败:{error}")),
};
}
#[cfg(not(any(target_os = "macos", windows)))]
{
let _ = credential_account(&server_origin)?;
Err("当前平台不支持系统凭据存储".to_string())
}
})
.await
.map_err(|error| format!("删除登录凭据任务失败:{error}"))?
}
#[cfg(test)]
mod tests {
use super::credential_account;
use super::{credential_account, LOGIN_CREDENTIAL_SERVICE, SESSION_CREDENTIAL_SERVICE};
#[test]
fn account_is_stable_for_same_origin() {
@@ -111,4 +177,22 @@ mod tests {
assert!(credential_account("http://ctms.example.com").is_err());
assert!(credential_account("http://localhost:8000").is_ok());
}
#[test]
fn rejects_origins_with_embedded_credentials() {
assert!(credential_account("https://user:secret@ctms.example.com").is_err());
}
#[test]
fn account_does_not_expose_server_origin() {
let account = credential_account("https://ctms.example.com/path").unwrap();
assert!(!account.contains("ctms.example.com"));
assert!(!account.contains("https"));
}
#[test]
fn login_credentials_use_a_separate_keyring_service() {
assert_ne!(SESSION_CREDENTIAL_SERVICE, LOGIN_CREDENTIAL_SERVICE);
}
}
+326 -30
View File
@@ -1,15 +1,91 @@
mod credentials;
mod updates;
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem, Submenu};
use tauri::{Emitter, Manager, Runtime};
use std::collections::HashSet;
use serde::Deserialize;
use tauri::menu::{
AboutMetadata, Menu, MenuItem, PredefinedMenuItem, Submenu, HELP_SUBMENU_ID, WINDOW_SUBMENU_ID,
};
use tauri::{
Emitter, Manager, RunEvent, Runtime, Theme, WebviewWindow, WebviewWindowBuilder, WindowEvent,
};
const DESKTOP_MENU_COMMAND_EVENT: &str = "ctms:desktop-menu-command";
const VIEW_MENU_ID: &str = "ctms.menu.view";
const NAVIGATION_MENU_ID: &str = "ctms.menu.navigation";
const REFRESH_COMMAND_ID: &str = "ctms.desktop.refresh";
const TOGGLE_SIDEBAR_COMMAND_ID: &str = "ctms.desktop.toggleSidebar";
const BACK_COMMAND_ID: &str = "ctms.desktop.back";
const FORWARD_COMMAND_ID: &str = "ctms.desktop.forward";
const DESKTOP_FULLSCREEN_CHANGED_EVENT: &str = "ctms:desktop-fullscreen-changed";
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DesktopMenuShortcuts {
back: String,
forward: String,
refresh: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
enum DesktopThemePreference {
System,
Light,
Dark,
}
fn desktop_menu<R: Runtime>(handle: &tauri::AppHandle<R>) -> tauri::Result<Menu<R>> {
let package = handle.package_info();
let config = handle.config();
let about = AboutMetadata {
name: Some(package.name.clone()),
version: Some(package.version.to_string()),
// macOS falls back to CFBundleVersion when this value is omitted, which
// renders duplicated text such as `Version 0.1.0 (0.1.0)`.
#[cfg(target_os = "macos")]
short_version: Some(String::new()),
copyright: config.bundle.copyright.clone(),
authors: config
.bundle
.publisher
.clone()
.map(|publisher| vec![publisher]),
// Supplying the icon explicitly keeps the native About panel from
// falling back to the generic macOS application/folder icon.
icon: handle.default_window_icon().cloned(),
..Default::default()
};
Menu::with_items(
handle,
&[
#[cfg(target_os = "macos")]
&Submenu::with_items(
handle,
package.name.clone(),
true,
&[
&PredefinedMenuItem::about(handle, Some("关于 CTMS"), Some(about.clone()))?,
&PredefinedMenuItem::separator(handle)?,
&MenuItem::with_id(
handle,
"ctms.desktop.preferences",
"设置…",
true,
Some("CmdOrCtrl+,"),
)?,
&PredefinedMenuItem::separator(handle)?,
&PredefinedMenuItem::services(handle, None)?,
&PredefinedMenuItem::separator(handle)?,
&PredefinedMenuItem::hide(handle, None)?,
&PredefinedMenuItem::hide_others(handle, None)?,
&PredefinedMenuItem::show_all(handle, None)?,
&PredefinedMenuItem::separator(handle)?,
&PredefinedMenuItem::quit(handle, None)?,
],
)?,
&Submenu::with_items(
handle,
"文件",
@@ -22,15 +98,17 @@ fn desktop_menu<R: Runtime>(handle: &tauri::AppHandle<R>) -> tauri::Result<Menu<
true,
Some("CmdOrCtrl+K"),
)?,
#[cfg(not(target_os = "macos"))]
&MenuItem::with_id(
handle,
"ctms.desktop.serverSettings",
"服务器设置",
"ctms.desktop.preferences",
"设置",
true,
None::<&str>,
Some("CmdOrCtrl+,"),
)?,
&PredefinedMenuItem::separator(handle)?,
&PredefinedMenuItem::close_window(handle, None)?,
#[cfg(not(target_os = "macos"))]
&PredefinedMenuItem::quit(handle, None)?,
],
)?,
@@ -48,44 +126,49 @@ fn desktop_menu<R: Runtime>(handle: &tauri::AppHandle<R>) -> tauri::Result<Menu<
&PredefinedMenuItem::select_all(handle, None)?,
],
)?,
&Submenu::with_items(
&Submenu::with_id_and_items(
handle,
"视图",
VIEW_MENU_ID,
"显示",
true,
&[
&MenuItem::with_id(
handle,
"ctms.desktop.refresh",
REFRESH_COMMAND_ID,
"刷新当前视图",
true,
Some("CmdOrCtrl+R"),
)?,
&MenuItem::with_id(
handle,
TOGGLE_SIDEBAR_COMMAND_ID,
"显示/隐藏导航栏",
true,
None::<&str>,
)?,
&PredefinedMenuItem::separator(handle)?,
&PredefinedMenuItem::fullscreen(handle, None)?,
],
)?,
&Submenu::with_items(
&Submenu::with_id_and_items(
handle,
NAVIGATION_MENU_ID,
"导航",
true,
&[
&MenuItem::with_id(handle, BACK_COMMAND_ID, "后退", true, Some("CmdOrCtrl+["))?,
&MenuItem::with_id(
handle,
"ctms.desktop.back",
"返回",
true,
Some("CmdOrCtrl+["),
)?,
&MenuItem::with_id(
handle,
"ctms.desktop.forward",
FORWARD_COMMAND_ID,
"前进",
true,
Some("CmdOrCtrl+]"),
)?,
],
)?,
&Submenu::with_items(
&Submenu::with_id_and_items(
handle,
WINDOW_SUBMENU_ID,
"窗口",
true,
&[
@@ -94,17 +177,20 @@ fn desktop_menu<R: Runtime>(handle: &tauri::AppHandle<R>) -> tauri::Result<Menu<
&PredefinedMenuItem::close_window(handle, None)?,
],
)?,
&Submenu::with_items(
&Submenu::with_id_and_items(
handle,
HELP_SUBMENU_ID,
"帮助",
true,
&[
#[cfg(not(target_os = "macos"))]
&PredefinedMenuItem::about(handle, Some("关于 CTMS"), Some(about))?,
&MenuItem::with_id(
handle,
"ctms.desktop.preferences",
"桌面偏好",
"ctms.desktop.help",
"CTMS 命令与导航",
true,
Some("CmdOrCtrl+,"),
None::<&str>,
)?,
],
)?,
@@ -118,8 +204,167 @@ fn emit_menu_command<R: Runtime>(app: &tauri::AppHandle<R>, command: &str) {
}
}
fn restore_main_window<R: Runtime>(app: &tauri::AppHandle<R>) -> Result<(), String> {
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
return Ok(());
}
let config = app
.config()
.app
.windows
.iter()
.find(|config| config.label == "main")
.cloned()
.ok_or_else(|| "未找到主窗口配置".to_string())?;
let window = WebviewWindowBuilder::from_config(app, &config)
.map_err(|error| format!("读取主窗口配置失败:{error}"))?
.build()
.map_err(|error| format!("重建主窗口失败:{error}"))?;
let _ = window.show();
let _ = window.set_focus();
Ok(())
}
fn is_allowed_shortcut_key(value: &str) -> bool {
matches!(
value,
"[" | "]"
| ","
| "."
| "/"
| ";"
| "="
| "-"
| "ArrowLeft"
| "ArrowRight"
| "ArrowUp"
| "ArrowDown"
| "Home"
| "End"
| "PageUp"
| "PageDown"
) || (value.len() == 1
&& value
.as_bytes()
.first()
.is_some_and(|value| value.is_ascii_uppercase() || value.is_ascii_digit()))
}
fn normalize_menu_shortcut(value: &str) -> Result<String, String> {
const RESERVED: &[&str] = &[
"Mod+K",
"Mod+,",
"Mod+W",
"Mod+Q",
"Mod+C",
"Mod+V",
"Mod+X",
"Mod+A",
"Mod+Z",
"Mod+Shift+Z",
];
if RESERVED.contains(&value) {
return Err("快捷键与系统或应用保留快捷键冲突".to_string());
}
let parts = value.split('+').collect::<Vec<_>>();
if parts.len() < 2 || parts[0] != "Mod" {
return Err("快捷键必须以 Mod 开头".to_string());
}
let key = parts.last().copied().unwrap_or_default();
if !is_allowed_shortcut_key(key) {
return Err("快捷键按键不受支持".to_string());
}
let mut modifiers = HashSet::new();
for modifier in &parts[1..parts.len() - 1] {
if !matches!(*modifier, "Shift" | "Alt") || !modifiers.insert(*modifier) {
return Err("快捷键修饰键不受支持或重复".to_string());
}
}
Ok(format!("CmdOrCtrl+{}", parts[1..].join("+")))
}
fn set_menu_accelerator<R: Runtime>(
app: &tauri::AppHandle<R>,
submenu_id: &str,
item_id: &str,
accelerator: &str,
) -> Result<(), String> {
let menu = app.menu().ok_or_else(|| "桌面菜单尚未初始化".to_string())?;
let submenu = menu
.get(submenu_id)
.and_then(|item| item.as_submenu().cloned())
.ok_or_else(|| format!("未找到桌面菜单:{submenu_id}"))?;
let item = submenu
.get(item_id)
.and_then(|item| item.as_menuitem().cloned())
.ok_or_else(|| format!("未找到桌面菜单项:{item_id}"))?;
item.set_accelerator(Some(accelerator))
.map_err(|error| format!("更新桌面菜单快捷键失败:{error}"))
}
#[tauri::command]
fn desktop_menu_set_shortcuts<R: Runtime>(
app: tauri::AppHandle<R>,
shortcuts: DesktopMenuShortcuts,
) -> Result<(), String> {
let back = normalize_menu_shortcut(&shortcuts.back)?;
let forward = normalize_menu_shortcut(&shortcuts.forward)?;
let refresh = normalize_menu_shortcut(&shortcuts.refresh)?;
if HashSet::from([back.as_str(), forward.as_str(), refresh.as_str()]).len() != 3 {
return Err("桌面快捷键不能重复".to_string());
}
set_menu_accelerator(&app, NAVIGATION_MENU_ID, BACK_COMMAND_ID, &back)?;
set_menu_accelerator(&app, NAVIGATION_MENU_ID, FORWARD_COMMAND_ID, &forward)?;
set_menu_accelerator(&app, VIEW_MENU_ID, REFRESH_COMMAND_ID, &refresh)?;
Ok(())
}
#[tauri::command]
fn desktop_window_set_theme<R: Runtime>(
window: WebviewWindow<R>,
theme: DesktopThemePreference,
) -> Result<(), String> {
let theme = match theme {
DesktopThemePreference::System => None,
DesktopThemePreference::Light => Some(Theme::Light),
DesktopThemePreference::Dark => Some(Theme::Dark),
};
window
.set_theme(theme)
.map_err(|error| format!("更新桌面窗口主题失败:{error}"))
}
#[tauri::command]
fn desktop_window_get_fullscreen<R: Runtime>(window: WebviewWindow<R>) -> Result<bool, String> {
window
.is_fullscreen()
.map_err(|error| format!("读取桌面窗口全屏状态失败:{error}"))
}
#[tauri::command]
fn desktop_window_set_fullscreen<R: Runtime>(
window: WebviewWindow<R>,
fullscreen: bool,
) -> Result<bool, String> {
window
.set_fullscreen(fullscreen)
.map_err(|error| format!("切换桌面窗口全屏状态失败:{error}"))?;
let _ = window.emit(DESKTOP_FULLSCREEN_CHANGED_EVENT, fullscreen);
Ok(fullscreen)
}
pub fn run() {
tauri::Builder::default()
let app = tauri::Builder::default()
.menu(desktop_menu)
.on_menu_event(|app, event| {
let command = event.id().as_ref();
@@ -128,11 +373,7 @@ pub fn run() {
}
})
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
let _ = restore_main_window(app);
}))
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
@@ -144,9 +385,64 @@ pub fn run() {
credentials::credential_get,
credentials::credential_set,
credentials::credential_delete,
credentials::login_credential_get,
credentials::login_credential_set,
credentials::login_credential_delete,
updates::desktop_update_check,
updates::desktop_update_install,
desktop_menu_set_shortcuts,
desktop_window_set_theme,
desktop_window_get_fullscreen,
desktop_window_set_fullscreen,
])
.run(tauri::generate_context!())
.expect("error while running CTMS desktop application");
.build(tauri::generate_context!())
.expect("error while building CTMS desktop application");
app.run(|app, event| {
#[cfg(target_os = "macos")]
if let RunEvent::Reopen { .. } = &event {
if let Err(error) = restore_main_window(app) {
eprintln!("{error}");
}
}
if let RunEvent::WindowEvent {
label,
event: WindowEvent::Resized(_),
..
} = &event
{
if label == "main" {
if let Some(window) = app.get_webview_window(label) {
if let Ok(fullscreen) = window.is_fullscreen() {
let _ = window.emit(DESKTOP_FULLSCREEN_CHANGED_EVENT, fullscreen);
}
}
}
}
});
}
#[cfg(test)]
mod tests {
use super::normalize_menu_shortcut;
#[test]
fn converts_runtime_shortcuts_to_native_accelerators() {
assert_eq!(
normalize_menu_shortcut("Mod+Shift+[").unwrap(),
"CmdOrCtrl+Shift+["
);
assert_eq!(
normalize_menu_shortcut("Mod+Alt+ArrowLeft").unwrap(),
"CmdOrCtrl+Alt+ArrowLeft"
);
}
#[test]
fn rejects_reserved_or_malformed_shortcuts() {
assert!(normalize_menu_shortcut("Mod+Q").is_err());
assert!(normalize_menu_shortcut("Ctrl+R").is_err());
assert!(normalize_menu_shortcut("Mod+Shift+Shift+R").is_err());
assert!(normalize_menu_shortcut("Mod+F12").is_err());
}
}
+16 -3
View File
@@ -17,17 +17,30 @@
"width": 1440,
"height": 900,
"minWidth": 1180,
"minHeight": 760
"minHeight": 760,
"decorations": true,
"titleBarStyle": "Overlay",
"hiddenTitle": true,
"backgroundColor": "#f9fafb",
"backgroundThrottling": "disabled",
"trafficLightPosition": { "x": 16, "y": 18 }
}
],
"security": {
"csp": "default-src 'self' customprotocol: asset:; connect-src 'self' ipc: http://ipc.localhost https: http://localhost:* http://127.0.0.1:*; img-src 'self' asset: blob: data: https: http://localhost:* http://127.0.0.1:*; style-src 'self' 'unsafe-inline'; font-src 'self' data:; frame-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self'"
"csp": "default-src 'self' customprotocol: asset:; script-src 'self'; connect-src 'self' ipc: http://ipc.localhost https: http://localhost:* http://127.0.0.1:*; img-src 'self' asset: blob: data: https: http://localhost:* http://127.0.0.1:*; style-src 'self' 'unsafe-inline'; font-src 'self' data:; frame-src 'self' blob: https: http://localhost:* http://127.0.0.1:*; object-src 'none'; base-uri 'self'; form-action 'self'"
}
},
"bundle": {
"active": true,
"targets": ["app", "dmg"],
"createUpdaterArtifacts": true
"createUpdaterArtifacts": true,
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
},
"plugins": {
"updater": {