fix: cross-verify update API claims against npm registry (#1082)

* fix: cross-verify update API claims against npm registry

* Update packages/web/server/lib/package-manager.js

Signed-off-by: Islam Nofl <islamnofl.official@gmail.com>

* fix: show live server version in AboutDialog instead of stale build-time constant

* fix: add comment to empty catch block to satisfy lint no-empty rule

* fix: preserve live about dialog version in electron

* fix: scope update checks by runtime

---------

Signed-off-by: Islam Nofl <islamnofl.official@gmail.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Islam Nofl
2026-05-01 12:23:58 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent c924e85a29
commit a23f5e7545
11 changed files with 340 additions and 23 deletions
@@ -24,6 +24,7 @@ import { UsageProgressBar } from '@/components/sections/usage/UsageProgressBar';
import { PaceIndicator } from '@/components/sections/usage/PaceIndicator';
import { formatPercent, formatWindowLabel, QUOTA_PROVIDERS, calculatePace, calculateExpectedUsagePercent } from '@/lib/quota';
import { useQuotaAutoRefresh, useQuotaStore } from '@/stores/useQuotaStore';
import { useUpdateStore } from '@/stores/useUpdateStore';
import { updateDesktopSettings } from '@/lib/persistence';
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
import type { UsageWindow } from '@/types';
@@ -58,6 +59,41 @@ type VSCodeView = 'sessions' | 'chat' | 'settings';
export const VSCodeLayout: React.FC = () => {
const { t } = useI18n();
const runtimeApis = useRuntimeAPIs();
const checkForUpdates = useUpdateStore((state) => state.checkForUpdates);
React.useEffect(() => {
const initialDelayMs = 3000;
const defaultIntervalMs = 60 * 60 * 1000;
const minIntervalMs = 5 * 60 * 1000;
const maxIntervalMs = 24 * 60 * 60 * 1000;
let disposed = false;
let timer: number | null = null;
const clampIntervalMs = (seconds: number): number => {
const ms = Math.round(seconds * 1000);
return Math.max(minIntervalMs, Math.min(maxIntervalMs, ms));
};
const scheduleNext = (delayMs: number) => {
if (disposed) return;
timer = window.setTimeout(async () => {
const suggestedSec = await checkForUpdates();
const nextDelay = typeof suggestedSec === 'number' && Number.isFinite(suggestedSec)
? clampIntervalMs(suggestedSec)
: defaultIntervalMs;
scheduleNext(nextDelay);
}, delayMs);
};
scheduleNext(initialDelayMs);
return () => {
disposed = true;
if (timer !== null) {
window.clearTimeout(timer);
}
};
}, [checkForUpdates]);
const viewMode = React.useMemo<'sidebar' | 'editor'>(() => {
const configured =
+18 -17
View File
@@ -9,8 +9,7 @@ import { debugUtils } from '@/lib/debug';
import { cn } from '@/lib/utils';
import { toast } from '@/components/ui';
import { useI18n } from '@/lib/i18n';
declare const __APP_VERSION__: string | undefined;
import { getDesktopAppVersion } from '@/lib/desktopNative';
interface AboutDialogProps {
open: boolean;
@@ -60,22 +59,24 @@ export const AboutDialog: React.FC<AboutDialogProps> = ({
React.useEffect(() => {
if (!open) return;
const isDesktop = typeof window !== 'undefined' && Boolean((window as unknown as { __TAURI__?: unknown }).__TAURI__);
if (isDesktop) {
const fetchVersion = async () => {
try {
const { getVersion } = await import('@tauri-apps/api/app');
const v = await getVersion();
setVersion(v);
} catch {
setVersion(typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : null);
const fetchVersion = async () => {
try {
const response = await fetch('/api/system/info');
if (response.ok) {
const data = await response.json();
if (typeof data.openchamberVersion === 'string' && data.openchamberVersion.trim()) {
setVersion(data.openchamberVersion);
return;
}
}
};
fetchVersion();
} else {
setVersion(typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : null);
}
} catch {
// Fall back to the native shell version when the web server is unavailable.
}
setVersion(await getDesktopAppVersion());
};
void fetchVersion();
}, [open]);
React.useEffect(() => {
+12 -1
View File
@@ -7,6 +7,7 @@ import {
downloadDesktopUpdate,
restartToApplyUpdate,
isDesktopLocalOriginActive,
isElectronShell,
isTauriShell,
isVSCodeRuntime,
isWebRuntime,
@@ -46,6 +47,12 @@ function detectDeviceClass(): 'mobile' | 'tablet' | 'desktop' | 'unknown' {
}
function detectArch(): 'arm64' | 'x64' | 'unknown' {
const vscodeArch = typeof window !== 'undefined'
? (window as { __VSCODE_CONFIG__?: { arch?: string } }).__VSCODE_CONFIG__?.arch?.toLowerCase?.()
: undefined;
if (vscodeArch === 'arm64' || vscodeArch === 'aarch64') return 'arm64';
if (vscodeArch === 'x64' || vscodeArch === 'amd64' || vscodeArch === 'x86_64') return 'x64';
const nav = typeof navigator !== 'undefined' ? (navigator as Navigator & { userAgentData?: { architecture?: string } }).userAgentData : undefined;
const fromUAData = nav?.architecture?.toLowerCase?.();
if (fromUAData === 'arm' || fromUAData === 'arm64' || fromUAData === 'aarch64') return 'arm64';
@@ -75,7 +82,7 @@ function mapRuntimeParams(runtime: ClientRuntime): URLSearchParams {
params.set('arch', detectArch());
params.set('platform', detectPlatform());
if (runtime === 'desktop') {
params.set('appType', 'desktop-tauri');
params.set('appType', isElectronShell() ? 'desktop-electron' : 'desktop-tauri');
params.set('instanceMode', isDesktopLocalOriginActive() ? 'local' : 'remote');
return params;
}
@@ -94,7 +101,11 @@ function mapRuntimeParams(runtime: ClientRuntime): URLSearchParams {
async function checkForWebUpdates(runtime: ClientRuntime, currentVersion?: string): Promise<UpdateInfo | null> {
try {
const params = mapRuntimeParams(runtime);
const vscodeVersion = typeof window !== 'undefined'
? (window as { __VSCODE_CONFIG__?: { extensionVersion?: string } }).__VSCODE_CONFIG__?.extensionVersion
: undefined;
if (currentVersion) params.set('currentVersion', currentVersion);
else if (runtime === 'vscode' && vscodeVersion) params.set('currentVersion', vscodeVersion);
const response = await fetch(`/api/openchamber/update-check?${params.toString()}`, {
method: 'GET',
headers: { Accept: 'application/json' },