fix for main origin 575a5adbeff0b46390fb08901c7166649255204c
This commit is contained in:
11 files changed
+1008
-703
No files matched your search
@@ -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'}.`);
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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`);
|
||||
Reference in new issue
Block a user