mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 19:17:28 +00:00
* 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
357 lines
9.1 KiB
TypeScript
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}`
|
|
}
|