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
37 changes: 27 additions & 10 deletions src/auth/oauth.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<OAuthTokens> {
const { randomBytes, createHash } = await import('crypto');
const codeVerifier = randomBytes(32).toString('base64url');
Expand Down Expand Up @@ -129,7 +146,7 @@ async function waitForCallback(port: number, expectedState: string): Promise<str
}

export async function startDeviceCodeFlow(
config: OAuthConfig = DEFAULT_OAUTH_CONFIG,
config: OAuthConfig = getOAuthConfig('global'),
): Promise<OAuthTokens> {
// Request device code
const codeRes = await fetch(config.deviceCodeUrl, {
Expand Down
14 changes: 10 additions & 4 deletions src/auth/refresh.ts
Original file line number Diff line number Diff line change
@@ -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<Region, string> = {
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<OAuthTokens> {
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({
Expand Down Expand Up @@ -39,7 +44,8 @@ export async function ensureFreshToken(creds: CredentialFile): Promise<string> {
}

// 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,
Expand Down
1 change: 1 addition & 0 deletions src/auth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 6 additions & 3 deletions src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -112,18 +112,21 @@ 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 = {
access_token: tokens.access_token,
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);
Expand Down
Loading