Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions src/main/rate-limits/grok-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
})
})
57 changes: 43 additions & 14 deletions src/main/rate-limits/grok-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ type GrokAuthEntry = {
oidc_client_id?: string
}

type TokenizedGrokAuthEntry = GrokAuthEntry & { key: string }

export type GrokAuthReadResult =
| { status: 'missing' }
| { status: 'error'; error: string }
Expand All @@ -43,15 +45,15 @@ 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
}
const entry = value as GrokAuthEntry
if (typeof entry.key !== 'string' || entry.key.length === 0) {
return null
}
return entry
return entry as TokenizedGrokAuthEntry
}

function parseExpiresAtMs(iso: string | undefined): number | null {
Expand All @@ -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)) {
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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: {
Expand All @@ -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')
}
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2581,7 +2581,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"
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -2581,7 +2581,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"
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -2581,7 +2581,7 @@
"blocked": {
"toast": {
"97c98f1afe": "設定を開く",
"7cf51f74fd": "SSH、tmux、Neovim、または fzf からコピーするには、Terminal 設定で TUI クリップボードへの書き込みを有効にします。",
"7cf51f74fd": "SSH、tmux、Neovim、fzf、または Grok からコピーするには、Terminal 設定で TUI クリップボードへの書き込みを有効にします。",
"89eaa3e80b": "Terminal のクリップボードへの書き込みがブロックされました"
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/i18n/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -2581,7 +2581,7 @@
"blocked": {
"toast": {
"97c98f1afe": "설정 열기",
"7cf51f74fd": "SSH, tmux, Neovim 또는 fzf에서 복사하려면 Terminal 설정에서 TUI 클립보드 쓰기를 활성화하세요.",
"7cf51f74fd": "SSH, tmux, Neovim, fzf 또는 Grok에서 복사하려면 Terminal 설정에서 TUI 클립보드 쓰기를 활성화하세요.",
"89eaa3e80b": "Terminal 클립보드 쓰기가 차단되었습니다."
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -2581,7 +2581,7 @@
"blocked": {
"toast": {
"97c98f1afe": "打开设置",
"7cf51f74fd": "在终端设置中启用 TUI 剪贴板写入,以从 SSH、tmux、Neovim 或 fzf 进行复制。",
"7cf51f74fd": "在终端设置中启用 TUI 剪贴板写入,以从 SSH、tmux、Neovim、fzfGrok 进行复制。",
"89eaa3e80b": "终端剪贴板写入被阻止"
}
}
Expand Down
Loading