diff --git a/src/auth/oauth.ts b/src/auth/oauth.ts index 41ad861..b4e9224 100644 --- a/src/auth/oauth.ts +++ b/src/auth/oauth.ts @@ -1,4 +1,5 @@ import type { OAuthTokens } from './types'; +import type { Region } from '../config/schema'; import { CLIError } from '../errors/base'; import { ExitCode } from '../errors/codes'; @@ -12,17 +13,33 @@ export interface OAuthConfig { callbackPort: number; } -const DEFAULT_OAUTH_CONFIG: OAuthConfig = { - clientId: 'mmx-cli', - authorizationUrl: 'https://platform.minimax.io/oauth/authorize', - tokenUrl: 'https://api.minimax.io/v1/oauth/token', - deviceCodeUrl: 'https://api.minimax.io/v1/oauth/device/code', - scopes: ['api'], - callbackPort: 18991, -}; +const OAUTH_ENDPOINTS = { + global: { + authorizationUrl: 'https://platform.minimax.io/oauth/authorize', + tokenUrl: 'https://api.minimax.io/v1/oauth/token', + deviceCodeUrl: 'https://api.minimax.io/v1/oauth/device/code', + }, + cn: { + authorizationUrl: 'https://platform.minimaxi.com/oauth/authorize', + tokenUrl: 'https://api.minimaxi.com/v1/oauth/token', + deviceCodeUrl: 'https://api.minimaxi.com/v1/oauth/device/code', + }, +} as const; + +export function getOAuthConfig(region: Region, options?: { callbackPort?: number }): OAuthConfig { + const endpoints = OAUTH_ENDPOINTS[region]; + return { + clientId: 'mmx-cli', + authorizationUrl: endpoints.authorizationUrl, + tokenUrl: endpoints.tokenUrl, + deviceCodeUrl: endpoints.deviceCodeUrl, + scopes: ['api'], + callbackPort: options?.callbackPort ?? 18991, + }; +} export async function startBrowserFlow( - config: OAuthConfig = DEFAULT_OAUTH_CONFIG, + config: OAuthConfig = getOAuthConfig('global'), ): Promise { const { randomBytes, createHash } = await import('crypto'); const codeVerifier = randomBytes(32).toString('base64url'); @@ -129,7 +146,7 @@ async function waitForCallback(port: number, expectedState: string): Promise { // Request device code const codeRes = await fetch(config.deviceCodeUrl, { diff --git a/src/auth/refresh.ts b/src/auth/refresh.ts index 2ab8daa..68f9c8c 100644 --- a/src/auth/refresh.ts +++ b/src/auth/refresh.ts @@ -1,15 +1,20 @@ import type { OAuthTokens, CredentialFile } from './types'; +import type { Region } from '../config/schema'; import { saveCredentials } from './credentials'; import { CLIError } from '../errors/base'; import { ExitCode } from '../errors/codes'; -// OAuth config — endpoints TBD pending MiniMax OAuth documentation -const TOKEN_URL = 'https://api.minimax.io/v1/oauth/token'; +const TOKEN_URLS: Record = { + global: 'https://api.minimax.io/v1/oauth/token', + cn: 'https://api.minimaxi.com/v1/oauth/token', +} as const; export async function refreshAccessToken( refreshToken: string, + region: Region = 'global', ): Promise { - const res = await fetch(TOKEN_URL, { + const tokenUrl = TOKEN_URLS[region]; + const res = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ @@ -39,7 +44,8 @@ export async function ensureFreshToken(creds: CredentialFile): Promise { } // Token expired or about to expire — refresh - const tokens = await refreshAccessToken(creds.refresh_token); + const region = (creds.region as Region) || 'global'; + const tokens = await refreshAccessToken(creds.refresh_token, region); const updated: CredentialFile = { access_token: tokens.access_token, diff --git a/src/auth/types.ts b/src/auth/types.ts index 0f39fbe..9805997 100644 --- a/src/auth/types.ts +++ b/src/auth/types.ts @@ -13,6 +13,7 @@ export interface CredentialFile { expires_at: string; // ISO 8601 token_type: 'Bearer'; account?: string; + region?: string; // Region at time of login, used for token refresh URL } export interface ResolvedCredential { diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index 7d1fecb..76abe7d 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -2,7 +2,7 @@ import { defineCommand } from '../../command'; import { CLIError } from '../../errors/base'; import { ExitCode } from '../../errors/codes'; import { saveCredentials } from '../../auth/credentials'; -import { startBrowserFlow, startDeviceCodeFlow } from '../../auth/oauth'; +import { startBrowserFlow, startDeviceCodeFlow, getOAuthConfig } from '../../auth/oauth'; import { requestJson } from '../../client/http'; import { quotaEndpoint } from '../../client/endpoints'; import { renderQuotaTable } from '../../output/quota-table'; @@ -112,11 +112,13 @@ export default defineCommand({ return; } + const oauthConfig = getOAuthConfig(config.region); + let tokens; if (flags.noBrowser) { - tokens = await startDeviceCodeFlow(); + tokens = await startDeviceCodeFlow(oauthConfig); } else { - tokens = await startBrowserFlow(); + tokens = await startBrowserFlow(oauthConfig); } const creds: CredentialFile = { @@ -124,6 +126,7 @@ export default defineCommand({ refresh_token: tokens.refresh_token, expires_at: new Date(Date.now() + tokens.expires_in * 1000).toISOString(), token_type: 'Bearer', + region: config.region, }; await saveCredentials(creds);