mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
fix(updater): humanize update-error card and hide raw error behind Show details
Windows auto-update failures surfaced the raw electron-updater message as the
card headline — most visibly the PowerShell "Command failed: … Get-Authenticode
Signature …" dump when antivirus/EDR blocks the post-download signature check.
That reads as a crash, not an actionable state.
Classify each failure and lead with one plain-language sentence + the right
action, keeping the raw error one click away:
- New shared classifier (updater-windows-signature-check): distinguishes an
AV/EDR-blocked signature check (environment) from a genuine wrong-publisher
mismatch (security). The two are mutually exclusive so a real integrity
failure is never softened into "try again".
- UpdateCard: raw error moves behind a collapsed "Show details" toggle; adds a
security-stop variant (wrong publisher → no retry, "Open official releases")
and the AV-blocked variant ("Update Verification Blocked"). HTTP/2 and generic
paths keep their existing actions, now with the same details disclosure.
- Main process records a windows_signature_check_blocked lifecycle event so we
can size the affected Windows cohort in the field.
Verified each error scenario in a running Electron build (signature-check
blocked, wrong-publisher security stop, HTTP/2, generic, and the expanded
details view).
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { app, BrowserWindow, powerMonitor } from 'electron'
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import type { UpdateCheckOptions, UpdateStatus } from '../shared/types'
|
||||
import { isWindowsSignatureCheckUnavailableFailure } from '../shared/updater-windows-signature-check'
|
||||
import { killAllPty } from './ipc/pty'
|
||||
import { withUpdaterSpan } from './observability/instrumentation'
|
||||
import { loadElectronAutoUpdater, type ElectronAutoUpdater } from './electron-updater-loader'
|
||||
@@ -523,6 +524,14 @@ function sendErrorStatus(message: string, userInitiated?: boolean): void {
|
||||
) {
|
||||
return
|
||||
}
|
||||
// Why: counts AV/EDR-blocked Windows signature checks in the field so we can
|
||||
// size the affected cohort before investing in bigger updater changes.
|
||||
if (isWindowsSignatureCheckUnavailableFailure(message)) {
|
||||
recordUpdaterLifecycle('windows_signature_check_blocked', undefined, {
|
||||
level: 'warn',
|
||||
message: 'Windows update signature check could not run'
|
||||
})
|
||||
}
|
||||
sendStatus({ state: 'error', message, userInitiated })
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,12 @@ import { useAppStore } from '../store'
|
||||
import { Card } from './ui/card'
|
||||
import { Button } from './ui/button'
|
||||
import { Progress } from './ui/progress'
|
||||
import { AlertCircle, Check, Loader2, Minus, Network, RotateCw, X } from 'lucide-react'
|
||||
import { AlertCircle, Check, Loader2, Minus, Network, RotateCw, ShieldAlert, X } from 'lucide-react'
|
||||
import type { ChangelogData } from '../../../shared/types'
|
||||
import {
|
||||
isWindowsSignatureCheckUnavailableFailure,
|
||||
isWindowsSignatureMismatchFailure
|
||||
} from '../../../shared/updater-windows-signature-check'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────
|
||||
@@ -37,11 +41,16 @@ export function isHttp2ProtocolError(message: string): boolean {
|
||||
}
|
||||
|
||||
type ErrorCardModel = {
|
||||
variant?: 'default' | 'http1Compatibility'
|
||||
variant?: 'default' | 'http1Compatibility' | 'security'
|
||||
title: string
|
||||
summary: string
|
||||
message: string
|
||||
/** Optional guidance box between the summary and the raw error output. */
|
||||
explainer?: string
|
||||
/** Raw error text, shown only when the user expands "Show details". */
|
||||
detail?: string
|
||||
releaseUrl: string
|
||||
/** Overrides the secondary button label (defaults to "Download Manually"). */
|
||||
manualLabel?: string
|
||||
primaryAction?: {
|
||||
label: string
|
||||
pendingLabel?: string
|
||||
@@ -337,7 +346,16 @@ export function UpdateCard() {
|
||||
})
|
||||
}
|
||||
|
||||
// Why: classify each failure so the card leads with one plain sentence and
|
||||
// the right action, and keeps the raw error behind "Show details" instead of
|
||||
// dumping a PowerShell/stack trace as the headline. Order matters: the
|
||||
// security-stop (wrong publisher) must win over the AV/EDR "check couldn't
|
||||
// run" case so a real integrity failure is never softened into "try again".
|
||||
const isHttp2UpdateError = status.state === 'error' && isHttp2ProtocolError(status.message)
|
||||
const isSignatureMismatchError =
|
||||
status.state === 'error' && isWindowsSignatureMismatchFailure(status.message)
|
||||
const isSignatureCheckBlockedError =
|
||||
status.state === 'error' && isWindowsSignatureCheckUnavailableFailure(status.message)
|
||||
const errorCard: ErrorCardModel | null =
|
||||
status.state === 'error'
|
||||
? isHttp2UpdateError
|
||||
@@ -345,7 +363,11 @@ export function UpdateCard() {
|
||||
variant: 'http1Compatibility',
|
||||
title: translate('auto.components.UpdateCard.1339b82cee', 'HTTP/2 Download Blocked'),
|
||||
summary: 'Orca can retry through HTTP/1.1 compatibility mode.',
|
||||
message: compatibilitySetupError ?? status.message,
|
||||
explainer: translate(
|
||||
'auto.components.UpdateCard.90559b14e3',
|
||||
'This turns on a process-wide Electron networking switch after restart. Use it for corporate VPNs or proxies that reject HTTP/2 update downloads.'
|
||||
),
|
||||
detail: compatibilitySetupError ?? status.message,
|
||||
releaseUrl: releaseUrlForVersion(cachedVersion),
|
||||
primaryAction: {
|
||||
label: translate('auto.components.UpdateCard.933c6fdf5b', 'Enable & Restart'),
|
||||
@@ -354,35 +376,69 @@ export function UpdateCard() {
|
||||
onClick: handleEnableHttp1Compatibility
|
||||
}
|
||||
}
|
||||
: {
|
||||
// Why: title is scoped to the operation that failed so check-time
|
||||
// failures (commonly GitHub-side) don't read as a bug in Orca.
|
||||
title: cachedVersion ? 'Update Error' : 'Update Check Failed',
|
||||
summary: cachedVersion
|
||||
? 'Could not complete the update.'
|
||||
: 'Could not check for updates.',
|
||||
message: status.message,
|
||||
releaseUrl: releaseUrlForVersion(cachedVersion),
|
||||
// Why: check-time failures are often transient (offline, GitHub
|
||||
// hiccup), so offer a Re-check next to "Download Manually" instead
|
||||
// of forcing the user into the manual fallback.
|
||||
primaryAction: cachedVersion
|
||||
? {
|
||||
: isSignatureMismatchError
|
||||
? {
|
||||
// Security stop: the installer is readable but signed by the wrong
|
||||
// publisher. No retry — only a path to the verified download.
|
||||
variant: 'security',
|
||||
title: translate('auto.components.UpdateCard.5b309b19f3', "Update Wasn't Installed"),
|
||||
summary: translate(
|
||||
'auto.components.UpdateCard.092f09fc14',
|
||||
"This download isn't signed by Orca, so we stopped it. Get the latest version from our official releases instead."
|
||||
),
|
||||
detail: status.message,
|
||||
releaseUrl: releaseUrlForVersion(cachedVersion),
|
||||
manualLabel: translate(
|
||||
'auto.components.UpdateCard.c9ff9b9ec2',
|
||||
'Open official releases'
|
||||
)
|
||||
}
|
||||
: isSignatureCheckBlockedError
|
||||
? {
|
||||
title: translate(
|
||||
'auto.components.UpdateCard.e944c2de43',
|
||||
'Update Verification Blocked'
|
||||
),
|
||||
summary: translate(
|
||||
'auto.components.UpdateCard.a05992a26b',
|
||||
"The signature check couldn't run — usually antivirus software blocking it. Retry, or download it manually; Windows verifies the installer when it runs."
|
||||
),
|
||||
detail: status.message,
|
||||
releaseUrl: releaseUrlForVersion(cachedVersion),
|
||||
primaryAction: {
|
||||
label: translate('auto.components.UpdateCard.48565a32bc', 'Retry Download'),
|
||||
onClick: handleUpdate
|
||||
}
|
||||
: {
|
||||
label: translate('auto.components.UpdateCard.6b0085010d', 'Re-check'),
|
||||
onClick: () => {
|
||||
void window.api.updater.check({ includePrerelease: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
: {
|
||||
// Why: title is scoped to the operation that failed so check-time
|
||||
// failures (commonly GitHub-side) don't read as a bug in Orca.
|
||||
title: cachedVersion ? 'Update Error' : 'Update Check Failed',
|
||||
summary: cachedVersion
|
||||
? 'Could not complete the update.'
|
||||
: 'Could not check for updates.',
|
||||
detail: status.message,
|
||||
releaseUrl: releaseUrlForVersion(cachedVersion),
|
||||
// Why: check-time failures are often transient (offline, GitHub
|
||||
// hiccup), so offer a Re-check next to "Download Manually" instead
|
||||
// of forcing the user into the manual fallback.
|
||||
primaryAction: cachedVersion
|
||||
? {
|
||||
label: translate('auto.components.UpdateCard.48565a32bc', 'Retry Download'),
|
||||
onClick: handleUpdate
|
||||
}
|
||||
: {
|
||||
label: translate('auto.components.UpdateCard.6b0085010d', 'Re-check'),
|
||||
onClick: () => {
|
||||
void window.api.updater.check({ includePrerelease: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
: installError
|
||||
? {
|
||||
title: translate('auto.components.UpdateCard.4cf109845a', 'Update Error'),
|
||||
summary: 'Could not restart to install the update.',
|
||||
message: installError,
|
||||
detail: installError,
|
||||
releaseUrl: releaseUrlForVersion(cachedVersion),
|
||||
primaryAction: {
|
||||
label: translate('auto.components.UpdateCard.2c2d3e03ca', 'Try Again'),
|
||||
@@ -494,8 +550,10 @@ export function UpdateCard() {
|
||||
<ErrorCardContent
|
||||
title={errorCard.title}
|
||||
summary={errorCard.summary}
|
||||
message={errorCard.message}
|
||||
explainer={errorCard.explainer}
|
||||
detail={errorCard.detail}
|
||||
releaseUrl={errorCard.releaseUrl}
|
||||
manualLabel={errorCard.manualLabel}
|
||||
variant={errorCard.variant}
|
||||
primaryAction={errorCard.primaryAction}
|
||||
onClose={handleCollapseWithAnimation}
|
||||
@@ -892,16 +950,20 @@ function ErrorCardContent({
|
||||
variant = 'default',
|
||||
title,
|
||||
summary,
|
||||
message,
|
||||
explainer,
|
||||
detail,
|
||||
releaseUrl,
|
||||
manualLabel,
|
||||
primaryAction,
|
||||
onClose
|
||||
}: {
|
||||
variant?: 'default' | 'http1Compatibility'
|
||||
variant?: 'default' | 'http1Compatibility' | 'security'
|
||||
title: string
|
||||
summary: string
|
||||
message: string
|
||||
explainer?: string
|
||||
detail?: string
|
||||
releaseUrl: string
|
||||
manualLabel?: string
|
||||
primaryAction?: {
|
||||
label: string
|
||||
pendingLabel?: string
|
||||
@@ -910,12 +972,22 @@ function ErrorCardContent({
|
||||
}
|
||||
onClose: () => void
|
||||
}) {
|
||||
// Why: the raw error is kept for support but starts collapsed so the card
|
||||
// leads with the plain-language summary, not a PowerShell/stack dump.
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
const isCompatibility = variant === 'http1Compatibility'
|
||||
const Icon = isCompatibility ? Network : AlertCircle
|
||||
const isSecurity = variant === 'security'
|
||||
const Icon = isCompatibility ? Network : isSecurity ? ShieldAlert : AlertCircle
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-md border border-border bg-muted/50 text-muted-foreground">
|
||||
<div
|
||||
className={`mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-md border bg-muted/50 ${
|
||||
isSecurity
|
||||
? 'border-destructive/30 text-destructive'
|
||||
: 'border-border text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
@@ -933,25 +1005,22 @@ function ErrorCardContent({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isCompatibility ? (
|
||||
{explainer ? (
|
||||
<div className="rounded-md border border-border/70 bg-muted/30 px-3 py-2">
|
||||
<p className="text-xs leading-relaxed text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.UpdateCard.90559b14e3',
|
||||
'This turns on a process-wide Electron networking switch after restart. Use it for corporate VPNs or proxies that reject HTTP/2 update downloads.'
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs leading-relaxed text-muted-foreground">{explainer}</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="rounded-md bg-muted/40 px-3 py-2">
|
||||
<p className="mb-1 text-[11px] font-medium uppercase text-muted-foreground">
|
||||
{translate('auto.components.UpdateCard.3553a8672f', 'Last error')}
|
||||
</p>
|
||||
<p className="scrollbar-sleek max-h-20 overflow-auto break-words font-mono text-xs leading-relaxed text-muted-foreground">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
{detail && showDetails ? (
|
||||
<div className="rounded-md bg-muted/40 px-3 py-2">
|
||||
<p className="mb-1 text-[11px] font-medium uppercase text-muted-foreground">
|
||||
{translate('auto.components.UpdateCard.3553a8672f', 'Last error')}
|
||||
</p>
|
||||
<p className="scrollbar-sleek max-h-20 overflow-auto break-words font-mono text-xs leading-relaxed text-muted-foreground">
|
||||
{detail}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex gap-2">
|
||||
{primaryAction && (
|
||||
@@ -976,11 +1045,23 @@ function ErrorCardContent({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void window.api.shell.openUrl(releaseUrl)}
|
||||
className={primaryAction ? 'flex-1' : 'w-full'}
|
||||
className="flex-1"
|
||||
>
|
||||
{translate('auto.components.UpdateCard.47126bcf57', 'Download Manually')}
|
||||
{manualLabel ?? translate('auto.components.UpdateCard.47126bcf57', 'Download Manually')}
|
||||
</Button>
|
||||
</div>
|
||||
{/* Why: the raw-error toggle sits on its own row so it never crowds the
|
||||
two action buttons out of the narrow card. */}
|
||||
{detail ? (
|
||||
<button
|
||||
className="-mt-1 self-start text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground"
|
||||
onClick={() => setShowDetails((prev) => !prev)}
|
||||
>
|
||||
{showDetails
|
||||
? translate('auto.components.UpdateCard.5194358929', 'Hide details')
|
||||
: translate('auto.components.UpdateCard.8bc9e17d8f', 'Show details')}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1815,7 +1815,14 @@
|
||||
"a726967bd3": "Dismiss",
|
||||
"d5253b54af": "error",
|
||||
"7ffc08506e": "check",
|
||||
"522df222b9": "spinner"
|
||||
"522df222b9": "spinner",
|
||||
"e944c2de43": "Update Verification Blocked",
|
||||
"5b309b19f3": "Update Wasn't Installed",
|
||||
"092f09fc14": "This download isn't signed by Orca, so we stopped it. Get the latest version from our official releases instead.",
|
||||
"c9ff9b9ec2": "Open official releases",
|
||||
"a05992a26b": "The signature check couldn't run — usually antivirus software blocking it. Retry, or download it manually; Windows verifies the installer when it runs.",
|
||||
"5194358929": "Hide details",
|
||||
"8bc9e17d8f": "Show details"
|
||||
},
|
||||
"WorktreeJumpPalette": {
|
||||
"ac037cfac2": "Move",
|
||||
|
||||
@@ -1815,7 +1815,14 @@
|
||||
"a726967bd3": "Descartar",
|
||||
"d5253b54af": "error",
|
||||
"7ffc08506e": "check",
|
||||
"522df222b9": "spinner"
|
||||
"522df222b9": "spinner",
|
||||
"e944c2de43": "Update Verification Blocked",
|
||||
"5b309b19f3": "Update Wasn't Installed",
|
||||
"092f09fc14": "This download isn't signed by Orca, so we stopped it. Get the latest version from our official releases instead.",
|
||||
"c9ff9b9ec2": "Open official releases",
|
||||
"a05992a26b": "The signature check couldn't run — usually antivirus software blocking it. Retry, or download it manually; Windows verifies the installer when it runs.",
|
||||
"5194358929": "Hide details",
|
||||
"8bc9e17d8f": "Show details"
|
||||
},
|
||||
"WorktreeJumpPalette": {
|
||||
"ac037cfac2": "Mover",
|
||||
|
||||
@@ -1815,7 +1815,14 @@
|
||||
"a726967bd3": "閉じる",
|
||||
"d5253b54af": "エラー",
|
||||
"7ffc08506e": "チェック",
|
||||
"522df222b9": "スピナー"
|
||||
"522df222b9": "スピナー",
|
||||
"e944c2de43": "Update Verification Blocked",
|
||||
"5b309b19f3": "Update Wasn't Installed",
|
||||
"092f09fc14": "This download isn't signed by Orca, so we stopped it. Get the latest version from our official releases instead.",
|
||||
"c9ff9b9ec2": "Open official releases",
|
||||
"a05992a26b": "The signature check couldn't run — usually antivirus software blocking it. Retry, or download it manually; Windows verifies the installer when it runs.",
|
||||
"5194358929": "Hide details",
|
||||
"8bc9e17d8f": "Show details"
|
||||
},
|
||||
"WorktreeJumpPalette": {
|
||||
"ac037cfac2": "移動",
|
||||
|
||||
@@ -1815,7 +1815,14 @@
|
||||
"a726967bd3": "닫기",
|
||||
"d5253b54af": "오류",
|
||||
"7ffc08506e": "확인",
|
||||
"522df222b9": "스피너"
|
||||
"522df222b9": "스피너",
|
||||
"e944c2de43": "Update Verification Blocked",
|
||||
"5b309b19f3": "Update Wasn't Installed",
|
||||
"092f09fc14": "This download isn't signed by Orca, so we stopped it. Get the latest version from our official releases instead.",
|
||||
"c9ff9b9ec2": "Open official releases",
|
||||
"a05992a26b": "The signature check couldn't run — usually antivirus software blocking it. Retry, or download it manually; Windows verifies the installer when it runs.",
|
||||
"5194358929": "Hide details",
|
||||
"8bc9e17d8f": "Show details"
|
||||
},
|
||||
"WorktreeJumpPalette": {
|
||||
"ac037cfac2": "이동",
|
||||
|
||||
@@ -1815,7 +1815,14 @@
|
||||
"a726967bd3": "关闭",
|
||||
"d5253b54af": "错误",
|
||||
"7ffc08506e": "查看",
|
||||
"522df222b9": "旋转器"
|
||||
"522df222b9": "旋转器",
|
||||
"e944c2de43": "Update Verification Blocked",
|
||||
"5b309b19f3": "Update Wasn't Installed",
|
||||
"092f09fc14": "This download isn't signed by Orca, so we stopped it. Get the latest version from our official releases instead.",
|
||||
"c9ff9b9ec2": "Open official releases",
|
||||
"a05992a26b": "The signature check couldn't run — usually antivirus software blocking it. Retry, or download it manually; Windows verifies the installer when it runs.",
|
||||
"5194358929": "Hide details",
|
||||
"8bc9e17d8f": "Show details"
|
||||
},
|
||||
"WorktreeJumpPalette": {
|
||||
"ac037cfac2": "手机",
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
isWindowsSignatureCheckUnavailableFailure,
|
||||
isWindowsSignatureMismatchFailure
|
||||
} from './updater-windows-signature-check'
|
||||
|
||||
describe('isWindowsSignatureCheckUnavailableFailure', () => {
|
||||
it('matches the PowerShell command failure shape (timeout / non-zero exit)', () => {
|
||||
expect(
|
||||
isWindowsSignatureCheckUnavailableFailure(
|
||||
'Command failed: set "PSModulePath=" & chcp 65001 >NUL & powershell.exe -NoProfile ' +
|
||||
'-NonInteractive -InputFormat None -Command "Get-AuthenticodeSignature -LiteralPath ' +
|
||||
"'C:\\Users\\u\\AppData\\Local\\orca-updater\\pending\\orca-windows-setup.exe' | " +
|
||||
'ConvertTo-Json -Compress"'
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('matches the stderr failure shape', () => {
|
||||
expect(
|
||||
isWindowsSignatureCheckUnavailableFailure(
|
||||
'Cannot execute Get-AuthenticodeSignature, stderr: Access is denied. ' +
|
||||
'Failing signature validation due to unknown stderr.'
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('does not match a genuine signature mismatch', () => {
|
||||
expect(
|
||||
isWindowsSignatureCheckUnavailableFailure(
|
||||
'New version 1.4.144 is not signed by the application owner: ' +
|
||||
'publisherNames: SignPath Foundation, raw info: {"Status": 0}'
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('does not match unrelated update errors', () => {
|
||||
expect(isWindowsSignatureCheckUnavailableFailure('net::ERR_HTTP2_PROTOCOL_ERROR')).toBe(false)
|
||||
expect(
|
||||
isWindowsSignatureCheckUnavailableFailure('Cannot find channel "latest.yml" (404)')
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isWindowsSignatureMismatchFailure', () => {
|
||||
it('matches the wrong-publisher integrity failure', () => {
|
||||
expect(
|
||||
isWindowsSignatureMismatchFailure(
|
||||
'New version 1.4.144 is not signed by the application owner: ' +
|
||||
'publisherNames: SignPath Foundation, raw info: {"Status": 0}'
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('is mutually exclusive with the check-unavailable classifier', () => {
|
||||
const mismatch = 'New version 1.4.144 is not signed by the application owner: publisherNames: X'
|
||||
expect(isWindowsSignatureMismatchFailure(mismatch)).toBe(true)
|
||||
expect(isWindowsSignatureCheckUnavailableFailure(mismatch)).toBe(false)
|
||||
|
||||
const blocked = 'Command failed: … Get-AuthenticodeSignature -LiteralPath …'
|
||||
expect(isWindowsSignatureCheckUnavailableFailure(blocked)).toBe(true)
|
||||
expect(isWindowsSignatureMismatchFailure(blocked)).toBe(false)
|
||||
})
|
||||
|
||||
it('does not match unrelated errors', () => {
|
||||
expect(isWindowsSignatureMismatchFailure('net::ERR_HTTP2_PROTOCOL_ERROR')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
// Why: electron-updater verifies Windows installers by spawning PowerShell's
|
||||
// Get-AuthenticodeSignature. Antivirus/EDR interception, its hardcoded 20s
|
||||
// timeout, or stalled revocation lookups kill that spawn, and the raw
|
||||
// child-process failure becomes the update error. Detect that shape so the UI
|
||||
// can explain it instead of dumping the command line. A true signature
|
||||
// mismatch ("not signed by the application owner") must NOT match — that is a
|
||||
// real integrity failure, not environment interference.
|
||||
export function isWindowsSignatureCheckUnavailableFailure(message: string): boolean {
|
||||
const normalized = message.toLowerCase()
|
||||
if (normalized.includes('not signed by the application owner')) {
|
||||
return false
|
||||
}
|
||||
return normalized.includes('get-authenticodesignature')
|
||||
}
|
||||
|
||||
// Why: electron-updater throws ERR_UPDATER_INVALID_SIGNATURE with this exact
|
||||
// phrase when the downloaded installer is validly readable but signed by the
|
||||
// wrong publisher — a genuine integrity failure, NOT environment interference.
|
||||
// This must drive a security-stop message (no silent "retry" framing) and stay
|
||||
// distinct from isWindowsSignatureCheckUnavailableFailure above.
|
||||
export function isWindowsSignatureMismatchFailure(message: string): boolean {
|
||||
return message.toLowerCase().includes('not signed by the application owner')
|
||||
}
|
||||
Reference in New Issue
Block a user