Skip to content
Closed
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
1 change: 1 addition & 0 deletions config/packaged-runtime-node-modules.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const PACKAGED_RUNTIME_PACKAGE_ROOTS = [
'jsonc-parser',
'node-pty',
'posthog-node',
'proper-lockfile',
// serve-sim (for CLI JS entry + closure + state/middleware + to make packaged require('serve-sim') + its internal relatives work; mirrors other runtime JS like ws/yaml/zod. Natives/dylibs still via extraResources + the node_modules/serve-sim copy in resources from builder. Client if added too.
'serve-sim',
'qrcode',
Expand Down
8 changes: 8 additions & 0 deletions config/scripts/electron-builder-config.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,14 @@ describe('electron-builder config', () => {
).toBe(true)
})

it('includes proper-lockfile in the packaged runtime closure', () => {
const packagedTargets = createPackagedRuntimeNodeModuleResources().map(
(resource) => resource.to
)

expect(packagedTargets).toContain(join('node_modules', 'proper-lockfile'))
})

it('prunes non-target @parcel/watcher platform subpackages from packaged runtime resources', async () => {
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-parcel-watcher-prune-'))
try {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"jsonc-parser": "^3.3.1",
"node-pty": "^1.1.0",
"posthog-node": "^5.33.3",
"proper-lockfile": "4.1.2",
"qrcode": "^1.5.4",
"react-i18next": "^17.0.8",
"serve-sim": "^0.1.40",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions src/main/rate-limits/kimi-credential-location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { createHash } from 'node:crypto'
import { homedir } from 'node:os'
import { join } from 'node:path'

const DEFAULT_OAUTH_HOST = 'https://auth.kimi.com'
const DEFAULT_BASE_URL = 'https://api.kimi.com/coding/v1'

export type KimiCredentialLocation = {
home: string
oauthHost: string
baseUrl: string
storageName: string
credentialsPath: string
lockTarget: string
tokenUrl: string
usageUrl: string
}

type KimiEnvironment = Partial<Record<string, string | undefined>>

export function resolveKimiCredentialLocation(
env: KimiEnvironment = process.env,
fallbackHome = homedir()
): KimiCredentialLocation {
const home = env.KIMI_CODE_HOME || join(fallbackHome, '.kimi-code')
const oauthHost = (env.KIMI_CODE_OAUTH_HOST ?? env.KIMI_OAUTH_HOST ?? DEFAULT_OAUTH_HOST)
.trim()
.replace(/\/+$/, '')
// Why: match oauthHost trimming so whitespace in KIMI_CODE_BASE_URL cannot
// skew storageName hashing or produce a malformed usageUrl.
const baseUrl = (env.KIMI_CODE_BASE_URL ?? DEFAULT_BASE_URL).trim().replace(/\/+$/, '')
const isDefault = oauthHost === DEFAULT_OAUTH_HOST && baseUrl === DEFAULT_BASE_URL
const suffix = isDefault
? ''
: `-env-${createHash('sha256')
.update(JSON.stringify({ oauthHost, baseUrl }))
.digest('hex')
.slice(0, 16)}`
const storageName = `kimi-code${suffix}`

return {
home,
oauthHost,
baseUrl,
storageName,
credentialsPath: join(home, 'credentials', `${storageName}.json`),
lockTarget: join(home, 'oauth', storageName),
tokenUrl: `${oauthHost}/api/oauth/token`,
usageUrl: `${baseUrl}/usages`
}
}
226 changes: 219 additions & 7 deletions src/main/rate-limits/kimi-fetcher.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { join } from 'node:path'

const netFetchMock = vi.hoisted(() => vi.fn())
const fsState = vi.hoisted<{ credentials: string | null; readError: Error | null }>(() => ({
const fsState = vi.hoisted<{
credentials: string | null
readError: Error | null
writes: Map<string, string>
writeOptions: Map<string, unknown>
renames: { from: string; to: string }[]
readPaths: string[]
}>(() => ({
credentials: null,
readError: null
readError: null,
writes: new Map(),
writeOptions: new Map(),
renames: [],
readPaths: []
}))

vi.mock('electron', () => ({
Expand All @@ -12,7 +24,8 @@ vi.mock('electron', () => ({

vi.mock('node:fs', () => ({
existsSync: () => fsState.credentials !== null,
readFileSync: () => {
readFileSync: (path: string) => {
fsState.readPaths.push(path)
if (fsState.readError) {
throw fsState.readError
}
Expand All @@ -21,13 +34,35 @@ vi.mock('node:fs', () => ({
}
return fsState.credentials
},
writeFileSync: () => {},
renameSync: () => {}
writeFileSync: (path: string, value: string, options?: unknown) => {
fsState.writes.set(path, value)
fsState.writeOptions.set(path, options)
},
renameSync: (from: string, to: string) => {
fsState.renames.push({ from, to })
const value = fsState.writes.get(from)
if (value !== undefined) {
fsState.credentials = value
fsState.writes.delete(from)
}
}
}))

vi.mock('node:os', () => ({ homedir: () => '/home/test' }))

vi.mock('../../shared/secure-file', () => ({
writeSecureJsonFile: (path: string, value: unknown) => {
const tmpPath = `${path}.test.tmp`
fsState.writes.set(tmpPath, `${JSON.stringify(value, null, 2)}\n`)
fsState.writeOptions.set(tmpPath, { encoding: 'utf-8', mode: 0o600 })
fsState.renames.push({ from: tmpPath, to: path })
fsState.credentials = fsState.writes.get(tmpPath) ?? null
fsState.writes.delete(tmpPath)
}
}))

import { fetchKimiRateLimits } from './kimi-fetcher'
import { resolveKimiCredentialLocation } from './kimi-credential-location'

function jsonResponse(body: unknown, status = 200): Response {
return {
Expand Down Expand Up @@ -57,9 +92,14 @@ function freshCredentials(): string {

describe('fetchKimiRateLimits', () => {
beforeEach(() => {
vi.stubEnv('KIMI_DISABLE_OAUTH_LOCK', '1')
netFetchMock.mockReset()
fsState.credentials = null
fsState.readError = null
fsState.writes.clear()
fsState.writeOptions.clear()
fsState.renames = []
fsState.readPaths = []
})

afterEach(() => {
Expand Down Expand Up @@ -133,13 +173,185 @@ describe('fetchKimiRateLimits', () => {
expect(result.weekly).toBeNull()
})

it('does NOT refresh or call the API when the token is expired (read-only)', async () => {
// expires_at in the past → token stale; fetcher must not hit the network.
it('refreshes an expired access token before fetching usage', async () => {
fsState.credentials = JSON.stringify({
access_token: 'tok-old',
refresh_token: 'refresh-old',
expires_at: 1,
token_type: 'Bearer',
scope: 'kimi-code'
})
netFetchMock
.mockResolvedValueOnce(
jsonResponse({
access_token: 'tok-new',
refresh_token: 'refresh-new',
expires_in: 900,
token_type: 'Bearer',
scope: 'kimi-code'
})
)
.mockResolvedValueOnce(jsonResponse(USAGE_RESPONSE))

const result = await fetchKimiRateLimits()

expect(result.status).toBe('ok')
expect(netFetchMock).toHaveBeenCalledTimes(2)
const [refreshUrl, refreshInit] = netFetchMock.mock.calls[0]
expect(refreshUrl).toBe('https://auth.kimi.com/api/oauth/token')
const refreshBody = new URLSearchParams(refreshInit.body as string)
expect(refreshBody.get('client_id')).toBe('17e5f671-d194-4dfb-9706-5516cb48c098')
expect(refreshBody.get('grant_type')).toBe('refresh_token')
expect(refreshBody.get('refresh_token')).toBe('refresh-old')
const [, usageInit] = netFetchMock.mock.calls[1]
expect((usageInit.headers as Record<string, string>).Authorization).toBe('Bearer tok-new')
expect(fsState.renames).toHaveLength(1)
const [{ from: tmpPath }] = fsState.renames
expect(fsState.writeOptions.get(tmpPath)).toEqual({ encoding: 'utf-8', mode: 0o600 })
expect(JSON.parse(fsState.credentials ?? '{}')).toMatchObject({
access_token: 'tok-new',
refresh_token: 'refresh-new',
expires_in: 900,
token_type: 'Bearer',
scope: 'kimi-code'
})
expect(JSON.parse(fsState.credentials ?? '{}').expires_at).toBeGreaterThan(1)
})

it('uses credentials refreshed by the Kimi CLI when its refresh wins the race', async () => {
fsState.credentials = JSON.stringify({
access_token: 'tok-old',
refresh_token: 'refresh-old',
expires_at: 1
})
netFetchMock
.mockImplementationOnce(async () => {
fsState.credentials = JSON.stringify({
access_token: 'tok-cli',
refresh_token: 'refresh-cli',
expires_at: 99_999_999_999
})
return jsonResponse({ error: 'invalid_grant' }, 400)
})
.mockResolvedValueOnce(jsonResponse(USAGE_RESPONSE))

const result = await fetchKimiRateLimits()

expect(result.status).toBe('ok')
expect(netFetchMock).toHaveBeenCalledTimes(2)
const [, usageInit] = netFetchMock.mock.calls[1]
expect((usageInit.headers as Record<string, string>).Authorization).toBe('Bearer tok-cli')
expect(fsState.renames).toHaveLength(0)
})

it('returns user-safe unauthorized refresh copy after persisting revocation', async () => {
fsState.credentials = JSON.stringify({
access_token: 'tok-old',
refresh_token: 'refresh-old',
expires_at: 1
})
netFetchMock.mockResolvedValueOnce(jsonResponse({ error: 'invalid_grant' }, 401))

const result = await fetchKimiRateLimits()

expect(result.status).toBe('error')
expect(result.error).toBe('Kimi token refresh unauthorized - run kimi login')
expect(JSON.parse(fsState.credentials ?? '{}')).toMatchObject({
access_token: '',
refresh_token: ''
})
})

it('does NOT refresh or call the API when the expired token has no refresh token', async () => {
fsState.credentials = JSON.stringify({ access_token: 'tok-old', expires_at: 1 })

const result = await fetchKimiRateLimits()
expect(result.status).toBe('error')
expect(result.error).toMatch(/expired/i)
expect(netFetchMock).not.toHaveBeenCalled()
})

it('uses only the endpoint-scoped credential for refresh and usage', async () => {
vi.stubEnv('KIMI_CODE_HOME', '/kimi-home')
vi.stubEnv('KIMI_CODE_OAUTH_HOST', 'https://auth.example.com')
vi.stubEnv('KIMI_CODE_BASE_URL', 'https://api.example.com/coding/v1')
fsState.credentials = JSON.stringify({
access_token: 'scoped-old',
refresh_token: 'scoped-refresh',
expires_at: 1
})
netFetchMock
.mockResolvedValueOnce(
jsonResponse({
access_token: 'scoped-new',
refresh_token: 'scoped-rotated',
expires_in: 900
})
)
.mockResolvedValueOnce(jsonResponse(USAGE_RESPONSE))

const result = await fetchKimiRateLimits()

const scopedPath = join('/kimi-home', 'credentials', 'kimi-code-env-3e58296a497444bf.json')
expect(result.status).toBe('ok')
expect(fsState.readPaths).toEqual([scopedPath, scopedPath])
expect(fsState.readPaths).not.toContain('/kimi-home/credentials/kimi-code.json')
expect(netFetchMock.mock.calls[0][0]).toBe('https://auth.example.com/api/oauth/token')
expect(netFetchMock.mock.calls[1][0]).toBe('https://api.example.com/coding/v1/usages')
expect(fsState.renames[0].to).toBe(scopedPath)
})
})

describe('resolveKimiCredentialLocation', () => {
it.each([
['https://auth.kimi.com', 'https://api.kimi.com/coding/v1', 'kimi-code.json'],
[
'https://auth.example.com',
'https://api.kimi.com/coding/v1',
'kimi-code-env-d5c8af65e27873b9.json'
],
[
'https://auth.kimi.com',
'https://api.example.com/coding/v1',
'kimi-code-env-bf2242cfc5d51611.json'
],
[
'https://auth.example.com',
'https://api.example.com/coding/v1',
'kimi-code-env-3e58296a497444bf.json'
]
])('matches the Kimi credential vector for %s and %s', (oauthHost, baseUrl, filename) => {
const location = resolveKimiCredentialLocation(
{
KIMI_CODE_HOME: '/kimi-home',
KIMI_CODE_OAUTH_HOST: oauthHost,
KIMI_CODE_BASE_URL: baseUrl
},
'/fallback-home'
)

expect(location.credentialsPath).toBe(join('/kimi-home', 'credentials', filename))
})

it('scopes credentials, lock, and request URLs to the normalized endpoint pair', () => {
const location = resolveKimiCredentialLocation(
{
KIMI_CODE_HOME: '/kimi-home',
KIMI_CODE_OAUTH_HOST: ' https://auth.example.com/// ',
KIMI_CODE_BASE_URL: ' https://api.example.com/coding/v1/// '
},
'/fallback-home'
)

expect(location).toEqual({
home: '/kimi-home',
oauthHost: 'https://auth.example.com',
baseUrl: 'https://api.example.com/coding/v1',
storageName: 'kimi-code-env-3e58296a497444bf',
credentialsPath: join('/kimi-home', 'credentials', 'kimi-code-env-3e58296a497444bf.json'),
lockTarget: join('/kimi-home', 'oauth', 'kimi-code-env-3e58296a497444bf'),
tokenUrl: 'https://auth.example.com/api/oauth/token',
usageUrl: 'https://api.example.com/coding/v1/usages'
})
})
})
Loading
Loading