Files
orca/src/shared/orca-profiles.ts
T
Jinwoo Hong c4730d692f fix(cloud-auth): show that a sign-in is in progress, with copy-link and cancel, on every Orca sign-in button (#30410)
* fix(cloud-auth): show the browser sign-in wait and offer its link from Relay sign-in

Clicking 'Sign in for Relay' handed the sign-in page to the OS browser and
then showed nothing, so a browser that never came forward looked like a dead
button, and the 5-minute wait ended silently. The Relay sign-in row now shows
the wait with a Copy sign-in link button, and a wait that times out toasts.

STA-10940

* fix(cloud-auth): show the sign-in wait on every Orca sign-in button

The waiting text and Copy sign-in link now come from one shared notice used by
Mobile Relay, Orca Account, Artifacts (settings, page and publish popover),
Share Skills, the unexpected sign-out card and dev tools. The copyable link is
dropped as soon as the browser answers, and a copy with no waiting link
reports a failure instead of doing nothing.

STA-10940

* refactor(cloud-auth): main owns the waiting sign-in link; renderer only mirrors it

The notice now shows exactly while main holds a live link: main announces the
link when the browser wait starts and clears it the moment the browser
answers, so 'Waiting for sign-in in your browser' no longer outlives the wait
during the token exchange. Only the newest attempt may publish its link, so an
older attempt reporting late cannot replace or later wipe it. The renderer's
separate pending flag and its resets are gone.

STA-10940

* fix(cloud-auth): tolerate a sign-in link read that returns no promise

The app-wide IPC tests stub every bridge method as returning nothing, and the
one-time link read called .then on that result.

STA-10940

* fix(cloud-auth): a torn-down link subscription ignores its late snapshot; restore auth bridge tests

A window's one-time sign-in link read could land after its subscription was
disposed and overwrite a newer value from the replacement subscription. The
auth-status refresh tests the earlier rewrite dropped are back, alongside the
link cases.

STA-10940

* feat(cloud-auth): sign-in buttons show Signing in… with copy-link and cancel links

Every Orca sign-in button greys out to 'Signing in…' with a spinner until the
sign-in ends. While the browser has not answered, a line offers 'Copy sign-in
link' and 'Cancel'; Cancel aborts the browser wait so a lost tab never locks
the button. Main owns the in-flight sign-in (waiting on the browser, then
finishing) and every window mirrors it.

STA-10940
2026-10-18 19:28:04 -04:00

357 lines
9.1 KiB
TypeScript

import { ORCA_BROWSER_PARTITION } from './constants'
import type { ExecutionHostId } from './execution-host'
export const ORCA_PROFILE_INDEX_SCHEMA_VERSION = 1
export const DEFAULT_LOCAL_ORCA_PROFILE_ID = 'local-default'
export const DEFAULT_LOCAL_ORCA_PROFILE_NAME = 'Personal'
/** Main -> renderer push when the stored auth status changed without the renderer asking. */
export const ORCA_PROFILE_AUTH_STATUS_CHANGED_CHANNEL = 'orcaProfiles:authStatusChanged'
export const ORCA_PROFILE_SIGN_IN_PROGRESS_CHANGED_CHANNEL = 'orcaProfiles:signInProgressChanged'
const LEGACY_ORCA_BROWSER_SESSION_PARTITION_PREFIX = 'persist:orca-browser-session-'
export type OrcaProfileAvatar = {
kind: 'initials'
initials: string
color: 'neutral'
}
export type OrcaProfileKind = 'local' | 'cloud-linked'
export type OrcaProfileCloudSummary = {
cloudProfileId: string
userId: string
email: string
displayName?: string
activeOrgId?: string
activeOrgName?: string
linkedAt: number
}
export type OrcaCloudOrgSummary = {
orgId: string
name: string
role?: string
}
export type OrcaCloudCapabilityFlags = Record<string, boolean>
export type OrcaCloudCapabilities = {
flags: OrcaCloudCapabilityFlags
refreshedAt: number
}
export type OrcaCloudSessionPersistence = 'none' | 'encrypted' | 'memory-only' | 'dev-plaintext'
export type OrcaProfileAuthState = 'local' | 'unconfigured' | 'connected' | 'reconnect-required'
export type OrcaProfileAuthStatus = {
activeProfileId: string
configured: boolean
state: OrcaProfileAuthState
persistence: OrcaCloudSessionPersistence
cloud?: OrcaProfileCloudSummary
organizations?: OrcaCloudOrgSummary[]
capabilities?: OrcaCloudCapabilities
credentialError?: string
setupMessage?: string
}
export type OrcaProfileSummary = {
id: string
name: string
avatar: OrcaProfileAvatar
kind: OrcaProfileKind
createdAt: number
updatedAt: number
lastOpenedAt: number
cloud?: OrcaProfileCloudSummary
}
export type OrcaProfileIndex = {
schemaVersion: number
activeProfileId: string
profiles: OrcaProfileSummary[]
}
export type OrcaProfileListState = {
activeProfileId: string
profiles: OrcaProfileSummary[]
}
export type OrcaProfileListResult = OrcaProfileListState & {
// Why: gates the full multi-profile switcher UI; default builds show a
// single-profile account menu instead.
multiProfileUi: boolean
}
export type CreateLocalOrcaProfileArgs = {
name?: string
}
export type CreateLocalOrcaProfileResult = OrcaProfileListState & {
profile: OrcaProfileSummary
}
export type CreateCloudLinkedOrcaProfileArgs = {
orgId?: string
name?: string
}
export type SwitchOrcaProfileArgs = {
profileId: string
}
export type SwitchOrcaProfileResult = {
status: 'already-active' | 'relaunching'
}
export type TransferOrcaProfileProjectMode = 'move' | 'copy'
export type TransferOrcaProfileProjectArgs = {
sourceProfileId: string
targetProfileId: string
repoId: string
mode: TransferOrcaProfileProjectMode
}
export type FindOrcaProfileProjectsByPathArgs = {
path: string
connectionId?: string | null
executionHostId?: ExecutionHostId | null
excludeProfileId?: string | null
}
export type OrcaProfileProjectPresence = {
profileId: string
profileName: string
profileKind: OrcaProfileKind
repoId: string
repoName: string
}
export type FindOrcaProfileProjectsByPathResult = {
projects: OrcaProfileProjectPresence[]
}
export type TransferOrcaProfileProjectResult =
| {
status: 'transferred'
mode: TransferOrcaProfileProjectMode
sourceProfileId: string
targetProfileId: string
sourceRepoId: string
targetRepoId: string
targetProjectId: string | null
willRelaunch?: boolean
}
| {
status: 'duplicate-target'
sourceProfileId: string
targetProfileId: string
sourceRepoId: string
duplicateRepoId: string
}
/** A sign-in in flight; `link` is set only while the browser has not answered yet. */
export type OrcaCloudSignInProgress = { link: string | null }
export type ConnectCurrentOrcaProfileResult =
| {
status: 'connected'
auth: OrcaProfileAuthStatus
activeProfileId: string
profiles: OrcaProfileSummary[]
}
| {
status: 'unconfigured'
auth: OrcaProfileAuthStatus
}
| {
status: 'cancelled'
auth: OrcaProfileAuthStatus
}
| {
status: 'timed-out'
auth: OrcaProfileAuthStatus
}
| {
status: 'failed'
auth: OrcaProfileAuthStatus
error: string
}
export type CreateCloudLinkedOrcaProfileResult =
| {
status: 'created'
auth: OrcaProfileAuthStatus
activeProfileId: string
profiles: OrcaProfileSummary[]
profile: OrcaProfileSummary
}
| {
status: 'unconfigured' | 'reconnect-required'
auth: OrcaProfileAuthStatus
}
| {
status: 'failed'
auth: OrcaProfileAuthStatus
error: string
}
export type SignOutCurrentOrcaProfileResult = {
status: 'signed-out'
auth: OrcaProfileAuthStatus
activeProfileId: string
profiles: OrcaProfileSummary[]
}
export type SelectOrcaProfileOrgArgs = {
orgId: string
}
export type SelectOrcaProfileOrgResult =
| {
status: 'selected'
auth: OrcaProfileAuthStatus
activeProfileId: string
profiles: OrcaProfileSummary[]
}
| {
status: 'unconfigured' | 'reconnect-required'
auth: OrcaProfileAuthStatus
}
| {
status: 'failed'
auth: OrcaProfileAuthStatus
error: string
}
export type RefreshCurrentOrcaProfileAuthResult =
| {
status: 'refreshed'
auth: OrcaProfileAuthStatus
activeProfileId: string
profiles: OrcaProfileSummary[]
}
| {
status: 'local' | 'unconfigured' | 'reconnect-required'
auth: OrcaProfileAuthStatus
}
| {
status: 'failed'
auth: OrcaProfileAuthStatus
error: string
}
// Why: organization roles are a fixed server-side enum; the desktop UI mirrors
// exactly these three so role selects can't drift from what the API accepts.
export type OrcaOrgRole = 'owner' | 'admin' | 'member'
export type OrcaOrgMember = {
// Why: null for teammates provisioned server-side who never signed into Orca;
// mutation actions are disabled for them since the API keys on a real userId.
userId: string | null
email: string
displayName?: string
role: OrcaOrgRole
}
export type OrcaOrgPendingInvite = {
email: string
role: OrcaOrgRole
createdAt: number
}
export type OrcaOrgMembersRoster = {
members: OrcaOrgMember[]
pendingInvites: OrcaOrgPendingInvite[]
viewerRole: OrcaOrgRole
canManageMembers: boolean
}
export type OrcaProfileOrgMembersListArgs = {
orgId: string
}
export type OrcaProfileOrgMemberInviteArgs = {
orgId: string
email: string
role: OrcaOrgRole
}
export type OrcaProfileOrgInviteRevokeArgs = {
orgId: string
email: string
}
export type OrcaProfileOrgMemberChangeRoleArgs = {
orgId: string
userId: string
role: OrcaOrgRole
}
export type OrcaProfileOrgMemberRemoveArgs = {
orgId: string
userId: string
}
export type OrcaProfileOrgMembersListResult =
| { status: 'ok'; roster: OrcaOrgMembersRoster }
| { status: 'unconfigured' | 'reconnect-required' }
| { status: 'failed'; error: string }
export type OrcaOrgInviteConflictReason = 'already_member' | 'already_invited'
export type OrcaOrgMutationInvalidReason = 'cannot_change_own_role' | 'cannot_remove_self'
export type OrcaProfileOrgMemberMutationResult =
| { status: 'ok' }
| { status: 'unconfigured' | 'reconnect-required' | 'forbidden' | 'not-found' }
| { status: 'conflict'; reason: OrcaOrgInviteConflictReason }
| { status: 'invalid'; reason: OrcaOrgMutationInvalidReason }
| { status: 'failed'; error: string }
export function createDefaultLocalOrcaProfile(now: number): OrcaProfileSummary {
return {
id: DEFAULT_LOCAL_ORCA_PROFILE_ID,
name: DEFAULT_LOCAL_ORCA_PROFILE_NAME,
avatar: { kind: 'initials', initials: 'P', color: 'neutral' },
kind: 'local',
createdAt: now,
updatedAt: now,
lastOpenedAt: now
}
}
function profilePartitionHash(value: string): string {
let hash = 2166136261
for (let i = 0; i < value.length; i++) {
hash ^= value.charCodeAt(i)
hash = Math.imul(hash, 16777619)
}
return (hash >>> 0).toString(16).padStart(8, '0')
}
export function getOrcaProfileBrowserPartitionSegment(profileId: string): string {
const safe = profileId.replace(/[^A-Za-z0-9_-]/g, '_').slice(0, 48) || 'profile'
return `${safe}-${profilePartitionHash(profileId)}`
}
export function getOrcaProfileBrowserDefaultPartition(profileId: string): string {
if (profileId === DEFAULT_LOCAL_ORCA_PROFILE_ID) {
return ORCA_BROWSER_PARTITION
}
return `persist:orca-profile-${getOrcaProfileBrowserPartitionSegment(profileId)}-browser-default`
}
export function getOrcaProfileBrowserSessionPartition(
profileId: string,
browserSessionProfileId: string
): string {
if (profileId === DEFAULT_LOCAL_ORCA_PROFILE_ID) {
return `${LEGACY_ORCA_BROWSER_SESSION_PARTITION_PREFIX}${browserSessionProfileId}`
}
return `persist:orca-profile-${getOrcaProfileBrowserPartitionSegment(
profileId
)}-browser-session-${browserSessionProfileId}`
}