mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
fix(grok): prefer auth.x.ai session and mention Grok in OSC52 toast (#8392)
* fix(grok): prefer auth.x.ai session and mention Grok in OSC52 toast auth.json can hold multiple issuer keys; Object.values order could pick a stale entry before the live auth.x.ai OIDC session. Prefer auth.x.ai keys and fall back only when none exist. Also list Grok in the OSC52 blocked clipboard toast so Grok TUI copy failures point at the same setting. * fix(grok): harden preferred auth session selection Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -45,4 +45,112 @@ describe('readGrokAuthSession', () => {
|
||||
error: 'Grok auth file is invalid'
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['https://auth.x.ai', 'https://auth.x.ai::client'])(
|
||||
'prefers the %s issuer entry over an earlier alternate issuer',
|
||||
async (preferredIssuer) => {
|
||||
vi.doMock('node:fs', () => ({
|
||||
existsSync: vi.fn(() => true),
|
||||
readFileSync: vi.fn(() =>
|
||||
JSON.stringify({
|
||||
'https://stale.example.com::client': {
|
||||
key: 'stale-token',
|
||||
user_id: 'stale-user',
|
||||
email: 'stale@example.com',
|
||||
expires_at: '2099-01-01T00:00:00.000Z'
|
||||
},
|
||||
[preferredIssuer]: {
|
||||
key: 'live-token',
|
||||
user_id: 'live-user',
|
||||
email: 'live@example.com',
|
||||
team_id: 'team-1',
|
||||
expires_at: '2099-06-01T00:00:00.000Z',
|
||||
oidc_client_id: 'client-1'
|
||||
}
|
||||
})
|
||||
)
|
||||
}))
|
||||
const { readGrokAuthSession } = await import('./grok-auth')
|
||||
|
||||
expect(readGrokAuthSession()).toEqual({
|
||||
status: 'ok',
|
||||
session: {
|
||||
accessToken: 'live-token',
|
||||
userId: 'live-user',
|
||||
email: 'live@example.com',
|
||||
teamId: 'team-1',
|
||||
expiresAtMs: Date.parse('2099-06-01T00:00:00.000Z'),
|
||||
oidcClientId: 'client-1'
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('falls back to the first tokenized entry when no auth.x.ai key exists', async () => {
|
||||
vi.doMock('node:fs', () => ({
|
||||
existsSync: vi.fn(() => true),
|
||||
readFileSync: vi.fn(() =>
|
||||
JSON.stringify({
|
||||
'https://alternate.example.com::client': {
|
||||
key: 'alt-token',
|
||||
user_id: 'alt-user',
|
||||
email: 'alt@example.com',
|
||||
expires_at: '2099-01-01T00:00:00.000Z'
|
||||
}
|
||||
})
|
||||
)
|
||||
}))
|
||||
const { readGrokAuthSession } = await import('./grok-auth')
|
||||
|
||||
expect(readGrokAuthSession()).toEqual({
|
||||
status: 'ok',
|
||||
session: {
|
||||
accessToken: 'alt-token',
|
||||
userId: 'alt-user',
|
||||
email: 'alt@example.com',
|
||||
teamId: null,
|
||||
expiresAtMs: Date.parse('2099-01-01T00:00:00.000Z'),
|
||||
oidcClientId: null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('skips an expired auth.x.ai client entry when a fresh one follows it', async () => {
|
||||
vi.doMock('node:fs', () => ({
|
||||
existsSync: vi.fn(() => true),
|
||||
readFileSync: vi.fn(() =>
|
||||
JSON.stringify({
|
||||
'https://auth.x.ai::old-client': {
|
||||
key: 'expired-token',
|
||||
expires_at: '2020-01-01T00:00:00.000Z'
|
||||
},
|
||||
'https://auth.x.ai::current-client': {
|
||||
key: 'fresh-token',
|
||||
expires_at: '2099-01-01T00:00:00.000Z'
|
||||
}
|
||||
})
|
||||
)
|
||||
}))
|
||||
const { readGrokAuthSession } = await import('./grok-auth')
|
||||
|
||||
expect(readGrokAuthSession()).toMatchObject({
|
||||
status: 'ok',
|
||||
session: { accessToken: 'fresh-token' }
|
||||
})
|
||||
})
|
||||
|
||||
it('does not resurrect an alternate issuer when an auth.x.ai entry is tokenless', async () => {
|
||||
vi.doMock('node:fs', () => ({
|
||||
existsSync: vi.fn(() => true),
|
||||
readFileSync: vi.fn(() =>
|
||||
JSON.stringify({
|
||||
'https://alternate.example.com::client': { key: 'stale-token' },
|
||||
'https://auth.x.ai::client': { user_id: 'signed-out-user' }
|
||||
})
|
||||
)
|
||||
}))
|
||||
const { readGrokAuthSession } = await import('./grok-auth')
|
||||
|
||||
expect(readGrokAuthSession()).toEqual({ status: 'missing' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,6 +29,8 @@ type GrokAuthEntry = {
|
||||
oidc_client_id?: string
|
||||
}
|
||||
|
||||
type TokenizedGrokAuthEntry = GrokAuthEntry & { key: string }
|
||||
|
||||
export type GrokAuthReadResult =
|
||||
| { status: 'missing' }
|
||||
| { status: 'error'; error: string }
|
||||
@@ -43,7 +45,7 @@ function getGrokAuthReadError(err: unknown): string {
|
||||
return 'Unable to read Grok auth file'
|
||||
}
|
||||
|
||||
function parseAuthEntry(value: unknown): GrokAuthEntry | null {
|
||||
function parseAuthEntry(value: unknown): TokenizedGrokAuthEntry | null {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return null
|
||||
}
|
||||
@@ -51,7 +53,7 @@ function parseAuthEntry(value: unknown): GrokAuthEntry | null {
|
||||
if (typeof entry.key !== 'string' || entry.key.length === 0) {
|
||||
return null
|
||||
}
|
||||
return entry
|
||||
return entry as TokenizedGrokAuthEntry
|
||||
}
|
||||
|
||||
function parseExpiresAtMs(iso: string | undefined): number | null {
|
||||
@@ -62,6 +64,24 @@ function parseExpiresAtMs(iso: string | undefined): number | null {
|
||||
return Number.isFinite(ms) ? ms : null
|
||||
}
|
||||
|
||||
// Why: stale alternate issuers can precede the default xAI OAuth session in auth.json.
|
||||
const PREFERRED_GROK_AUTH_ISSUER = 'https://auth.x.ai'
|
||||
|
||||
function sessionFromAuthEntry(authEntry: TokenizedGrokAuthEntry): GrokAuthSession {
|
||||
return {
|
||||
accessToken: authEntry.key,
|
||||
userId: typeof authEntry.user_id === 'string' ? authEntry.user_id : null,
|
||||
email: typeof authEntry.email === 'string' ? authEntry.email : null,
|
||||
teamId: typeof authEntry.team_id === 'string' ? authEntry.team_id : null,
|
||||
expiresAtMs: parseExpiresAtMs(authEntry.expires_at),
|
||||
oidcClientId: typeof authEntry.oidc_client_id === 'string' ? authEntry.oidc_client_id : null
|
||||
}
|
||||
}
|
||||
|
||||
function isPreferredGrokAuthKey(key: string): boolean {
|
||||
return key === PREFERRED_GROK_AUTH_ISSUER || key.startsWith(`${PREFERRED_GROK_AUTH_ISSUER}::`)
|
||||
}
|
||||
|
||||
export function readGrokAuthSession(): GrokAuthReadResult {
|
||||
const path = getGrokAuthPath()
|
||||
if (!existsSync(path)) {
|
||||
@@ -72,23 +92,32 @@ export function readGrokAuthSession(): GrokAuthReadResult {
|
||||
if (typeof parsed !== 'object' || parsed === null) {
|
||||
return { status: 'error', error: 'Grok auth file is invalid' }
|
||||
}
|
||||
for (const entry of Object.values(parsed)) {
|
||||
let preferredKeySeen = false
|
||||
let expiredPreferred: GrokAuthSession | null = null
|
||||
let fallback: GrokAuthSession | null = null
|
||||
for (const [key, entry] of Object.entries(parsed as Record<string, unknown>)) {
|
||||
const isPreferred = isPreferredGrokAuthKey(key)
|
||||
preferredKeySeen ||= isPreferred
|
||||
const authEntry = parseAuthEntry(entry)
|
||||
if (!authEntry?.key) {
|
||||
if (!authEntry) {
|
||||
continue
|
||||
}
|
||||
return {
|
||||
status: 'ok',
|
||||
session: {
|
||||
accessToken: authEntry.key,
|
||||
userId: typeof authEntry.user_id === 'string' ? authEntry.user_id : null,
|
||||
email: typeof authEntry.email === 'string' ? authEntry.email : null,
|
||||
teamId: typeof authEntry.team_id === 'string' ? authEntry.team_id : null,
|
||||
expiresAtMs: parseExpiresAtMs(authEntry.expires_at),
|
||||
oidcClientId:
|
||||
typeof authEntry.oidc_client_id === 'string' ? authEntry.oidc_client_id : null
|
||||
const session = sessionFromAuthEntry(authEntry)
|
||||
if (isPreferred) {
|
||||
if (isGrokAccessTokenFresh(session)) {
|
||||
return { status: 'ok', session }
|
||||
}
|
||||
expiredPreferred ??= session
|
||||
continue
|
||||
}
|
||||
if (!fallback) {
|
||||
fallback = session
|
||||
}
|
||||
}
|
||||
// Why: alternate issuers are compatibility fallbacks only when no default entry exists.
|
||||
const selectedSession = expiredPreferred ?? (preferredKeySeen ? null : fallback)
|
||||
if (selectedSession) {
|
||||
return { status: 'ok', session: selectedSession }
|
||||
}
|
||||
// Why: a token-less file (e.g. after grok logout) means signed out, not a
|
||||
// failure — 'error' would keep a status-bar alert visible for that user.
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import en from '@/i18n/locales/en.json'
|
||||
import es from '@/i18n/locales/es.json'
|
||||
import ja from '@/i18n/locales/ja.json'
|
||||
import ko from '@/i18n/locales/ko.json'
|
||||
import zh from '@/i18n/locales/zh.json'
|
||||
import { OSC52_CLIPBOARD_SETTING_ID } from './osc52-clipboard-setting-anchor'
|
||||
import type * as Osc52ClipboardBlockedToastModule from './osc52-clipboard-blocked-toast'
|
||||
|
||||
@@ -41,6 +46,7 @@ describe('showOsc52ClipboardBlockedToast', () => {
|
||||
|
||||
showOsc52ClipboardBlockedToast()
|
||||
|
||||
expect(toastInfoMock.mock.calls[0]?.[1]?.description).toContain('Grok')
|
||||
const options = toastInfoMock.mock.calls[0]?.[1]
|
||||
expect(options).toMatchObject({
|
||||
action: {
|
||||
@@ -67,4 +73,14 @@ describe('showOsc52ClipboardBlockedToast', () => {
|
||||
|
||||
expect(toastInfoMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('mentions Grok in every supported locale', () => {
|
||||
const locales = [en, es, ja, ko, zh]
|
||||
|
||||
for (const locale of locales) {
|
||||
expect(
|
||||
locale.auto.components.terminal.pane.osc52.clipboard.blocked.toast['7cf51f74fd']
|
||||
).toContain('Grok')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,7 +19,7 @@ export function showOsc52ClipboardBlockedToast(): void {
|
||||
{
|
||||
description: translate(
|
||||
'auto.components.terminal.pane.osc52.clipboard.blocked.toast.7cf51f74fd',
|
||||
'Enable TUI clipboard writes in Terminal settings to copy from SSH, tmux, Neovim, or fzf.'
|
||||
'Enable TUI clipboard writes in Terminal settings to copy from SSH, tmux, Neovim, fzf, or Grok.'
|
||||
),
|
||||
duration: 12_000,
|
||||
action: {
|
||||
|
||||
@@ -2588,7 +2588,7 @@
|
||||
"blocked": {
|
||||
"toast": {
|
||||
"97c98f1afe": "Open Setting",
|
||||
"7cf51f74fd": "Enable TUI clipboard writes in Terminal settings to copy from SSH, tmux, Neovim, or fzf.",
|
||||
"7cf51f74fd": "Enable TUI clipboard writes in Terminal settings to copy from SSH, tmux, Neovim, fzf, or Grok.",
|
||||
"89eaa3e80b": "Terminal clipboard write blocked"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2588,7 +2588,7 @@
|
||||
"blocked": {
|
||||
"toast": {
|
||||
"97c98f1afe": "Abrir ajustes",
|
||||
"7cf51f74fd": "Habilita las escrituras al portapapeles TUI en los ajustes de Terminal para copiar desde SSH, tmux, Neovim o fzf.",
|
||||
"7cf51f74fd": "Habilita las escrituras al portapapeles TUI en los ajustes de Terminal para copiar desde SSH, tmux, Neovim, fzf o Grok.",
|
||||
"89eaa3e80b": "Escritura al portapapeles del terminal bloqueada"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2588,7 +2588,7 @@
|
||||
"blocked": {
|
||||
"toast": {
|
||||
"97c98f1afe": "設定を開く",
|
||||
"7cf51f74fd": "SSH、tmux、Neovim、または fzf からコピーするには、Terminal 設定で TUI クリップボードへの書き込みを有効にします。",
|
||||
"7cf51f74fd": "SSH、tmux、Neovim、fzf、または Grok からコピーするには、Terminal 設定で TUI クリップボードへの書き込みを有効にします。",
|
||||
"89eaa3e80b": "Terminal のクリップボードへの書き込みがブロックされました"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2588,7 +2588,7 @@
|
||||
"blocked": {
|
||||
"toast": {
|
||||
"97c98f1afe": "설정 열기",
|
||||
"7cf51f74fd": "SSH, tmux, Neovim 또는 fzf에서 복사하려면 Terminal 설정에서 TUI 클립보드 쓰기를 활성화하세요.",
|
||||
"7cf51f74fd": "SSH, tmux, Neovim, fzf 또는 Grok에서 복사하려면 Terminal 설정에서 TUI 클립보드 쓰기를 활성화하세요.",
|
||||
"89eaa3e80b": "Terminal 클립보드 쓰기가 차단되었습니다."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2588,7 +2588,7 @@
|
||||
"blocked": {
|
||||
"toast": {
|
||||
"97c98f1afe": "打开设置",
|
||||
"7cf51f74fd": "在终端设置中启用 TUI 剪贴板写入,以从 SSH、tmux、Neovim 或 fzf 进行复制。",
|
||||
"7cf51f74fd": "在终端设置中启用 TUI 剪贴板写入,以从 SSH、tmux、Neovim、fzf 或 Grok 进行复制。",
|
||||
"89eaa3e80b": "终端剪贴板写入被阻止"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user