Add native self-hosted instance connection to fluxer_desktop

Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 +
tools/ci) with a "Connect to a Different Server" menu item and popout
that lets the desktop app switch to any self-hosted Fluxer instance,
plus fixes for well-known discovery on single-domain self-hosted
deployments and a false-positive ERR_ABORTED on same-origin client
redirects during the switch. Defaults to chat.fluxr.chat and uses an
isolated userData directory from the official build.
This commit is contained in:
2026-07-01 18:22:43 -04:00
commit 682afacd30
1763 changed files with 613720 additions and 0 deletions
+705
View File
@@ -0,0 +1,705 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use napi::Task;
use napi::bindgen_prelude::{AsyncTask, Env, Error, Result, Status};
use napi_derive::napi;
#[napi(object)]
#[derive(Debug)]
pub struct CreateShortcutOptions {
#[napi(js_name = "lnkPath")]
pub lnk_path: Option<String>,
pub target: Option<String>,
pub args: Option<String>,
#[napi(js_name = "appUserModelId")]
pub app_user_model_id: Option<String>,
#[napi(js_name = "toastActivatorClsid")]
pub toast_activator_clsid: Option<String>,
#[napi(js_name = "iconPath")]
pub icon_path: Option<String>,
#[napi(js_name = "iconIndex")]
pub icon_index: Option<i32>,
#[napi(js_name = "workingDir")]
pub working_dir: Option<String>,
pub description: Option<String>,
}
#[napi(object)]
#[derive(Debug)]
pub struct SetRunValueOptions {
pub name: Option<String>,
pub command: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ShortcutOptions {
lnk_path: String,
target: String,
args: Option<String>,
app_user_model_id: Option<String>,
toast_activator_clsid: Option<String>,
icon_path: Option<String>,
icon_index: i32,
working_dir: Option<String>,
description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct RunValueOptions {
name: String,
command: String,
}
pub struct CreateShortcutTask {
options: ShortcutOptions,
}
pub struct SetRunValueTask {
options: RunValueOptions,
}
pub struct DeleteRunValueTask {
name: String,
}
pub struct GetRunValueTask {
name: String,
}
#[napi(js_name = "createShortcut")]
pub fn create_shortcut(opts: CreateShortcutOptions) -> Result<AsyncTask<CreateShortcutTask>> {
Ok(AsyncTask::new(CreateShortcutTask {
options: validate_options(opts)?,
}))
}
#[napi(js_name = "setCurrentUserRunValue")]
pub fn set_current_user_run_value(opts: SetRunValueOptions) -> Result<AsyncTask<SetRunValueTask>> {
Ok(AsyncTask::new(SetRunValueTask {
options: validate_run_value_options(opts)?,
}))
}
#[napi(js_name = "deleteCurrentUserRunValue")]
pub fn delete_current_user_run_value(name: String) -> Result<AsyncTask<DeleteRunValueTask>> {
Ok(AsyncTask::new(DeleteRunValueTask {
name: require_non_empty(Some(name), "name")?,
}))
}
#[napi(js_name = "getCurrentUserRunValue")]
pub fn get_current_user_run_value(name: String) -> Result<AsyncTask<GetRunValueTask>> {
Ok(AsyncTask::new(GetRunValueTask {
name: require_non_empty(Some(name), "name")?,
}))
}
#[cfg(target_os = "windows")]
#[napi(js_name = "getUserNotificationState")]
pub fn get_user_notification_state() -> Result<String> {
platform::get_user_notification_state()
}
impl Task for CreateShortcutTask {
type Output = ();
type JsValue = ();
fn compute(&mut self) -> Result<Self::Output> {
platform::create_shortcut(&self.options)
}
fn resolve(&mut self, _env: Env, _output: Self::Output) -> Result<Self::JsValue> {
Ok(())
}
}
impl Task for SetRunValueTask {
type Output = ();
type JsValue = ();
fn compute(&mut self) -> Result<Self::Output> {
platform::set_current_user_run_value(&self.options.name, &self.options.command)
}
fn resolve(&mut self, _env: Env, _output: Self::Output) -> Result<Self::JsValue> {
Ok(())
}
}
impl Task for DeleteRunValueTask {
type Output = ();
type JsValue = ();
fn compute(&mut self) -> Result<Self::Output> {
platform::delete_current_user_run_value(&self.name)
}
fn resolve(&mut self, _env: Env, _output: Self::Output) -> Result<Self::JsValue> {
Ok(())
}
}
impl Task for GetRunValueTask {
type Output = Option<String>;
type JsValue = Option<String>;
fn compute(&mut self) -> Result<Self::Output> {
platform::get_current_user_run_value(&self.name)
}
fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
Ok(output)
}
}
fn validate_options(opts: CreateShortcutOptions) -> Result<ShortcutOptions> {
Ok(ShortcutOptions {
lnk_path: require_non_empty(opts.lnk_path, "lnkPath")?,
target: require_non_empty(opts.target, "target")?,
args: opts.args,
app_user_model_id: opts.app_user_model_id,
toast_activator_clsid: opts.toast_activator_clsid,
icon_path: opts.icon_path,
icon_index: opts.icon_index.unwrap_or(0),
working_dir: opts.working_dir,
description: opts.description,
})
}
fn validate_run_value_options(opts: SetRunValueOptions) -> Result<RunValueOptions> {
Ok(RunValueOptions {
name: require_non_empty(opts.name, "name")?,
command: require_non_empty(opts.command, "command")?,
})
}
fn require_non_empty(value: Option<String>, property: &'static str) -> Result<String> {
match value {
Some(value) if !value.is_empty() => Ok(value),
_ => Err(Error::new(
Status::InvalidArg,
format!("{property} is required and must be non-empty"),
)),
}
}
#[cfg(target_os = "windows")]
mod platform {
use super::ShortcutOptions;
use napi::bindgen_prelude::{Error, Result, Status};
use std::ffi::c_void;
use std::mem::ManuallyDrop;
use windows::Win32::Foundation::{
ERROR_FILE_NOT_FOUND, ERROR_SUCCESS, PROPERTYKEY, WIN32_ERROR,
};
use windows::Win32::System::Com::StructuredStorage::{
PROPVARIANT, PROPVARIANT_0, PROPVARIANT_0_0, PROPVARIANT_0_0_0,
};
use windows::Win32::System::Com::{
CLSCTX_INPROC_SERVER, CLSIDFromString, COINIT_APARTMENTTHREADED, CoCreateInstance,
CoInitializeEx, CoUninitialize, IPersistFile,
};
use windows::Win32::System::Registry::{
HKEY, HKEY_CURRENT_USER, KEY_SET_VALUE, REG_OPTION_NON_VOLATILE, REG_SZ, RRF_RT_REG_SZ,
RegCloseKey, RegCreateKeyExW, RegDeleteValueW, RegGetValueW, RegOpenKeyExW, RegSetValueExW,
};
use windows::Win32::System::Variant::{VT_CLSID, VT_LPWSTR};
use windows::Win32::UI::Shell::PropertiesSystem::IPropertyStore;
use windows::Win32::UI::Shell::{
IShellLinkW, QUNS_ACCEPTS_NOTIFICATIONS, QUNS_APP, QUNS_BUSY, QUNS_NOT_PRESENT,
QUNS_PRESENTATION_MODE, QUNS_QUIET_TIME, QUNS_RUNNING_D3D_FULL_SCREEN,
SHQueryUserNotificationState, ShellLink,
};
use windows::core::{GUID, Interface, PCWSTR, PWSTR};
const CURRENT_USER_RUN_SUBKEY: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\Run";
const PKEY_APP_USER_MODEL_ID: PROPERTYKEY = PROPERTYKEY {
fmtid: GUID::from_u128(0x9f4c2855_9f79_4b39_a8d0_e1d42de1d5f3),
pid: 5,
};
const PKEY_APP_USER_MODEL_TOAST_ACTIVATOR_CLSID: PROPERTYKEY = PROPERTYKEY {
fmtid: GUID::from_u128(0x9f4c2855_9f79_4b39_a8d0_e1d42de1d5f3),
pid: 26,
};
pub(super) fn get_user_notification_state() -> Result<String> {
let state = unsafe { SHQueryUserNotificationState() }
.map_err(|err| hresult_error("SHQueryUserNotificationState failed", err))?;
let label = match state.0 {
value if value == QUNS_NOT_PRESENT.0 => "not-present",
value if value == QUNS_BUSY.0 => "busy",
value if value == QUNS_RUNNING_D3D_FULL_SCREEN.0 => "running-d3d-full-screen",
value if value == QUNS_PRESENTATION_MODE.0 => "presentation-mode",
value if value == QUNS_ACCEPTS_NOTIFICATIONS.0 => "accepts-notifications",
value if value == QUNS_QUIET_TIME.0 => "quiet-time",
value if value == QUNS_APP.0 => "app",
_ => "unknown",
};
Ok(label.to_string())
}
pub(super) fn create_shortcut(opts: &ShortcutOptions) -> Result<()> {
let _com = ComApartment::initialize()?;
unsafe {
let link: IShellLinkW = CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)
.map_err(|err| hresult_error("CoCreateInstance(CLSID_ShellLink) failed", err))?;
let target = wide_null(&opts.target);
link.SetPath(PCWSTR(target.as_ptr()))
.map_err(|err| hresult_error("IShellLinkW::SetPath failed", err))?;
if let Some(args) = opts.args.as_deref() {
let args = wide_null(args);
link.SetArguments(PCWSTR(args.as_ptr()))
.map_err(|err| hresult_error("IShellLinkW::SetArguments failed", err))?;
}
if let Some(icon_path) = opts.icon_path.as_deref() {
let icon_path = wide_null(icon_path);
link.SetIconLocation(PCWSTR(icon_path.as_ptr()), opts.icon_index)
.map_err(|err| hresult_error("IShellLinkW::SetIconLocation failed", err))?;
}
if let Some(working_dir) = opts.working_dir.as_deref() {
let working_dir = wide_null(working_dir);
link.SetWorkingDirectory(PCWSTR(working_dir.as_ptr()))
.map_err(|err| hresult_error("IShellLinkW::SetWorkingDirectory failed", err))?;
}
if let Some(description) = opts.description.as_deref() {
let description = wide_null(description);
link.SetDescription(PCWSTR(description.as_ptr()))
.map_err(|err| hresult_error("IShellLinkW::SetDescription failed", err))?;
}
if opts.app_user_model_id.is_some() || opts.toast_activator_clsid.is_some() {
let property_store: IPropertyStore = link
.cast()
.map_err(|err| hresult_error("QueryInterface(IPropertyStore) failed", err))?;
if let Some(app_user_model_id) = opts.app_user_model_id.as_deref() {
set_string_property(
&property_store,
&PKEY_APP_USER_MODEL_ID,
app_user_model_id,
"IPropertyStore::SetValue(PKEY_AppUserModel_ID) failed",
)?;
}
if let Some(toast_activator_clsid) = opts.toast_activator_clsid.as_deref() {
set_guid_property(
&property_store,
&PKEY_APP_USER_MODEL_TOAST_ACTIVATOR_CLSID,
toast_activator_clsid,
"IPropertyStore::SetValue(PKEY_AppUserModel_ToastActivatorCLSID) failed",
)?;
}
property_store
.Commit()
.map_err(|err| hresult_error("IPropertyStore::Commit failed", err))?;
}
let persist: IPersistFile = link
.cast()
.map_err(|err| hresult_error("QueryInterface(IPersistFile) failed", err))?;
let lnk_path = wide_null(&opts.lnk_path);
persist
.Save(PCWSTR(lnk_path.as_ptr()), true)
.map_err(|err| hresult_error("IPersistFile::Save failed", err))?;
}
Ok(())
}
pub(super) fn set_current_user_run_value(name: &str, command: &str) -> Result<()> {
let key = RunKey::create()?;
let name = wide_null(name);
let command = wide_null(command);
let data =
unsafe { std::slice::from_raw_parts(command.as_ptr().cast::<u8>(), command.len() * 2) };
let status =
unsafe { RegSetValueExW(key.raw(), PCWSTR(name.as_ptr()), None, REG_SZ, Some(data)) };
check_win32(status, "RegSetValueExW(HKCU Run) failed")
}
pub(super) fn delete_current_user_run_value(name: &str) -> Result<()> {
let Some(key) = RunKey::open_for_set()? else {
return Ok(());
};
let name = wide_null(name);
let status = unsafe { RegDeleteValueW(key.raw(), PCWSTR(name.as_ptr())) };
if status == ERROR_FILE_NOT_FOUND {
return Ok(());
}
check_win32(status, "RegDeleteValueW(HKCU Run) failed")
}
pub(super) fn get_current_user_run_value(name: &str) -> Result<Option<String>> {
let subkey = wide_null(CURRENT_USER_RUN_SUBKEY);
let name = wide_null(name);
let mut value_type = REG_SZ;
let mut byte_len = 0u32;
let status = unsafe {
RegGetValueW(
HKEY_CURRENT_USER,
PCWSTR(subkey.as_ptr()),
PCWSTR(name.as_ptr()),
RRF_RT_REG_SZ,
Some(&mut value_type),
None,
Some(&mut byte_len),
)
};
if status == ERROR_FILE_NOT_FOUND {
return Ok(None);
}
check_win32(status, "RegGetValueW(HKCU Run size) failed")?;
if byte_len == 0 {
return Ok(Some(String::new()));
}
let mut buffer = vec![0u16; byte_len.div_ceil(2) as usize];
let status = unsafe {
RegGetValueW(
HKEY_CURRENT_USER,
PCWSTR(subkey.as_ptr()),
PCWSTR(name.as_ptr()),
RRF_RT_REG_SZ,
Some(&mut value_type),
Some(buffer.as_mut_ptr().cast::<c_void>()),
Some(&mut byte_len),
)
};
check_win32(status, "RegGetValueW(HKCU Run data) failed")?;
let mut code_units = (byte_len / 2) as usize;
while code_units > 0 && buffer[code_units - 1] == 0 {
code_units -= 1;
}
String::from_utf16(&buffer[..code_units])
.map(Some)
.map_err(|_| Error::new(Status::GenericFailure, "HKCU Run value is not valid UTF-16"))
}
struct RunKey(HKEY);
impl RunKey {
fn create() -> Result<Self> {
let subkey = wide_null(CURRENT_USER_RUN_SUBKEY);
let mut key = HKEY::default();
let status = unsafe {
RegCreateKeyExW(
HKEY_CURRENT_USER,
PCWSTR(subkey.as_ptr()),
None,
PCWSTR::null(),
REG_OPTION_NON_VOLATILE,
KEY_SET_VALUE,
None,
&mut key,
None,
)
};
check_win32(status, "RegCreateKeyExW(HKCU Run) failed")?;
Ok(Self(key))
}
fn open_for_set() -> Result<Option<Self>> {
let subkey = wide_null(CURRENT_USER_RUN_SUBKEY);
let mut key = HKEY::default();
let status = unsafe {
RegOpenKeyExW(
HKEY_CURRENT_USER,
PCWSTR(subkey.as_ptr()),
None,
KEY_SET_VALUE,
&mut key,
)
};
if status == ERROR_FILE_NOT_FOUND {
return Ok(None);
}
check_win32(status, "RegOpenKeyExW(HKCU Run) failed")?;
Ok(Some(Self(key)))
}
fn raw(&self) -> HKEY {
self.0
}
}
impl Drop for RunKey {
fn drop(&mut self) {
unsafe {
let _ = RegCloseKey(self.0);
}
}
}
struct ComApartment;
impl ComApartment {
fn initialize() -> Result<Self> {
unsafe {
CoInitializeEx(None, COINIT_APARTMENTTHREADED)
.ok()
.map_err(|err| hresult_error("CoInitializeEx failed", err))?;
}
Ok(Self)
}
}
impl Drop for ComApartment {
fn drop(&mut self) {
unsafe {
CoUninitialize();
}
}
}
fn wide_null(value: &str) -> Vec<u16> {
value.encode_utf16().chain(std::iter::once(0)).collect()
}
fn propvariant_from_wide_string(wide: &mut [u16]) -> PROPVARIANT {
PROPVARIANT {
Anonymous: PROPVARIANT_0 {
Anonymous: ManuallyDrop::new(PROPVARIANT_0_0 {
vt: VT_LPWSTR,
wReserved1: 0,
wReserved2: 0,
wReserved3: 0,
Anonymous: PROPVARIANT_0_0_0 {
pwszVal: PWSTR(wide.as_mut_ptr()),
},
}),
},
}
}
fn propvariant_from_guid(guid: &mut GUID) -> PROPVARIANT {
PROPVARIANT {
Anonymous: PROPVARIANT_0 {
Anonymous: ManuallyDrop::new(PROPVARIANT_0_0 {
vt: VT_CLSID,
wReserved1: 0,
wReserved2: 0,
wReserved3: 0,
Anonymous: PROPVARIANT_0_0_0 {
puuid: guid as *mut GUID,
},
}),
},
}
}
fn set_string_property(
property_store: &IPropertyStore,
key: &PROPERTYKEY,
value: &str,
label: &'static str,
) -> Result<()> {
let mut wide = wide_null(value);
unsafe {
let prop = propvariant_from_wide_string(&mut wide);
property_store
.SetValue(key as *const PROPERTYKEY, &prop as *const PROPVARIANT)
.map_err(|err| hresult_error(label, err))
}
}
fn set_guid_property(
property_store: &IPropertyStore,
key: &PROPERTYKEY,
value: &str,
label: &'static str,
) -> Result<()> {
let wide = wide_null(value);
unsafe {
let mut guid = CLSIDFromString(PCWSTR(wide.as_ptr()))
.map_err(|err| hresult_error("CLSIDFromString failed", err))?;
let prop = propvariant_from_guid(&mut guid);
property_store
.SetValue(key as *const PROPERTYKEY, &prop as *const PROPVARIANT)
.map_err(|err| hresult_error(label, err))
}
}
fn hresult_error(label: &'static str, err: windows::core::Error) -> Error {
Error::new(
Status::GenericFailure,
format!("{label} (hr 0x{:x})", err.code().0 as u32),
)
}
fn check_win32(status: WIN32_ERROR, label: &'static str) -> Result<()> {
if status == ERROR_SUCCESS {
Ok(())
} else {
Err(win32_error(label, status))
}
}
fn win32_error(label: &'static str, status: WIN32_ERROR) -> Error {
Error::new(
Status::GenericFailure,
format!("{label} (win32 {})", status.0),
)
}
}
#[cfg(not(target_os = "windows"))]
mod platform {
use super::ShortcutOptions;
use napi::bindgen_prelude::{Error, Result, Status};
pub(super) fn create_shortcut(_opts: &ShortcutOptions) -> Result<()> {
Err(Error::new(
Status::GenericFailure,
"win-shell not supported on this platform",
))
}
pub(super) fn set_current_user_run_value(_name: &str, _command: &str) -> Result<()> {
Err(Error::new(
Status::GenericFailure,
"win-shell not supported on this platform",
))
}
pub(super) fn delete_current_user_run_value(_name: &str) -> Result<()> {
Err(Error::new(
Status::GenericFailure,
"win-shell not supported on this platform",
))
}
pub(super) fn get_current_user_run_value(_name: &str) -> Result<Option<String>> {
Err(Error::new(
Status::GenericFailure,
"win-shell not supported on this platform",
))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn valid_raw_options() -> CreateShortcutOptions {
CreateShortcutOptions {
lnk_path: Some("C:\\Users\\Example\\Desktop\\Fluxer.lnk".to_string()),
target: Some("C:\\Program Files\\Fluxer\\Fluxer.exe".to_string()),
args: None,
app_user_model_id: None,
toast_activator_clsid: None,
icon_path: None,
icon_index: None,
working_dir: None,
description: None,
}
}
fn valid_run_value_options() -> SetRunValueOptions {
SetRunValueOptions {
name: Some("velopack.fluxer_desktop_canary".to_string()),
command: Some(
"\"C:\\Users\\Example\\AppData\\Local\\Fluxer\\Fluxer.exe\" --autostart"
.to_string(),
),
}
}
#[test]
fn validation_rejects_missing_lnk_path() {
let mut opts = valid_raw_options();
opts.lnk_path = None;
let err = validate_options(opts).expect_err("missing lnkPath should be rejected");
assert_eq!(err.status, Status::InvalidArg);
assert_eq!(err.reason, "lnkPath is required and must be non-empty");
}
#[test]
fn validation_rejects_empty_target() {
let mut opts = valid_raw_options();
opts.target = Some(String::new());
let err = validate_options(opts).expect_err("empty target should be rejected");
assert_eq!(err.status, Status::InvalidArg);
assert_eq!(err.reason, "target is required and must be non-empty");
}
#[test]
fn validation_defaults_icon_index_to_zero() {
let opts = validate_options(valid_raw_options()).expect("options should validate");
assert_eq!(opts.icon_index, 0);
}
#[test]
fn validation_preserves_optional_fields() {
let mut raw = valid_raw_options();
raw.args = Some("--startup".to_string());
raw.icon_path = Some("C:\\Program Files\\Fluxer\\Fluxer.exe".to_string());
raw.icon_index = Some(2);
raw.working_dir = Some("C:\\Program Files\\Fluxer".to_string());
raw.description = Some("Fluxer desktop client".to_string());
raw.app_user_model_id = Some("velopack.fluxer_desktop".to_string());
raw.toast_activator_clsid = Some("{48EEF21B-F3AE-431E-8CF2-386FFB2143F2}".to_string());
let opts = validate_options(raw).expect("options should validate");
assert_eq!(opts.args.as_deref(), Some("--startup"));
assert_eq!(
opts.app_user_model_id.as_deref(),
Some("velopack.fluxer_desktop")
);
assert_eq!(
opts.toast_activator_clsid.as_deref(),
Some("{48EEF21B-F3AE-431E-8CF2-386FFB2143F2}")
);
assert_eq!(
opts.icon_path.as_deref(),
Some("C:\\Program Files\\Fluxer\\Fluxer.exe")
);
assert_eq!(opts.icon_index, 2);
assert_eq!(
opts.working_dir.as_deref(),
Some("C:\\Program Files\\Fluxer")
);
assert_eq!(opts.description.as_deref(), Some("Fluxer desktop client"));
}
#[test]
fn run_value_validation_rejects_missing_name() {
let mut opts = valid_run_value_options();
opts.name = None;
let err = validate_run_value_options(opts).expect_err("missing name should be rejected");
assert_eq!(err.status, Status::InvalidArg);
assert_eq!(err.reason, "name is required and must be non-empty");
}
#[test]
fn run_value_validation_rejects_empty_command() {
let mut opts = valid_run_value_options();
opts.command = Some(String::new());
let err = validate_run_value_options(opts).expect_err("empty command should be rejected");
assert_eq!(err.status, Status::InvalidArg);
assert_eq!(err.reason, "command is required and must be non-empty");
}
#[test]
fn run_value_validation_preserves_fields() {
let opts =
validate_run_value_options(valid_run_value_options()).expect("options should validate");
assert_eq!(opts.name, "velopack.fluxer_desktop_canary");
assert_eq!(
opts.command,
"\"C:\\Users\\Example\\AppData\\Local\\Fluxer\\Fluxer.exe\" --autostart"
);
}
#[cfg(not(target_os = "windows"))]
#[test]
fn non_windows_worker_preserves_stub_error_contract() {
let opts = validate_options(valid_raw_options()).expect("options should validate");
let err = platform::create_shortcut(&opts).expect_err("non-Windows should fail");
assert_eq!(err.status, Status::GenericFailure);
assert_eq!(err.reason, "win-shell not supported on this platform");
}
}