fix(relay): make fenceAndCloseNow stop the liveness safety net (#12482)

The 5-minute liveness tick from #12432 survived fenceAndCloseNow(), so a
tick landing between the pre-sign-out fence and the profile wipe could
briefly resurrect a broker (benign but soft — the entitlement check
bails afterward). The fence now clears the interval; the next auth
mutation re-arms it via refreshDemand, and the safety net otherwise
behaves identically. Found in release review of the #12432 cherry-pick.

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
Jinwoo Hong
2026-08-04 02:19:36 -07:00
committed by Jinjing
co-authored by OrcaWin
parent 7600e9a0e3
commit a8cad67166
2 changed files with 52 additions and 4 deletions
@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { MobilePairingConnectionContext } from '../runtime-rpc'
import { DesktopRelayService, pairingAuthorizationForContext } from './desktop-relay-service'
@@ -61,6 +61,47 @@ describe('pairingAuthorizationForContext', () => {
})
})
describe('liveness safety net lifecycle', () => {
afterEach(() => vi.useRealTimers())
it('fences hard: the tick stops on fence and re-arms on the next auth mutation', () => {
// Why: a tick surviving the pre-sign-out fence could catch the window
// before the profile wipe and briefly resurrect a broker.
vi.useFakeTimers()
const coordinator = {
reconcile: vi.fn(),
ensureLive: vi.fn(),
fenceAndCloseNow: vi.fn(),
stop: vi.fn()
}
const service = Object.create(DesktopRelayService.prototype) as DesktopRelayService
Object.assign(service, {
coordinator,
demandLedger: { nextPendingExpiry: () => null },
stopped: false,
livenessTimer: null,
demandExpiryTimer: null
})
service.start()
vi.advanceTimersByTime(5 * 60_000)
expect(coordinator.ensureLive).toHaveBeenCalledTimes(1)
service.fenceAndCloseNow()
expect(coordinator.fenceAndCloseNow).toHaveBeenCalledOnce()
vi.advanceTimersByTime(30 * 60_000)
expect(coordinator.ensureLive).toHaveBeenCalledTimes(1)
service.authMutated()
vi.advanceTimersByTime(5 * 60_000)
expect(coordinator.ensureLive).toHaveBeenCalledTimes(2)
service.stop()
vi.advanceTimersByTime(30 * 60_000)
expect(coordinator.ensureLive).toHaveBeenCalledTimes(2)
})
})
describe('local-only mobile pairing', () => {
it('refuses endpoint discovery and provisioning without opening Relay demand', async () => {
const registry = {
@@ -96,9 +96,6 @@ export class DesktopRelayService {
start(): void {
this.refreshDemand()
if (!this.livenessTimer) {
this.livenessTimer = setInterval(() => this.ensureLive(), RELAY_LIVENESS_INTERVAL_MS)
}
}
// Safe to call from any wake signal (power resume, network change).
@@ -113,6 +110,13 @@ export class DesktopRelayService {
}
fenceAndCloseNow(): void {
// Why: a fence must be hard — a surviving liveness tick could catch the
// window between the pre-sign-out fence and the profile wipe and briefly
// resurrect a broker. The next auth mutation re-arms via refreshDemand.
if (this.livenessTimer) {
clearInterval(this.livenessTimer)
this.livenessTimer = null
}
this.coordinator.fenceAndCloseNow()
}
@@ -307,6 +311,9 @@ export class DesktopRelayService {
if (this.stopped) {
return
}
if (!this.livenessTimer) {
this.livenessTimer = setInterval(() => this.ensureLive(), RELAY_LIVENESS_INTERVAL_MS)
}
if (this.demandExpiryTimer) {
clearTimeout(this.demandExpiryTimer)
this.demandExpiryTimer = null