fix(cloud-auth): ignore a stale sign-in after a later wait succeeds

A second Sign in click still starts a new loopback wait. Completing that
newer wait links the session; finishing the older tab afterwards is
cancelled instead of overwriting the linked identity or toasting again.
This commit is contained in:
Jinwoo-H
2026-09-16 15:53:35 -04:00
parent 5388a0a557
commit f58bf1ca03
5 changed files with 446 additions and 163 deletions
@@ -0,0 +1,137 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type {
OrcaCloudCapabilities,
OrcaCloudOrgSummary,
OrcaProfileCloudSummary
} from '../../shared/orca-profiles'
const { beginOrcaCloudPkceFlowMock, exchangeOrcaCloudAuthCodeMock, safeStorageMock } = vi.hoisted(
() => ({
beginOrcaCloudPkceFlowMock: vi.fn(),
exchangeOrcaCloudAuthCodeMock: vi.fn(),
safeStorageMock: {
decryptString: vi.fn((value: Buffer) => value.toString('utf-8')),
encryptString: vi.fn((value: string) => Buffer.from(value, 'utf-8')),
isEncryptionAvailable: vi.fn(() => true)
}
})
)
let userDataPath = ''
vi.mock('electron', () => ({
app: {
getPath: () => userDataPath
},
safeStorage: safeStorageMock
}))
vi.mock('./profile-cloud-pkce', () => ({
beginOrcaCloudPkceFlow: beginOrcaCloudPkceFlowMock
}))
vi.mock('./profile-cloud-client', () => ({
createOrcaCloudProfile: vi.fn(),
exchangeOrcaCloudAuthCode: exchangeOrcaCloudAuthCodeMock,
revokeOrcaCloudSession: vi.fn(),
selectOrcaCloudOrg: vi.fn()
}))
import { connectCurrentOrcaProfile, getCurrentOrcaProfileAuthStatus } from './profile-cloud-service'
const earlierCloud: OrcaProfileCloudSummary = {
cloudProfileId: 'cloud-profile-1',
userId: 'user-1',
email: 'nina@example.com',
displayName: 'Nina',
linkedAt: 10
}
const laterCloud: OrcaProfileCloudSummary = {
...earlierCloud,
cloudProfileId: 'cloud-profile-2',
userId: 'user-2',
email: 'ada@example.com'
}
const capabilities: OrcaCloudCapabilities = {
flags: { share: true },
refreshedAt: 11
}
const organizations: OrcaCloudOrgSummary[] = [{ orgId: 'org-1', name: 'Acme', role: 'Admin' }]
describe('Orca cloud overlapping connect', () => {
beforeEach(() => {
userDataPath = mkdtempSync(join(tmpdir(), 'orca-cloud-connect-overlap-'))
beginOrcaCloudPkceFlowMock.mockReset()
exchangeOrcaCloudAuthCodeMock.mockReset()
safeStorageMock.decryptString.mockReset()
safeStorageMock.encryptString.mockReset()
safeStorageMock.isEncryptionAvailable.mockReset()
safeStorageMock.decryptString.mockImplementation((value: Buffer) => value.toString('utf-8'))
safeStorageMock.encryptString.mockImplementation((value: string) => Buffer.from(value, 'utf-8'))
safeStorageMock.isEncryptionAvailable.mockReturnValue(true)
vi.stubEnv('ORCA_CLOUD_API_URL', 'https://orca-cloud.example')
vi.stubEnv('ORCA_CLOUD_CLIENT_ID', 'desktop-client')
})
afterEach(() => {
rmSync(userDataPath, { recursive: true, force: true })
vi.unstubAllEnvs()
})
it('does not let an earlier sign-in overwrite a later successful connect', async () => {
let finishFirst!: (value: {
code: string
codeVerifier: string
nonce: string
redirectUri: string
state: string
}) => void
beginOrcaCloudPkceFlowMock
.mockReturnValueOnce(
new Promise((resolve) => {
finishFirst = resolve
})
)
.mockResolvedValueOnce({
code: 'later-code',
codeVerifier: 'later-verifier',
nonce: 'later-nonce',
redirectUri: 'http://127.0.0.1:4101/auth/callback',
state: 'later-state'
})
exchangeOrcaCloudAuthCodeMock.mockImplementation(async (_config, args) => ({
accessToken: 'access-token',
refreshToken: 'refresh-token',
expiresAt: Date.now() + 3_600_000,
cloud: args.code === 'later-code' ? laterCloud : earlierCloud,
organizations,
capabilities
}))
const first = connectCurrentOrcaProfile(userDataPath)
const later = connectCurrentOrcaProfile(userDataPath)
await expect(later).resolves.toMatchObject({ status: 'connected' })
expect(getCurrentOrcaProfileAuthStatus(userDataPath).cloud?.email).toBe('ada@example.com')
finishFirst({
code: 'earlier-code',
codeVerifier: 'earlier-verifier',
nonce: 'earlier-nonce',
redirectUri: 'http://127.0.0.1:4100/auth/callback',
state: 'earlier-state'
})
await expect(first).resolves.toMatchObject({ status: 'cancelled' })
expect(exchangeOrcaCloudAuthCodeMock).toHaveBeenCalledTimes(1)
expect(exchangeOrcaCloudAuthCodeMock).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({ code: 'later-code' })
)
expect(getCurrentOrcaProfileAuthStatus(userDataPath).cloud?.email).toBe('ada@example.com')
})
})
@@ -36,6 +36,9 @@ import { selectCloudOrgWithMutationFence } from './profile-cloud-org-selection'
export { refreshCurrentOrcaProfileAuth } from './profile-cloud-capability-refresh'
let nextCloudConnectAttempt = 0
let linkedCloudConnectAttempt = 0
function isUserCancelledAuthError(message: string): boolean {
return message === 'orca_cloud_auth_timeout' || message === 'orca_cloud_auth_denied'
}
@@ -73,14 +76,28 @@ export async function connectCurrentOrcaProfile(
}
}
const attempt = ++nextCloudConnectAttempt
try {
const code = await beginOrcaCloudPkceFlow(configState.config, active.profile.id)
if (attempt < linkedCloudConnectAttempt) {
return {
status: 'cancelled',
auth: getCurrentOrcaProfileAuthStatus(userDataPath)
}
}
const exchange = await exchangeOrcaCloudAuthCode(configState.config, {
...code,
localProfileId: active.profile.id
})
if (attempt < linkedCloudConnectAttempt) {
return {
status: 'cancelled',
auth: getCurrentOrcaProfileAuthStatus(userDataPath)
}
}
saveOrcaCloudSessionExchange(active.profile.id, userDataPath, exchange)
const list = linkOrcaProfileToCloud(active.profile.id, exchange.cloud, userDataPath)
linkedCloudConnectAttempt = attempt
return {
status: 'connected',
auth: getCurrentOrcaProfileAuthStatus(userDataPath),
@@ -63,7 +63,6 @@ function storeState(): Record<string, unknown> {
closeArtifactsPage: mocks.closePage,
connectCurrentOrcaProfile: mocks.connect,
orcaProfileAuthStatus: mocks.authStatus,
refreshCurrentOrcaProfileAuth: mocks.refreshAuth,
settings: mocks.settings,
updateSettings: mocks.updateSettings,
@@ -0,0 +1,107 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type {
ConnectCurrentOrcaProfileResult,
OrcaProfileAuthStatus,
OrcaProfileListState
} from '../../../../shared/orca-profiles'
import { createTestStore } from './store-test-helpers'
const { toastErrorMock, toastSuccessMock } = vi.hoisted(() => ({
toastErrorMock: vi.fn(),
toastSuccessMock: vi.fn()
}))
vi.mock('sonner', () => ({
toast: {
error: toastErrorMock,
info: vi.fn(),
success: toastSuccessMock,
warning: vi.fn()
}
}))
const listState: OrcaProfileListState = {
activeProfileId: 'local-default',
profiles: [
{
id: 'local-default',
name: 'Personal',
avatar: { kind: 'initials', initials: 'P', color: 'neutral' },
kind: 'local',
createdAt: 1,
updatedAt: 1,
lastOpenedAt: 1
}
]
}
const connectedCloud = {
cloudProfileId: 'cloud-profile-1',
userId: 'user-1',
email: 'nina@example.com',
linkedAt: 3
}
const connectedAuthStatus: OrcaProfileAuthStatus = {
activeProfileId: 'local-default',
configured: true,
state: 'connected',
persistence: 'encrypted',
cloud: connectedCloud,
organizations: [{ orgId: 'org-1', name: 'Acme', role: 'Admin' }],
capabilities: { flags: { share: true }, refreshedAt: 4 }
}
const orcaProfilesApi = {
connectCurrent: vi.fn()
}
describe('orca profile overlapping connect actions', () => {
beforeEach(() => {
vi.resetAllMocks()
toastErrorMock.mockReset()
toastSuccessMock.mockReset()
vi.stubGlobal('window', {
api: { orcaProfiles: orcaProfilesApi }
})
})
it('keeps the later sign-in and one success toast when both waits complete', async () => {
const laterCloud = { ...connectedCloud, userId: 'user-2', email: 'ada@example.com' }
const laterAuthStatus: OrcaProfileAuthStatus = {
...connectedAuthStatus,
cloud: laterCloud
}
const earlierConnected: ConnectCurrentOrcaProfileResult = {
status: 'connected',
auth: connectedAuthStatus,
activeProfileId: 'local-default',
profiles: [{ ...listState.profiles[0], kind: 'cloud-linked', cloud: connectedCloud }]
}
const laterConnected: ConnectCurrentOrcaProfileResult = {
status: 'connected',
auth: laterAuthStatus,
activeProfileId: 'local-default',
profiles: [{ ...listState.profiles[0], kind: 'cloud-linked', cloud: laterCloud }]
}
let finishFirst!: (value: ConnectCurrentOrcaProfileResult) => void
orcaProfilesApi.connectCurrent
.mockReturnValueOnce(
new Promise<ConnectCurrentOrcaProfileResult>((resolve) => {
finishFirst = resolve
})
)
.mockResolvedValueOnce(laterConnected)
const store = createTestStore()
const first = store.getState().connectCurrentOrcaProfile()
const second = store.getState().connectCurrentOrcaProfile()
await expect(second).resolves.toEqual(laterConnected)
finishFirst(earlierConnected)
await expect(first).resolves.toEqual(earlierConnected)
expect(toastSuccessMock).toHaveBeenCalledOnce()
expect(toastErrorMock).not.toHaveBeenCalled()
expect(store.getState().orcaProfileAuthStatus).toEqual(laterAuthStatus)
expect(store.getState().orcaProfiles).toEqual(laterConnected.profiles)
})
})
@@ -29,186 +29,209 @@ export const createOrcaProfilesAuthActions: StateCreator<
[],
[],
OrcaProfilesAuthActions
> = (set, get) => ({
createCloudLinkedOrcaProfile: async (args) => {
try {
const result = await window.api.orcaProfiles.createCloudLinked(args)
set({
orcaProfileAuthStatus: result.auth,
...(result.status === 'created'
? {
activeOrcaProfileId: result.activeProfileId,
orcaProfiles: result.profiles
}
: {})
})
if (result.status === 'created') {
toast.success(
translate('auto.store.slices.orca.profiles.319d7cf39b', 'Cloud profile created')
)
} else if (result.status === 'reconnect-required') {
toast.error(
translate('auto.store.slices.orca.profiles.d6e764e7db', 'Reconnect this profile')
)
} else if (result.status === 'failed') {
> = (set, get) => {
let nextConnectAttempt = 0
let appliedConnectAttempt = 0
return {
createCloudLinkedOrcaProfile: async (args) => {
try {
const result = await window.api.orcaProfiles.createCloudLinked(args)
set({
orcaProfileAuthStatus: result.auth,
...(result.status === 'created'
? {
activeOrcaProfileId: result.activeProfileId,
orcaProfiles: result.profiles
}
: {})
})
if (result.status === 'created') {
toast.success(
translate('auto.store.slices.orca.profiles.319d7cf39b', 'Cloud profile created')
)
} else if (result.status === 'reconnect-required') {
toast.error(
translate('auto.store.slices.orca.profiles.d6e764e7db', 'Reconnect this profile')
)
} else if (result.status === 'failed') {
toast.error(
translate(
'auto.store.slices.orca.profiles.f0c9e11a6d',
'Failed to create cloud profile'
),
{ description: result.error }
)
}
return result
} catch (err) {
console.error('Failed to create Orca cloud profile:', err)
toast.error(
translate('auto.store.slices.orca.profiles.f0c9e11a6d', 'Failed to create cloud profile'),
{ description: result.error }
)
}
return result
} catch (err) {
console.error('Failed to create Orca cloud profile:', err)
toast.error(
translate('auto.store.slices.orca.profiles.f0c9e11a6d', 'Failed to create cloud profile'),
{
description: err instanceof Error ? err.message : String(err)
}
)
return null
}
},
connectCurrentOrcaProfile: async () => {
try {
// Why: a pending browser callback must not block retry. Another click
// starts a second PKCE wait; the earlier tab can still complete.
const alreadyConnected = get().orcaProfileAuthStatus?.state === 'connected'
const result = await window.api.orcaProfiles.connectCurrent()
set({
orcaProfileAuthStatus: result.auth,
...(result.status === 'connected'
? {
activeOrcaProfileId: result.activeProfileId,
orcaProfiles: result.profiles
}
: {})
})
if (result.status === 'unconfigured') {
toast.error(
translate(
'auto.store.slices.orca.profiles.8b8fa73174',
'Orca Cloud sign-in is not configured'
),
{
description: result.auth.setupMessage
}
)
} else if (
result.status === 'failed' &&
!alreadyConnected &&
result.auth.state !== 'connected'
) {
toast.error(
translate('auto.store.slices.orca.profiles.33290e88ed', 'Failed to connect profile'),
{ description: result.error }
)
} else if (result.status === 'connected' && !alreadyConnected) {
toast.success(translate('auto.store.slices.orca.profiles.9fcb07a796', 'Profile connected'))
}
return result
} catch (err) {
console.error('Failed to connect Orca profile:', err)
if (get().orcaProfileAuthStatus?.state !== 'connected') {
toast.error(
translate('auto.store.slices.orca.profiles.33290e88ed', 'Failed to connect profile'),
{
description: err instanceof Error ? err.message : String(err)
}
)
return null
}
return null
}
},
},
refreshCurrentOrcaProfileAuth: async () => {
try {
const result = await window.api.orcaProfiles.refreshAuth()
set({
orcaProfileAuthStatus: result.auth,
...(result.status === 'refreshed'
? {
activeOrcaProfileId: result.activeProfileId,
orcaProfiles: result.profiles
connectCurrentOrcaProfile: async () => {
const attempt = ++nextConnectAttempt
try {
// Why: a pending browser callback must not block retry. Another click
// starts a second PKCE wait; an older wait is ignored after a newer
// one has already linked.
const result = await window.api.orcaProfiles.connectCurrent()
if (attempt < appliedConnectAttempt) {
return result
}
const alreadyConnected = get().orcaProfileAuthStatus?.state === 'connected'
set({
orcaProfileAuthStatus: result.auth,
...(result.status === 'connected'
? {
activeOrcaProfileId: result.activeProfileId,
orcaProfiles: result.profiles
}
: {})
})
if (result.status === 'connected') {
appliedConnectAttempt = attempt
if (!alreadyConnected) {
toast.success(
translate('auto.store.slices.orca.profiles.9fcb07a796', 'Profile connected')
)
}
} else if (result.status === 'unconfigured') {
toast.error(
translate(
'auto.store.slices.orca.profiles.8b8fa73174',
'Orca Cloud sign-in is not configured'
),
{
description: result.auth.setupMessage
}
: {})
})
if (result.status === 'reconnect-required') {
toast.error(
translate('auto.store.slices.orca.profiles.d6e764e7db', 'Reconnect this profile')
)
} else if (result.status === 'failed') {
)
} else if (result.status === 'failed' && !alreadyConnected) {
toast.error(
translate('auto.store.slices.orca.profiles.33290e88ed', 'Failed to connect profile'),
{ description: result.error }
)
}
return result
} catch (err) {
console.error('Failed to connect Orca profile:', err)
if (
attempt >= appliedConnectAttempt &&
get().orcaProfileAuthStatus?.state !== 'connected'
) {
toast.error(
translate('auto.store.slices.orca.profiles.33290e88ed', 'Failed to connect profile'),
{
description: err instanceof Error ? err.message : String(err)
}
)
}
return null
}
},
refreshCurrentOrcaProfileAuth: async () => {
try {
const result = await window.api.orcaProfiles.refreshAuth()
set({
orcaProfileAuthStatus: result.auth,
...(result.status === 'refreshed'
? {
activeOrcaProfileId: result.activeProfileId,
orcaProfiles: result.profiles
}
: {})
})
if (result.status === 'reconnect-required') {
toast.error(
translate('auto.store.slices.orca.profiles.d6e764e7db', 'Reconnect this profile')
)
} else if (result.status === 'failed') {
toast.error(
translate(
'auto.store.slices.orca.profiles.2f6c78a039',
'Failed to refresh profile auth'
),
{ description: result.error }
)
}
return result
} catch (err) {
console.error('Failed to refresh Orca profile auth:', err)
toast.error(
translate('auto.store.slices.orca.profiles.2f6c78a039', 'Failed to refresh profile auth'),
{ description: result.error }
{
description: err instanceof Error ? err.message : String(err)
}
)
return null
}
return result
} catch (err) {
console.error('Failed to refresh Orca profile auth:', err)
toast.error(
translate('auto.store.slices.orca.profiles.2f6c78a039', 'Failed to refresh profile auth'),
{
description: err instanceof Error ? err.message : String(err)
}
)
return null
}
},
},
signOutCurrentOrcaProfile: async () => {
try {
const result = await window.api.orcaProfiles.signOutCurrent()
set({
activeOrcaProfileId: result.activeProfileId,
orcaProfiles: result.profiles,
orcaProfileAuthStatus: result.auth
})
toast.success(
translate('auto.store.slices.orca.profiles.a37b5e6d37', 'Signed out of profile')
)
return result
} catch (err) {
console.error('Failed to sign out of Orca profile:', err)
toast.error(translate('auto.store.slices.orca.profiles.83600521e7', 'Failed to sign out'), {
description: err instanceof Error ? err.message : String(err)
})
return null
}
},
selectOrcaProfileOrg: async (orgId) => {
try {
const result = await window.api.orcaProfiles.selectOrg({ orgId })
set({
orcaProfileAuthStatus: result.auth,
...(result.status === 'selected'
? {
activeOrcaProfileId: result.activeProfileId,
orcaProfiles: result.profiles
}
: {})
})
if (result.status === 'reconnect-required') {
toast.error(
translate('auto.store.slices.orca.profiles.d6e764e7db', 'Reconnect this profile')
signOutCurrentOrcaProfile: async () => {
try {
const result = await window.api.orcaProfiles.signOutCurrent()
set({
activeOrcaProfileId: result.activeProfileId,
orcaProfiles: result.profiles,
orcaProfileAuthStatus: result.auth
})
toast.success(
translate('auto.store.slices.orca.profiles.a37b5e6d37', 'Signed out of profile')
)
} else if (result.status === 'failed') {
return result
} catch (err) {
console.error('Failed to sign out of Orca profile:', err)
toast.error(translate('auto.store.slices.orca.profiles.83600521e7', 'Failed to sign out'), {
description: err instanceof Error ? err.message : String(err)
})
return null
}
},
selectOrcaProfileOrg: async (orgId) => {
try {
const result = await window.api.orcaProfiles.selectOrg({ orgId })
set({
orcaProfileAuthStatus: result.auth,
...(result.status === 'selected'
? {
activeOrcaProfileId: result.activeProfileId,
orcaProfiles: result.profiles
}
: {})
})
if (result.status === 'reconnect-required') {
toast.error(
translate('auto.store.slices.orca.profiles.d6e764e7db', 'Reconnect this profile')
)
} else if (result.status === 'failed') {
toast.error(
translate(
'auto.store.slices.orca.profiles.76deec8f58',
'Failed to switch organization'
),
{ description: result.error }
)
}
return result
} catch (err) {
console.error('Failed to switch Orca profile org:', err)
toast.error(
translate('auto.store.slices.orca.profiles.76deec8f58', 'Failed to switch organization'),
{ description: result.error }
{
description: err instanceof Error ? err.message : String(err)
}
)
return null
}
return result
} catch (err) {
console.error('Failed to switch Orca profile org:', err)
toast.error(
translate('auto.store.slices.orca.profiles.76deec8f58', 'Failed to switch organization'),
{
description: err instanceof Error ? err.message : String(err)
}
)
return null
}
}
})
}