feat(desktop): sign every Windows release artifact (#1548)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-10 23:22:19 +02:00
1 parent 873c203b5d
commit 6e66c92dca
89 files changed
+2408 -2740

No files matched your search

@@ -6,7 +6,6 @@ import DeveloperMode from '@app/features/devtools/state/DeveloperMode';
import {UNKNOWN_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {
formatClientBuildInfo,
formatDesktopBuildVariantLabel,
formatReleaseChannelLabel,
getClientInfo,
getClientInfoSync,
@@ -84,12 +83,7 @@ export const ClientInfo = observer(() => {
const buildVersion = Config.PUBLIC_BUILD_VERSION || 'dev';
const desktopReleaseChannel = desktopChannel ? formatReleaseChannelLabel(desktopChannel) : null;
const primaryDesktopReleaseChannel = desktopReleaseChannel ?? releaseChannel;
const desktopBuildVariant =
clientInfo.desktopBuildVariant && clientInfo.desktopBuildVariant !== 'default'
? formatDesktopBuildVariantLabel(clientInfo.desktopBuildVariant)
: null;
const desktopVersionLabel =
desktopVersion && desktopBuildVariant ? `${desktopVersion} (${desktopBuildVariant})` : desktopVersion;
const desktopVersionLabel = desktopVersion;
const desktopBuildLabel = desktopVersion
? i18n._(DESKTOP_BUILD_DESCRIPTOR, {
desktopChannel: primaryDesktopReleaseChannel,
@@ -126,7 +126,6 @@ class Updater {
currentVersion: string | null = null;
channel: string | null = null;
private desktopArch: string | null = null;
private desktopBuildVariant: string | null = null;
private isNative: boolean;
private backgroundCheckStarted = false;
private backgroundCheckInterval: number | null = null;
@@ -234,10 +233,6 @@ class Updater {
return getUpdaterDisplayVersion(this.snapshot);
}
get buildVariant(): string | null {
return this.desktopBuildVariant;
}
private transition(event: UpdaterMachineEvent): void {
runInAction(() => {
this.snapshot = transitionUpdaterMachineSnapshot(this.snapshot, event);
@@ -258,7 +253,6 @@ class Updater {
runInAction(() => {
this.currentVersion = info.desktopVersion ?? null;
this.channel = info.desktopChannel ?? null;
this.desktopBuildVariant = info.desktopBuildVariant ?? null;
this.desktopArch = info.desktopArch ?? info.arch ?? null;
});
} catch (error) {
@@ -8,12 +8,9 @@ import type {
RegistrationResponseJSON,
} from '@simplewebauthn/browser';
export type DesktopBuildVariant = 'default' | 'windows-game-capture';
export interface DesktopInfo {
version: string;
channel: 'stable' | 'canary';
buildVariant: DesktopBuildVariant;
arch: string;
hardwareArch: string;
runningUnderRosetta: boolean;
@@ -308,7 +305,6 @@ export interface AppMetricsSnapshot {
export interface ElectronAPI {
platform: NodeJS.Platform;
buildVariant: DesktopBuildVariant;
getDesktopInfo: () => Promise<DesktopInfo>;
getGpuInfo?: () => Promise<GpuInfo>;
getAppMetrics?: () => Promise<AppMetricsSnapshot>;
@@ -19,7 +19,6 @@ export interface ClientInfo {
arch?: string;
desktopVersion?: string;
desktopChannel?: string;
desktopBuildVariant?: string;
desktopArch?: string;
desktopOS?: string;
desktopRunningUnderRosetta?: boolean;
@@ -47,13 +46,6 @@ export function formatReleaseChannelLabel(value: string): string {
return normalized.charAt(0).toUpperCase() + normalized.slice(1);
}
export function formatDesktopBuildVariantLabel(value: string): string {
if (value === 'windows-game-capture') {
return 'Windows Game Capture';
}
return value;
}
const ARCHITECTURE_PATTERNS: ReadonlyArray<{
pattern: RegExp;
label: string;
@@ -191,7 +183,6 @@ function getDesktopContextFromInfo(desktopInfo: DesktopInfo): Partial<ClientInfo
return {
desktopVersion: normalize(desktopInfo.version),
desktopChannel: normalize(desktopInfo.channel),
desktopBuildVariant: normalize(desktopInfo.buildVariant),
desktopArch: normalizeArchitectureValue(desktopInfo.hardwareArch ?? desktopInfo.arch),
desktopOS: normalize(desktopInfo.os),
desktopRunningUnderRosetta: desktopInfo.runningUnderRosetta,
@@ -312,14 +303,9 @@ export function formatClientBuildInfo(info: ClientInfo, options: {unknownLabel?:
const desktopChannel = info.desktopChannel ? formatReleaseChannelLabel(info.desktopChannel) : null;
const hasDesktopBuild = Boolean(info.desktopVersion);
const primaryDesktopChannel = desktopChannel ?? releaseChannel;
const desktopBuildVariant =
info.desktopBuildVariant && info.desktopBuildVariant !== 'default'
? formatDesktopBuildVariantLabel(info.desktopBuildVariant)
: null;
const webChannelPrefix = hasDesktopBuild && primaryDesktopChannel === releaseChannel ? '' : `${releaseChannel} `;
const parts = [
info.desktopVersion ? `${primaryDesktopChannel} Desktop ${info.desktopVersion}` : '',
desktopBuildVariant ? `Desktop variant ${desktopBuildVariant}` : '',
`${webChannelPrefix}Web ${buildVersion}`,
osDescription,
shouldShowBrowserInfo ? browserInfo : '',
@@ -406,7 +392,6 @@ export async function getGatewayClientProperties(geo?: {latitude?: string | null
build_version: Config.PUBLIC_BUILD_VERSION ?? 'dev',
desktop_app_version: info.desktopVersion ?? null,
desktop_app_channel: info.desktopChannel ?? null,
desktop_app_variant: info.desktopBuildVariant ?? null,
desktop_arch: info.desktopArch ?? info.arch ?? null,
desktop_os: info.desktopOS ?? info.osName ?? null,
e2ee_capable: isLiveKitE2EECapable(),
@@ -4414,6 +4414,10 @@ class MediaEngineFacade extends Store {
options?: DeviceScreenShareCaptureOptions,
publishOptions?: TrackPublishOptions,
): Promise<void> {
if (!isNativeVoiceEngineSelected()) {
await voiceEngineV2AppScreenShareExecutionAdapter.startDeviceScreenShare(this.room, options, publishOptions);
return;
}
await voiceEngineV2AppScreenShareExecutionAdapter.startNativeDeviceScreenShare(
await this.getNativeDeviceScreenShareCaptureOptions(options),
{
@@ -4512,6 +4516,13 @@ class MediaEngineFacade extends Store {
options?: ScreenShareCaptureOptions,
publishOptions?: TrackPublishOptions,
): Promise<boolean> {
if (!isNativeVoiceEngineSelected()) {
return voiceEngineV2AppScreenShareExecutionAdapter.replaceActiveDisplayScreenShare(
this.room,
options,
publishOptions,
);
}
return voiceEngineV2AppScreenShareExecutionAdapter.replaceActiveNativeDisplayScreenShareFromActiveSource(
options,
publishOptions,
@@ -4522,6 +4533,13 @@ class MediaEngineFacade extends Store {
options?: DeviceScreenShareCaptureOptions,
publishOptions?: TrackPublishOptions,
): Promise<boolean> {
if (!isNativeVoiceEngineSelected()) {
return voiceEngineV2AppScreenShareExecutionAdapter.replaceActiveDeviceScreenShare(
this.room,
options,
publishOptions,
);
}
return voiceEngineV2AppScreenShareExecutionAdapter.replaceActiveNativeDeviceScreenShare(
await this.getNativeDeviceScreenShareCaptureOptions(options),
publishOptions,
@@ -55,7 +55,10 @@ let nativeSupportPromise: Promise<boolean> | null = null;
let nativePrewarmPromise: Promise<void> | null = null;
let nativeCapabilitiesSnapshot: VoiceEngineV2BridgeCapabilities | null = null;
const NATIVE_VOICE_ENGINE_FORCE_DISABLED: boolean = true;
function isNativeVoiceEngineRequired(): boolean {
if (NATIVE_VOICE_ENGINE_FORCE_DISABLED) return false;
return isElectronPlatform();
}
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {beforeEach, describe, expect, it, vi} from 'vitest';
const nativeVoiceEngineSelected = vi.fn<() => boolean>();
const electronApi = vi.fn<() => unknown>();
vi.mock('@app/features/voice/engine/native_voice_engine/NativeVoiceEngineSelection', () => ({
isNativeVoiceEngineSelected: () => nativeVoiceEngineSelected(),
}));
vi.mock('@app/features/ui/utils/NativeUtils', () => ({
getElectronAPI: () => electronApi(),
}));
const {
isVoiceEngineV2AppNativeAudioDeviceBridgeAvailable,
isVoiceEngineV2AppNativeScreenShareAudioBridgeAvailable,
isVoiceEngineV2AppNativeScreenShareBridgeAvailable,
isVoiceEngineV2AppNativeScreenShareEncodingUpdateAvailable,
} = await import('./VoiceEngineV2AppNativeBridge');
const FULLY_CAPABLE_BRIDGE = {
getCapabilities: async () => ({
screenShare: true,
screenShareAudio: true,
screenShareEncodingUpdate: true,
}),
listAudioInputDevices: async () => [],
listAudioOutputDevices: async () => [],
};
describe('VoiceEngineV2AppNativeBridge availability', () => {
beforeEach(() => {
electronApi.mockReturnValue({voiceEngine: FULLY_CAPABLE_BRIDGE});
});
it('reports native capabilities when the native voice engine is selected', async () => {
nativeVoiceEngineSelected.mockReturnValue(true);
expect(await isVoiceEngineV2AppNativeScreenShareBridgeAvailable()).toBe(true);
expect(await isVoiceEngineV2AppNativeScreenShareEncodingUpdateAvailable()).toBe(true);
expect(await isVoiceEngineV2AppNativeScreenShareAudioBridgeAvailable()).toBe(true);
expect(isVoiceEngineV2AppNativeAudioDeviceBridgeAvailable()).toBe(true);
});
it('reports nothing available when the native voice engine is not selected, even on a fully capable bridge', async () => {
nativeVoiceEngineSelected.mockReturnValue(false);
expect(await isVoiceEngineV2AppNativeScreenShareBridgeAvailable()).toBe(false);
expect(await isVoiceEngineV2AppNativeScreenShareEncodingUpdateAvailable()).toBe(false);
expect(await isVoiceEngineV2AppNativeScreenShareAudioBridgeAvailable()).toBe(false);
expect(isVoiceEngineV2AppNativeAudioDeviceBridgeAvailable()).toBe(false);
});
it('does not query the bridge at all when the native voice engine is not selected', async () => {
nativeVoiceEngineSelected.mockReturnValue(false);
const getCapabilities = vi.fn(async () => ({screenShare: true}));
electronApi.mockReturnValue({voiceEngine: {...FULLY_CAPABLE_BRIDGE, getCapabilities}});
await isVoiceEngineV2AppNativeScreenShareBridgeAvailable();
expect(getCapabilities).not.toHaveBeenCalled();
});
});
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {getElectronAPI} from '@app/features/ui/utils/NativeUtils';
import {isNativeVoiceEngineSelected} from '@app/features/voice/engine/native_voice_engine/NativeVoiceEngineSelection';
import type {VoiceEngineV2BridgeApi, VoiceEngineV2BridgeCapabilities} from '@fluxer/voice_engine_v2/bridge';
function buildMissingBridgeError(context: string): Error {
@@ -26,21 +27,25 @@ export async function getVoiceEngineV2AppNativeBridgeCapabilities(): Promise<Voi
}
export async function isVoiceEngineV2AppNativeScreenShareBridgeAvailable(): Promise<boolean> {
if (!isNativeVoiceEngineSelected()) return false;
const capabilities = await getVoiceEngineV2AppNativeBridgeCapabilities();
return capabilities?.screenShare === true;
}
export async function isVoiceEngineV2AppNativeScreenShareEncodingUpdateAvailable(): Promise<boolean> {
if (!isNativeVoiceEngineSelected()) return false;
const capabilities = await getVoiceEngineV2AppNativeBridgeCapabilities();
return capabilities?.screenShareEncodingUpdate === true;
}
export async function isVoiceEngineV2AppNativeScreenShareAudioBridgeAvailable(): Promise<boolean> {
if (!isNativeVoiceEngineSelected()) return false;
const capabilities = await getVoiceEngineV2AppNativeBridgeCapabilities();
return capabilities?.screenShareAudio === true;
}
export function isVoiceEngineV2AppNativeAudioDeviceBridgeAvailable(): boolean {
if (!isNativeVoiceEngineSelected()) return false;
const bridge = getVoiceEngineV2AppNativeBridge();
if (!bridge) return false;
return typeof bridge.listAudioInputDevices === 'function' && typeof bridge.listAudioOutputDevices === 'function';
@@ -2,6 +2,7 @@
import DeveloperOptions from '@app/features/devtools/state/DeveloperOptions';
import {getElectronAPI} from '@app/features/ui/utils/NativeUtils';
import {isNativeVoiceEngineSelected} from '@app/features/voice/engine/native_voice_engine/NativeVoiceEngineSelection';
import {
markScreenShareCaptureActive,
markScreenShareCaptureEnded,
@@ -199,6 +200,7 @@ export interface NativeEngineScreenCapture {
}
export async function isNativeScreenCaptureAvailable(): Promise<boolean> {
if (!isNativeVoiceEngineSelected()) return false;
const electronApi = getElectronAPI();
const api = electronApi?.nativeScreenCapture ?? getNativeScreenCaptureApi();
if (!api) return false;
@@ -765,6 +765,7 @@ class VoiceSettings {
}
getBackgroundImageId(): string {
if (!areVoiceBackgroundsAvailable()) return NONE_BACKGROUND_ID;
return this.backgroundImageId;
}
@@ -1119,10 +1120,6 @@ class VoiceSettings {
backgroundImages = backgroundImages.slice(0, 3);
}
}
if (!areVoiceBackgroundsAvailable()) {
backgroundImages = [];
backgroundImageId = NONE_BACKGROUND_ID;
}
if (backgroundImageId !== NONE_BACKGROUND_ID && backgroundImageId !== BLUR_BACKGROUND_ID) {
const imageExists = backgroundImages.some((img: BackgroundImage) => img.id === backgroundImageId);
if (!imageExists) {
@@ -0,0 +1,53 @@
// @vitest-environment happy-dom
// SPDX-License-Identifier: AGPL-3.0-or-later
import {beforeEach, describe, expect, it, vi} from 'vitest';
const voiceBackgroundsAvailable = vi.fn<() => boolean>();
vi.mock('@app/features/voice/utils/VoiceBackgroundAvailability', () => ({
areVoiceBackgroundsAvailable: () => voiceBackgroundsAvailable(),
}));
vi.mock('@app/features/app/utils/LimitResolverAdapter', () => ({
LimitResolver: {resolve: () => 0},
}));
vi.mock('@app/features/app/utils/LimitUtils', () => ({
isLimitToggleEnabled: () => false,
}));
const {default: VoiceSettings, NONE_BACKGROUND_ID} = await import('./VoiceSettings');
const SAVED_BACKGROUND = {id: 'saved-background', createdAt: 1, mediaKind: 'static' as const};
describe('VoiceSettings background persistence when voice backgrounds are unavailable', () => {
beforeEach(() => {
voiceBackgroundsAvailable.mockReturnValue(true);
VoiceSettings.updateSettings({backgroundImages: [SAVED_BACKGROUND], backgroundImageId: SAVED_BACKGROUND.id});
voiceBackgroundsAvailable.mockReturnValue(false);
});
it('keeps the uploaded background list across a no-op revalidation', () => {
VoiceSettings.updateSettings({});
expect(VoiceSettings.backgroundImages).toEqual([SAVED_BACKGROUND]);
});
it('keeps the stored selection so it comes back once backgrounds are available again', () => {
VoiceSettings.updateSettings({});
expect(VoiceSettings.backgroundImageId).toBe(SAVED_BACKGROUND.id);
voiceBackgroundsAvailable.mockReturnValue(true);
expect(VoiceSettings.getBackgroundImageId()).toBe(SAVED_BACKGROUND.id);
});
it('never hands out an active background while the feature is unavailable', () => {
VoiceSettings.updateSettings({});
expect(VoiceSettings.getBackgroundImageId()).toBe(NONE_BACKGROUND_ID);
expect(VoiceSettings.getBackgroundImages()).toEqual([SAVED_BACKGROUND]);
});
it('still drops a selection that no longer matches a stored background', () => {
VoiceSettings.updateSettings({backgroundImages: []});
expect(VoiceSettings.backgroundImageId).toBe(NONE_BACKGROUND_ID);
});
});
-4
View File
@@ -4,8 +4,6 @@ import type {VoiceEngineV2BridgeApi} from '@fluxer/voice_engine_v2/bridge';
import type {AuthenticationResponseJSON, RegistrationResponseJSON} from '@simplewebauthn/browser';
export type InputMonitoringPermissionStatus = 'granted' | 'denied' | 'not-determined' | 'unsupported';
export type DesktopBuildVariant = 'default' | 'windows-game-capture';
export interface DesktopSource {
id: string;
name: string;
@@ -20,7 +18,6 @@ export interface DesktopSource {
export interface DesktopInfo {
version: string;
channel: 'stable' | 'canary';
buildVariant: DesktopBuildVariant;
arch: string;
hardwareArch: string;
runningUnderRosetta: boolean;
@@ -352,7 +349,6 @@ export interface AppMetricsSnapshot {
export interface ElectronAPI {
platform: 'darwin' | 'win32' | 'linux' | string;
buildChannel: 'stable' | 'canary';
buildVariant: DesktopBuildVariant;
openExternal(url: string): Promise<void>;
downloadFile(url: string, suggestedName: string): Promise<DownloadResult>;
onUpdaterEvent(callback: (event: UpdaterEvent) => void): () => void;