diff --git a/.changeset/oauth-host-identity-platform.md b/.changeset/oauth-host-identity-platform.md index 20908a83b..69ac01432 100644 --- a/.changeset/oauth-host-identity-platform.md +++ b/.changeset/oauth-host-identity-platform.md @@ -2,4 +2,4 @@ "@moonshot-ai/kimi-code-oauth": minor --- -Rework the host identity type: rename `userAgentProduct` to `productName` and add a required `platform` field, so every host explicitly declares the `X-Msh-Platform` value it reports instead of silently inheriting the CLI's. +Rework the host identity type: rename `userAgentProduct` to `productName` and add a required `platform` field, so every host explicitly declares the `X-Msh-Platform` value it reports instead of silently inheriting the CLI's. OAuth requests now also send the product User-Agent (with the optional runtime suffix), so the OAuth host can tell client families and surfaces apart. diff --git a/packages/oauth/src/oauth-manager.ts b/packages/oauth/src/oauth-manager.ts index 1f16b8a05..b9a389ff8 100644 --- a/packages/oauth/src/oauth-manager.ts +++ b/packages/oauth/src/oauth-manager.ts @@ -22,7 +22,7 @@ import { pollDeviceToken, refreshAccessToken, requestDeviceAuthorization } from import type { DevicePollResult, RefreshOptions } from './oauth'; import type { TokenStorage } from './storage'; import { classifyToken, revokedTombstone, type TokenState } from './token-state'; -import type { DeviceAuthorization, DeviceHeaders, OAuthFlowConfig, TokenInfo } from './types'; +import type { DeviceAuthorization, DeviceHeaders, OAuthFlowConfig, OAuthRequestHeaders, TokenInfo } from './types'; const MIN_REFRESH_THRESHOLD_SECONDS = 300; const REFRESH_THRESHOLD_RATIO = 0.5; @@ -68,7 +68,7 @@ export interface OAuthManagerOptions { readonly pollDeviceImpl?: | ((config: OAuthFlowConfig, deviceCode: string) => Promise) | undefined; - readonly deviceHeaders?: (() => DeviceHeaders | undefined) | undefined; + readonly deviceHeaders?: (() => OAuthRequestHeaders | undefined) | undefined; /** * Root directory for per-provider lock files; resolves to * `{configDir}/oauth/{providerName}.lock`. @@ -104,7 +104,7 @@ export class OAuthManager { private readonly refreshImpl: NonNullable; private readonly requestImpl: NonNullable; private readonly pollImpl: NonNullable; - private readonly deviceHeaders: (() => DeviceHeaders | undefined) | undefined; + private readonly deviceHeaders: (() => OAuthRequestHeaders | undefined) | undefined; private readonly configDir: string | undefined; private readonly onRefresh: ((outcome: OAuthRefreshOutcome) => void) | undefined; @@ -155,7 +155,7 @@ export class OAuthManager { this.configDir = options.configDir ?? envConfigDir; } - private resolveDeviceHeaders(): DeviceHeaders | undefined { + private resolveDeviceHeaders(): OAuthRequestHeaders | undefined { return this.deviceHeaders?.(); } diff --git a/packages/oauth/src/oauth.ts b/packages/oauth/src/oauth.ts index f55d22c2e..df14d3c82 100644 --- a/packages/oauth/src/oauth.ts +++ b/packages/oauth/src/oauth.ts @@ -17,7 +17,7 @@ import { OAuthUnauthorizedError, RetryableRefreshError, } from './errors'; -import type { DeviceAuthorization, DeviceHeaders, OAuthFlowConfig, TokenInfo } from './types'; +import type { DeviceAuthorization, DeviceHeaders, OAuthFlowConfig, OAuthRequestHeaders, TokenInfo } from './types'; import { isRecord } from './utils'; const RETRYABLE_STATUSES = new Set([429, 500, 502, 503, 504]); @@ -59,7 +59,7 @@ const DEFAULT_HTTP_TIMEOUT_MS = 30_000; async function postForm( url: string, params: Record, - deviceHeaders?: DeviceHeaders | undefined, + deviceHeaders?: OAuthRequestHeaders | undefined, options?: { timeoutMs?: number; signal?: AbortSignal }, ): Promise<{ status: number; data: Record }> { const timeoutMs = options?.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS; @@ -118,7 +118,7 @@ function describeFetchFailure(error: unknown): string { export async function requestDeviceAuthorization( config: OAuthFlowConfig, - options: { readonly deviceHeaders?: DeviceHeaders | undefined }, + options: { readonly deviceHeaders?: OAuthRequestHeaders | undefined }, ): Promise { const url = `${config.oauthHost.replace(/\/$/, '')}/api/oauth/device_authorization`; const { status, data } = await postForm( @@ -168,7 +168,7 @@ export type DevicePollResult = export async function pollDeviceToken( config: OAuthFlowConfig, deviceCode: string, - options: { readonly deviceHeaders?: DeviceHeaders | undefined }, + options: { readonly deviceHeaders?: OAuthRequestHeaders | undefined }, ): Promise { const url = `${config.oauthHost.replace(/\/$/, '')}/api/oauth/token`; const { status, data } = await postForm( @@ -213,7 +213,7 @@ export async function pollDeviceToken( // ── refreshAccessToken ──────────────────────────────────────────────── export interface RefreshOptions { - readonly deviceHeaders?: DeviceHeaders | undefined; + readonly deviceHeaders?: OAuthRequestHeaders | undefined; readonly maxRetries?: number | undefined; /** * Backoff between retries in ms. Defaults to `2 ** attempt * 1000` (1s, 2s). @@ -289,4 +289,4 @@ export async function refreshAccessToken( throw lastError ?? new OAuthError('Token refresh failed after retries.'); } -export type { DeviceHeaders }; +export type { DeviceHeaders, OAuthRequestHeaders }; diff --git a/packages/oauth/src/toolkit.ts b/packages/oauth/src/toolkit.ts index 766c66796..e9809f4b4 100644 --- a/packages/oauth/src/toolkit.ts +++ b/packages/oauth/src/toolkit.ts @@ -6,7 +6,6 @@ import { OAuthUnauthorizedError } from './errors'; import { assertKimiHostIdentity, createKimiDefaultHeaders, - createKimiDeviceHeaders, type KimiHostIdentity, } from './identity'; import { @@ -393,10 +392,12 @@ export class KimiOAuthToolkit { identity === undefined ? undefined : () => - createKimiDeviceHeaders({ + // Full identity headers (User-Agent + X-Msh-*): the OAuth host + // reads the platform for the client family and the UA (suffix) + // for the runtime surface, e.g. kimi web's `(web)`. + createKimiDefaultHeaders({ homeDir: this.homeDir, - version: identity.version, - platform: identity.platform, + ...identity, }), ...this.managerOptions, }); diff --git a/packages/oauth/src/types.ts b/packages/oauth/src/types.ts index 9073f8614..66f597467 100644 --- a/packages/oauth/src/types.ts +++ b/packages/oauth/src/types.ts @@ -54,6 +54,12 @@ export interface DeviceHeaders { readonly 'X-Msh-Device-Id': string; } +/** Headers sent with OAuth HTTP requests: the `X-Msh-*` device set, plus a + product User-Agent when the caller carries a host identity — the OAuth + host needs both to tell client families (platform) and runtime surfaces + (UA suffix) apart. */ +export type OAuthRequestHeaders = Record; + /** JSON wire format for token persistence (snake_case, Python-compatible). */ export interface TokenInfoWire { readonly access_token: string;