diff --git a/.changeset/fix-macos-device-version.md b/.changeset/fix-macos-device-version.md new file mode 100644 index 0000000..a6bd431 --- /dev/null +++ b/.changeset/fix-macos-device-version.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/kimi-code": patch +"@moonshot-ai/kimi-code-sdk": patch +--- + +Report the macOS product version in OAuth device information instead of the Darwin kernel version. diff --git a/packages/oauth/src/identity.ts b/packages/oauth/src/identity.ts index 82a22da..aa54e16 100644 --- a/packages/oauth/src/identity.ts +++ b/packages/oauth/src/identity.ts @@ -7,6 +7,7 @@ * production state. */ +import { execFileSync } from 'node:child_process'; import { randomUUID } from 'node:crypto'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { arch, hostname, release, type } from 'node:os'; @@ -111,11 +112,23 @@ function deviceModel(): string { const os = type(); const version = release(); const osArch = arch(); - if (os === 'Darwin') return `macOS ${version} ${osArch}`; + if (os === 'Darwin') return `macOS ${macOsProductVersion() ?? version} ${osArch}`; if (os === 'Windows_NT') return `Windows ${version} ${osArch}`; return `${os} ${version} ${osArch}`.trim(); } +function macOsProductVersion(): string | undefined { + try { + const version = execFileSync('/usr/bin/sw_vers', ['-productVersion'], { + encoding: 'utf-8', + timeout: 1000, + }).trim(); + return version.length > 0 ? version : undefined; + } catch { + return undefined; + } +} + function asciiHeader(value: string, fallback = 'unknown'): string { const cleaned = value.replaceAll(/[^\u0020-\u007E]/g, '').trim(); return cleaned.length > 0 ? cleaned : fallback; diff --git a/packages/oauth/test/identity.test.ts b/packages/oauth/test/identity.test.ts index f33a9b9..493c1f2 100644 --- a/packages/oauth/test/identity.test.ts +++ b/packages/oauth/test/identity.test.ts @@ -147,4 +147,33 @@ describe('ascii header value sanitization', () => { vi.resetModules(); } }); + + it('falls back to Darwin kernel version when sw_vers is unavailable', async () => { + vi.resetModules(); + vi.doMock('node:os', async () => ({ + ...(await vi.importActual('node:os')), + hostname: () => 'my-mac', + release: () => '25.5.0', + type: () => 'Darwin', + arch: () => 'arm64', + })); + // Force the sw_vers lookup to fail so the test is deterministic on macOS too, + // where the real binary would otherwise return the host's product version. + vi.doMock('node:child_process', async () => ({ + ...(await vi.importActual('node:child_process')), + execFileSync: () => { + throw new Error('ENOENT'); + }, + })); + + try { + const { createKimiDeviceHeaders } = await import('../src/identity'); + const headers = createKimiDeviceHeaders({ homeDir: tempHome(), version: '1.0.0' }); + expect(headers['X-Msh-Device-Model']).toBe('macOS 25.5.0 arm64'); + } finally { + vi.doUnmock('node:os'); + vi.doUnmock('node:child_process'); + vi.resetModules(); + } + }); });