fix for main origin 575a5adbeff0b46390fb08901c7166649255204c

This commit is contained in:
gandc committed 2026-10-11 03:11:48 +03:00
1 parent 703bf076e2
commit eef54a61cc
11 files changed
+1008 -703

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
import {spawnSync} from 'node:child_process';
import {readFileSync} from 'node:fs';
import {dirname, resolve} from 'node:path';
import {fileURLToPath} from 'node:url';
const patchDir = resolve(dirname(fileURLToPath(import.meta.url)), '../patches');
const source = resolve(process.argv[2] || 'fluxer-src');
const names = readFileSync(resolve(patchDir, 'series'), 'utf8').split(/\r?\n/).filter(Boolean);
if (!names.length) throw new Error('The patch series is empty');
const patches = names.map((name) => {
if (!/^[0-9]{4}-[a-z0-9-]+\.patch$/.test(name)) throw new Error(`Invalid patch name: ${name}`);
return resolve(patchDir, name);
});
function apply(...options) {
const result = spawnSync('git', ['-C', source, 'apply', '--whitespace=error-all', ...options, ...patches], {
stdio: 'inherit',
});
if (result.error) throw result.error;
if (result.status !== 0) throw new Error(`git apply ${options.join(' ')} failed (${result.status})`);
}
apply('--check');
if (!process.argv.includes('--check')) apply();
console.log(`Validated ${patches.length} GSCORD patches${process.argv.includes('--check') ? '' : ' and applied them'}.`);
+189
View File
@@ -0,0 +1,189 @@
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {stripTypeScriptTypes} from 'node:module';
import {resolve} from 'node:path';
import {pathToFileURL} from 'node:url';
import vm from 'node:vm';
import {test} from 'node:test';
const source = resolve(process.env.FLUXER_SOURCE || 'fluxer-src');
const desktop = resolve(source, 'fluxer_desktop');
const read = (file) => readFileSync(resolve(desktop, file), 'utf8');
const {parseGscordRelease} = await import(pathToFileURL(resolve(desktop, 'src/main/GscordRelease.ts')));
const compatibility = read('src/preload/GscordCompatibility.ts');
function evaluate(globals = {}) {
return vm.createContext({...globals});
}
function run(ts, context) {
vm.runInContext(stripTypeScriptTypes(ts).replace(/^export /gm, ''), context);
}
function rtcContext() {
class Peer {
constructor(config) { this.config = config; }
setConfiguration(config) { this.config = config; }
}
const context = evaluate({window: {RTCPeerConnection: Peer}, console: {info() {}}});
run(compatibility, context);
context.installForceAllIceTransportPolicy();
return {context, Peer, Wrapped: context.window.RTCPeerConnection};
}
test('URLs, homepage and initial server use GSCORD', () => {
const constants = read('src/common/Constants.ts');
for (const name of ['STABLE_APP_URL', 'CANARY_APP_URL', 'STABLE_MIGRATED_APP_ORIGIN', 'CANARY_MIGRATED_APP_ORIGIN']) {
assert.ok(constants.includes(`export const ${name} = 'https://ds.gandc.ru';`));
}
assert.equal(JSON.parse(read('package.json')).homepage, 'https://ds.gandc.ru');
const discovery = read('src/main/DesktopRuntimeDiscovery.ts');
const start = discovery.indexOf('export function desktopRuntimeInitialInput');
const code = discovery.slice(start, discovery.indexOf('\nasync function fetchRuntimePlan', start));
let override = null;
const context = evaluate({BUILD_CHANNEL: 'stable', CHANNEL_APP_URLS: {stable: 'https://ds.gandc.ru'},
getLaunchInstanceEndpointOverride: () => override});
run(code, context);
assert.equal(context.desktopRuntimeInitialInput(), 'https://ds.gandc.ru');
override = 'https://another.example';
assert.equal(context.desktopRuntimeInitialInput(), override);
});
test('ICE constructor overrides relay without mutating caller configuration', () => {
const {Peer, Wrapped} = rtcContext();
const config = Object.freeze({iceTransportPolicy: 'relay', iceServers: [{urls: 'stun:example.test'}]});
const peer = new Wrapped(config);
assert.equal(peer.config.iceTransportPolicy, 'all');
assert.equal(config.iceTransportPolicy, 'relay');
assert.equal(peer.config.iceServers, config.iceServers);
assert.ok(peer instanceof Peer);
assert.ok(peer instanceof Wrapped);
});
test('ICE setConfiguration overrides relay and preserves native validation path', () => {
const {Wrapped} = rtcContext();
const peer = new Wrapped();
const config = Object.freeze({iceTransportPolicy: 'relay'});
peer.setConfiguration(config);
assert.equal(peer.config.iceTransportPolicy, 'all');
assert.equal(config.iceTransportPolicy, 'relay');
const other = {iceTransportPolicy: 'all'};
peer.setConfiguration(other);
assert.equal(peer.config, other);
peer.setConfiguration(undefined);
assert.equal(peer.config, undefined);
});
test('ICE proxy preserves subclass construction and default configuration', () => {
const {Wrapped} = rtcContext();
class Derived extends Wrapped {}
const peer = new Derived();
assert.ok(peer instanceof Derived);
assert.equal(peer.config, undefined);
});
test('ICE installer tolerates missing WebRTC', () => {
const context = evaluate({window: {}, console});
run(compatibility, context);
context.installForceAllIceTransportPolicy();
});
test('LiveKit workaround restores compression for RTC socket and suppresses it on close', () => {
class Compression {}
class Socket {
constructor(url) { this.url = url; }
addEventListener(name, callback) { this[name] = callback; }
}
const window = {WebSocket: Socket, CompressionStream: Compression, location: {href: 'https://ds.gandc.ru'}};
const context = evaluate({window, URL, console: {info() {}}});
run(compatibility, context);
context.installDisableLiveKitOfferWithJoin();
assert.equal(window.CompressionStream, undefined);
new window.WebSocket('wss://ds.gandc.ru/gateway');
assert.equal(window.CompressionStream, undefined);
const rtc = new window.WebSocket('wss://voice.example/rtc/v1?token=test');
assert.equal(window.CompressionStream, Compression);
rtc.close();
assert.equal(window.CompressionStream, undefined);
});
test('preload installs both self-contained callbacks in main world', () => {
const preload = read('src/preload/index.ts');
for (const name of ['installForceAllIceTransportPolicy', 'installDisableLiveKitOfferWithJoin']) {
assert.ok(preload.includes(`contextBridge.executeInMainWorld({func: ${name}});`));
}
});
test('navigation allows exact SSO and callback origins but rejects lookalikes and reserved paths', () => {
const windowCode = read('src/main/Window.ts');
const start = windowCode.indexOf('function isAllowedSsoNavigation');
const end = windowCode.indexOf('function preventUntrustedChildNavigation', start);
const local = 'fluxer://app/';
const context = evaluate({URL, logger: {warn() {}},
isLocalAppURL: (url) => url.startsWith(local),
isLocalAppRendererDocumentURL: (url) => url === `${local}channels/@me`,
isTrustedOrigin: (url) => url === `${local}channels/@me`});
run(windowCode.slice(start, end), context);
for (const [url, blocked] of [
['https://sso.gandc.ru/login', false], ['https://ds.gandc.ru/callback', false],
[`${local}channels/@me`, false], [`${local}__reserved`, true],
['https://sso.gandc.ru.evil.test', true], ['http://sso.gandc.ru', true],
['https://sso.gandc.ru:8443', true], ['https://sso.gandc.ru@evil.test', true], ['invalid', true],
]) {
let prevented = false;
context.preventUntrustedNavigation({preventDefault() { prevented = true; }}, url, true);
assert.equal(prevented, blocked, url);
}
assert.equal((windowCode.match(/webContents.on\('will-redirect'/g) || []).length, 2);
const trusted = windowCode.slice(windowCode.indexOf('export function isTrustedOrigin'), windowCode.indexOf('function getSanitizedPath'));
assert.ok(!trusted.includes('isAllowedSsoNavigation'));
});
const asset = (name) => ({name, browser_download_url: `https://git.gandc.ru/assets/${name}`});
const release = {tag_name: 'v1.0.99', published_at: '2026-10-11T00:00:00Z', assets: [
asset('Fluxer-1.0.99-linux-x86_64.AppImage'), asset('Fluxer-1.0.99-linux-amd64.deb'),
asset('Fluxer-1.0.99-linux-arm64.AppImage'), asset('Fluxer-1.0.99-linux-arm64.deb'),
asset('Fluxer-1.0.99-portable-win-x64.exe'), asset('Fluxer-1.0.99-win-arm64.exe'),
asset('unrelated.zip'), null, {name: 'broken'},
]};
test('Gitea parser selects only the matching platform and architecture', () => {
for (const arch of ['x64', 'arm64']) {
const linux = parseGscordRelease(release, 'linux', arch);
assert.equal(linux.version, '1.0.99');
assert.equal(linux.pubDate, release.published_at);
assert.deepEqual(Object.keys(linux.files).sort(), ['appimage', 'deb']);
assert.ok(linux.files.appimage.url.includes(arch === 'x64' ? 'x86_64' : 'arm64'));
const win = parseGscordRelease(release, 'win32', arch);
assert.deepEqual(Object.keys(win.files), ['setup']);
assert.ok(win.files.setup.url.endsWith(`win-${arch}.exe`));
}
});
test('Gitea parser accepts current portable filename and handles absent or malformed assets', () => {
const parsed = parseGscordRelease({tag_name: '1.0.100', assets: [asset('Fluxer-1.0.100-win-x64.exe')]}, 'win32', 'x64');
assert.ok(parsed.files.setup);
assert.deepEqual(parseGscordRelease({tag_name: 'v1.0.1'}, 'linux', 'x64').files, {});
for (const payload of [null, {}, {tag_name: ' '}, {tag_name: 'v'}]) assert.throws(() => parseGscordRelease(payload));
});
test('manual updater offers actual Gitea assets rather than fabricated URLs', () => {
let code = read('src/main/UpdaterDownloads.ts').replace(/^import[\s\S]*?from '[^']+';\n/gm, '');
const context = evaluate({process: {platform: 'linux'}, DESKTOP_DOWNLOAD_ARCH: 'x64',
DESKTOP_ARTIFACT_PRODUCT_NAME: 'Fluxer', LINUX_MANUAL_ARCH_TOKENS: {deb: {x64: 'amd64'}},
LINUX_MANUAL_FORMAT_EXTENSIONS: {deb: '.deb'}, isLinuxManualDesktopFormat: () => true});
run(code, context);
const info = {version: '1.0.99', files: {deb: {url: 'https://git.gandc.ru/assets/real.deb', sha256: null}}};
const options = context.getManualDownloadOptions(info);
assert.equal(options.length, 1);
assert.equal(options[0].url, info.files.deb.url);
assert.equal(context.getManualDownloadUrl({version: '1.0.99', files: {}}), 'https://git.gandc.ru/gscord/gscord-desktop/releases');
});
test('GSCORD portable Windows and Linux including AppImage use manual updates', () => {
const code = read('src/main/ShellUpdateCapability.ts');
const start = code.indexOf('export function decideShellUpdatePlan');
const context = evaluate({ShellUpdateCapability: {MANUAL_DOWNLOAD: 'manual', MANAGED_PACKAGE: 'managed', SELF_UPDATE: 'self'}});
run(code.slice(start, code.indexOf('function resolveRunningAppImage', start)), context);
const runtime = {packaged: true, channel: 'stable', flatpak: false, portable: false, appImage: {path: 'test'}};
for (const platform of ['win32', 'linux']) assert.equal(context.decideShellUpdatePlan({...runtime, platform}).capability, 'manual');
assert.equal(context.decideShellUpdatePlan({...runtime, platform: 'linux', flatpak: true}).capability, 'managed');
assert.equal(context.decideShellUpdatePlan({...runtime, platform: 'linux', packaged: false}).reason, 'unpackaged');
});
+27
View File
@@ -0,0 +1,27 @@
import {spawnSync} from 'node:child_process';
import {appendFileSync, existsSync, mkdirSync, readFileSync} from 'node:fs';
import {resolve} from 'node:path';
const source = resolve('fluxer-src');
function git(...args) {
const result = spawnSync('git', args, {encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit']});
if (result.error) throw result.error;
if (result.status !== 0) throw new Error(`git ${args[0]} failed (${result.status})`);
if (result.stdout) process.stdout.write(result.stdout);
return result.stdout.trim();
}
// checkout cleans the job workspace; never reuse a source tree from a previous run.
if (existsSync(source)) throw new Error(`Source directory already exists: ${source}`);
mkdirSync(source);
git('-C', source, 'init');
git('-C', source, 'config', 'core.autocrlf', 'false');
git('-C', source, 'remote', 'add', 'origin', 'https://github.com/fluxerapp/fluxer.git');
const ref = process.env.UPSTREAM_SHA || 'main';
if (ref !== 'main' && !/^[0-9a-f]{40}$/.test(ref)) throw new Error('UPSTREAM_SHA must be a full commit SHA');
git('-C', source, 'fetch', '--depth=1', 'origin', ref);
git('-C', source, 'checkout', '--detach', 'FETCH_HEAD');
const sha = git('-C', source, 'rev-parse', 'HEAD');
const packageManager = JSON.parse(readFileSync(resolve(source, 'package.json'), 'utf8')).packageManager;
console.log(`Fluxer revision: ${sha}; package manager: ${packageManager}`);
if (process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, `sha=${sha}\n`);
if (process.env.GITHUB_ENV) appendFileSync(process.env.GITHUB_ENV, `WORKDIR=${source}\n`);